From 2dda52291e78b6b97791965ecd61c94ee91a44f4 Mon Sep 17 00:00:00 2001 From: Kevin Knight <57677197+kevin-m-knight-gs@users.noreply.github.com> Date: Wed, 25 Sep 2024 08:52:20 -0400 Subject: [PATCH] Fix names for functions with units in the signature (#866) * Add TextTools * Fix parsing start node path and properties in GraphPath * Fix names ConcreteFunctionDefinitions with units in the signature * Add test for checking package children have valid identifiers as names --- ...ncreteFunctionDefinitionNameProcessor.java | 2 +- .../function/FunctionDescriptor.java | 12 +- .../pure/m3/navigation/graph/GraphPath.java | 45 +++- .../m3/navigation/graph/TestGraphPath.java | 177 ++++++++++--- .../AbstractCompiledStateIntegrityTest.java | 95 ++++++- .../tests/elements/function/TestFunction.java | 241 ++++++++---------- .../function/TestFunctionDescriptor.java | 6 +- .../finos/legend/pure/m4/tools/TextTools.java | 151 +++++++++++ .../legend/pure/m4/tools/TestTextTools.java | 106 ++++++++ 9 files changed, 631 insertions(+), 204 deletions(-) create mode 100644 legend-pure-core/legend-pure-m4/src/main/java/org/finos/legend/pure/m4/tools/TextTools.java create mode 100644 legend-pure-core/legend-pure-m4/src/test/java/org/finos/legend/pure/m4/tools/TestTextTools.java diff --git a/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/compiler/postprocessing/ConcreteFunctionDefinitionNameProcessor.java b/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/compiler/postprocessing/ConcreteFunctionDefinitionNameProcessor.java index c186ad2156..ee730f7964 100644 --- a/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/compiler/postprocessing/ConcreteFunctionDefinitionNameProcessor.java +++ b/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/compiler/postprocessing/ConcreteFunctionDefinitionNameProcessor.java @@ -105,7 +105,7 @@ private static StringBuilder appendSignatureStringForType(StringBuilder builder, } if (Measure.isUnit(rawType, processorSupport)) { - return builder.append(rawType.getValueForMetaPropertyToOne(M3Properties.measure).getName()).append('~').append(rawType.getName()); + return builder.append(rawType.getValueForMetaPropertyToOne(M3Properties.measure).getName()).append('$').append(rawType.getName()); } if (PackageableElement.isPackageableElement(rawType, processorSupport)) { diff --git a/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/navigation/function/FunctionDescriptor.java b/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/navigation/function/FunctionDescriptor.java index fc261e0e52..54bd9e06fb 100644 --- a/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/navigation/function/FunctionDescriptor.java +++ b/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/navigation/function/FunctionDescriptor.java @@ -37,7 +37,9 @@ import org.finos.legend.pure.m3.serialization.grammar.m3parser.antlr.M3Parser.TypeContext; import org.finos.legend.pure.m3.serialization.grammar.m3parser.antlr.M3Parser.UnitNameContext; import org.finos.legend.pure.m4.coreinstance.CoreInstance; +import org.finos.legend.pure.m4.serialization.grammar.StringEscape; import org.finos.legend.pure.m4.tools.SafeAppendable; +import org.finos.legend.pure.m4.tools.TextTools; import java.util.List; @@ -206,7 +208,7 @@ private static void descriptorTypeAndMultToId(StringBuilder builder, FunctionTyp private static StringBuilder descriptorTypeToId(StringBuilder builder, TypeContext typeContext) { UnitNameContext unitNameContext = typeContext.unitName(); - return builder.append((unitNameContext == null) ? typeContext.qualifiedName().getText() : unitNameContext.getText()); + return builder.append((unitNameContext == null) ? typeContext.qualifiedName().getText() : unitNameContext.getText().replace('~', '$')); } private static StringBuilder descriptorMultToId(StringBuilder builder, MultiplicityArgumentContext multContext) @@ -265,12 +267,10 @@ private static FunctionDescriptorContext parse(String text) throws InvalidFuncti private static FunctionDescriptorContext validateParseResult(FunctionDescriptorContext result, String text) { // ensure there's no unparsed text left over - for (int i = result.getStop().getStopIndex() + 1, len = text.length(); i < len; i++) + int nonWhitespaceIndex = TextTools.indexOfNonWhitespace(text, result.getStop().getStopIndex() + 1); + if (nonWhitespaceIndex != -1) { - if (!Character.isWhitespace(text.charAt(i))) - { - throw new RuntimeException("Unparsed text from index " + i + ": '" + text.substring(i) + "'"); - } + throw new RuntimeException("Unparsed text from index " + nonWhitespaceIndex + ": '" + StringEscape.escape(text.substring(nonWhitespaceIndex)) + "'"); } // validate types diff --git a/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/navigation/graph/GraphPath.java b/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/navigation/graph/GraphPath.java index 9222b51f17..c7e77591d7 100644 --- a/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/navigation/graph/GraphPath.java +++ b/legend-pure-core/legend-pure-m3-core/src/main/java/org/finos/legend/pure/m3/navigation/graph/GraphPath.java @@ -35,6 +35,7 @@ import org.finos.legend.pure.m4.coreinstance.SourceInformation; import org.finos.legend.pure.m4.serialization.grammar.StringEscape; import org.finos.legend.pure.m4.tools.SafeAppendable; +import org.finos.legend.pure.m4.tools.TextTools; import java.util.List; import java.util.Objects; @@ -440,6 +441,14 @@ public void setStartNodePath(String path) { throw new IllegalArgumentException("Invalid GraphPath start node path '" + StringEscape.escape(path) + "'", (e instanceof ParseCancellationException) ? e.getCause() : e); } + + // check that there's nothing more in the string (except possibly whitespace) + int nonWhitespace = TextTools.indexOfNonWhitespace(path, context.getStop().getStopIndex() + 1); + if (nonWhitespace != -1) + { + throw new IllegalArgumentException("Invalid GraphPath start node path '" + StringEscape.escape(path) + "': error at index " + nonWhitespace); + } + this.startNodePath = context.getText(); } @@ -479,14 +488,10 @@ public Builder fromDescription(String description) } // check that there's nothing more in the string (except possibly whitespace) - for (int i = context.getStop().getStopIndex() + 1, len = description.length(); i < len;) + int nonWhitespace = TextTools.indexOfNonWhitespace(description, context.getStop().getStopIndex() + 1); + if (nonWhitespace != -1) { - int codePoint = description.codePointAt(i); - if (!Character.isWhitespace(codePoint)) - { - throw new IllegalArgumentException("Invalid GraphPath description '" + StringEscape.escape(description) + "': error at index " + i); - } - i += Character.charCount(codePoint); + throw new IllegalArgumentException("Invalid GraphPath description '" + StringEscape.escape(description) + "': error at index " + nonWhitespace); } // build graph path @@ -582,14 +587,24 @@ private Builder addEdge(Edge pathElement) private String validateProperty(String property) { initParser(Objects.requireNonNull(property, "property may not be null")); + M3Parser.PropertyNameContext context; try { - return this.parser.propertyName().getText(); + context = this.parser.propertyName(); } catch (Exception e) { throw new IllegalArgumentException("Invalid property name '" + StringEscape.escape(property) + "'", (e instanceof ParseCancellationException) ? e.getCause() : e); } + + // check that there's nothing more in the string (except possibly whitespace) + int nonWhitespace = TextTools.indexOfNonWhitespace(property, context.getStop().getStopIndex() + 1); + if (nonWhitespace != -1) + { + throw new IllegalArgumentException("Invalid property name '" + StringEscape.escape(property) + "': error at index " + nonWhitespace); + } + + return context.getText(); } private int validateIndex(int index) @@ -604,14 +619,24 @@ private int validateIndex(int index) private String validateKeyProperty(String keyProperty) { initParser(Objects.requireNonNull(keyProperty, "key property may not be null")); + M3Parser.PropertyNameContext context; try { - return this.parser.propertyName().getText(); + context = this.parser.propertyName(); } catch (Exception e) { - throw new IllegalArgumentException("Invalid key property name '" + StringEscape.escape(keyProperty) + "\"", (e instanceof ParseCancellationException) ? e.getCause() : e); + throw new IllegalArgumentException("Invalid key property name '" + StringEscape.escape(keyProperty) + "'", (e instanceof ParseCancellationException) ? e.getCause() : e); + } + + // check that there's nothing more in the string (except possibly whitespace) + int nonWhitespace = TextTools.indexOfNonWhitespace(keyProperty, context.getStop().getStopIndex() + 1); + if (nonWhitespace != -1) + { + throw new IllegalArgumentException("Invalid key property name '" + StringEscape.escape(keyProperty) + "': error at index " + nonWhitespace); } + + return context.getText(); } private String validateKey(String key) diff --git a/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/navigation/graph/TestGraphPath.java b/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/navigation/graph/TestGraphPath.java index d58ebcedae..f4226a64a4 100644 --- a/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/navigation/graph/TestGraphPath.java +++ b/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/navigation/graph/TestGraphPath.java @@ -87,6 +87,9 @@ public void testGetDescription() Assert.assertEquals( "test::domain::ClassA.properties['name with escaped text, \\'\\n\\b\\\\, and other unusual characters, \"#$%^.'].genericType.rawType", GraphPath.builder("test::domain::ClassA").addToManyPropertyValueWithKey("properties", "name", "name with escaped text, '\n\b\\, and other unusual characters, \"#$%^.").addToOneProperties("genericType", "rawType").build().getDescription()); + Assert.assertEquals( + "test::domain::ClassA.properties[functionName='prop2'].genericType.rawType", + GraphPath.builder("test::domain::ClassA").addToManyPropertyValueWithKey("properties", "functionName", "prop2").addToOneProperties("genericType", "rawType").build().getDescription()); Assert.assertEquals( "::", @@ -561,76 +564,88 @@ private void assertResolveFully(GraphPath path, CoreInstance... nodes) public void testSubpath() { Assert.assertEquals( - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum"), - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").subpath(0)); + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength"), + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength").subpath(0)); Assert.assertEquals( "Index: 1; size: 0", Assert.assertThrows( IndexOutOfBoundsException.class, - () -> GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").subpath(1)).getMessage()); + () -> GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength").subpath(1)).getMessage()); Assert.assertEquals( "Index: -1; size: 0", Assert.assertThrows( IndexOutOfBoundsException.class, - () -> GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").subpath(-1)).getMessage()); + () -> GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength").subpath(-1)).getMessage()); Assert.assertEquals( - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum"), - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum", "measure").subpath(0)); + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength"), + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit").subpath(0)); + Assert.assertEquals( + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength"), + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit", "measure").subpath(0)); + Assert.assertEquals( + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit"), + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit", "measure").subpath(-1)); Assert.assertEquals( - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum"), - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum", "measure").subpath(-1)); + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit"), + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit", "measure").subpath(1)); Assert.assertEquals( - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum", "measure"), - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum", "measure").subpath(1)); + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit", "measure"), + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit", "measure").subpath(2)); Assert.assertEquals( - "Index: 2; size: 1", + "Index: 3; size: 2", Assert.assertThrows( IndexOutOfBoundsException.class, - () -> GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum", "measure").subpath(2)).getMessage()); + () -> GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit", "measure").subpath(3)).getMessage()); Assert.assertEquals( - "Index: -2; size: 1", + "Index: -3; size: 2", Assert.assertThrows( IndexOutOfBoundsException.class, - () -> GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum", "measure").subpath(-2)).getMessage()); + () -> GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit", "measure").subpath(-3)).getMessage()); Assert.assertEquals( - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum"), - GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").addToOneProperties("measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(0)); + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength"), + GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength").addToOneProperties("canonicalUnit", "measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(0)); + Assert.assertEquals( + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength"), + GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength").addToOneProperties("canonicalUnit", "measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(-5)); Assert.assertEquals( - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum"), - GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").addToOneProperties("measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(-4)); + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit"), + GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength").addToOneProperties("canonicalUnit", "measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(1)); Assert.assertEquals( - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum", "measure"), - GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").addToOneProperties("measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(1)); + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit"), + GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength").addToOneProperties("canonicalUnit", "measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(-4)); Assert.assertEquals( - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum", "measure"), - GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").addToOneProperties("measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(-3)); + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit", "measure"), + GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength").addToOneProperties("canonicalUnit", "measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(2)); Assert.assertEquals( - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum", "measure", "canonicalUnit"), - GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").addToOneProperties("measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(2)); + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit", "measure"), + GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength").addToOneProperties("canonicalUnit", "measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(-3)); Assert.assertEquals( - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum", "measure", "canonicalUnit"), - GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").addToOneProperties("measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(-2)); + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit", "measure", "canonicalUnit"), + GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength").addToOneProperties("canonicalUnit", "measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(3)); Assert.assertEquals( - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum", "measure", "canonicalUnit", "measure"), - GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").addToOneProperties("measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(3)); + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit", "measure", "canonicalUnit"), + GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength").addToOneProperties("canonicalUnit", "measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(-2)); Assert.assertEquals( - GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength~Cubitum", "measure", "canonicalUnit", "measure"), - GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").addToOneProperties("measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(-1)); + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit", "measure", "canonicalUnit", "measure"), + GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength").addToOneProperties("canonicalUnit", "measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(4)); Assert.assertEquals( - GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").addToOneProperties("measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build(), - GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").addToOneProperties("measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(4)); + GraphPath.buildPath("meta::pure::functions::meta::tests::model::RomanLength", "canonicalUnit", "measure", "canonicalUnit", "measure"), + GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength").addToOneProperties("canonicalUnit", "measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(-1)); Assert.assertEquals( - "Index: 5; size: 4", + GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength").addToOneProperties("canonicalUnit", "measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build(), + GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength").addToOneProperties("canonicalUnit", "measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(5)); + Assert.assertEquals( + "Index: 6; size: 5", Assert.assertThrows( IndexOutOfBoundsException.class, - () -> GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").addToOneProperties("measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(5)).getMessage()); + () -> GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength").addToOneProperties("canonicalUnit", "measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(6)).getMessage()); Assert.assertEquals( - "Index: -5; size: 4", + "Index: -6; size: 5", Assert.assertThrows( IndexOutOfBoundsException.class, - () -> GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength~Cubitum").addToOneProperties("measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(-5)).getMessage()); + () -> GraphPath.builder("meta::pure::functions::meta::tests::model::RomanLength").addToOneProperties("canonicalUnit", "measure", "canonicalUnit", "measure").addToManyPropertyValueWithName("nonCanonicalUnits", "Actus").build().subpath(-6)).getMessage()); } @Test @@ -846,11 +861,14 @@ public void testDoesNotParse() "#test::domain::ClassA", "test::domain::ClassA.properties/0/", "test::domain::ClassA.properties(0)", + "test::domain::ClassA.properties[invalid key='value']", "test::domain.model::ClassA", "test::domain::model::ClassA::", "Integer.generalizations.general.rawType.", "!Integer.generalizations.general.rawType", - "Integer.generalizations.general.rawType etc...") + "Integer.generalizations.general.rawType etc...", + "meta::pure::functions::meta::tests::model::RomanLength~Cubitum", + "meta::pure::functions::meta::tests::model::RomanLength~Cubitum.measure") .forEach(s -> { IllegalArgumentException e = Assert.assertThrows("'" + StringEscape.escape(s) + "'", IllegalArgumentException.class, () -> GraphPath.parse(s)); @@ -863,6 +881,91 @@ public void testDoesNotParse() }); } + @Test + public void testStartNodePath() + { + ArrayAdapter.adapt( + "", + " ", + "\t\n", + "the quick brown fox jumped over the lazy dog", + "@#$%!@#$%", + ",", + "test::domain::ClassA!", + "#test::domain::ClassA", + "test::domain::ClassA.properties/0/", + "test::domain::ClassA.properties(0)", + "test::domain::ClassA.properties[invalid key='value']", + "test::domain.model::ClassA", + "test::domain::model::ClassA::", + "test::domain::model::ClassA.properties[0]", + "Integer.generalizations.general.rawType.", + "!Integer.generalizations.general.rawType", + "Integer.generalizations.general.rawType etc...", + "meta::pure::functions::meta::tests::model::RomanLength~Cubitum", + "meta::pure::functions::meta::tests::model::RomanLength~Cubitum.measure") + .forEach(s -> + { + IllegalArgumentException e = Assert.assertThrows("'" + StringEscape.escape(s) + "'", IllegalArgumentException.class, () -> GraphPath.builder().setStartNodePath(s)); + String expectedPrefix = "Invalid GraphPath start node path '" + StringEscape.escape(s) + "'"; + String message = e.getMessage(); + if (!message.startsWith(expectedPrefix)) + { + Assert.assertEquals(expectedPrefix, e.getMessage()); + } + }); + } + + @Test + public void testPropertyName() + { + GraphPath.Builder builder = GraphPath.builder().withStartNodePath("test::domain::ClassA"); + Assert.assertEquals("property may not be null", Assert.assertThrows(NullPointerException.class, () -> builder.addToOneProperty(null)).getMessage()); + ArrayAdapter.adapt( + "", + " ", + "\t\n", + "the quick brown fox jumped over the lazy dog", + "@#$%!@#$%", + ",", + "pro%perty") + .forEach(s -> + { + IllegalArgumentException e = Assert.assertThrows("'" + StringEscape.escape(s) + "'", IllegalArgumentException.class, () -> builder.addToOneProperty(s)); + String expectedPrefix = "Invalid property name '" + StringEscape.escape(s) + "'"; + String message = e.getMessage(); + if (!message.startsWith(expectedPrefix)) + { + Assert.assertEquals(expectedPrefix, e.getMessage()); + } + }); + } + + @Test + public void testPropertyKeyName() + { + GraphPath.Builder builder = GraphPath.builder().withStartNodePath("test::domain::ClassA"); + Assert.assertEquals("key property may not be null", Assert.assertThrows(NullPointerException.class, () -> builder.addToManyPropertyValueWithKey("properties", null, "key")).getMessage()); + ArrayAdapter.adapt( + "", + " ", + "\t\n", + "the quick brown fox jumped over the lazy dog", + "@#$%!@#$%", + ",", + "pro%perty") + .forEach(s -> + { + IllegalArgumentException e = Assert.assertThrows("'" + StringEscape.escape(s) + "'", IllegalArgumentException.class, () -> builder.addToManyPropertyValueWithKey("properties", s, "key")); + String expectedPrefix = "Invalid key property name '" + StringEscape.escape(s) + "'"; + String message = e.getMessage(); + if (!message.startsWith(expectedPrefix)) + { + Assert.assertEquals(expectedPrefix, e.getMessage()); + } + }); + } + @Test public void testEdgeVisitor() { diff --git a/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/tests/AbstractCompiledStateIntegrityTest.java b/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/tests/AbstractCompiledStateIntegrityTest.java index 48f3cfc536..2a0e1b5f59 100644 --- a/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/tests/AbstractCompiledStateIntegrityTest.java +++ b/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/tests/AbstractCompiledStateIntegrityTest.java @@ -14,6 +14,10 @@ package org.finos.legend.pure.m3.tests; +import org.antlr.v4.runtime.BailErrorStrategy; +import org.antlr.v4.runtime.CharStreams; +import org.antlr.v4.runtime.CommonTokenStream; +import org.antlr.v4.runtime.atn.PredictionMode; import org.eclipse.collections.api.RichIterable; import org.eclipse.collections.api.block.predicate.Predicate; import org.eclipse.collections.api.factory.Lists; @@ -33,6 +37,7 @@ import org.finos.legend.pure.m3.compiler.Context; import org.finos.legend.pure.m3.compiler.ReferenceUsage; import org.finos.legend.pure.m3.coreinstance.Package; +import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.ModelElement; import org.finos.legend.pure.m3.coreinstance.meta.pure.metamodel.type.Unit; import org.finos.legend.pure.m3.navigation.Instance; import org.finos.legend.pure.m3.navigation.M3Paths; @@ -55,6 +60,8 @@ import org.finos.legend.pure.m3.serialization.filesystem.usercodestorage.classpath.ClassLoaderCodeStorage; import org.finos.legend.pure.m3.serialization.filesystem.usercodestorage.composite.CompositeCodeStorage; import org.finos.legend.pure.m3.serialization.grammar.Parser; +import org.finos.legend.pure.m3.serialization.grammar.m3parser.antlr.M3Lexer; +import org.finos.legend.pure.m3.serialization.grammar.m3parser.antlr.M3Parser; import org.finos.legend.pure.m3.serialization.grammar.m3parser.inlinedsl.InlineDSL; import org.finos.legend.pure.m3.serialization.runtime.PrintPureRuntimeStatus; import org.finos.legend.pure.m3.serialization.runtime.PureRuntime; @@ -474,27 +481,93 @@ public void testAllElementsWithPackagesArePackageChildren() } @Test - public void testAllPackageChildrenHaveNames() + public void testAllPackageChildrenHaveValidNames() { - MutableMap> badElements = Maps.mutable.empty(); + MutableMap> noNames = Maps.mutable.empty(); + MutableMap> invalidNames = Maps.mutable.empty(); + M3Lexer lexer = new M3Lexer(null); + lexer.removeErrorListeners(); + M3Parser parser = new M3Parser(null); + parser.removeErrorListeners(); + parser.setErrorHandler(new BailErrorStrategy()); + parser.getInterpreter().setPredictionMode(PredictionMode.SLL); PackageTreeIterable.newRootPackageTreeIterable(repository).forEach(pkg -> pkg._children().forEach(child -> { - if (child.getValueForMetaPropertyToOne(M3Properties.name) == null) + String name = child._name(); + if (name == null) { - badElements.getIfAbsentPut(pkg, Lists.mutable::empty).add(child); + noNames.getIfAbsentPut(pkg, Lists.mutable::empty).add(child); + } + else + { + lexer.setInputStream(CharStreams.fromString(name)); + parser.setTokenStream(new CommonTokenStream(lexer)); + try + { + M3Parser.IdentifierContext context = parser.identifier(); + if (context.getStop().getStopIndex() + 1 < name.length()) + { + // some of the name was not parsed, so was not valid identifier text + invalidNames.getIfAbsentPut(pkg, Lists.mutable::empty).add(child); + } + } + catch (Exception ignore) + { + invalidNames.getIfAbsentPut(pkg, Lists.mutable::empty).add(child); + } } })); - if (badElements.notEmpty()) + if (noNames.notEmpty() || invalidNames.notEmpty()) { - StringBuilder builder = new StringBuilder("The following packages have children with no name:"); - badElements.forEachKeyValue((pkg, children) -> + StringBuilder builder = new StringBuilder(); + if (noNames.notEmpty()) { - if (children.notEmpty()) + builder.append("The following packages have children with no name:"); + noNames.forEachKeyValue((pkg, children) -> { PackageableElement.writeUserPathForPackageableElement(builder.append("\n\t"), pkg); - children.appendString(builder, "\n\t\t", "\n\t\t", ""); - } - }); + children.forEach(child -> + { + builder.append("\n\t\t"); + CoreInstance classifier = processorSupport.getClassifier(child); + if (classifier == null) + { + builder.append(child); + } + else + { + PackageableElement.writeUserPathForPackageableElement(builder.append("instance of "), classifier); + } + SourceInformation sourceInfo = child.getSourceInformation(); + if (sourceInfo != null) + { + sourceInfo.appendMessage(builder.append("\n\t\t\tsource: ")); + } + }); + }); + } + if (invalidNames.notEmpty()) + { + (noNames.isEmpty() ? builder : builder.append('\n')).append("The following packages have children with invalid names:"); + invalidNames.forEachKeyValue((pkg, children) -> + { + PackageableElement.writeUserPathForPackageableElement(builder.append("\n\t"), pkg); + children.forEach(child -> + { + builder.append("\n\t\t").append(child._name()); + CoreInstance classifier = processorSupport.getClassifier(child); + if (classifier != null) + { + PackageableElement.writeUserPathForPackageableElement(builder.append("\n\t\t\tclassifier: "), classifier); + } + SourceInformation sourceInfo = child.getSourceInformation(); + if (sourceInfo != null) + { + sourceInfo.appendMessage(builder.append("\n\t\t\tsource: ")); + } + }); + }); + } Assert.fail(builder.toString()); } } diff --git a/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/tests/elements/function/TestFunction.java b/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/tests/elements/function/TestFunction.java index a0b844cbd8..4895b4fc06 100644 --- a/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/tests/elements/function/TestFunction.java +++ b/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/tests/elements/function/TestFunction.java @@ -15,7 +15,7 @@ package org.finos.legend.pure.m3.tests.elements.function; import org.finos.legend.pure.m3.navigation.Printer; -import org.finos.legend.pure.m3.tests.AbstractPureTestWithCoreCompiledPlatform; +import org.finos.legend.pure.m3.tests.AbstractPureTestWithCoreCompiled; import org.finos.legend.pure.m4.coreinstance.CoreInstance; import org.finos.legend.pure.m4.exception.PureCompilationException; import org.junit.After; @@ -23,7 +23,7 @@ import org.junit.BeforeClass; import org.junit.Test; -public class TestFunction extends AbstractPureTestWithCoreCompiledPlatform +public class TestFunction extends AbstractPureTestWithCoreCompiled { @BeforeClass public static void setUp() @@ -42,107 +42,77 @@ public void clearRuntime() @Test public void testFunctionTypeWithWrongTypes() { - try - { - compileTestSource("fromString.pure", "function myFunc(f:{String[1]->{String[1]->Booelean[1]}[1]}[*]):String[1]\n" + - "{\n" + - " 'ee';\n" + - "}\n"); - Assert.fail(); - } - catch (Exception e) - { - assertPureException(PureCompilationException.class, "Booelean has not been defined!", 1, 43, e); - } + PureCompilationException e = Assert.assertThrows(PureCompilationException.class, () -> compileTestSource( + "fromString.pure", + "function myFunc(f:{String[1]->{String[1]->Booelean[1]}[1]}[*]):String[1]\n" + + "{\n" + + " 'ee';\n" + + "}\n")); + assertPureException(PureCompilationException.class, "Booelean has not been defined!", 1, 43, e); } @Test public void testNewWithUnknownType() { - try - { - compileTestSource("fromString.pure", "function myFunc():String[1]\n" + - "{\n" + - " ^XErrorType(name = 'ok');\n" + - "}\n"); - Assert.fail(); - } - catch (Exception e) - { - assertPureException(PureCompilationException.class, "XErrorType has not been defined!", 3, 6, e); - } + PureCompilationException e = Assert.assertThrows(PureCompilationException.class, () -> compileTestSource( + "fromString.pure", + "function myFunc():String[1]\n" + + "{\n" + + " ^XErrorType(name = 'ok');\n" + + "}\n")); + assertPureException(PureCompilationException.class, "XErrorType has not been defined!", 3, 6, e); } @Test public void testCastWithUnknownType() { - try - { - compileTestSource("fromString.pure", "function myFunc():String[1]\n" + - "{\n" + - " 'a'->cast(@Error);\n" + - "}\n"); - Assert.fail(); - } - catch (Exception e) - { - assertPureException(PureCompilationException.class, "Error has not been defined!", 3, 16, e); - } + PureCompilationException e = Assert.assertThrows(PureCompilationException.class, () -> compileTestSource( + "fromString.pure", + "function myFunc():String[1]\n" + + "{\n" + + " 'a'->cast(@Error);\n" + + "}\n")); + assertPureException(PureCompilationException.class, "Error has not been defined!", 3, 16, e); } @Test public void testToMultiplicityWithUnknownMul() { - try - { - compileTestSource("fromString.pure", "function myFunc<|o>():String[o]\n" + - "{\n" + - " 'a'->toMultiplicity(@[x]);\n" + - "}\n"); - Assert.fail(); - } - catch (Exception e) - { - assertPureException(PureCompilationException.class, "The multiplicity parameter x is unknown!", 3, 25, e); - } + PureCompilationException e = Assert.assertThrows(PureCompilationException.class, () -> compileTestSource( + "fromString.pure", + "function myFunc<|o>():String[o]\n" + + "{\n" + + " 'a'->toMultiplicity(@[x]);\n" + + "}\n")); + assertPureException(PureCompilationException.class, "The multiplicity parameter x is unknown!", 3, 25, e); } @Test public void testToMultiplicityWithWrongMul() { - try - { - compileTestSource("fromString.pure", "function myFunc<|o>():String[o]\n" + - "{\n" + - " 'a';\n" + - "}\n"); - Assert.fail(); - } - catch (Exception e) - { - assertPureException(PureCompilationException.class, "Return multiplicity error in function 'myFunc'; found: [1]; expected: [o]", 3, 5, e); - } + PureCompilationException e = Assert.assertThrows(PureCompilationException.class, () -> compileTestSource( + "fromString.pure", + "function myFunc<|o>():String[o]\n" + + "{\n" + + " 'a';\n" + + "}\n")); + assertPureException(PureCompilationException.class, "Return multiplicity error in function 'myFunc'; found: [1]; expected: [o]", 3, 5, e); } @Test public void testCastWithUnknownGeneric() { - try - { - compileTestSource("fromString.pure", "Class A{}\n" + - "\n" + - "function myFunc():String[1]\n" + - "{\n" + - " 'a'->cast(@A);\n" + - "}\n"); - Assert.fail(); - } - catch (Exception e) - { - assertPureException(PureCompilationException.class, "Error has not been defined!", 5, 18, e); - } + PureCompilationException e = Assert.assertThrows(PureCompilationException.class, () -> compileTestSource( + "fromString.pure", + "Class A{}\n" + + "\n" + + "function myFunc():String[1]\n" + + "{\n" + + " 'a'->cast(@A);\n" + + "}\n")); + assertPureException(PureCompilationException.class, "Error has not been defined!", 5, 18, e); } @Test @@ -153,76 +123,58 @@ public void testReturnTypeValidationWithTypeParameter() "{\n" + " []\n" + "}"); - try - { - compileTestSource("fromString2.pure", "function test2(t:T[1]):T[1]\n" + - "{\n" + - " 5\n" + - "}"); - Assert.fail("Expected compilation exception"); - } - catch (Exception e) - { - assertPureException(PureCompilationException.class, "Return type error in function 'test2'; found: Integer; expected: T", 3, 5, e); - } - try - { - compileTestSource("fromString3.pure", "function test3(t:T[1], u:U[1]):T[1]\n" + - "{\n" + - " $u\n" + - "}"); - Assert.fail("Expected compilation exception"); - } - catch (Exception e) - { - assertPureException(PureCompilationException.class, "Return type error in function 'test3'; found: U; expected: T", 3, 6, e); - } + PureCompilationException e1 = Assert.assertThrows(PureCompilationException.class, () -> compileTestSource( + "fromString2.pure", + "function test2(t:T[1]):T[1]\n" + + "{\n" + + " 5\n" + + "}")); + assertPureException(PureCompilationException.class, "Return type error in function 'test2'; found: Integer; expected: T", 3, 5, e1); + + PureCompilationException e2 = Assert.assertThrows(PureCompilationException.class, () -> compileTestSource( + "fromString3.pure", + "function test3(t:T[1], u:U[1]):T[1]\n" + + "{\n" + + " $u\n" + + "}")); + assertPureException(PureCompilationException.class, "Return type error in function 'test3'; found: U; expected: T", 3, 6, e2); } @Test public void testReturnMultiplicityValidationWithMultiplicityParameter() { - try - { - compileTestSource("fromString.pure", "function test1<|m>(a:Any[m]):Any[m]\n" + - "{\n" + - " 1\n" + - "}"); - } - catch (Exception e) - { - assertPureException(PureCompilationException.class, "Return multiplicity error in function 'test1'; found: [1]; expected: [m]", 3, 5, e); - } + PureCompilationException e1 = Assert.assertThrows(PureCompilationException.class, () -> compileTestSource( + "fromString.pure", + "function test1<|m>(a:Any[m]):Any[m]\n" + + "{\n" + + " 1\n" + + "}")); + assertPureException(PureCompilationException.class, "Return multiplicity error in function 'test1'; found: [1]; expected: [m]", 3, 5, e1); - try - { - compileTestSource("fromString2.pure", "function test2<|m,n>(a:Any[m], b:Any[n]):Any[m]\n" + - "{\n" + - " $b\n" + - "}"); - } - catch (Exception e) - { - assertPureException(PureCompilationException.class, "Return multiplicity error in function 'test2'; found: [n]; expected: [m]", 3, 6, e); - } + PureCompilationException e2 = Assert.assertThrows(PureCompilationException.class, () -> compileTestSource( + "fromString2.pure", + "function test2<|m,n>(a:Any[m], b:Any[n]):Any[m]\n" + + "{\n" + + " $b\n" + + "}")); + assertPureException(PureCompilationException.class, "Return multiplicity error in function 'test2'; found: [n]; expected: [m]", 3, 6, e2); } @Test public void testSimple() { - runtime.createInMemorySource("fromString.pure", - "Class a::A{val:String[1];}" + - "function myFunction(func:a::A[1]):String[1]" + - "{" + - " ^a::A(val='ok').val;" + - "}" + - "" + + compileTestSource("fromString.pure", + "Class a::A{val:String[1];}\n" + + "function myFunction(func:a::A[1]):String[1]\n" + + "{\n" + + " ^a::A(val='ok').val;\n" + + "}\n" + + "\n" + "function test():Nil[0]\n" + "{\n" + - " print(myFunction_A_1__String_1_,1);" + + " print(myFunction_A_1__String_1_,1);\n" + "}"); - runtime.compile(); CoreInstance func = runtime.getFunction("test():Nil[0]"); Assert.assertEquals("test__Nil_0_ instance ConcreteFunctionDefinition\n" + @@ -407,11 +359,13 @@ public void testSimple() @Test public void testFunction() { - compileTestSource("fromString.pure", "Class Employee {name:String[1];}" + - "function getValue(source:Any[1], prop:String[1]):Any[*]\n" + - "{\n" + - " Employee.all()->filter(t:Employee[1]|$t.name == 'cool');\n" + - "}"); + compileTestSource( + "fromString.pure", + "Class Employee {name:String[1];}" + + "function getValue(source:Any[1], prop:String[1]):Any[*]\n" + + "{\n" + + " Employee.all()->filter(t:Employee[1]|$t.name == 'cool');\n" + + "}"); Assert.assertEquals("getValue_Any_1__String_1__Any_MANY_ instance ConcreteFunctionDefinition\n" + " classifierGenericType(Property):\n" + @@ -807,4 +761,19 @@ public void testFunction() } + @Test + public void testFunctionWithUnitName() + { + compileTestSource( + "fromString.pure", + "import meta::pure::functions::meta::tests::model::*;\n" + + "function pkg::myTestFunction(input:RomanLength~Cubitum[1]):RomanLength~Pes[1]\n" + + "{\n" + + " let valuePes = RomanLength~Cubitum.conversionFunction->toOne()->cast(@Function<{Number[1]->Number[1]}>)->eval($input->getUnitValue());\n" + + " newUnit(RomanLength~Actus, $valuePes)->cast(@RomanLength~Pes);\n" + + "}\n" + ); + CoreInstance func = runtime.getCoreInstance("pkg::myTestFunction_RomanLength$Cubitum_1__RomanLength$Pes_1_"); + Assert.assertEquals("myTestFunction_RomanLength$Cubitum_1__RomanLength$Pes_1_", func.getName()); + } } diff --git a/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/tests/function/TestFunctionDescriptor.java b/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/tests/function/TestFunctionDescriptor.java index c02e2f96b3..4b0cf6782e 100644 --- a/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/tests/function/TestFunctionDescriptor.java +++ b/legend-pure-core/legend-pure-m3-core/src/test/java/org/finos/legend/pure/m3/tests/function/TestFunctionDescriptor.java @@ -105,8 +105,8 @@ public void testFunctionDescriptorToIdWithTypeParameters() throws InvalidFunctio @Test public void testFunctionDescriptorToIdUnits() throws InvalidFunctionDescriptorException { - Assert.assertEquals("pkg::myFunc__Mass~Gram_1_", FunctionDescriptor.functionDescriptorToId("pkg::myFunc():Mass~Gram[1]")); - Assert.assertEquals("pkg::myFunc__Mass~Gram_1_", FunctionDescriptor.functionDescriptorToId(" \t \tpkg::myFunc( ) : Mass ~ Gram[\t\t1\t]")); + Assert.assertEquals("pkg::myFunc__Mass$Gram_1_", FunctionDescriptor.functionDescriptorToId("pkg::myFunc():Mass~Gram[1]")); + Assert.assertEquals("pkg::myFunc__Mass$Gram_1_", FunctionDescriptor.functionDescriptorToId(" \t \tpkg::myFunc( ) : Mass ~ Gram[\t\t1\t]")); } @Test @@ -221,7 +221,7 @@ public void testGetFunctionDescriptor() throws InvalidFunctionDescriptorExceptio "pkg1::pkg2::test2(String[0..5], Integer[0..*], Integer[0..*]):Date[2..2]"); assertFunctionDescriptor( processorSupport, - "my::test::testUnits_Mass~Kilogram_1__Mass~Kilogram_1_", + "my::test::testUnits_Mass$Kilogram_1__Mass$Kilogram_1_", "my::test::testUnits(Mass~Kilogram[1]):Mass~Kilogram[1]", "my::test::testUnits(\tMass~Kilogram[1] ) : Mass~Kilogram[1..1]"); diff --git a/legend-pure-core/legend-pure-m4/src/main/java/org/finos/legend/pure/m4/tools/TextTools.java b/legend-pure-core/legend-pure-m4/src/main/java/org/finos/legend/pure/m4/tools/TextTools.java new file mode 100644 index 0000000000..539daa3dfc --- /dev/null +++ b/legend-pure-core/legend-pure-m4/src/main/java/org/finos/legend/pure/m4/tools/TextTools.java @@ -0,0 +1,151 @@ +// Copyright 2024 Goldman Sachs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.finos.legend.pure.m4.tools; + +public class TextTools +{ + /** + * Return the index of the first non-whitespace character found in the given text, or -1 if no non-whitespace + * character is found. + * + * @param text text + * @return index of first non-whitespace character or -1 + * @see #indexOfWhitespace(String) + * @see #isBlank + */ + public static int indexOfNonWhitespace(String text) + { + return indexOfNonWhitespace(text, 0); + } + + /** + * Return the index of the first non-whitespace character found in a region of text (starting from start), or -1 if + * no non-whitespace character is found. + * + * @param text text + * @param start start of region (inclusive) + * @return index of first non-whitespace character or -1 + * @see #indexOfWhitespace(String, int) + * @see #isBlank + */ + public static int indexOfNonWhitespace(String text, int start) + { + return indexOfNonWhitespace(text, start, text.length()); + } + + /** + * Return the index of the first non-whitespace character found in a region of text, or -1 if no non-whitespace + * character is found. + * + * @param text text + * @param start start of region (inclusive) + * @param end end of region (exclusive) + * @return index of first non-whitespace character or -1 + * @see #indexOfWhitespace(String, int, int) + * @see #isBlank + */ + public static int indexOfNonWhitespace(String text, int start, int end) + { + checkRegionBounds(text, start, end); + int codePoint; + for (int i = start; i < end; i += Character.charCount(codePoint)) + { + codePoint = text.codePointAt(i); + if (!Character.isWhitespace(codePoint)) + { + return i; + } + } + return -1; + } + + /** + * Return the index of the first whitespace character found in the given text, or -1 if no whitespace character is + * found. + * + * @param text text + * @return index of first whitespace character or -1 + * @see #indexOfNonWhitespace(String) + * @see #isBlank + */ + public static int indexOfWhitespace(String text) + { + return indexOfWhitespace(text, 0); + } + + /** + * Return the index of the first whitespace character found in a region of text (starting from start), or -1 if no + * whitespace character is found. + * + * @param text text + * @param start start of region (inclusive) + * @return index of first whitespace character or -1 + * @see #indexOfNonWhitespace(String, int) + * @see #isBlank + */ + public static int indexOfWhitespace(String text, int start) + { + return indexOfWhitespace(text, start, text.length()); + } + + /** + * Return the index of the first whitespace character found in a region of text, or -1 if no whitespace character is + * found. + * + * @param text text + * @param start start of region (inclusive) + * @param end end of region (exclusive) + * @return index of first whitespace character or -1 + * @see #indexOfNonWhitespace(String, int, int) + * @see #isBlank + */ + public static int indexOfWhitespace(String text, int start, int end) + { + checkRegionBounds(text, start, end); + int codePoint; + for (int i = start; i < end; i += Character.charCount(codePoint)) + { + codePoint = text.codePointAt(i); + if (Character.isWhitespace(codePoint)) + { + return i; + } + } + return -1; + } + + /** + * Return whether a region of text is blank, meaning it is empty or contains only whitespace. + * + * @param text text + * @param start start of region (inclusive) + * @param end end of region (exclusive) + * @return whether the region of text is blank + * @see #indexOfNonWhitespace(String, int, int) + */ + public static boolean isBlank(String text, int start, int end) + { + checkRegionBounds(text, start, end); + return indexOfNonWhitespace(text, start, end) == -1; + } + + private static void checkRegionBounds(String text, int start, int end) + { + if ((start < 0) || (start > end) || (end > text.length())) + { + throw new StringIndexOutOfBoundsException("start " + start + ", end " + end + ", length " + text.length()); + } + } +} diff --git a/legend-pure-core/legend-pure-m4/src/test/java/org/finos/legend/pure/m4/tools/TestTextTools.java b/legend-pure-core/legend-pure-m4/src/test/java/org/finos/legend/pure/m4/tools/TestTextTools.java new file mode 100644 index 0000000000..4c2902636c --- /dev/null +++ b/legend-pure-core/legend-pure-m4/src/test/java/org/finos/legend/pure/m4/tools/TestTextTools.java @@ -0,0 +1,106 @@ +// Copyright 2024 Goldman Sachs +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package org.finos.legend.pure.m4.tools; + +import org.junit.Assert; +import org.junit.Test; + +public class TestTextTools +{ + @Test + public void testIndexOfWhitespace() + { + Assert.assertEquals(-1, TextTools.indexOfWhitespace("")); + Assert.assertEquals(-1, TextTools.indexOfWhitespace("no_whitespace_here")); + + String text = "start of whitespace{ \u2028\u2029\t\n\u000B\f\r\u001C\u001D\u001E\u001F}end of whitespace"; + Assert.assertEquals(5, TextTools.indexOfWhitespace(text)); + Assert.assertEquals(8, TextTools.indexOfWhitespace(text, 6)); + for (int i = 9; i < 20; i++) + { + Assert.assertEquals(Integer.toString(i), 20, TextTools.indexOfWhitespace(text, i)); + } + for (int i = 20; i < 32; i++) + { + Assert.assertEquals(Integer.toString(i), i, TextTools.indexOfWhitespace(text, i)); + } + Assert.assertEquals(36, TextTools.indexOfWhitespace(text, 32)); + Assert.assertEquals(39, TextTools.indexOfWhitespace(text, 37)); + Assert.assertEquals(-1, TextTools.indexOfWhitespace(text, 40)); + } + + @Test + public void testIndexOfNonWhitespace() + { + Assert.assertEquals(-1, TextTools.indexOfNonWhitespace("")); + Assert.assertEquals(-1, TextTools.indexOfNonWhitespace(" \u2028\u2029\t\n\u000B\f\r\u001C\u001D\u001E\u001F")); + + String text = "start of whitespace{ \u2028\u2029\t\n\u000B\f\r\u001C\u001D\u001E\u001F}end of whitespace"; + for (int i = 0; i < 5; i++) + { + Assert.assertEquals(Integer.toString(i), i, TextTools.indexOfNonWhitespace(text, i)); + } + Assert.assertEquals(6, TextTools.indexOfNonWhitespace(text, 5)); + for (int i = 6; i < 8; i++) + { + Assert.assertEquals(Integer.toString(i), i, TextTools.indexOfNonWhitespace(text, i)); + } + Assert.assertEquals(9, TextTools.indexOfNonWhitespace(text, 8)); + for (int i = 9; i < 20; i++) + { + Assert.assertEquals(Integer.toString(i), i, TextTools.indexOfNonWhitespace(text, i)); + } + Assert.assertEquals(32, TextTools.indexOfNonWhitespace(text, 20)); + } + + @Test + public void testIsBlank() + { + assertIsBlank("", 0, 0); + + String text = "start of whitespace{ \u2028\u2029\t\n\u000B\f\r\u001C\u001D\u001E\u001F}end of whitespace"; + assertIsNotBlank(text, 0, text.length()); + assertIsNotBlank(text, 0, 20); + assertIsNotBlank(text, 25, text.length()); + assertIsBlank(text, 5, 6); + assertIsBlank(text, 20, 32); + + String qbf = "the quick\tbrown\nfox\rjumps over the lazy dog"; + for (int i = 0; i < qbf.length(); i++) + { + assertIsBlank(qbf, i, i); + } + assertIsNotBlank(qbf, 0, qbf.length()); + assertIsNotBlank(qbf, 0, 4); + assertIsNotBlank(qbf, 2, 4); + assertIsBlank(qbf, 3, 4); + assertIsBlank(qbf, 9, 10); + assertIsBlank(qbf, 15, 16); + assertIsBlank(qbf, 19, 20); + } + + private void assertIsNotBlank(String string, int start, int end) + { + Assert.assertFalse(TextTools.isBlank(string, start, end)); + } + + private void assertIsBlank(String string, int start, int end) + { + if (!TextTools.isBlank(string, start, end)) + { + Assert.fail("isBlank(\"" + string + "\", " + start + ", " + end + "); non-whitespace index: " + TextTools.indexOfNonWhitespace(string, start, end)); + } + } +}