diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java index 0067d44e4..fcdd17f44 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshDeploymentManifestSchema.java @@ -18,6 +18,8 @@ import org.springframework.ide.vscode.commons.util.Assert; import org.springframework.ide.vscode.commons.util.Renderable; import org.springframework.ide.vscode.commons.util.Renderables; import org.springframework.ide.vscode.commons.util.ValueParsers; +import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache; +import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; import org.springframework.ide.vscode.commons.yaml.schema.YType; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory; import org.springframework.ide.vscode.commons.yaml.schema.YTypeFactory.AbstractType; @@ -72,8 +74,10 @@ public class BoshDeploymentManifestSchema implements YamlSchema { private YType t_stemcell_alias_name_def; private YType t_release_name_def; private YType t_var_name_def; + private final ASTTypeCache astTypes; - public BoshDeploymentManifestSchema() { + public BoshDeploymentManifestSchema(ASTTypeCache astTypes) { + this.astTypes = astTypes; TYPE_UTIL = f.TYPE_UTIL; V2_TOPLEVEL_TYPE = createV2Schema(); @@ -228,6 +232,10 @@ public class BoshDeploymentManifestSchema implements YamlSchema { for (String v1Prop : DEPRECATED_V1_PROPS) { addProp(v2Schema, v1Prop, t_any).isDeprecated("Deprecated: '"+v1Prop+"' is a V1 schema property. Consider migrating your deployment manifest to V2"); } + + for (YType defType : getDefinitionTypes()) { + v2Schema.require(Constraints.uniqueDefinition(this.astTypes, defType, YamlSchemaProblems.problemType("BOSH_DUPLICATE_"+defType))); + } return v2Schema; } diff --git a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java index 5116889cc..5951962bd 100644 --- a/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java +++ b/headless-services/bosh-language-server/src/main/java/org/springframework/ide/vscode/bosh/BoshLanguageServer.java @@ -44,7 +44,8 @@ public class BoshLanguageServer extends SimpleLanguageServer { super("vscode-bosh"); YamlASTProvider parser = new YamlParser(yaml); SimpleTextDocumentService documents = getTextDocumentService(); - BoshDeploymentManifestSchema schema = new BoshDeploymentManifestSchema(); + ASTTypeCache astTypeCache = new ASTTypeCache(); + BoshDeploymentManifestSchema schema = new BoshDeploymentManifestSchema(astTypeCache); YamlStructureProvider structureProvider = YamlStructureProvider.DEFAULT; YamlAssistContextProvider contextProvider = new SchemaBasedYamlAssistContextProvider(schema); @@ -54,7 +55,6 @@ public class BoshLanguageServer extends SimpleLanguageServer { HoverInfoProvider infoProvider = new YamlHoverInfoProvider(parser, structureProvider, contextProvider); VscodeHoverEngine hoverEngine = new VscodeHoverEngineAdapter(this, infoProvider); YamlQuickfixes quickfixes = new YamlQuickfixes(getQuickfixRegistry(), getTextDocumentService(), structureProvider); - ASTTypeCache astTypeCache = new ASTTypeCache(); YamlSchemaBasedReconcileEngine engine = new YamlSchemaBasedReconcileEngine(parser, schema, quickfixes); engine.setTypeCollector(astTypeCache); documents.onDocumentSymbol(new TypeBasedYamlSymbolHandler(documents, astTypeCache, schema.getDefinitionTypes())); diff --git a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/concourse/BoshEditorTest.java b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/concourse/BoshEditorTest.java index 2ba69cb40..d7e8fe810 100644 --- a/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/concourse/BoshEditorTest.java +++ b/headless-services/bosh-language-server/src/test/java/org/springframework/ide/vscode/concourse/BoshEditorTest.java @@ -681,4 +681,62 @@ public class BoshEditorTest { "blobstore_secure_link_secret|Variable" ); } + + @Test public void duplicateSymbolChecking() throws Exception { + Editor editor = harness.newEditor( + "name: foo\n" + + "variables:\n" + + "- name: dup_var\n" + + " type: password\n" + + "- name: blobstore_admin_users_password\n" + + " type: password\n" + + "- name: blobstore_secure_link_secret\n" + + " type: password\n" + + "- name: dup_var\n" + + " type: ssh\n" + + "stemcells:\n" + + "- alias: default\n" + + " os: ubuntu-trusty\n" + + " version: '3421.11'\n" + + "- alias: dup_cell\n" + + " os: ubuntu-trusty\n" + + " version: '3421.11'\n" + + "- alias: dup_cell\n" + + " os: ubuntu-trusty\n" + + " version: '3421.11'\n" + + "instance_groups:\n" + + "- name: foo-group\n" + + " networks:\n" + + " - name: the-network\n" + + " static_ips: []\n" + + " default: []\n" + + "- name: bar-group\n" + + " networks:\n" + + " - name: the-network\n" + + " static_ips: []\n" + + " default: []\n" + + "- name: bar-group\n" + + " networks:\n" + + " - name: the-network\n" + + " static_ips: []\n" + + " default: []\n" + + "releases:\n" + + "- name: one-release\n" + + "- name: other-release\n" + + "- name: one-release\n" + ); + editor.ignoreProblem(YamlSchemaProblems.MISSING_PROPERTY); + + editor.assertProblems( + "dup_var|Duplicate 'VariableName'", + "dup_var|Duplicate 'VariableName'", + "dup_cell|Duplicate 'StemcellAliasName'", + "dup_cell|Duplicate 'StemcellAliasName'", + "bar-group|Duplicate 'InstanceGroupName'", + "bar-group|Duplicate 'InstanceGroupName'", + "one-release|Duplicate 'ReleaseName'", + "one-release|Duplicate 'ReleaseName'" + ); + } + } \ No newline at end of file diff --git a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/constraints/Constraints.java b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/constraints/Constraints.java index c26f580b9..b84a605e9 100644 --- a/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/constraints/Constraints.java +++ b/headless-services/commons/commons-yaml/src/main/java/org/springframework/ide/vscode/commons/yaml/schema/constraints/Constraints.java @@ -16,15 +16,20 @@ import static org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaPr import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.function.Function; import org.springframework.ide.vscode.commons.languageserver.reconcile.IProblemCollector; +import org.springframework.ide.vscode.commons.languageserver.reconcile.ProblemType; import org.springframework.ide.vscode.commons.util.Assert; +import org.springframework.ide.vscode.commons.util.StringUtil; import org.springframework.ide.vscode.commons.util.text.IDocument; import org.springframework.ide.vscode.commons.yaml.ast.NodeUtil; +import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache; +import org.springframework.ide.vscode.commons.yaml.reconcile.ASTTypeCache.NodeTypes; import org.springframework.ide.vscode.commons.yaml.reconcile.YamlSchemaProblems; import org.springframework.ide.vscode.commons.yaml.schema.DynamicSchemaContext; import org.springframework.ide.vscode.commons.yaml.schema.SchemaContextAware; @@ -33,7 +38,11 @@ import org.yaml.snakeyaml.nodes.MappingNode; import org.yaml.snakeyaml.nodes.Node; import org.yaml.snakeyaml.nodes.NodeTuple; +import com.google.common.collect.ArrayListMultimap; import com.google.common.collect.ImmutableSet; +import com.google.common.collect.Multimap; +import com.google.common.collect.MultimapBuilder; +import com.google.common.collect.Multiset; /** * Various static methods for constructing/composing {@link Constraint}s. @@ -156,4 +165,33 @@ public class Constraints { }; } + /** + * Check that all nodes of a given type, across the AST represent unique names. + */ + public static Constraint uniqueDefinition(ASTTypeCache astTypes, YType defType, ProblemType problemType) { + return (DynamicSchemaContext dc, Node parent, Node _ignored_node, YType type, IProblemCollector problems) -> { + NodeTypes nodeTypes = astTypes.getNodeTypes(dc.getDocument().getUri()); + if (nodeTypes!=null) { + Collection nodes = nodeTypes.getNodes(defType); + if (nodes!=null && !nodes.isEmpty()) { + Multimap name2nodes = ArrayListMultimap.create(); + for (Node node : nodes) { + String name = NodeUtil.asScalar(node); + if (StringUtil.hasText(name)) { + name2nodes.put(name, node); + } + } + for (String name : name2nodes.keys()) { + Collection nodesForName = name2nodes.get(name); + if (nodesForName.size()>1) { + for (Node duplicateNode : nodesForName) { + problems.accept(YamlSchemaProblems.problem(problemType, "Duplicate '"+defType+"'", duplicateNode)); + } + } + } + } + } + }; + } + } \ No newline at end of file