diff --git a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentRegion.java b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentRegion.java index 7e88d495a..9f0e83f0e 100644 --- a/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentRegion.java +++ b/headless-services/commons/commons-util/src/main/java/org/springframework/ide/vscode/commons/util/text/DocumentRegion.java @@ -351,4 +351,12 @@ public class DocumentRegion implements CharSequence, IRegion { return getStart(); } + public boolean isAtStartOfLine() { + if (start==0) { + return true; + } + String charBefore = this.textBefore(1).toString(); + return charBefore.equals("\n"); //This should work on windows too because windows uses "\r\n" but that still ends with "\n". + } + } \ No newline at end of file diff --git a/headless-services/commons/java-properties/src/main/java/org/springframework/ide/vscode/java/properties/parser/PropertiesAst.java b/headless-services/commons/java-properties/src/main/java/org/springframework/ide/vscode/java/properties/parser/PropertiesAst.java index 70653e559..b292ab7d2 100644 --- a/headless-services/commons/java-properties/src/main/java/org/springframework/ide/vscode/java/properties/parser/PropertiesAst.java +++ b/headless-services/commons/java-properties/src/main/java/org/springframework/ide/vscode/java/properties/parser/PropertiesAst.java @@ -11,6 +11,7 @@ package org.springframework.ide.vscode.java.properties.parser; import java.util.List; +import java.util.function.Predicate; import java.util.stream.Collectors; /** @@ -34,7 +35,11 @@ public final class PropertiesAst { public List getAllNodes() { return nodes; } - + + public List getNodes(Predicate test) { + return nodes.stream().filter(test).collect(Collectors.toList()); + } + /** * Retrieves AST nodes of specific type * @param clazz Type of AST nodes diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/DuplicateNameChecker.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/DuplicateNameChecker.java index d1dd179bb..cdfc1e624 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/DuplicateNameChecker.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/DuplicateNameChecker.java @@ -66,4 +66,8 @@ public class DuplicateNameChecker { "Duplicate property '"+decodedKey+"'", nameRegion)); } + public void startNewSubDocument() { + seen.clear(); + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/SpringPropertiesReconcileEngine.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/SpringPropertiesReconcileEngine.java index ac871fd97..c525c6ad9 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/SpringPropertiesReconcileEngine.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/properties/reconcile/SpringPropertiesReconcileEngine.java @@ -48,6 +48,7 @@ import org.springframework.ide.vscode.java.properties.antlr.parser.AntlrParser; import org.springframework.ide.vscode.java.properties.parser.ParseResults; import org.springframework.ide.vscode.java.properties.parser.Parser; import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.KeyValuePair; +import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Comment; import org.springframework.ide.vscode.java.properties.parser.PropertiesAst.Node; import org.springframework.ide.vscode.java.properties.parser.PropertiesFileEscapes; @@ -108,31 +109,38 @@ public class SpringPropertiesReconcileEngine implements IReconcileEngine { return; } - results.ast.getNodes(KeyValuePair.class).forEach(pair -> { + results.ast.getNodes(n -> n instanceof KeyValuePair || n instanceof Comment).forEach(node -> { try { - DocumentRegion propertyNameRegion = createRegion(doc, pair.getKey()); - String keyName = PropertiesFileEscapes.unescape(propertyNameRegion.toString()); - duplicateNameChecker.check(propertyNameRegion); - PropertyInfo validProperty = SpringPropertyIndex.findLongestValidProperty(index, keyName); - if (validProperty!=null) { - //TODO: Remove last remnants of 'IRegion trimmedRegion' here and replace - // it all with just passing around 'fullName' DocumentRegion. This may require changes - // in PropertyNavigator (probably these changes are also for the better making it simpler as well) - if (validProperty.isDeprecated()) { - problemCollector.accept(problemDeprecated(propertyNameRegion, validProperty, quickFixes.DEPRECATED_PROPERTY)); + if (node instanceof Comment) { + if (isDocumentMarker(doc, (Comment)node)) { + duplicateNameChecker.startNewSubDocument(); } - int offset = validProperty.getId().length() + propertyNameRegion.getStart(); - PropertyNavigator navigator = new PropertyNavigator(doc, problemCollector, typeUtilProvider.getTypeUtil(sourceLinks, doc), propertyNameRegion); - Type valueType = navigator.navigate(offset, TypeParser.parse(validProperty.getType())); - if (valueType!=null) { - reconcileType(doc, valueType, pair.getValue(), problemCollector); - } - } else { //validProperty==null - //The name is invalid, with no 'prefix' of the name being a valid property name. - PropertyInfo similarEntry = index.findLongestCommonPrefixEntry(propertyNameRegion.toString()); - CharSequence validPrefix = commonPrefix(similarEntry.getId(), keyName); - problemCollector.accept(problemUnkownProperty(propertyNameRegion, similarEntry, validPrefix, quickFixes.MISSING_PROPERTY)); - } //end: validProperty==null + } else if (node instanceof KeyValuePair) { + KeyValuePair pair = (KeyValuePair) node; + DocumentRegion propertyNameRegion = createRegion(doc, pair.getKey()); + String keyName = PropertiesFileEscapes.unescape(propertyNameRegion.toString()); + duplicateNameChecker.check(propertyNameRegion); + PropertyInfo validProperty = SpringPropertyIndex.findLongestValidProperty(index, keyName); + if (validProperty!=null) { + //TODO: Remove last remnants of 'IRegion trimmedRegion' here and replace + // it all with just passing around 'fullName' DocumentRegion. This may require changes + // in PropertyNavigator (probably these changes are also for the better making it simpler as well) + if (validProperty.isDeprecated()) { + problemCollector.accept(problemDeprecated(propertyNameRegion, validProperty, quickFixes.DEPRECATED_PROPERTY)); + } + int offset = validProperty.getId().length() + propertyNameRegion.getStart(); + PropertyNavigator navigator = new PropertyNavigator(doc, problemCollector, typeUtilProvider.getTypeUtil(sourceLinks, doc), propertyNameRegion); + Type valueType = navigator.navigate(offset, TypeParser.parse(validProperty.getType())); + if (valueType!=null) { + reconcileType(doc, valueType, pair.getValue(), problemCollector); + } + } else { //validProperty==null + //The name is invalid, with no 'prefix' of the name being a valid property name. + PropertyInfo similarEntry = index.findLongestCommonPrefixEntry(propertyNameRegion.toString()); + CharSequence validPrefix = commonPrefix(similarEntry.getId(), keyName); + problemCollector.accept(problemUnkownProperty(propertyNameRegion, similarEntry, validPrefix, quickFixes.MISSING_PROPERTY)); + } //end: validProperty==null + } } catch (Exception e) { log.error("", e); } @@ -144,6 +152,12 @@ public class SpringPropertiesReconcileEngine implements IReconcileEngine { } } + private boolean isDocumentMarker(IDocument doc, Comment node) { + DocumentRegion region = createRegion(doc, node).trimEnd(); + String t = region.toString(); + return t.equals("#---") && region.isAtStartOfLine(); + } + protected SpringPropertyProblem problemDeprecated(DocumentRegion region, PropertyInfo property, QuickfixType fixType) { SpringPropertyProblem p = problem(PROP_DEPRECATED, TypeUtil.deprecatedPropertyMessage( diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/editor/harness/PropertyIndexHarness.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/editor/harness/PropertyIndexHarness.java index b830f9849..610af5466 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/editor/harness/PropertyIndexHarness.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/editor/harness/PropertyIndexHarness.java @@ -283,6 +283,7 @@ public class PropertyIndexHarness { data("spring.batch.job.enabled", "java.lang.Boolean", "true", "Execute all Spring Batch jobs in the context on startup."); data("spring.batch.job.names", "java.lang.String", "", "Comma-separated list of job names to execute on startup. By default, all Jobs\n found in the context are executed."); data("spring.batch.schema", "java.lang.String", "classpath:org/springframework/batch/core/schema-@@platform@@.sql", "Path to the SQL file to use to initialize the database schema."); + data("spring.config.activate.on-profile", "java.lang.String[]", null, "Activate on blah."); data("spring.config.location", "java.lang.String", null, "Config file locations."); data("spring.config.name", "java.lang.String", "application", "Config file name."); data("spring.dao.exceptiontranslation.enabled", "java.lang.Boolean", "true", "Enable the PersistenceExceptionTranslationPostProcessor."); diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationPropertiesEditorTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationPropertiesEditorTest.java index 70c60ceb3..c8f344a11 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationPropertiesEditorTest.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/test/ApplicationPropertiesEditorTest.java @@ -77,6 +77,66 @@ public class ApplicationPropertiesEditorTest extends AbstractPropsEditorTest { private static final ProjectCustomizer WITH_EMPTY_APPLICATION_YML = projectContents -> { projectContents.createFile("src/main/resources/application.yml", ""); }; + + @Test public void reconcilesWithMultiDocuments() throws Exception { + //See: https://github.com/spring-projects/sts4/issues/533 + + defaultTestData(); + + //lowest bar: just disable the warning + Editor editor = newEditor( + "spring.config.activate.on-profile=foo\n" + + "server.port=8888\n" + + "#---\n" + + "spring.config.activate.on-profile=bar\n" + + "server.port=8080\n" + ); + editor.assertProblems(/*NONE*/); + + //better: still detect duplicates within same section + editor = newEditor( + "spring.application.name=frodo\n" + + "spring.config.activate.on-profile=foo\n" + + "server.port=8888\n" + + "spring.application.name=frodo\n" + + "#---\n" + + "spring.config.activate.on-profile=bar\n" + + "server.port=8080\n" + + "server.port=9090\n" + ); + editor.assertProblems( + "spring.application.name|Duplicate", + "spring.application.name|Duplicate", + "server.port|Duplicate", + "server.port|Duplicate" + ); + + //nitpick 1: leading spaces before the marker means... it is not a marker + editor = newEditor( + "spring.config.activate.on-profile=foo\n" + + "server.port=8888\n" + + " #---\n" + + "spring.config.activate.on-profile=bar\n" + + "server.port=8080\n" + ); + editor.assertProblems( + "spring.config.activate.on-profile|Duplicate", + "server.port|Duplicate", + "spring.config.activate.on-profile|Duplicate", + "server.port|Duplicate" + ); + + //nitpick 2: trailing spaces after the marker are ignored + editor = newEditor( + "spring.config.activate.on-profile=foo\n" + + "server.port=8888\n" + + "#--- \t\n" + + "spring.config.activate.on-profile=bar\n" + + "server.port=8080\n" + ); + editor.assertProblems(/*NONE*/); + + } @Test public void inheritedPojoProperties() throws Exception { //See https://github.com/spring-projects/sts4/issues/116