From da8a117b98c5e8d3e04d831d420519822b79ed45 Mon Sep 17 00:00:00 2001 From: Ryan Nowak Date: Tue, 9 Feb 2021 13:29:05 -0800 Subject: [PATCH] Lexically scoped child resources Fixes: #1363 This change add support for lexical scoping/nesting of child resources. That it, for a resource type: `My.RP/someType@2020-01-01` there's now a simplified syntax for including a resource: `My.RP/someType/childType@2020-01-01` inside the body of the parent. This syntax also allows simplication of the type name of the child, limits its scope/visibility and implies an automatic `dependsOn` to the parent resource. The goal is to be the most idiomatic way to declare multiple resources to be deployed with a parent/child/grandchild/etc relationship. This also includes the new "nested resource access" operator which allows lookup of a nested resource: ``` output someOutput string = parent:child.properties.size `` --- docs/spec/expressions.md | 23 +- docs/spec/resources.md | 37 + .../NestedResourceTests.cs | 477 +++++++++++ src/Bicep.Core.Samples/DataSets.cs | 2 + .../Files/NestedResources_LF/main.bicep | 68 ++ .../NestedResources_LF/main.diagnostics.bicep | 78 ++ .../NestedResources_LF/main.formatted.bicep | 68 ++ .../Files/NestedResources_LF/main.json | 120 +++ .../NestedResources_LF/main.symbols.bicep | 83 ++ .../NestedResources_LF/main.syntax.bicep | 571 +++++++++++++ .../NestedResources_LF/main.tokens.bicep | 358 +++++++++ .../DiagnosticCollectionExtensions.cs | 8 + .../Parsing/ExpressionTestVisitor.cs | 7 + .../Parsing/ParserTests.cs | 26 + .../Resource/ResourceTypeReferenceTests.cs | 53 +- .../Utils/ParserHelper.cs | 2 +- .../Diagnostics/DiagnosticBuilder.cs | 21 + .../Emit/EmitLimitationCalculator.cs | 14 +- src/Bicep.Core/Emit/ExpressionConverter.cs | 63 +- src/Bicep.Core/Emit/ExpressionEmitter.cs | 11 + .../Emit/ResourceDependencyVisitor.cs | 5 +- src/Bicep.Core/Emit/ScopeHelper.cs | 2 +- src/Bicep.Core/Emit/TemplateWriter.cs | 68 +- src/Bicep.Core/Parsing/ExpressionFlags.cs | 24 + src/Bicep.Core/Parsing/Parser.cs | 183 +++-- .../PrettyPrint/DocumentBuildVisitor.cs | 3 + .../Resources/ResourceTypeReference.cs | 56 ++ src/Bicep.Core/Semantics/Binder.cs | 36 +- .../Semantics/DeclarationVisitor.cs | 65 +- src/Bicep.Core/Semantics/FileSymbol.cs | 10 +- src/Bicep.Core/Semantics/IBinder.cs | 6 +- src/Bicep.Core/Semantics/ILanguageScope.cs | 2 +- src/Bicep.Core/Semantics/LocalScope.cs | 9 +- .../Semantics/NameBindingVisitor.cs | 749 ++++++++++-------- .../Semantics/ResourceAncestorGraph.cs | 40 + .../Semantics/ResourceAncestorVisitor.cs | 53 ++ .../Semantics/ResourceSymbolVisitor.cs | 32 + src/Bicep.Core/Semantics/SemanticModel.cs | 5 + src/Bicep.Core/Semantics/SymbolExtensions.cs | 7 + src/Bicep.Core/Syntax/ISyntaxHierarchy.cs | 66 ++ src/Bicep.Core/Syntax/ISyntaxVisitor.cs | 2 + src/Bicep.Core/Syntax/ObjectSyntax.cs | 5 + src/Bicep.Core/Syntax/ResourceAccessSyntax.cs | 31 + .../Syntax/ResourceDeclarationSyntax.cs | 77 +- src/Bicep.Core/Syntax/SyntaxHierarchy.cs | 31 +- src/Bicep.Core/Syntax/SyntaxRewriteVisitor.cs | 15 + src/Bicep.Core/Syntax/SyntaxVisitor.cs | 15 +- .../TypeSystem/CyclicCheckVisitor.cs | 84 +- .../TypeSystem/DeclaredTypeManager.cs | 2 +- .../TypeSystem/DeployTimeConstantVisitor.cs | 11 +- .../TypeSystem/TypeAssignmentVisitor.cs | 47 +- .../ParentChildResourceNameRewriter.cs | 2 +- .../Helpers/IntegrationTestHelper.cs | 12 +- .../HoverTests.cs | 13 +- .../RenameSymbolTests.cs | 4 +- .../Completions/BicepCompletionProvider.cs | 49 +- .../Handlers/BicepDocumentSymbolHandler.cs | 2 +- src/Bicep.LangServer/SemanticTokenVisitor.cs | 6 + .../LanguageHelpers/SemanticTokenVisitor.cs | 6 + 59 files changed, 3387 insertions(+), 538 deletions(-) create mode 100644 src/Bicep.Core.IntegrationTests/NestedResourceTests.cs create mode 100644 src/Bicep.Core.Samples/Files/NestedResources_LF/main.bicep create mode 100644 src/Bicep.Core.Samples/Files/NestedResources_LF/main.diagnostics.bicep create mode 100644 src/Bicep.Core.Samples/Files/NestedResources_LF/main.formatted.bicep create mode 100644 src/Bicep.Core.Samples/Files/NestedResources_LF/main.json create mode 100644 src/Bicep.Core.Samples/Files/NestedResources_LF/main.symbols.bicep create mode 100644 src/Bicep.Core.Samples/Files/NestedResources_LF/main.syntax.bicep create mode 100644 src/Bicep.Core.Samples/Files/NestedResources_LF/main.tokens.bicep create mode 100644 src/Bicep.Core/Parsing/ExpressionFlags.cs create mode 100644 src/Bicep.Core/Semantics/ResourceAncestorGraph.cs create mode 100644 src/Bicep.Core/Semantics/ResourceAncestorVisitor.cs create mode 100644 src/Bicep.Core/Semantics/ResourceSymbolVisitor.cs create mode 100644 src/Bicep.Core/Syntax/ISyntaxHierarchy.cs create mode 100644 src/Bicep.Core/Syntax/ResourceAccessSyntax.cs diff --git a/docs/spec/expressions.md b/docs/spec/expressions.md index 605476d41aa..3da56c36165 100644 --- a/docs/spec/expressions.md +++ b/docs/spec/expressions.md @@ -6,7 +6,7 @@ The operators below are listed in descending order of precedence (the higher the | Symbol | Type of Operation | Associativity | |:-|:-|:-| -| `(` `)` `[` `]` `.` | Parentheses, property accessors and array indexers | Left to right | +| `(` `)` `[` `]` `.` `:` | Parentheses, array indexers, property accessors, and nested-resource accessor | Left to right | | `!` `-` | Unary | Right to left | | `%` `*` `/` | Multiplicative | Left to right | | `+` `-` | Additive | Left to right | @@ -106,6 +106,27 @@ Given the above declaration, the expression `x.y.z` would evaluate to the litera Property accessors can be used with any object. This includes parameters and variables of object types and object literals. Using a property accessor on an expression of non-object type is an error. +## Nested resource accessors +Nested resource accessors are used to access resources that are declared inside another resource. Only top-level resources are considered top-level declarations. + +``` +resource myParent 'My.Rp/parentType@2020-01-01' = { + name: 'myParent' + location: 'West US' + + // declares a nested resource inside 'myParent' + resource myChild 'childType' = { + name: 'myChild' + properties: { + displayName: 'Child Resource' + } + } +} + +// using nested resource access to access a property of a nested resource +output displayName string = myParent:myChild.properties.displayName +``` + ## Array indexers Array indexers serve two purposes. Most commonly, they are used to access items in an array. However, they can also be used to access properties of objects via expressions or string literals. diff --git a/docs/spec/resources.md b/docs/spec/resources.md index 109bf6a2b35..407e7ef82d6 100644 --- a/docs/spec/resources.md +++ b/docs/spec/resources.md @@ -42,6 +42,29 @@ resource dnsZone 'Microsoft.Network/dnszones@2018-05-01' = { } ``` +## Resource nesting + +A resource declaration may appear inside another resource declaration when the inner resource is a child type of the other type. + +``` +resource myParent 'My.Rp/parentType@2020-01-01' = { + name: 'myParent' + location: 'West US' + + // declares a resource of type 'My.Rp/parentType/childType@2020-01-01' + resource myChild 'childType' = { + name: 'myChild' + properties: { + displayName: 'child in ${parent.location}' + } + } +} +``` + +When used in this form the nested declaration must use a simple `name` with a single segment. + +A nested resource declaration must appear at the top level of syntax of the containing resource. Declarations may be nested arbirarily deep, as long as each level is a child type of its containing resource. A nested resource may access properties of its containing resource. A containing resource may not access properties of the resources it contains, this would cause a cyclic-dependency. + ## Resource dependencies All declared resources will be deployed concurrently in the compiled template. Order of resource deployment can be influenced in the following ways: ### Explicit dependency @@ -78,6 +101,20 @@ resource otherResource 'Microsoft.Example/examples@2020-06-01' = { } ``` +A nested resource has an implicit dependency on its containing resource. + +``` +resource myParent 'My.Rp/parentType@2020-01-01' = { + name: 'myParent' + location: 'West US' + + // depends on 'myParent' implicitly + resource myChild 'childType' = { + name: 'myChild' + } +} +``` + ## Conditions > Requires Bicep CLI v0.2.212 or later diff --git a/src/Bicep.Core.IntegrationTests/NestedResourceTests.cs b/src/Bicep.Core.IntegrationTests/NestedResourceTests.cs new file mode 100644 index 00000000000..93fa3c23000 --- /dev/null +++ b/src/Bicep.Core.IntegrationTests/NestedResourceTests.cs @@ -0,0 +1,477 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System.IO; +using System.Linq; +using System.Text; +using Bicep.Core.Diagnostics; +using Bicep.Core.Emit; +using Bicep.Core.Semantics; +using Bicep.Core.TypeSystem; +using Bicep.Core.UnitTests; +using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.Utils; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bicep.Core.IntegrationTests +{ + [TestClass] + public class NestedResourceTests + { + [TestMethod] + public void NestedResources_symbols_are_bound() + { + var program = @" +resource parent 'My.RP/parentType@2020-01-01' = { + name: 'parent' + properties: { + size: 'large' + } + + resource child 'childType' = { + name: 'child' + properties: { + style: 'very cool' + } + } + + resource sibling 'childType@2020-01-02' = { + name: 'sibling' + properties: { + style: child.properties.style + size: parent.properties.size + } + } +} +"; + + var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program)); + var model = compilation.GetEntrypointSemanticModel(); + + model.GetAllDiagnostics().Should().BeEmpty(); + + var expected = new [] + { + new { name = "child", type = "My.RP/parentType/childType@2020-01-01", }, + new { name = "parent", type = "My.RP/parentType@2020-01-01", }, + new { name = "sibling", type = "My.RP/parentType/childType@2020-01-02", }, + }; + + model.Root.GetAllResourceDeclarations() + .Select(s => new { name = s.Name, type = (s.Type as ResourceType)?.TypeReference.FormatName(), }) + .OrderBy(n => n.name) + .Should().BeEquivalentTo(expected); + } + + [TestMethod] + public void NestedResources_resource_can_contain_property_called_resource() + { + var program = @" +resource parent 'My.RP/parentType@2020-01-01' = { + name: 'parent' + properties: { + size: 'large' + } + resource: 'yes please' + + resource child 'childType' = { + name: 'child' + properties: { + style: 'very cool' + } + } +} +"; + + var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program)); + var model = compilation.GetEntrypointSemanticModel(); + + // The property "resource" is not allowed ... + model.GetAllDiagnostics().Should().HaveCount(1); + model.GetAllDiagnostics().Single().Should().HaveCodeAndSeverity("BCP038", DiagnosticLevel.Error); + + var expected = new [] + { + new { name = "child", type = "My.RP/parentType/childType@2020-01-01", }, + new { name = "parent", type = "My.RP/parentType@2020-01-01", }, + }; + + model.Root.GetAllResourceDeclarations() + .Select(s => new { name = s.Name, type = (s.Type as ResourceType)?.TypeReference.FormatName(), }) + .OrderBy(n => n.name) + .Should().BeEquivalentTo(expected); + } + + [TestMethod] + public void NestedResources_valid_resource_references() + { + var program = @" +resource parent 'My.RP/parentType@2020-01-01' = { + name: 'parent' + properties: { + size: 'large' + } + + resource child 'childType' = { + name: 'child' + properties: { + style: 'very cool' + } + + resource grandchild 'grandchildType' = { + name: 'grandchild' + properties: { + temperature: 'ice-cold' + } + } + } + + resource sibling 'childType@2020-01-02' = { + name: 'sibling' + properties: { + style: parent:child.properties.style + size: parent.properties.size + temperatureC: child:grandchild.properties.temperature + temperatureF: parent:child:grandchild.properties.temperature + } + } +} + +output fromChild string = parent:child.properties.style +output fromGrandchild string = parent:child:grandchild.properties.style +"; + + var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program)); + var model = compilation.GetEntrypointSemanticModel(); + + model.GetAllDiagnostics().Should().BeEmpty(); + + var parent = model.Root.GetAllResourceDeclarations().Single(r => r.Name == "parent"); + var references = model.FindReferences(parent); + references.Should().HaveCount(6); + + var child = model.Root.GetAllResourceDeclarations().Single(r => r.Name == "child"); + references = model.FindReferences(child); + references.Should().HaveCount(6); + + var grandchild = model.Root.GetAllResourceDeclarations().Single(r => r.Name == "grandchild"); + references = model.FindReferences(grandchild); + references.Should().HaveCount(4); + + var sibling = model.Root.GetAllResourceDeclarations().Single(r => r.Name == "sibling"); + references = model.FindReferences(sibling); + references.Should().HaveCount(1); + + var emitter = new TemplateEmitter(compilation.GetEntrypointSemanticModel(), BicepTestConstants.DevAssemblyFileVersion); + using var outputStream = new MemoryStream(); + emitter.Emit(outputStream); + + outputStream.Seek(0L, SeekOrigin.Begin); + var text = Encoding.UTF8.GetString(outputStream.GetBuffer()); + } + + [TestMethod] + public void NestedResources_invalid_resource_references() + { + var program = @" +var notResource = 'hi' +resource parent 'My.RP/parentType@2020-01-01' = { + name: 'parent' + properties: { + size: 'large' + } + + resource child 'childType' = { + name: 'child' + properties: { + style: 'very cool' + } + + resource grandchild 'grandchildType' = { + name: 'grandchild' + properties: { + temperature: 'ice-cold' + } + } + } +} + +output fromVariable string = notResource:child.properties.style +output fromChildInvalid string = parent:child2.properties.style +output fromGrandchildInvalid string = parent:child:cousin.properties.temperature +"; + + var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program)); + var model = compilation.GetEntrypointSemanticModel(); + + model.GetAllDiagnostics().Should().HaveDiagnostics(new []{ + ("BCP155", DiagnosticLevel.Error, "Cannot access nested resources of type \"'hi'\". A resource type is required."), + ("BCP156", DiagnosticLevel.Error, "The resource \"parent\" does not contain a nested resource named \"child2\". Known nested resources are: \"child\"."), + ("BCP156", DiagnosticLevel.Error, "The resource \"child\" does not contain a nested resource named \"cousin\". Known nested resources are: \"grandchild\"."), + }); + } + + [TestMethod] + public void NestedResources_child_cannot_be_referenced_outside_of_scope() + { + var program = @" +resource parent 'My.RP/parentType@2020-01-01' = { + name: 'parent' + properties: { + } + + resource child 'childType' = { + name: 'child' + properties: { + style: 'very cool' + } + } +} + +resource other 'My.RP/parentType@2020-01-01' = { + name: 'other' + properties: { + style: child.properties.style + } +} +"; + + var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program)); + var diagnostics = compilation.GetEntrypointSemanticModel().GetAllDiagnostics(); + diagnostics.Should().HaveDiagnostics(new[] { + ("BCP057", DiagnosticLevel.Error, "The name \"child\" does not exist in the current context."), + }); + } + + [TestMethod] + public void NestedResources_child_cannot_specify_qualified_type() + { + var program = @" +resource parent 'My.RP/parentType@2020-01-01' = { + name: 'parent' + properties: { + } + + resource child 'My.RP/parentType/childType@2020-01-01' = { + name: 'child' + properties: { + style: 'very cool' + } + } +} +"; + + var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program)); + var diagnostics = compilation.GetEntrypointSemanticModel().GetAllDiagnostics(); + diagnostics.Should().HaveDiagnostics(new[] { + ("BCP153", DiagnosticLevel.Error, "The resource type segment \"My.RP/parentType/childType@2020-01-01\" is invalid. Nested resources must specify a single type segment, and optionally can specify an api version using the format \"@\"."), + }); + } + + [TestMethod] + public void NestedResources_error_in_base_type() + { + var program = @" +resource parent 'My.RP/parentType@invalid-version' = { + name: 'parent' + properties: { + } + + resource child 'My.RP/parentType/childType@2020-01-01' = { + name: 'child' + properties: { + style: 'very cool' + } + } +} +"; + + var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program)); + var diagnostics = compilation.GetEntrypointSemanticModel().GetAllDiagnostics(); + diagnostics.Should().HaveDiagnostics(new[] { + ("BCP029", DiagnosticLevel.Error, "The resource type is not valid. Specify a valid resource type of format \"/@\"."), + ("BCP154", DiagnosticLevel.Error, "The resource type cannot be determined due to an error in containing resource \"parent\"."), + }); + } + + [TestMethod] + public void NestedResources_error_in_parent_type() + { + var program = @" +resource parent 'My.RP/parentType@2020-01-01' = { + name: 'parent' + properties: { + } + + // Error here + resource child 'My.RP/parentType/childType@2020-01-01' = { + name: 'child' + properties: { + } + + resource grandchild 'granchildType' = { + name: 'grandchild' + properties: { + } + } + } +} +"; + + var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program)); + var diagnostics = compilation.GetEntrypointSemanticModel().GetAllDiagnostics(); + diagnostics.Should().HaveDiagnostics(new[] { + ("BCP153", DiagnosticLevel.Error, "The resource type segment \"My.RP/parentType/childType@2020-01-01\" is invalid. Nested resources must specify a single type segment, and optionally can specify an api version using the format \"@\"."), + ("BCP154", DiagnosticLevel.Error, "The resource type cannot be determined due to an error in containing resource \"child\"."), + }); + } + + [TestMethod] + public void NestedResources_child_cycle_is_detected_correctly() + { + var program = @" +resource parent 'My.RP/parentType@2020-01-01' = { + name: 'parent' + properties: { + style: child.properties.style + } + + resource child 'childType' = { + name: 'child' + properties: { + style: 'very cool' + } + } +} +"; + + var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program)); + compilation.GetEntrypointSemanticModel().GetAllDiagnostics().Should().HaveDiagnostics(new[] { + ("BCP080", DiagnosticLevel.Error, "The expression is involved in a cycle (\"child\" -> \"parent\")."), + }); + } + + [TestMethod] // With more than one level of nesting the name just isn't visible. + public void NestedResources_grandchild_cycle_results_in_binding_failure() + { + var program = @" +resource parent 'My.RP/parentType@2020-01-01' = { + name: 'parent' + properties: { + style: grandchild.properties.style + } + + resource child 'childType' = { + name: 'child' + properties: { + } + + resource grandchild 'grandchildType' = { + name: 'grandchild' + properties: { + style: 'very cool' + } + } + } +} +"; + + var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program)); + compilation.GetEntrypointSemanticModel().GetAllDiagnostics().Should().HaveDiagnostics(new[] { + ("BCP057", DiagnosticLevel.Error, "The name \"grandchild\" does not exist in the current context."), + }); + } + + [TestMethod] + public void NestedResources_ancestors_are_detected() + { + var program = @" +resource parent 'My.RP/parentType@2020-01-01' = { + name: 'parent' + properties: { + } + + resource child 'childType' = { + name: 'child' + properties: { + } + + resource grandchild 'grandchildType' = { + name: 'grandchild' + properties: { + } + } + } +} +"; + + var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program)); + var model = compilation.GetEntrypointSemanticModel(); + model.GetAllDiagnostics().Should().BeEmpty(); + + var parent = model.Root.GetAllResourceDeclarations().Single(r => r.Name == "parent"); + model.ResourceAncestors.GetAncestors(parent).Should().BeEmpty(); + + var child = model.Root.GetAllResourceDeclarations().Single(r => r.Name == "child"); + model.ResourceAncestors.GetAncestors(child).Should().Equal(new []{ parent, }); + + var grandchild = model.Root.GetAllResourceDeclarations().Single(r => r.Name == "grandchild"); + model.ResourceAncestors.GetAncestors(grandchild).Should().Equal(new []{ parent, child, }); // order matters + } + + [TestMethod] + public void NestedResources_scopes_isolate_names() + { + var program = @" +resource parent 'My.RP/parentType@2020-01-01' = { + name: 'parent' + properties: { + } + + resource child 'childType' = { + name: 'child' + properties: { + } + + resource grandchild 'grandchildType' = { + name: 'grandchild' + properties: { + } + } + } + + resource sibling 'childType' = { + name: 'sibling' + properties: { + } + + resource grandchild 'grandchildType' = { + name: 'grandchild' + properties: { + } + } + } +} +"; + + var compilation = new Compilation(TestResourceTypeProvider.Create(), SyntaxTreeGroupingFactory.CreateFromText(program)); + var model = compilation.GetEntrypointSemanticModel(); + model.GetAllDiagnostics().Should().BeEmpty(); + + var parent = model.Root.GetAllResourceDeclarations().Single(r => r.Name == "parent"); + model.ResourceAncestors.GetAncestors(parent).Should().BeEmpty(); + + var child = model.Root.GetAllResourceDeclarations().Single(r => r.Name == "child"); + model.ResourceAncestors.GetAncestors(child).Should().Equal(new []{ parent, }); + + var childGrandChild = (ResourceSymbol)model.GetSymbolInfo(child.DeclaringResource.GetBody().Resources.Single())!; + model.ResourceAncestors.GetAncestors(childGrandChild).Should().Equal(new []{ parent, child, }); + + var sibling = model.Root.GetAllResourceDeclarations().Single(r => r.Name == "sibling"); + model.ResourceAncestors.GetAncestors(child).Should().Equal(new []{ parent, }); + + var siblingGrandChild = (ResourceSymbol)model.GetSymbolInfo(sibling.DeclaringResource.GetBody().Resources.Single())!; + model.ResourceAncestors.GetAncestors(siblingGrandChild).Should().Equal(new []{ parent, sibling, }); + } + } +} \ No newline at end of file diff --git a/src/Bicep.Core.Samples/DataSets.cs b/src/Bicep.Core.Samples/DataSets.cs index aa03380fc7f..c4d66fc9208 100644 --- a/src/Bicep.Core.Samples/DataSets.cs +++ b/src/Bicep.Core.Samples/DataSets.cs @@ -36,6 +36,8 @@ public static class DataSets public static DataSet Outputs_CRLF => CreateDataSet(); + public static DataSet NestedResources_LF => CreateDataSet(); + public static DataSet Parameters_CRLF => CreateDataSet(); public static DataSet Parameters_LF => CreateDataSet(); diff --git a/src/Bicep.Core.Samples/Files/NestedResources_LF/main.bicep b/src/Bicep.Core.Samples/Files/NestedResources_LF/main.bicep new file mode 100644 index 00000000000..57d1b8e6b5a --- /dev/null +++ b/src/Bicep.Core.Samples/Files/NestedResources_LF/main.bicep @@ -0,0 +1,68 @@ +resource basicParent 'My.Rp/parentType@2020-12-01' = { + name: 'basicParent' + properties: { + size: 'large' + } + + resource basicChild 'childType' = { + name: 'basicChild' + properties: { + size: basicParent.properties.large + style: 'cool' + } + + resource basicGrandchild 'grandchildType' = { + name: 'basicGrandchild' + properties: { + size: basicParent.properties.size + style: basicChild.properties.style + } + } + } + + resource basicSibling 'childType' = { + name: 'basicSibling' + properties: { + size: basicParent.properties.size + style: basicChild:basicGrandchild.properties.style + } + } +} + +output referenceBasicChild string = basicParent:basicChild.properties.size +output referenceBasicGrandchild string = basicParent:basicChild:basicGrandchild.properties.style + +resource existingParent 'My.Rp/parentType@2020-12-01' existing = { + name: 'existingParent' + + resource existingChild 'childType' existing = { + name: 'existingChild' + + resource existingGrandchild 'grandchildType' = { + name: 'existingGrandchild' + properties: { + size: existingParent.properties.size + style: existingChild.properties.style + } + } + } +} + +param createParent bool +param createChild bool +param createGrandchild bool +resource conditionParent 'My.Rp/parentType@2020-12-01' = if (createParent) { + name: 'conditionParent' + + resource conditionChild 'childType' = if (createChild) { + name: 'conditionChild' + + resource conditionGrandchild 'grandchildType' = if (createGrandchild) { + name: 'conditionGrandchild' + properties: { + size: conditionParent.properties.size + style: conditionChild.properties.style + } + } + } +} \ No newline at end of file diff --git a/src/Bicep.Core.Samples/Files/NestedResources_LF/main.diagnostics.bicep b/src/Bicep.Core.Samples/Files/NestedResources_LF/main.diagnostics.bicep new file mode 100644 index 00000000000..216ced26c8b --- /dev/null +++ b/src/Bicep.Core.Samples/Files/NestedResources_LF/main.diagnostics.bicep @@ -0,0 +1,78 @@ +resource basicParent 'My.Rp/parentType@2020-12-01' = { +//@[21:50) [BCP081 (Warning)] Resource type "My.Rp/parentType@2020-12-01" does not have types available. |'My.Rp/parentType@2020-12-01'| + name: 'basicParent' + properties: { + size: 'large' + } + + resource basicChild 'childType' = { +//@[22:33) [BCP081 (Warning)] Resource type "My.Rp/parentType/childType@2020-12-01" does not have types available. |'childType'| + name: 'basicChild' + properties: { + size: basicParent.properties.large + style: 'cool' + } + + resource basicGrandchild 'grandchildType' = { +//@[29:45) [BCP081 (Warning)] Resource type "My.Rp/parentType/childType/grandchildType@2020-12-01" does not have types available. |'grandchildType'| + name: 'basicGrandchild' + properties: { + size: basicParent.properties.size + style: basicChild.properties.style + } + } + } + + resource basicSibling 'childType' = { +//@[24:35) [BCP081 (Warning)] Resource type "My.Rp/parentType/childType@2020-12-01" does not have types available. |'childType'| + name: 'basicSibling' + properties: { + size: basicParent.properties.size + style: basicChild:basicGrandchild.properties.style + } + } +} + +output referenceBasicChild string = basicParent:basicChild.properties.size +output referenceBasicGrandchild string = basicParent:basicChild:basicGrandchild.properties.style + +resource existingParent 'My.Rp/parentType@2020-12-01' existing = { +//@[24:53) [BCP081 (Warning)] Resource type "My.Rp/parentType@2020-12-01" does not have types available. |'My.Rp/parentType@2020-12-01'| + name: 'existingParent' + + resource existingChild 'childType' existing = { +//@[25:36) [BCP081 (Warning)] Resource type "My.Rp/parentType/childType@2020-12-01" does not have types available. |'childType'| + name: 'existingChild' + + resource existingGrandchild 'grandchildType' = { +//@[32:48) [BCP081 (Warning)] Resource type "My.Rp/parentType/childType/grandchildType@2020-12-01" does not have types available. |'grandchildType'| + name: 'existingGrandchild' + properties: { + size: existingParent.properties.size + style: existingChild.properties.style + } + } + } +} + +param createParent bool +param createChild bool +param createGrandchild bool +resource conditionParent 'My.Rp/parentType@2020-12-01' = if (createParent) { +//@[25:54) [BCP081 (Warning)] Resource type "My.Rp/parentType@2020-12-01" does not have types available. |'My.Rp/parentType@2020-12-01'| + name: 'conditionParent' + + resource conditionChild 'childType' = if (createChild) { +//@[26:37) [BCP081 (Warning)] Resource type "My.Rp/parentType/childType@2020-12-01" does not have types available. |'childType'| + name: 'conditionChild' + + resource conditionGrandchild 'grandchildType' = if (createGrandchild) { +//@[33:49) [BCP081 (Warning)] Resource type "My.Rp/parentType/childType/grandchildType@2020-12-01" does not have types available. |'grandchildType'| + name: 'conditionGrandchild' + properties: { + size: conditionParent.properties.size + style: conditionChild.properties.style + } + } + } +} diff --git a/src/Bicep.Core.Samples/Files/NestedResources_LF/main.formatted.bicep b/src/Bicep.Core.Samples/Files/NestedResources_LF/main.formatted.bicep new file mode 100644 index 00000000000..ff0cd186ab4 --- /dev/null +++ b/src/Bicep.Core.Samples/Files/NestedResources_LF/main.formatted.bicep @@ -0,0 +1,68 @@ +resource basicParent 'My.Rp/parentType@2020-12-01' = { + name: 'basicParent' + properties: { + size: 'large' + } + + resource basicChild 'childType' = { + name: 'basicChild' + properties: { + size: basicParent.properties.large + style: 'cool' + } + + resource basicGrandchild 'grandchildType' = { + name: 'basicGrandchild' + properties: { + size: basicParent.properties.size + style: basicChild.properties.style + } + } + } + + resource basicSibling 'childType' = { + name: 'basicSibling' + properties: { + size: basicParent.properties.size + style: basicChild:basicGrandchild.properties.style + } + } +} + +output referenceBasicChild string = basicParent:basicChild.properties.size +output referenceBasicGrandchild string = basicParent:basicChild:basicGrandchild.properties.style + +resource existingParent 'My.Rp/parentType@2020-12-01' existing = { + name: 'existingParent' + + resource existingChild 'childType' existing = { + name: 'existingChild' + + resource existingGrandchild 'grandchildType' = { + name: 'existingGrandchild' + properties: { + size: existingParent.properties.size + style: existingChild.properties.style + } + } + } +} + +param createParent bool +param createChild bool +param createGrandchild bool +resource conditionParent 'My.Rp/parentType@2020-12-01' = if (createParent) { + name: 'conditionParent' + + resource conditionChild 'childType' = if (createChild) { + name: 'conditionChild' + + resource conditionGrandchild 'grandchildType' = if (createGrandchild) { + name: 'conditionGrandchild' + properties: { + size: conditionParent.properties.size + style: conditionChild.properties.style + } + } + } +} diff --git a/src/Bicep.Core.Samples/Files/NestedResources_LF/main.json b/src/Bicep.Core.Samples/Files/NestedResources_LF/main.json new file mode 100644 index 00000000000..8a3257e028a --- /dev/null +++ b/src/Bicep.Core.Samples/Files/NestedResources_LF/main.json @@ -0,0 +1,120 @@ +{ + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": { + "createParent": { + "type": "bool" + }, + "createChild": { + "type": "bool" + }, + "createGrandchild": { + "type": "bool" + } + }, + "functions": [], + "resources": [ + { + "type": "My.Rp/parentType/childType/grandchildType", + "apiVersion": "2020-12-01", + "name": "[format('{0}/{1}/{2}', 'basicParent', 'basicChild', 'basicGrandchild')]", + "properties": { + "size": "[reference(resourceId('My.Rp/parentType', 'basicParent')).size]", + "style": "[reference(resourceId('My.Rp/parentType/childType', split(format('{0}/{1}', 'basicParent', 'basicChild'), '/')[0], split(format('{0}/{1}', 'basicParent', 'basicChild'), '/')[1])).style]" + }, + "dependsOn": [ + "[resourceId('My.Rp/parentType/childType', split(format('{0}/{1}', 'basicParent', 'basicChild'), '/')[0], split(format('{0}/{1}', 'basicParent', 'basicChild'), '/')[1])]", + "[resourceId('My.Rp/parentType', 'basicParent')]" + ] + }, + { + "type": "My.Rp/parentType/childType", + "apiVersion": "2020-12-01", + "name": "[format('{0}/{1}', 'basicParent', 'basicChild')]", + "properties": { + "size": "[reference(resourceId('My.Rp/parentType', 'basicParent')).large]", + "style": "cool" + }, + "dependsOn": [ + "[resourceId('My.Rp/parentType', 'basicParent')]" + ] + }, + { + "type": "My.Rp/parentType/childType", + "apiVersion": "2020-12-01", + "name": "[format('{0}/{1}', 'basicParent', 'basicSibling')]", + "properties": { + "size": "[reference(resourceId('My.Rp/parentType', 'basicParent')).size]", + "style": "[reference(resourceId('My.Rp/parentType/childType/grandchildType', split(format('{0}/{1}/{2}', 'basicParent', 'basicChild', 'basicGrandchild'), '/')[0], split(format('{0}/{1}/{2}', 'basicParent', 'basicChild', 'basicGrandchild'), '/')[1], split(format('{0}/{1}/{2}', 'basicParent', 'basicChild', 'basicGrandchild'), '/')[2])).style]" + }, + "dependsOn": [ + "[resourceId('My.Rp/parentType/childType', split(format('{0}/{1}', 'basicParent', 'basicChild'), '/')[0], split(format('{0}/{1}', 'basicParent', 'basicChild'), '/')[1])]", + "[resourceId('My.Rp/parentType', 'basicParent')]" + ] + }, + { + "type": "My.Rp/parentType/childType/grandchildType", + "apiVersion": "2020-12-01", + "name": "[format('{0}/{1}/{2}', 'existingParent', 'existingChild', 'existingGrandchild')]", + "properties": { + "size": "[reference(resourceId('My.Rp/parentType', 'existingParent'), '2020-12-01').size]", + "style": "[reference(resourceId('My.Rp/parentType/childType', split(format('{0}/{1}', 'existingParent', 'existingChild'), '/')[0], split(format('{0}/{1}', 'existingParent', 'existingChild'), '/')[1]), '2020-12-01').style]" + }, + "dependsOn": [] + }, + { + "condition": "[and(and(parameters('createParent'), parameters('createChild')), parameters('createGrandchild'))]", + "type": "My.Rp/parentType/childType/grandchildType", + "apiVersion": "2020-12-01", + "name": "[format('{0}/{1}/{2}', 'conditionParent', 'conditionChild', 'conditionGrandchild')]", + "properties": { + "size": "[reference(resourceId('My.Rp/parentType', 'conditionParent')).size]", + "style": "[reference(resourceId('My.Rp/parentType/childType', split(format('{0}/{1}', 'conditionParent', 'conditionChild'), '/')[0], split(format('{0}/{1}', 'conditionParent', 'conditionChild'), '/')[1])).style]" + }, + "dependsOn": [ + "[resourceId('My.Rp/parentType/childType', split(format('{0}/{1}', 'conditionParent', 'conditionChild'), '/')[0], split(format('{0}/{1}', 'conditionParent', 'conditionChild'), '/')[1])]", + "[resourceId('My.Rp/parentType', 'conditionParent')]" + ] + }, + { + "condition": "[and(parameters('createParent'), parameters('createChild'))]", + "type": "My.Rp/parentType/childType", + "apiVersion": "2020-12-01", + "name": "[format('{0}/{1}', 'conditionParent', 'conditionChild')]", + "dependsOn": [ + "[resourceId('My.Rp/parentType', 'conditionParent')]" + ] + }, + { + "type": "My.Rp/parentType", + "apiVersion": "2020-12-01", + "name": "basicParent", + "properties": { + "size": "large" + } + }, + { + "condition": "[parameters('createParent')]", + "type": "My.Rp/parentType", + "apiVersion": "2020-12-01", + "name": "conditionParent" + } + ], + "outputs": { + "referenceBasicChild": { + "type": "string", + "value": "[reference(resourceId('My.Rp/parentType/childType', split(format('{0}/{1}', 'basicParent', 'basicChild'), '/')[0], split(format('{0}/{1}', 'basicParent', 'basicChild'), '/')[1])).size]" + }, + "referenceBasicGrandchild": { + "type": "string", + "value": "[reference(resourceId('My.Rp/parentType/childType/grandchildType', split(format('{0}/{1}/{2}', 'basicParent', 'basicChild', 'basicGrandchild'), '/')[0], split(format('{0}/{1}/{2}', 'basicParent', 'basicChild', 'basicGrandchild'), '/')[1], split(format('{0}/{1}/{2}', 'basicParent', 'basicChild', 'basicGrandchild'), '/')[2])).style]" + } + }, + "metadata": { + "_generator": { + "name": "bicep", + "version": "dev", + "templateHash": "3280919504232588082" + } + } +} \ No newline at end of file diff --git a/src/Bicep.Core.Samples/Files/NestedResources_LF/main.symbols.bicep b/src/Bicep.Core.Samples/Files/NestedResources_LF/main.symbols.bicep new file mode 100644 index 00000000000..6e190a90e77 --- /dev/null +++ b/src/Bicep.Core.Samples/Files/NestedResources_LF/main.symbols.bicep @@ -0,0 +1,83 @@ +resource basicParent 'My.Rp/parentType@2020-12-01' = { +//@[9:20) Resource basicParent. Type: My.Rp/parentType@2020-12-01. Declaration start char: 0, length: 658 + name: 'basicParent' + properties: { + size: 'large' + } + + resource basicChild 'childType' = { +//@[11:21) Resource basicChild. Type: My.Rp/parentType/childType@2020-12-01. Declaration start char: 2, length: 347 + name: 'basicChild' + properties: { + size: basicParent.properties.large + style: 'cool' + } + + resource basicGrandchild 'grandchildType' = { +//@[13:28) Resource basicGrandchild. Type: My.Rp/parentType/childType/grandchildType@2020-12-01. Declaration start char: 4, length: 194 + name: 'basicGrandchild' + properties: { + size: basicParent.properties.size + style: basicChild.properties.style + } + } + } + + resource basicSibling 'childType' = { +//@[11:23) Resource basicSibling. Type: My.Rp/parentType/childType@2020-12-01. Declaration start char: 2, length: 187 + name: 'basicSibling' + properties: { + size: basicParent.properties.size + style: basicChild:basicGrandchild.properties.style + } + } +} + +output referenceBasicChild string = basicParent:basicChild.properties.size +//@[7:26) Output referenceBasicChild. Type: string. Declaration start char: 0, length: 74 +output referenceBasicGrandchild string = basicParent:basicChild:basicGrandchild.properties.style +//@[7:31) Output referenceBasicGrandchild. Type: string. Declaration start char: 0, length: 96 + +resource existingParent 'My.Rp/parentType@2020-12-01' existing = { +//@[9:23) Resource existingParent. Type: My.Rp/parentType@2020-12-01. Declaration start char: 0, length: 386 + name: 'existingParent' + + resource existingChild 'childType' existing = { +//@[11:24) Resource existingChild. Type: My.Rp/parentType/childType@2020-12-01. Declaration start char: 2, length: 289 + name: 'existingChild' + + resource existingGrandchild 'grandchildType' = { +//@[13:31) Resource existingGrandchild. Type: My.Rp/parentType/childType/grandchildType@2020-12-01. Declaration start char: 4, length: 206 + name: 'existingGrandchild' + properties: { + size: existingParent.properties.size + style: existingChild.properties.style + } + } + } +} + +param createParent bool +//@[6:18) Parameter createParent. Type: bool. Declaration start char: 0, length: 23 +param createChild bool +//@[6:17) Parameter createChild. Type: bool. Declaration start char: 0, length: 22 +param createGrandchild bool +//@[6:22) Parameter createGrandchild. Type: bool. Declaration start char: 0, length: 27 +resource conditionParent 'My.Rp/parentType@2020-12-01' = if (createParent) { +//@[9:24) Resource conditionParent. Type: My.Rp/parentType@2020-12-01. Declaration start char: 0, length: 433 + name: 'conditionParent' + + resource conditionChild 'childType' = if (createChild) { +//@[11:25) Resource conditionChild. Type: My.Rp/parentType/childType@2020-12-01. Declaration start char: 2, length: 325 + name: 'conditionChild' + + resource conditionGrandchild 'grandchildType' = if (createGrandchild) { +//@[13:32) Resource conditionGrandchild. Type: My.Rp/parentType/childType/grandchildType@2020-12-01. Declaration start char: 4, length: 232 + name: 'conditionGrandchild' + properties: { + size: conditionParent.properties.size + style: conditionChild.properties.style + } + } + } +} diff --git a/src/Bicep.Core.Samples/Files/NestedResources_LF/main.syntax.bicep b/src/Bicep.Core.Samples/Files/NestedResources_LF/main.syntax.bicep new file mode 100644 index 00000000000..fd76cb27081 --- /dev/null +++ b/src/Bicep.Core.Samples/Files/NestedResources_LF/main.syntax.bicep @@ -0,0 +1,571 @@ +resource basicParent 'My.Rp/parentType@2020-12-01' = { +//@[0:658) ResourceDeclarationSyntax +//@[0:8) Identifier |resource| +//@[9:20) IdentifierSyntax +//@[9:20) Identifier |basicParent| +//@[21:50) StringSyntax +//@[21:50) StringComplete |'My.Rp/parentType@2020-12-01'| +//@[51:52) Assignment |=| +//@[53:658) ObjectSyntax +//@[53:54) LeftBrace |{| +//@[54:55) NewLine |\n| + name: 'basicParent' +//@[2:21) ObjectPropertySyntax +//@[2:6) IdentifierSyntax +//@[2:6) Identifier |name| +//@[6:7) Colon |:| +//@[8:21) StringSyntax +//@[8:21) StringComplete |'basicParent'| +//@[21:22) NewLine |\n| + properties: { +//@[2:37) ObjectPropertySyntax +//@[2:12) IdentifierSyntax +//@[2:12) Identifier |properties| +//@[12:13) Colon |:| +//@[14:37) ObjectSyntax +//@[14:15) LeftBrace |{| +//@[15:16) NewLine |\n| + size: 'large' +//@[4:17) ObjectPropertySyntax +//@[4:8) IdentifierSyntax +//@[4:8) Identifier |size| +//@[8:9) Colon |:| +//@[10:17) StringSyntax +//@[10:17) StringComplete |'large'| +//@[17:18) NewLine |\n| + } +//@[2:3) RightBrace |}| +//@[3:5) NewLine |\n\n| + + resource basicChild 'childType' = { +//@[2:349) ResourceDeclarationSyntax +//@[2:10) Identifier |resource| +//@[11:21) IdentifierSyntax +//@[11:21) Identifier |basicChild| +//@[22:33) StringSyntax +//@[22:33) StringComplete |'childType'| +//@[34:35) Assignment |=| +//@[36:349) ObjectSyntax +//@[36:37) LeftBrace |{| +//@[37:38) NewLine |\n| + name: 'basicChild' +//@[4:22) ObjectPropertySyntax +//@[4:8) IdentifierSyntax +//@[4:8) Identifier |name| +//@[8:9) Colon |:| +//@[10:22) StringSyntax +//@[10:22) StringComplete |'basicChild'| +//@[22:23) NewLine |\n| + properties: { +//@[4:84) ObjectPropertySyntax +//@[4:14) IdentifierSyntax +//@[4:14) Identifier |properties| +//@[14:15) Colon |:| +//@[16:84) ObjectSyntax +//@[16:17) LeftBrace |{| +//@[17:18) NewLine |\n| + size: basicParent.properties.large +//@[6:40) ObjectPropertySyntax +//@[6:10) IdentifierSyntax +//@[6:10) Identifier |size| +//@[10:11) Colon |:| +//@[12:40) PropertyAccessSyntax +//@[12:34) PropertyAccessSyntax +//@[12:23) VariableAccessSyntax +//@[12:23) IdentifierSyntax +//@[12:23) Identifier |basicParent| +//@[23:24) Dot |.| +//@[24:34) IdentifierSyntax +//@[24:34) Identifier |properties| +//@[34:35) Dot |.| +//@[35:40) IdentifierSyntax +//@[35:40) Identifier |large| +//@[40:41) NewLine |\n| + style: 'cool' +//@[6:19) ObjectPropertySyntax +//@[6:11) IdentifierSyntax +//@[6:11) Identifier |style| +//@[11:12) Colon |:| +//@[13:19) StringSyntax +//@[13:19) StringComplete |'cool'| +//@[19:20) NewLine |\n| + } +//@[4:5) RightBrace |}| +//@[5:7) NewLine |\n\n| + + resource basicGrandchild 'grandchildType' = { +//@[4:198) ResourceDeclarationSyntax +//@[4:12) Identifier |resource| +//@[13:28) IdentifierSyntax +//@[13:28) Identifier |basicGrandchild| +//@[29:45) StringSyntax +//@[29:45) StringComplete |'grandchildType'| +//@[46:47) Assignment |=| +//@[48:198) ObjectSyntax +//@[48:49) LeftBrace |{| +//@[49:50) NewLine |\n| + name: 'basicGrandchild' +//@[6:29) ObjectPropertySyntax +//@[6:10) IdentifierSyntax +//@[6:10) Identifier |name| +//@[10:11) Colon |:| +//@[12:29) StringSyntax +//@[12:29) StringComplete |'basicGrandchild'| +//@[29:30) NewLine |\n| + properties: { +//@[6:112) ObjectPropertySyntax +//@[6:16) IdentifierSyntax +//@[6:16) Identifier |properties| +//@[16:17) Colon |:| +//@[18:112) ObjectSyntax +//@[18:19) LeftBrace |{| +//@[19:20) NewLine |\n| + size: basicParent.properties.size +//@[8:41) ObjectPropertySyntax +//@[8:12) IdentifierSyntax +//@[8:12) Identifier |size| +//@[12:13) Colon |:| +//@[14:41) PropertyAccessSyntax +//@[14:36) PropertyAccessSyntax +//@[14:25) VariableAccessSyntax +//@[14:25) IdentifierSyntax +//@[14:25) Identifier |basicParent| +//@[25:26) Dot |.| +//@[26:36) IdentifierSyntax +//@[26:36) Identifier |properties| +//@[36:37) Dot |.| +//@[37:41) IdentifierSyntax +//@[37:41) Identifier |size| +//@[41:42) NewLine |\n| + style: basicChild.properties.style +//@[8:42) ObjectPropertySyntax +//@[8:13) IdentifierSyntax +//@[8:13) Identifier |style| +//@[13:14) Colon |:| +//@[15:42) PropertyAccessSyntax +//@[15:36) PropertyAccessSyntax +//@[15:25) VariableAccessSyntax +//@[15:25) IdentifierSyntax +//@[15:25) Identifier |basicChild| +//@[25:26) Dot |.| +//@[26:36) IdentifierSyntax +//@[26:36) Identifier |properties| +//@[36:37) Dot |.| +//@[37:42) IdentifierSyntax +//@[37:42) Identifier |style| +//@[42:43) NewLine |\n| + } +//@[6:7) RightBrace |}| +//@[7:8) NewLine |\n| + } +//@[4:5) RightBrace |}| +//@[5:6) NewLine |\n| + } +//@[2:3) RightBrace |}| +//@[3:5) NewLine |\n\n| + + resource basicSibling 'childType' = { +//@[2:189) ResourceDeclarationSyntax +//@[2:10) Identifier |resource| +//@[11:23) IdentifierSyntax +//@[11:23) Identifier |basicSibling| +//@[24:35) StringSyntax +//@[24:35) StringComplete |'childType'| +//@[36:37) Assignment |=| +//@[38:189) ObjectSyntax +//@[38:39) LeftBrace |{| +//@[39:40) NewLine |\n| + name: 'basicSibling' +//@[4:24) ObjectPropertySyntax +//@[4:8) IdentifierSyntax +//@[4:8) Identifier |name| +//@[8:9) Colon |:| +//@[10:24) StringSyntax +//@[10:24) StringComplete |'basicSibling'| +//@[24:25) NewLine |\n| + properties: { +//@[4:120) ObjectPropertySyntax +//@[4:14) IdentifierSyntax +//@[4:14) Identifier |properties| +//@[14:15) Colon |:| +//@[16:120) ObjectSyntax +//@[16:17) LeftBrace |{| +//@[17:18) NewLine |\n| + size: basicParent.properties.size +//@[6:39) ObjectPropertySyntax +//@[6:10) IdentifierSyntax +//@[6:10) Identifier |size| +//@[10:11) Colon |:| +//@[12:39) PropertyAccessSyntax +//@[12:34) PropertyAccessSyntax +//@[12:23) VariableAccessSyntax +//@[12:23) IdentifierSyntax +//@[12:23) Identifier |basicParent| +//@[23:24) Dot |.| +//@[24:34) IdentifierSyntax +//@[24:34) Identifier |properties| +//@[34:35) Dot |.| +//@[35:39) IdentifierSyntax +//@[35:39) Identifier |size| +//@[39:40) NewLine |\n| + style: basicChild:basicGrandchild.properties.style +//@[6:56) ObjectPropertySyntax +//@[6:11) IdentifierSyntax +//@[6:11) Identifier |style| +//@[11:12) Colon |:| +//@[13:56) PropertyAccessSyntax +//@[13:50) PropertyAccessSyntax +//@[13:39) ResourceAccessSyntax +//@[13:23) VariableAccessSyntax +//@[13:23) IdentifierSyntax +//@[13:23) Identifier |basicChild| +//@[23:24) Colon |:| +//@[24:39) IdentifierSyntax +//@[24:39) Identifier |basicGrandchild| +//@[39:40) Dot |.| +//@[40:50) IdentifierSyntax +//@[40:50) Identifier |properties| +//@[50:51) Dot |.| +//@[51:56) IdentifierSyntax +//@[51:56) Identifier |style| +//@[56:57) NewLine |\n| + } +//@[4:5) RightBrace |}| +//@[5:6) NewLine |\n| + } +//@[2:3) RightBrace |}| +//@[3:4) NewLine |\n| +} +//@[0:1) RightBrace |}| +//@[1:3) NewLine |\n\n| + +output referenceBasicChild string = basicParent:basicChild.properties.size +//@[0:74) OutputDeclarationSyntax +//@[0:6) Identifier |output| +//@[7:26) IdentifierSyntax +//@[7:26) Identifier |referenceBasicChild| +//@[27:33) TypeSyntax +//@[27:33) Identifier |string| +//@[34:35) Assignment |=| +//@[36:74) PropertyAccessSyntax +//@[36:69) PropertyAccessSyntax +//@[36:58) ResourceAccessSyntax +//@[36:47) VariableAccessSyntax +//@[36:47) IdentifierSyntax +//@[36:47) Identifier |basicParent| +//@[47:48) Colon |:| +//@[48:58) IdentifierSyntax +//@[48:58) Identifier |basicChild| +//@[58:59) Dot |.| +//@[59:69) IdentifierSyntax +//@[59:69) Identifier |properties| +//@[69:70) Dot |.| +//@[70:74) IdentifierSyntax +//@[70:74) Identifier |size| +//@[74:75) NewLine |\n| +output referenceBasicGrandchild string = basicParent:basicChild:basicGrandchild.properties.style +//@[0:96) OutputDeclarationSyntax +//@[0:6) Identifier |output| +//@[7:31) IdentifierSyntax +//@[7:31) Identifier |referenceBasicGrandchild| +//@[32:38) TypeSyntax +//@[32:38) Identifier |string| +//@[39:40) Assignment |=| +//@[41:96) PropertyAccessSyntax +//@[41:90) PropertyAccessSyntax +//@[41:79) ResourceAccessSyntax +//@[41:63) ResourceAccessSyntax +//@[41:52) VariableAccessSyntax +//@[41:52) IdentifierSyntax +//@[41:52) Identifier |basicParent| +//@[52:53) Colon |:| +//@[53:63) IdentifierSyntax +//@[53:63) Identifier |basicChild| +//@[63:64) Colon |:| +//@[64:79) IdentifierSyntax +//@[64:79) Identifier |basicGrandchild| +//@[79:80) Dot |.| +//@[80:90) IdentifierSyntax +//@[80:90) Identifier |properties| +//@[90:91) Dot |.| +//@[91:96) IdentifierSyntax +//@[91:96) Identifier |style| +//@[96:98) NewLine |\n\n| + +resource existingParent 'My.Rp/parentType@2020-12-01' existing = { +//@[0:386) ResourceDeclarationSyntax +//@[0:8) Identifier |resource| +//@[9:23) IdentifierSyntax +//@[9:23) Identifier |existingParent| +//@[24:53) StringSyntax +//@[24:53) StringComplete |'My.Rp/parentType@2020-12-01'| +//@[54:62) Identifier |existing| +//@[63:64) Assignment |=| +//@[65:386) ObjectSyntax +//@[65:66) LeftBrace |{| +//@[66:67) NewLine |\n| + name: 'existingParent' +//@[2:24) ObjectPropertySyntax +//@[2:6) IdentifierSyntax +//@[2:6) Identifier |name| +//@[6:7) Colon |:| +//@[8:24) StringSyntax +//@[8:24) StringComplete |'existingParent'| +//@[24:26) NewLine |\n\n| + + resource existingChild 'childType' existing = { +//@[2:291) ResourceDeclarationSyntax +//@[2:10) Identifier |resource| +//@[11:24) IdentifierSyntax +//@[11:24) Identifier |existingChild| +//@[25:36) StringSyntax +//@[25:36) StringComplete |'childType'| +//@[37:45) Identifier |existing| +//@[46:47) Assignment |=| +//@[48:291) ObjectSyntax +//@[48:49) LeftBrace |{| +//@[49:50) NewLine |\n| + name: 'existingChild' +//@[4:25) ObjectPropertySyntax +//@[4:8) IdentifierSyntax +//@[4:8) Identifier |name| +//@[8:9) Colon |:| +//@[10:25) StringSyntax +//@[10:25) StringComplete |'existingChild'| +//@[25:27) NewLine |\n\n| + + resource existingGrandchild 'grandchildType' = { +//@[4:210) ResourceDeclarationSyntax +//@[4:12) Identifier |resource| +//@[13:31) IdentifierSyntax +//@[13:31) Identifier |existingGrandchild| +//@[32:48) StringSyntax +//@[32:48) StringComplete |'grandchildType'| +//@[49:50) Assignment |=| +//@[51:210) ObjectSyntax +//@[51:52) LeftBrace |{| +//@[52:53) NewLine |\n| + name: 'existingGrandchild' +//@[6:32) ObjectPropertySyntax +//@[6:10) IdentifierSyntax +//@[6:10) Identifier |name| +//@[10:11) Colon |:| +//@[12:32) StringSyntax +//@[12:32) StringComplete |'existingGrandchild'| +//@[32:33) NewLine |\n| + properties: { +//@[6:118) ObjectPropertySyntax +//@[6:16) IdentifierSyntax +//@[6:16) Identifier |properties| +//@[16:17) Colon |:| +//@[18:118) ObjectSyntax +//@[18:19) LeftBrace |{| +//@[19:20) NewLine |\n| + size: existingParent.properties.size +//@[8:44) ObjectPropertySyntax +//@[8:12) IdentifierSyntax +//@[8:12) Identifier |size| +//@[12:13) Colon |:| +//@[14:44) PropertyAccessSyntax +//@[14:39) PropertyAccessSyntax +//@[14:28) VariableAccessSyntax +//@[14:28) IdentifierSyntax +//@[14:28) Identifier |existingParent| +//@[28:29) Dot |.| +//@[29:39) IdentifierSyntax +//@[29:39) Identifier |properties| +//@[39:40) Dot |.| +//@[40:44) IdentifierSyntax +//@[40:44) Identifier |size| +//@[44:45) NewLine |\n| + style: existingChild.properties.style +//@[8:45) ObjectPropertySyntax +//@[8:13) IdentifierSyntax +//@[8:13) Identifier |style| +//@[13:14) Colon |:| +//@[15:45) PropertyAccessSyntax +//@[15:39) PropertyAccessSyntax +//@[15:28) VariableAccessSyntax +//@[15:28) IdentifierSyntax +//@[15:28) Identifier |existingChild| +//@[28:29) Dot |.| +//@[29:39) IdentifierSyntax +//@[29:39) Identifier |properties| +//@[39:40) Dot |.| +//@[40:45) IdentifierSyntax +//@[40:45) Identifier |style| +//@[45:46) NewLine |\n| + } +//@[6:7) RightBrace |}| +//@[7:8) NewLine |\n| + } +//@[4:5) RightBrace |}| +//@[5:6) NewLine |\n| + } +//@[2:3) RightBrace |}| +//@[3:4) NewLine |\n| +} +//@[0:1) RightBrace |}| +//@[1:3) NewLine |\n\n| + +param createParent bool +//@[0:23) ParameterDeclarationSyntax +//@[0:5) Identifier |param| +//@[6:18) IdentifierSyntax +//@[6:18) Identifier |createParent| +//@[19:23) TypeSyntax +//@[19:23) Identifier |bool| +//@[23:24) NewLine |\n| +param createChild bool +//@[0:22) ParameterDeclarationSyntax +//@[0:5) Identifier |param| +//@[6:17) IdentifierSyntax +//@[6:17) Identifier |createChild| +//@[18:22) TypeSyntax +//@[18:22) Identifier |bool| +//@[22:23) NewLine |\n| +param createGrandchild bool +//@[0:27) ParameterDeclarationSyntax +//@[0:5) Identifier |param| +//@[6:22) IdentifierSyntax +//@[6:22) Identifier |createGrandchild| +//@[23:27) TypeSyntax +//@[23:27) Identifier |bool| +//@[27:28) NewLine |\n| +resource conditionParent 'My.Rp/parentType@2020-12-01' = if (createParent) { +//@[0:433) ResourceDeclarationSyntax +//@[0:8) Identifier |resource| +//@[9:24) IdentifierSyntax +//@[9:24) Identifier |conditionParent| +//@[25:54) StringSyntax +//@[25:54) StringComplete |'My.Rp/parentType@2020-12-01'| +//@[55:56) Assignment |=| +//@[57:433) IfConditionSyntax +//@[57:59) Identifier |if| +//@[60:74) ParenthesizedExpressionSyntax +//@[60:61) LeftParen |(| +//@[61:73) VariableAccessSyntax +//@[61:73) IdentifierSyntax +//@[61:73) Identifier |createParent| +//@[73:74) RightParen |)| +//@[75:433) ObjectSyntax +//@[75:76) LeftBrace |{| +//@[76:77) NewLine |\n| + name: 'conditionParent' +//@[2:25) ObjectPropertySyntax +//@[2:6) IdentifierSyntax +//@[2:6) Identifier |name| +//@[6:7) Colon |:| +//@[8:25) StringSyntax +//@[8:25) StringComplete |'conditionParent'| +//@[25:27) NewLine |\n\n| + + resource conditionChild 'childType' = if (createChild) { +//@[2:327) ResourceDeclarationSyntax +//@[2:10) Identifier |resource| +//@[11:25) IdentifierSyntax +//@[11:25) Identifier |conditionChild| +//@[26:37) StringSyntax +//@[26:37) StringComplete |'childType'| +//@[38:39) Assignment |=| +//@[40:327) IfConditionSyntax +//@[40:42) Identifier |if| +//@[43:56) ParenthesizedExpressionSyntax +//@[43:44) LeftParen |(| +//@[44:55) VariableAccessSyntax +//@[44:55) IdentifierSyntax +//@[44:55) Identifier |createChild| +//@[55:56) RightParen |)| +//@[57:327) ObjectSyntax +//@[57:58) LeftBrace |{| +//@[58:59) NewLine |\n| + name: 'conditionChild' +//@[4:26) ObjectPropertySyntax +//@[4:8) IdentifierSyntax +//@[4:8) Identifier |name| +//@[8:9) Colon |:| +//@[10:26) StringSyntax +//@[10:26) StringComplete |'conditionChild'| +//@[26:28) NewLine |\n\n| + + resource conditionGrandchild 'grandchildType' = if (createGrandchild) { +//@[4:236) ResourceDeclarationSyntax +//@[4:12) Identifier |resource| +//@[13:32) IdentifierSyntax +//@[13:32) Identifier |conditionGrandchild| +//@[33:49) StringSyntax +//@[33:49) StringComplete |'grandchildType'| +//@[50:51) Assignment |=| +//@[52:236) IfConditionSyntax +//@[52:54) Identifier |if| +//@[55:73) ParenthesizedExpressionSyntax +//@[55:56) LeftParen |(| +//@[56:72) VariableAccessSyntax +//@[56:72) IdentifierSyntax +//@[56:72) Identifier |createGrandchild| +//@[72:73) RightParen |)| +//@[74:236) ObjectSyntax +//@[74:75) LeftBrace |{| +//@[75:76) NewLine |\n| + name: 'conditionGrandchild' +//@[6:33) ObjectPropertySyntax +//@[6:10) IdentifierSyntax +//@[6:10) Identifier |name| +//@[10:11) Colon |:| +//@[12:33) StringSyntax +//@[12:33) StringComplete |'conditionGrandchild'| +//@[33:34) NewLine |\n| + properties: { +//@[6:120) ObjectPropertySyntax +//@[6:16) IdentifierSyntax +//@[6:16) Identifier |properties| +//@[16:17) Colon |:| +//@[18:120) ObjectSyntax +//@[18:19) LeftBrace |{| +//@[19:20) NewLine |\n| + size: conditionParent.properties.size +//@[8:45) ObjectPropertySyntax +//@[8:12) IdentifierSyntax +//@[8:12) Identifier |size| +//@[12:13) Colon |:| +//@[14:45) PropertyAccessSyntax +//@[14:40) PropertyAccessSyntax +//@[14:29) VariableAccessSyntax +//@[14:29) IdentifierSyntax +//@[14:29) Identifier |conditionParent| +//@[29:30) Dot |.| +//@[30:40) IdentifierSyntax +//@[30:40) Identifier |properties| +//@[40:41) Dot |.| +//@[41:45) IdentifierSyntax +//@[41:45) Identifier |size| +//@[45:46) NewLine |\n| + style: conditionChild.properties.style +//@[8:46) ObjectPropertySyntax +//@[8:13) IdentifierSyntax +//@[8:13) Identifier |style| +//@[13:14) Colon |:| +//@[15:46) PropertyAccessSyntax +//@[15:40) PropertyAccessSyntax +//@[15:29) VariableAccessSyntax +//@[15:29) IdentifierSyntax +//@[15:29) Identifier |conditionChild| +//@[29:30) Dot |.| +//@[30:40) IdentifierSyntax +//@[30:40) Identifier |properties| +//@[40:41) Dot |.| +//@[41:46) IdentifierSyntax +//@[41:46) Identifier |style| +//@[46:47) NewLine |\n| + } +//@[6:7) RightBrace |}| +//@[7:8) NewLine |\n| + } +//@[4:5) RightBrace |}| +//@[5:6) NewLine |\n| + } +//@[2:3) RightBrace |}| +//@[3:4) NewLine |\n| +} +//@[0:1) RightBrace |}| +//@[1:1) EndOfFile || diff --git a/src/Bicep.Core.Samples/Files/NestedResources_LF/main.tokens.bicep b/src/Bicep.Core.Samples/Files/NestedResources_LF/main.tokens.bicep new file mode 100644 index 00000000000..1a98665a86d --- /dev/null +++ b/src/Bicep.Core.Samples/Files/NestedResources_LF/main.tokens.bicep @@ -0,0 +1,358 @@ +resource basicParent 'My.Rp/parentType@2020-12-01' = { +//@[0:8) Identifier |resource| +//@[9:20) Identifier |basicParent| +//@[21:50) StringComplete |'My.Rp/parentType@2020-12-01'| +//@[51:52) Assignment |=| +//@[53:54) LeftBrace |{| +//@[54:55) NewLine |\n| + name: 'basicParent' +//@[2:6) Identifier |name| +//@[6:7) Colon |:| +//@[8:21) StringComplete |'basicParent'| +//@[21:22) NewLine |\n| + properties: { +//@[2:12) Identifier |properties| +//@[12:13) Colon |:| +//@[14:15) LeftBrace |{| +//@[15:16) NewLine |\n| + size: 'large' +//@[4:8) Identifier |size| +//@[8:9) Colon |:| +//@[10:17) StringComplete |'large'| +//@[17:18) NewLine |\n| + } +//@[2:3) RightBrace |}| +//@[3:5) NewLine |\n\n| + + resource basicChild 'childType' = { +//@[2:10) Identifier |resource| +//@[11:21) Identifier |basicChild| +//@[22:33) StringComplete |'childType'| +//@[34:35) Assignment |=| +//@[36:37) LeftBrace |{| +//@[37:38) NewLine |\n| + name: 'basicChild' +//@[4:8) Identifier |name| +//@[8:9) Colon |:| +//@[10:22) StringComplete |'basicChild'| +//@[22:23) NewLine |\n| + properties: { +//@[4:14) Identifier |properties| +//@[14:15) Colon |:| +//@[16:17) LeftBrace |{| +//@[17:18) NewLine |\n| + size: basicParent.properties.large +//@[6:10) Identifier |size| +//@[10:11) Colon |:| +//@[12:23) Identifier |basicParent| +//@[23:24) Dot |.| +//@[24:34) Identifier |properties| +//@[34:35) Dot |.| +//@[35:40) Identifier |large| +//@[40:41) NewLine |\n| + style: 'cool' +//@[6:11) Identifier |style| +//@[11:12) Colon |:| +//@[13:19) StringComplete |'cool'| +//@[19:20) NewLine |\n| + } +//@[4:5) RightBrace |}| +//@[5:7) NewLine |\n\n| + + resource basicGrandchild 'grandchildType' = { +//@[4:12) Identifier |resource| +//@[13:28) Identifier |basicGrandchild| +//@[29:45) StringComplete |'grandchildType'| +//@[46:47) Assignment |=| +//@[48:49) LeftBrace |{| +//@[49:50) NewLine |\n| + name: 'basicGrandchild' +//@[6:10) Identifier |name| +//@[10:11) Colon |:| +//@[12:29) StringComplete |'basicGrandchild'| +//@[29:30) NewLine |\n| + properties: { +//@[6:16) Identifier |properties| +//@[16:17) Colon |:| +//@[18:19) LeftBrace |{| +//@[19:20) NewLine |\n| + size: basicParent.properties.size +//@[8:12) Identifier |size| +//@[12:13) Colon |:| +//@[14:25) Identifier |basicParent| +//@[25:26) Dot |.| +//@[26:36) Identifier |properties| +//@[36:37) Dot |.| +//@[37:41) Identifier |size| +//@[41:42) NewLine |\n| + style: basicChild.properties.style +//@[8:13) Identifier |style| +//@[13:14) Colon |:| +//@[15:25) Identifier |basicChild| +//@[25:26) Dot |.| +//@[26:36) Identifier |properties| +//@[36:37) Dot |.| +//@[37:42) Identifier |style| +//@[42:43) NewLine |\n| + } +//@[6:7) RightBrace |}| +//@[7:8) NewLine |\n| + } +//@[4:5) RightBrace |}| +//@[5:6) NewLine |\n| + } +//@[2:3) RightBrace |}| +//@[3:5) NewLine |\n\n| + + resource basicSibling 'childType' = { +//@[2:10) Identifier |resource| +//@[11:23) Identifier |basicSibling| +//@[24:35) StringComplete |'childType'| +//@[36:37) Assignment |=| +//@[38:39) LeftBrace |{| +//@[39:40) NewLine |\n| + name: 'basicSibling' +//@[4:8) Identifier |name| +//@[8:9) Colon |:| +//@[10:24) StringComplete |'basicSibling'| +//@[24:25) NewLine |\n| + properties: { +//@[4:14) Identifier |properties| +//@[14:15) Colon |:| +//@[16:17) LeftBrace |{| +//@[17:18) NewLine |\n| + size: basicParent.properties.size +//@[6:10) Identifier |size| +//@[10:11) Colon |:| +//@[12:23) Identifier |basicParent| +//@[23:24) Dot |.| +//@[24:34) Identifier |properties| +//@[34:35) Dot |.| +//@[35:39) Identifier |size| +//@[39:40) NewLine |\n| + style: basicChild:basicGrandchild.properties.style +//@[6:11) Identifier |style| +//@[11:12) Colon |:| +//@[13:23) Identifier |basicChild| +//@[23:24) Colon |:| +//@[24:39) Identifier |basicGrandchild| +//@[39:40) Dot |.| +//@[40:50) Identifier |properties| +//@[50:51) Dot |.| +//@[51:56) Identifier |style| +//@[56:57) NewLine |\n| + } +//@[4:5) RightBrace |}| +//@[5:6) NewLine |\n| + } +//@[2:3) RightBrace |}| +//@[3:4) NewLine |\n| +} +//@[0:1) RightBrace |}| +//@[1:3) NewLine |\n\n| + +output referenceBasicChild string = basicParent:basicChild.properties.size +//@[0:6) Identifier |output| +//@[7:26) Identifier |referenceBasicChild| +//@[27:33) Identifier |string| +//@[34:35) Assignment |=| +//@[36:47) Identifier |basicParent| +//@[47:48) Colon |:| +//@[48:58) Identifier |basicChild| +//@[58:59) Dot |.| +//@[59:69) Identifier |properties| +//@[69:70) Dot |.| +//@[70:74) Identifier |size| +//@[74:75) NewLine |\n| +output referenceBasicGrandchild string = basicParent:basicChild:basicGrandchild.properties.style +//@[0:6) Identifier |output| +//@[7:31) Identifier |referenceBasicGrandchild| +//@[32:38) Identifier |string| +//@[39:40) Assignment |=| +//@[41:52) Identifier |basicParent| +//@[52:53) Colon |:| +//@[53:63) Identifier |basicChild| +//@[63:64) Colon |:| +//@[64:79) Identifier |basicGrandchild| +//@[79:80) Dot |.| +//@[80:90) Identifier |properties| +//@[90:91) Dot |.| +//@[91:96) Identifier |style| +//@[96:98) NewLine |\n\n| + +resource existingParent 'My.Rp/parentType@2020-12-01' existing = { +//@[0:8) Identifier |resource| +//@[9:23) Identifier |existingParent| +//@[24:53) StringComplete |'My.Rp/parentType@2020-12-01'| +//@[54:62) Identifier |existing| +//@[63:64) Assignment |=| +//@[65:66) LeftBrace |{| +//@[66:67) NewLine |\n| + name: 'existingParent' +//@[2:6) Identifier |name| +//@[6:7) Colon |:| +//@[8:24) StringComplete |'existingParent'| +//@[24:26) NewLine |\n\n| + + resource existingChild 'childType' existing = { +//@[2:10) Identifier |resource| +//@[11:24) Identifier |existingChild| +//@[25:36) StringComplete |'childType'| +//@[37:45) Identifier |existing| +//@[46:47) Assignment |=| +//@[48:49) LeftBrace |{| +//@[49:50) NewLine |\n| + name: 'existingChild' +//@[4:8) Identifier |name| +//@[8:9) Colon |:| +//@[10:25) StringComplete |'existingChild'| +//@[25:27) NewLine |\n\n| + + resource existingGrandchild 'grandchildType' = { +//@[4:12) Identifier |resource| +//@[13:31) Identifier |existingGrandchild| +//@[32:48) StringComplete |'grandchildType'| +//@[49:50) Assignment |=| +//@[51:52) LeftBrace |{| +//@[52:53) NewLine |\n| + name: 'existingGrandchild' +//@[6:10) Identifier |name| +//@[10:11) Colon |:| +//@[12:32) StringComplete |'existingGrandchild'| +//@[32:33) NewLine |\n| + properties: { +//@[6:16) Identifier |properties| +//@[16:17) Colon |:| +//@[18:19) LeftBrace |{| +//@[19:20) NewLine |\n| + size: existingParent.properties.size +//@[8:12) Identifier |size| +//@[12:13) Colon |:| +//@[14:28) Identifier |existingParent| +//@[28:29) Dot |.| +//@[29:39) Identifier |properties| +//@[39:40) Dot |.| +//@[40:44) Identifier |size| +//@[44:45) NewLine |\n| + style: existingChild.properties.style +//@[8:13) Identifier |style| +//@[13:14) Colon |:| +//@[15:28) Identifier |existingChild| +//@[28:29) Dot |.| +//@[29:39) Identifier |properties| +//@[39:40) Dot |.| +//@[40:45) Identifier |style| +//@[45:46) NewLine |\n| + } +//@[6:7) RightBrace |}| +//@[7:8) NewLine |\n| + } +//@[4:5) RightBrace |}| +//@[5:6) NewLine |\n| + } +//@[2:3) RightBrace |}| +//@[3:4) NewLine |\n| +} +//@[0:1) RightBrace |}| +//@[1:3) NewLine |\n\n| + +param createParent bool +//@[0:5) Identifier |param| +//@[6:18) Identifier |createParent| +//@[19:23) Identifier |bool| +//@[23:24) NewLine |\n| +param createChild bool +//@[0:5) Identifier |param| +//@[6:17) Identifier |createChild| +//@[18:22) Identifier |bool| +//@[22:23) NewLine |\n| +param createGrandchild bool +//@[0:5) Identifier |param| +//@[6:22) Identifier |createGrandchild| +//@[23:27) Identifier |bool| +//@[27:28) NewLine |\n| +resource conditionParent 'My.Rp/parentType@2020-12-01' = if (createParent) { +//@[0:8) Identifier |resource| +//@[9:24) Identifier |conditionParent| +//@[25:54) StringComplete |'My.Rp/parentType@2020-12-01'| +//@[55:56) Assignment |=| +//@[57:59) Identifier |if| +//@[60:61) LeftParen |(| +//@[61:73) Identifier |createParent| +//@[73:74) RightParen |)| +//@[75:76) LeftBrace |{| +//@[76:77) NewLine |\n| + name: 'conditionParent' +//@[2:6) Identifier |name| +//@[6:7) Colon |:| +//@[8:25) StringComplete |'conditionParent'| +//@[25:27) NewLine |\n\n| + + resource conditionChild 'childType' = if (createChild) { +//@[2:10) Identifier |resource| +//@[11:25) Identifier |conditionChild| +//@[26:37) StringComplete |'childType'| +//@[38:39) Assignment |=| +//@[40:42) Identifier |if| +//@[43:44) LeftParen |(| +//@[44:55) Identifier |createChild| +//@[55:56) RightParen |)| +//@[57:58) LeftBrace |{| +//@[58:59) NewLine |\n| + name: 'conditionChild' +//@[4:8) Identifier |name| +//@[8:9) Colon |:| +//@[10:26) StringComplete |'conditionChild'| +//@[26:28) NewLine |\n\n| + + resource conditionGrandchild 'grandchildType' = if (createGrandchild) { +//@[4:12) Identifier |resource| +//@[13:32) Identifier |conditionGrandchild| +//@[33:49) StringComplete |'grandchildType'| +//@[50:51) Assignment |=| +//@[52:54) Identifier |if| +//@[55:56) LeftParen |(| +//@[56:72) Identifier |createGrandchild| +//@[72:73) RightParen |)| +//@[74:75) LeftBrace |{| +//@[75:76) NewLine |\n| + name: 'conditionGrandchild' +//@[6:10) Identifier |name| +//@[10:11) Colon |:| +//@[12:33) StringComplete |'conditionGrandchild'| +//@[33:34) NewLine |\n| + properties: { +//@[6:16) Identifier |properties| +//@[16:17) Colon |:| +//@[18:19) LeftBrace |{| +//@[19:20) NewLine |\n| + size: conditionParent.properties.size +//@[8:12) Identifier |size| +//@[12:13) Colon |:| +//@[14:29) Identifier |conditionParent| +//@[29:30) Dot |.| +//@[30:40) Identifier |properties| +//@[40:41) Dot |.| +//@[41:45) Identifier |size| +//@[45:46) NewLine |\n| + style: conditionChild.properties.style +//@[8:13) Identifier |style| +//@[13:14) Colon |:| +//@[15:29) Identifier |conditionChild| +//@[29:30) Dot |.| +//@[30:40) Identifier |properties| +//@[40:41) Dot |.| +//@[41:46) Identifier |style| +//@[46:47) NewLine |\n| + } +//@[6:7) RightBrace |}| +//@[7:8) NewLine |\n| + } +//@[4:5) RightBrace |}| +//@[5:6) NewLine |\n| + } +//@[2:3) RightBrace |}| +//@[3:4) NewLine |\n| +} +//@[0:1) RightBrace |}| +//@[1:1) EndOfFile || diff --git a/src/Bicep.Core.UnitTests/Assertions/DiagnosticCollectionExtensions.cs b/src/Bicep.Core.UnitTests/Assertions/DiagnosticCollectionExtensions.cs index 02a217952b3..6c96513ae36 100644 --- a/src/Bicep.Core.UnitTests/Assertions/DiagnosticCollectionExtensions.cs +++ b/src/Bicep.Core.UnitTests/Assertions/DiagnosticCollectionExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System; using System.Collections.Generic; +using System.Linq; using Bicep.Core.Diagnostics; using FluentAssertions; using FluentAssertions.Collections; @@ -23,6 +24,13 @@ public DiagnosticCollectionAssertions(IEnumerable diagnostics) { } + public AndConstraint BeEmpty() + { + AssertionExtensions.Should(Subject).BeEmpty("contained diagnostics: {0}", string.Join(Environment.NewLine, Subject.Select(d => d.ToString()))); + + return new AndConstraint(this); + } + public AndConstraint ContainDiagnostic(string code, DiagnosticLevel level, string message, string because = "", params object[] becauseArgs) { AssertionExtensions.Should(Subject).Contain(x => x.Code == code && x.Level == level && x.Message == message, because, becauseArgs); diff --git a/src/Bicep.Core.UnitTests/Parsing/ExpressionTestVisitor.cs b/src/Bicep.Core.UnitTests/Parsing/ExpressionTestVisitor.cs index 472cd7eb5aa..94043d7ed5f 100644 --- a/src/Bicep.Core.UnitTests/Parsing/ExpressionTestVisitor.cs +++ b/src/Bicep.Core.UnitTests/Parsing/ExpressionTestVisitor.cs @@ -45,6 +45,13 @@ public override void VisitPropertyAccessSyntax(PropertyAccessSyntax syntax) this.buffer.Append(')'); } + public override void VisitResourceAccessSyntax(ResourceAccessSyntax syntax) + { + this.buffer.Append('('); + base.VisitResourceAccessSyntax(syntax); + this.buffer.Append(')'); + } + public override void VisitArrayAccessSyntax(ArrayAccessSyntax syntax) { this.buffer.Append('('); diff --git a/src/Bicep.Core.UnitTests/Parsing/ParserTests.cs b/src/Bicep.Core.UnitTests/Parsing/ParserTests.cs index e0a8b95dd0c..dcef08b9ec3 100644 --- a/src/Bicep.Core.UnitTests/Parsing/ParserTests.cs +++ b/src/Bicep.Core.UnitTests/Parsing/ParserTests.cs @@ -246,9 +246,19 @@ public void PropertyAccessShouldParseSuccessfully(string text, string expected) RunExpressionTest(text, expected, typeof(PropertyAccessSyntax)); } + [DataTestMethod] + [DataRow("a:b","(a:b)")] + [DataRow("null:fail", "(null:fail)")] + [DataRow("foo():bar","(foo():bar)")] + public void ResourceAccessShouldParseSuccessfully(string text, string expected) + { + RunExpressionTest(text, expected, typeof(ResourceAccessSyntax)); + } + [DataTestMethod] [DataRow("a.b.c.foo()", "((a.b).c).foo()")] [DataRow("a.b.c.d.e.f.g.foo()", "((((((a.b).c).d).e).f).g).foo()")] + [DataRow("a:b:c.d:e:f:g.foo()", "((((((a:b):c).d):e):f):g).foo()")] public void InstanceFunctionCallShouldParseSuccessfully(string text, string expected) { RunExpressionTest(text, expected, typeof(InstanceFunctionCallSyntax)); @@ -263,6 +273,15 @@ public void MemberAccessShouldBeLeftToRightAssociative(string text, string expec RunExpressionTest(text, expected, typeof(BinaryOperationSyntax)); } + [DataTestMethod] + [DataRow("a:b:c + 0","(((a:b):c)+0)")] + [DataRow("(a:b[c]):c[d]+q()", "((((((a:b)[c])):c)[d])+q())")] + public void ResourceAccessShouldBeLeftToRightAssociative(string text, string expected) + { + // this also asserts that (), [], and . have equal precedence + RunExpressionTest(text, expected, typeof(BinaryOperationSyntax)); + } + [DataTestMethod] [DataRow("a + b.c * z[12].a && q[foo()] == c.a", "((a+((b.c)*((z[12]).a)))&&((q[foo()])==(c.a)))")] public void MemberAccessShouldHaveHighestPrecedence(string text, string expected) @@ -270,6 +289,13 @@ public void MemberAccessShouldHaveHighestPrecedence(string text, string expected RunExpressionTest(text, expected, typeof(BinaryOperationSyntax)); } + [DataTestMethod] + [DataRow("a + b:c * z[12]:a && q[foo()] == c:a", "((a+((b:c)*((z[12]):a)))&&((q[foo()])==(c:a)))")] + public void ResourceAccessShouldHaveHighestPrecedence(string text, string expected) + { + RunExpressionTest(text, expected, typeof(BinaryOperationSyntax)); + } + [DataTestMethod] [DataRow("a[b]","(a[b])")] [DataRow("1[b]", "(1[b])")] diff --git a/src/Bicep.Core.UnitTests/Resource/ResourceTypeReferenceTests.cs b/src/Bicep.Core.UnitTests/Resource/ResourceTypeReferenceTests.cs index 5df9687651b..1835ab56138 100644 --- a/src/Bicep.Core.UnitTests/Resource/ResourceTypeReferenceTests.cs +++ b/src/Bicep.Core.UnitTests/Resource/ResourceTypeReferenceTests.cs @@ -1,6 +1,5 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -using System; using Bicep.Core.Resources; using FluentAssertions; using Microsoft.VisualStudio.TestTools.UnitTesting; @@ -58,6 +57,58 @@ public void ValidType_FullyQualitifedTypeShouldBeCorrect(string value, string ex actual.Should().NotBeNull(); actual!.FullyQualifiedType.Should().Be(expectedFullyQualifiedType); } + + [DataTestMethod] + [DataRow("Microsoft.Compute/virtualMachines@2019-06-01")] // has a slash + [DataRow("Microsoft.Blueprint/blueprints/versions/artifacts@2018-11-01-preview")] // full type name + [DataRow("/artifacts@2018-11-01-preview")] // leading slash + [DataRow("artifacts@")] // version delimiter but no version + [DataRow("artifacts@2018-11-012222-preview")] // invalid version + public void TryParseSingleTypeSegment_InvalidTypeSegmentIsRejected(string value) + { + var success = ResourceTypeReference.TryParseSingleTypeSegment(value, out var type, out var version); + success.Should().BeFalse($"For input '{value}': type was '{type}', version was '{version}'"); + type.Should().BeNull(); + version.Should().BeNull(); + } + + [DataTestMethod] + [DataRow("virtualMachines", "virtualMachines", (string?)null)] + [DataRow("virtualMachines@2019-06-01", "virtualMachines", "2019-06-01")] + [DataRow("artifacts@2018-11-01-preview", "artifacts", "2018-11-01-preview")] + public void TryParseSingleTypeSegment_TypeSegmentIsParsed(string value, string expectedType, string expectedVersion) + { + var success = ResourceTypeReference.TryParseSingleTypeSegment(value, out var type, out var version); + success.Should().BeTrue($"For input '{value}': type was '{type}', version was '{version}'"); + type.Should().BeEquivalentTo(expectedType); + version.Should().BeEquivalentTo(expectedVersion); + } + + [TestMethod] + public void TryCombine_RejectsInvalidTypeSegment() + { + var baseType = ResourceTypeReference.Parse("My.RP/someType@2020-01-01"); + var typeSegments = new [] { "childType@2019-06-01", "childType/grandChildType", }; + var actual = ResourceTypeReference.TryCombine(baseType, typeSegments); + + actual.Should().BeNull(); + } + + [DataTestMethod] + [DataRow("My.RP/someType@2020-01-01", new string[]{ "childType", }, "My.RP/someType/childType@2020-01-01")] + [DataRow("My.RP/someType@2020-01-01", new string[]{ "childType", "grandchildType",}, "My.RP/someType/childType/grandchildType@2020-01-01")] + [DataRow("My.RP/someType@2020-01-01", new string[]{ "childType", "grandchildType", "greatGrandchildType"}, "My.RP/someType/childType/grandchildType/greatGrandchildType@2020-01-01")] + [DataRow("My.RP/someType@2020-01-01", new string[]{ "childType@2020-01-02", }, "My.RP/someType/childType@2020-01-02")] + [DataRow("My.RP/someType@2020-01-01", new string[]{ "childType", "grandchildType@2020-01-03", }, "My.RP/someType/childType/grandchildType@2020-01-03")] + [DataRow("My.RP/someType@2020-01-01", new string[]{ "childType@2020-01-02", "grandchildType", }, "My.RP/someType/childType/grandchildType@2020-01-02")] + public void TryCombine_CombinesValidTypeSegments(string baseTypeText, string[] typeSegments, string expected) + { + var baseType = ResourceTypeReference.Parse(baseTypeText); + var actual = ResourceTypeReference.TryCombine(baseType, typeSegments); + + actual.Should().NotBeNull(); + actual!.FormatName().Should().BeEquivalentTo(expected); + } } } diff --git a/src/Bicep.Core.UnitTests/Utils/ParserHelper.cs b/src/Bicep.Core.UnitTests/Utils/ParserHelper.cs index 9939d294fed..5c5a86bbe0e 100644 --- a/src/Bicep.Core.UnitTests/Utils/ParserHelper.cs +++ b/src/Bicep.Core.UnitTests/Utils/ParserHelper.cs @@ -15,7 +15,7 @@ public static ProgramSyntax Parse(string text) return parser.Program(); } - public static SyntaxBase ParseExpression(string text, bool allowComplexLiterals = true) => new Parser(text).Expression(allowComplexLiterals); + public static SyntaxBase ParseExpression(string text, ExpressionFlags expressionFlags = ExpressionFlags.AllowComplexLiterals) => new Parser(text).Expression(expressionFlags); } } diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index c4a4a53a702..9bc231b125f 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -10,6 +10,7 @@ using Bicep.Core.Parsing; using Bicep.Core.Resources; using Bicep.Core.Semantics; +using Bicep.Core.Syntax; using Bicep.Core.TypeSystem; namespace Bicep.Core.Diagnostics @@ -864,6 +865,26 @@ public Diagnostic RuntimePropertyNotAllowed(string property, IEnumerable TextSpan, "BCP152", $"Function \"{functionName}\" cannot be used as a decorator."); + + public ErrorDiagnostic InvalidResourceTypeSegment(string typeSegment) => new( + TextSpan, + "BCP153", + $"The resource type segment \"{typeSegment}\" is invalid. Nested resources must specify a single type segment, and optionally can specify an api version using the format \"@\"."); + + public ErrorDiagnostic InvalidAncestorResourceType(string resourceName) => new( + TextSpan, + "BCP154", + $"The resource type cannot be determined due to an error in containing resource \"{resourceName}\"."); + + public ErrorDiagnostic ResourceRequiredForResourceAccess(string wrongType) => new( + TextSpan, + "BCP155", + $"Cannot access nested resources of type \"{wrongType}\". A resource type is required."); + + public ErrorDiagnostic NestedResourceNotFound(string resourceName, string identifierName, IEnumerable nestedResourceNames) => new( + TextSpan, + "BCP156", + $"The resource \"{resourceName}\" does not contain a nested resource named \"{identifierName}\". Known nested resources are: {string.Join(", ", nestedResourceNames.Select(r => $"\"{r}\""))}."); } public static DiagnosticBuilderInternal ForPosition(TextSpan span) diff --git a/src/Bicep.Core/Emit/EmitLimitationCalculator.cs b/src/Bicep.Core/Emit/EmitLimitationCalculator.cs index 5ac6988effa..47e15304200 100644 --- a/src/Bicep.Core/Emit/EmitLimitationCalculator.cs +++ b/src/Bicep.Core/Emit/EmitLimitationCalculator.cs @@ -83,7 +83,7 @@ private static IEnumerable GetModuleDefinitions(SemanticModel private static IEnumerable GetResourceDefinitions(SemanticModel semanticModel, ImmutableDictionary resourceScopeData) { - foreach (var resource in semanticModel.Root.ResourceDeclarations) + foreach (var resource in semanticModel.Root.GetAllResourceDeclarations()) { if (resource.DeclaringResource.IsExistingResource()) { @@ -91,9 +91,15 @@ private static IEnumerable GetResourceDefinitions(SemanticMo continue; } - if (!resourceScopeData.TryGetValue(resource, out var scopeData)) + // Determine the scope - this is either something like a resource group/subscription or another resource + ResourceSymbol? scopeSymbol; + if (resourceScopeData.TryGetValue(resource, out var scopeData) && scopeData.ResourceScopeSymbol is ResourceSymbol) { - scopeData = null; + scopeSymbol = scopeData.ResourceScopeSymbol; + } + else + { + scopeSymbol = semanticModel.ResourceAncestors.GetAncestors(resource).LastOrDefault(); } if (resource.Type is not ResourceType resourceType || resource.SafeGetBodyPropertyValue(LanguageConstants.ResourceNamePropertyName) is not StringSyntax namePropertyValue) @@ -102,7 +108,7 @@ private static IEnumerable GetResourceDefinitions(SemanticMo continue; } - yield return new ResourceDefinition(resource.Name, scopeData?.ResourceScopeSymbol, resourceType.TypeReference.FullyQualifiedType, namePropertyValue); + yield return new ResourceDefinition(resource.Name, scopeSymbol, resourceType.TypeReference.FullyQualifiedType, namePropertyValue); } } } diff --git a/src/Bicep.Core/Emit/ExpressionConverter.cs b/src/Bicep.Core/Emit/ExpressionConverter.cs index d11a2961b3c..65c1c6ddb86 100644 --- a/src/Bicep.Core/Emit/ExpressionConverter.cs +++ b/src/Bicep.Core/Emit/ExpressionConverter.cs @@ -8,6 +8,7 @@ using Azure.Deployments.Core.Extensions; using Azure.Deployments.Expression.Expressions; using Bicep.Core.Extensions; +using Bicep.Core.Parsing; using Bicep.Core.Resources; using Bicep.Core.Semantics; using Bicep.Core.Syntax; @@ -98,6 +99,9 @@ public LanguageExpression ConvertExpression(SyntaxBase expression) case ArrayAccessSyntax arrayAccess: return ConvertArrayAccess(arrayAccess); + case ResourceAccessSyntax resourceAccess: + throw new InvalidOperationException("A resource access expression should not be emitted. The member access following it must handle this case."); + case PropertyAccessSyntax propertyAccess: return ConvertPropertyAccess(propertyAccess); @@ -123,7 +127,7 @@ SyntaxBase GetArrayExpression(LocalVariableSymbol localVariable) } var inaccessibleLocals = this.context.DataFlowAnalyzer.GetInaccessibleLocalsAfterSyntaxMove(nameSyntax, newContext); - switch(inaccessibleLocals.Count) + switch (inaccessibleLocals.Count) { case 0: // moving the name expression does not produce any inaccessible locals @@ -150,9 +154,9 @@ private LanguageExpression ConvertArrayAccess(ArrayAccessSyntax arrayAccess) { switch (this.context.SemanticModel.GetSymbolInfo(variableAccess)) { - case ResourceSymbol {IsCollection: true} resourceSymbol: + case ResourceSymbol { IsCollection: true } resourceSymbol: var resourceConverter = this.CreateConverterForIndexReplacement(ExpressionConverter.GetResourceNameSyntax(resourceSymbol), arrayAccess.IndexExpression, arrayAccess); - + // TODO: Can this return a language expression? return resourceConverter.ToFunctionExpression(arrayAccess.BaseExpression); @@ -206,8 +210,8 @@ private LanguageExpression ConvertPropertyAccess(PropertyAccessSyntax propertyAc return null; } - if (propertyAccess.BaseExpression is VariableAccessSyntax propVariableAccess && - context.SemanticModel.GetSymbolInfo(propVariableAccess) is ResourceSymbol resourceSymbol && + if ((propertyAccess.BaseExpression is VariableAccessSyntax || propertyAccess.BaseExpression is ResourceAccessSyntax) && + context.SemanticModel.GetSymbolInfo(propertyAccess.BaseExpression) is ResourceSymbol resourceSymbol && ConvertResourcePropertyAccess(resourceSymbol, indexExpression: null) is { } convertedSingle) { // we are doing property access on a single resource @@ -215,9 +219,9 @@ private LanguageExpression ConvertPropertyAccess(PropertyAccessSyntax propertyAc return convertedSingle; } - if(propertyAccess.BaseExpression is ArrayAccessSyntax propArrayAccess && + if (propertyAccess.BaseExpression is ArrayAccessSyntax propArrayAccess && propArrayAccess.BaseExpression is VariableAccessSyntax arrayVariableAccess && - context.SemanticModel.GetSymbolInfo(arrayVariableAccess) is ResourceSymbol resourceCollectionSymbol && + context.SemanticModel.GetSymbolInfo(arrayVariableAccess) is ResourceSymbol resourceCollectionSymbol && ConvertResourcePropertyAccess(resourceCollectionSymbol, propArrayAccess.IndexExpression) is { } convertedCollection) { @@ -257,16 +261,45 @@ grandChildArrayAccess.BaseExpression is VariableAccessSyntax grandGrandChildVari new JTokenExpression(propertyAccess.PropertyName.IdentifierName)); } - private LanguageExpression GetResourceNameExpression(ResourceSymbol resourceSymbol) + public LanguageExpression GetResourceNameExpression(ResourceSymbol resourceSymbol) { - SyntaxBase nameValueSyntax = GetResourceNameSyntax(resourceSymbol); - return this.ConvertExpression(nameValueSyntax); + var nameValueSyntax = GetResourceNameSyntax(resourceSymbol); + + // For a nested resource we need to compute the name + var ancestors = this.context.SemanticModel.ResourceAncestors.GetAncestors(resourceSymbol); + if (ancestors.Length == 0) + { + return ConvertExpression(nameValueSyntax); + } + + // Build an expression like '${parent.name}/${child.name}' + // + // This is a call to the `format` function with the first arg as a format string + // and the remaining args the actual name segments. + // + // args.Length = 1 (format string) + N (ancestor names) + 1 (resource name) + var args = new LanguageExpression[ancestors.Length + 2]; + + // {0}/{1}/{2}.... + var format = string.Join("/", Enumerable.Range(0, ancestors.Length + 1).Select(i => $"{{{i}}}")); + args[0] = new JTokenExpression(format); + + for (var i = 0; i < ancestors.Length; i++) + { + var ancestor = ancestors[i]; + var segment = GetResourceNameSyntax(ancestor); + args[i + 1] = ConvertExpression(segment); + } + + args[args.Length - 1] = ConvertExpression(nameValueSyntax); + + return CreateFunction("format", args); } public static SyntaxBase GetResourceNameSyntax(ResourceSymbol resourceSymbol) { // this condition should have already been validated by the type checker - return resourceSymbol.SafeGetBodyPropertyValue(LanguageConstants.ResourceNamePropertyName) ?? throw new ArgumentException($"Expected resource syntax body to contain property 'name'"); + return resourceSymbol.UnsafeGetBodyPropertyValue(LanguageConstants.ResourceNamePropertyName); } private LanguageExpression GetModuleNameExpression(ModuleSymbol moduleSymbol) @@ -374,14 +407,14 @@ private LanguageExpression GetLocalVariableExpression(LocalVariableSymbol localV case ForSyntax @for when ReferenceEquals(@for.ItemVariable, localVariableSymbol.DeclaringLocalVariable): // this is the "item" variable of a for-expression // to emit this we need to basically index the array expression by the copyIndex() function - - if(this.localReplacements.TryGetValue(localVariableSymbol, out var replacement)) + + if (this.localReplacements.TryGetValue(localVariableSymbol, out var replacement)) { // the current context has specified an expression to be used for this local variable symbol // to override the regular conversion to copyIndex() return replacement; } - + var arrayExpression = ToFunctionExpression(@for.Expression); var copyIndexName = this.context.SemanticModel.Binder.GetParent(@for) switch @@ -399,7 +432,7 @@ ObjectPropertySyntax property when property.TryGetKeyText() is { } key && Refere }; var copyIndexFunction = copyIndexName == null ? CreateFunction("copyIndex") : CreateFunction("copyIndex", new JTokenExpression(copyIndexName)); - + return AppendProperties(arrayExpression, copyIndexFunction); default: diff --git a/src/Bicep.Core/Emit/ExpressionEmitter.cs b/src/Bicep.Core/Emit/ExpressionEmitter.cs index ae524ad5db3..ce955629da3 100644 --- a/src/Bicep.Core/Emit/ExpressionEmitter.cs +++ b/src/Bicep.Core/Emit/ExpressionEmitter.cs @@ -79,6 +79,7 @@ public void EmitExpression(SyntaxBase syntax) case FunctionCallSyntax _: case ArrayAccessSyntax _: case PropertyAccessSyntax _: + case ResourceAccessSyntax _: case VariableAccessSyntax _: EmitLanguageExpression(syntax); @@ -117,6 +118,11 @@ public void EmitResourceIdReference(ModuleSymbol moduleSymbol, SyntaxBase? index writer.WriteValue(serialized); } + public LanguageExpression GetResourceNameExpression(ResourceSymbol resourceSymbol) + { + return converter.GetResourceNameExpression(resourceSymbol); + } + public LanguageExpression GetManagementGroupResourceId(SyntaxBase managementGroupNameProperty, bool fullyQualified) => converter.GenerateManagementGroupResourceId(managementGroupNameProperty, fullyQualified); @@ -129,6 +135,11 @@ public void EmitLanguageExpression(SyntaxBase syntax) return; } + if (syntax is ResourceAccessSyntax resourceAccess) + { + return; + } + if (syntax is FunctionCallSyntax functionCall && symbol is FunctionSymbol functionSymbol && string.Equals(functionSymbol.Name, "any", LanguageConstants.IdentifierComparison)) diff --git a/src/Bicep.Core/Emit/ResourceDependencyVisitor.cs b/src/Bicep.Core/Emit/ResourceDependencyVisitor.cs index 05df2a95edf..a6c7c5e37e3 100644 --- a/src/Bicep.Core/Emit/ResourceDependencyVisitor.cs +++ b/src/Bicep.Core/Emit/ResourceDependencyVisitor.cs @@ -55,11 +55,14 @@ public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax sy throw new InvalidOperationException("Unbound declaration"); } + // Resource ancestors are always dependencies. + var ancestors = this.model.ResourceAncestors.GetAncestors(resourceSymbol); + // save previous declaration as we may call this recursively var prevDeclaration = this.currentDeclaration; this.currentDeclaration = resourceSymbol; - this.resourceDependencies[resourceSymbol] = new HashSet(); + this.resourceDependencies[resourceSymbol] = new HashSet(ancestors.Select(a => new ResourceDependency(a, null))); base.VisitResourceDeclarationSyntax(syntax); // restore previous declaration diff --git a/src/Bicep.Core/Emit/ScopeHelper.cs b/src/Bicep.Core/Emit/ScopeHelper.cs index c4e18d6938f..68e32961053 100644 --- a/src/Bicep.Core/Emit/ScopeHelper.cs +++ b/src/Bicep.Core/Emit/ScopeHelper.cs @@ -354,7 +354,7 @@ void logInvalidScopeDiagnostic(IPositionable positionable, ResourceScope supplie var scopeInfo = new Dictionary(); - foreach (var resourceSymbol in semanticModel.Root.ResourceDeclarations) + foreach (var resourceSymbol in semanticModel.Root.GetAllResourceDeclarations()) { var resourceType = GetResourceType(resourceSymbol); if (resourceType is null) diff --git a/src/Bicep.Core/Emit/TemplateWriter.cs b/src/Bicep.Core/Emit/TemplateWriter.cs index 83fe22c08f7..e04030dff86 100644 --- a/src/Bicep.Core/Emit/TemplateWriter.cs +++ b/src/Bicep.Core/Emit/TemplateWriter.cs @@ -3,11 +3,13 @@ using System; using System.IO; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; using Azure.Deployments.Core.Extensions; using Azure.Deployments.Expression.Expressions; using Bicep.Core.Extensions; +using Bicep.Core.Parsing; using Bicep.Core.Semantics; using Bicep.Core.Syntax; using Bicep.Core.TypeSystem; @@ -36,6 +38,7 @@ public class TemplateWriter private static readonly ImmutableHashSet ResourcePropertiesToOmit = new [] { LanguageConstants.ResourceScopePropertyName, LanguageConstants.ResourceDependsOnPropertyName, + LanguageConstants.ResourceNamePropertyName, }.ToImmutableHashSet(); private static readonly ImmutableHashSet ModulePropertiesToOmit = new [] { @@ -287,7 +290,7 @@ private void EmitResources(JsonTextWriter memoryWriter, ExpressionEmitter emitte memoryWriter.WritePropertyName("resources"); memoryWriter.WriteStartArray(); - foreach (var resourceSymbol in this.context.SemanticModel.Root.ResourceDeclarations) + foreach (var resourceSymbol in this.context.SemanticModel.Root.GetAllResourceDeclarations()) { if (resourceSymbol.DeclaringResource.IsExistingResource()) { @@ -310,26 +313,83 @@ private void EmitResource(JsonTextWriter memoryWriter, ResourceSymbol resourceSy memoryWriter.WriteStartObject(); var typeReference = EmitHelpers.GetTypeReference(resourceSymbol); - SyntaxBase body = resourceSymbol.DeclaringResource.Value; + + // Note: conditions STACK with nesting. + // + // Children inherit the conditions of their parents, etc. This avoids a problem + // where we emit a dependsOn to something that's not in the template, or not + // being evaulated i the template. + var conditions = new List(); + var loops = new List<(string name, ForSyntax @for, SyntaxBase? input)>(); + + var ancestors = this.context.SemanticModel.ResourceAncestors.GetAncestors(resourceSymbol); + foreach (var ancestor in ancestors) + { + if (ancestor.DeclaringResource.Value is IfConditionSyntax ifCondition) + { + conditions.Add(ifCondition.ConditionExpression); + } + + if (ancestor.DeclaringResource.Value is ForSyntax @for) + { + loops.Add((ancestor.Name, @for, null)); + } + } + + // Unwrap the 'real' resource body if there's a condition + var body = resourceSymbol.DeclaringResource.Value; switch (body) { case IfConditionSyntax ifCondition: body = ifCondition.Body; - emitter.EmitProperty("condition", ifCondition.ConditionExpression); + conditions.Add(ifCondition.ConditionExpression); break; case ForSyntax @for: body = @for.Body; - emitter.EmitProperty("copy", () => emitter.EmitCopyObject(resourceSymbol.Name, @for, input: null)); + loops.Add((resourceSymbol.Name, @for, null)); break; } + if (conditions.Count == 1) + { + emitter.EmitProperty("condition", conditions[0]); + } + else if (conditions.Count > 1) + { + var @operator = new BinaryOperationSyntax( + conditions[0], + SyntaxFactory.CreateToken(TokenType.LogicalAnd), + conditions[1]); + for (var i = 2; i < conditions.Count; i++) + { + @operator = new BinaryOperationSyntax( + @operator, + SyntaxFactory.CreateToken(TokenType.LogicalAnd), + conditions[i]); + } + + emitter.EmitProperty("condition", @operator); + } + + if (loops.Count == 1) + { + emitter.EmitProperty("copy", () => emitter.EmitCopyObject(loops[0].name, loops[0].@for, loops[0].input)); + } + else if (loops.Count > 1) + { + throw new InvalidOperationException("nested loops are not supported"); + } + emitter.EmitProperty("type", typeReference.FullyQualifiedType); emitter.EmitProperty("apiVersion", typeReference.ApiVersion); if (context.SemanticModel.EmitLimitationInfo.ResourceScopeData.TryGetValue(resourceSymbol, out var scopeData) && scopeData.ResourceScopeSymbol is { } scopeResource) { emitter.EmitProperty("scope", () => emitter.EmitUnqualifiedResourceId(scopeResource)); } + + emitter.EmitProperty("name", emitter.GetResourceNameExpression(resourceSymbol)); + emitter.EmitObjectProperties((ObjectSyntax)body, ResourcePropertiesToOmit); this.EmitDependsOn(memoryWriter, resourceSymbol, emitter, body); diff --git a/src/Bicep.Core/Parsing/ExpressionFlags.cs b/src/Bicep.Core/Parsing/ExpressionFlags.cs new file mode 100644 index 00000000000..a0d7c03fc40 --- /dev/null +++ b/src/Bicep.Core/Parsing/ExpressionFlags.cs @@ -0,0 +1,24 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Globalization; +using System.Linq; +using Bicep.Core.Diagnostics; +using Bicep.Core.Extensions; +using Bicep.Core.Navigation; +using Bicep.Core.Syntax; + +namespace Bicep.Core.Parsing +{ + [Flags] + public enum ExpressionFlags + { + None = 0, + AllowComplexLiterals = 1, + AllowResourceDeclarations = 2, + InsideColonDelimitedContext = 4, + } +} \ No newline at end of file diff --git a/src/Bicep.Core/Parsing/Parser.cs b/src/Bicep.Core/Parsing/Parser.cs index c24a3105e35..6c30a964b5d 100644 --- a/src/Bicep.Core/Parsing/Parser.cs +++ b/src/Bicep.Core/Parsing/Parser.cs @@ -148,11 +148,27 @@ private static RecoveryFlags GetSuppressionFlag(SyntaxBase precedingNode, bool p return predicate ? RecoveryFlags.None : RecoveryFlags.SuppressDiagnostics; } + private static bool HasExpressionFlag(ExpressionFlags flags, ExpressionFlags check) + { + // Use this instead of Enum.HasFlag which boxes the enum and allocates. + return (flags & check) == check; + } + + private static ExpressionFlags WithExpressionFlag(ExpressionFlags flags, ExpressionFlags set) + { + return flags | set; + } + + private static ExpressionFlags WithoutExpressionFlag(ExpressionFlags flags, ExpressionFlags unset) + { + return flags & ~unset; + } + private SyntaxBase TargetScope(IEnumerable leadingNodes) { var keyword = ExpectKeyword(LanguageConstants.TargetScopeKeyword); var assignment = this.WithRecovery(this.Assignment, RecoveryFlags.None, TokenType.NewLine); - var value = this.WithRecovery(() => this.Expression(allowComplexLiterals: true), RecoveryFlags.None, TokenType.NewLine); + var value = this.WithRecovery(() => this.Expression(ExpressionFlags.AllowComplexLiterals), RecoveryFlags.None, TokenType.NewLine); return new TargetScopeSyntax(leadingNodes, keyword, assignment, value); } @@ -167,7 +183,7 @@ private SyntaxBase Decorator() if (Check(TokenType.LeftParen)) { - var functionCall = FunctionCallAccess(identifier, true); + var functionCall = FunctionCallAccess(identifier, ExpressionFlags.AllowComplexLiterals); current = new FunctionCallSyntax( functionCall.Identifier, @@ -188,7 +204,7 @@ private SyntaxBase Decorator() if (Check(TokenType.LeftParen)) { - var functionCall = FunctionCallAccess(identifier, true); + var functionCall = FunctionCallAccess(identifier, ExpressionFlags.AllowComplexLiterals); current = new InstanceFunctionCallSyntax( current, @@ -233,7 +249,7 @@ private SyntaxBase ParameterDeclaration(IEnumerable leadingNodes) TokenType.Assignment => this.ParameterDefaultValue(), // modifier is specified - TokenType.LeftBrace => this.Object(), + TokenType.LeftBrace => this.Object(ExpressionFlags.AllowComplexLiterals), _ => throw new ExpectedTokenException(current, b => b.ExpectedParameterContinuation()) }; @@ -247,7 +263,7 @@ private SyntaxBase ParameterDeclaration(IEnumerable leadingNodes) private SyntaxBase ParameterDefaultValue() { var assignmentToken = this.Expect(TokenType.Assignment, b => b.ExpectedCharacter("=")); - SyntaxBase defaultValue = this.WithRecovery(() => this.Expression(allowComplexLiterals: true), RecoveryFlags.None, TokenType.NewLine); + SyntaxBase defaultValue = this.WithRecovery(() => this.Expression(ExpressionFlags.AllowComplexLiterals), RecoveryFlags.None, TokenType.NewLine); return new ParameterDefaultValueSyntax(assignmentToken, defaultValue); } @@ -257,7 +273,7 @@ private SyntaxBase VariableDeclaration(IEnumerable leadingNodes) var keyword = ExpectKeyword(LanguageConstants.VariableKeyword); var name = this.IdentifierWithRecovery(b => b.ExpectedVariableIdentifier(), TokenType.Assignment, TokenType.NewLine); var assignment = this.WithRecovery(this.Assignment, GetSuppressionFlag(name), TokenType.NewLine); - var value = this.WithRecovery(() => this.Expression(allowComplexLiterals: true), GetSuppressionFlag(assignment), TokenType.NewLine); + var value = this.WithRecovery(() => this.Expression(ExpressionFlags.AllowComplexLiterals), GetSuppressionFlag(assignment), TokenType.NewLine); return new VariableDeclarationSyntax(leadingNodes, keyword, name, assignment, value); } @@ -268,7 +284,7 @@ private SyntaxBase OutputDeclaration(IEnumerable leadingNodes) var name = this.IdentifierWithRecovery(b => b.ExpectedOutputIdentifier(), TokenType.Identifier, TokenType.NewLine); var type = this.WithRecovery(() => Type(b => b.ExpectedOutputType()), GetSuppressionFlag(name), TokenType.Assignment, TokenType.NewLine); var assignment = this.WithRecovery(this.Assignment, GetSuppressionFlag(type), TokenType.NewLine); - var value = this.WithRecovery(() => this.Expression(allowComplexLiterals: true), GetSuppressionFlag(assignment), TokenType.NewLine); + var value = this.WithRecovery(() => this.Expression(ExpressionFlags.AllowComplexLiterals), GetSuppressionFlag(assignment), TokenType.NewLine); return new OutputDeclarationSyntax(leadingNodes, keyword, name, type, assignment, value); } @@ -298,9 +314,9 @@ private SyntaxBase ResourceDeclaration(IEnumerable leadingNodes) var current = reader.Peek(); return current.Type switch { - TokenType.Identifier when current.Text == LanguageConstants.IfKeyword => this.IfCondition(), - TokenType.LeftBrace => this.Object(), - TokenType.LeftSquare => this.ForExpression(requireObjectLiteral: true), + TokenType.Identifier when current.Text == LanguageConstants.IfKeyword => this.IfCondition(ExpressionFlags.AllowResourceDeclarations | ExpressionFlags.AllowComplexLiterals), + TokenType.LeftBrace => this.Object(ExpressionFlags.AllowResourceDeclarations | ExpressionFlags.AllowComplexLiterals), + TokenType.LeftSquare => this.ForExpression(ExpressionFlags.AllowResourceDeclarations | ExpressionFlags.AllowComplexLiterals, requireObjectLiteral: true), _ => throw new ExpectedTokenException(current, b => b.ExpectBodyStartOrIfOrLoopStart()) }; }, @@ -327,9 +343,9 @@ private SyntaxBase ModuleDeclaration(IEnumerable leadingNodes) var current = reader.Peek(); return current.Type switch { - TokenType.Identifier when current.Text == LanguageConstants.IfKeyword => this.IfCondition(), - TokenType.LeftBrace => this.Object(), - TokenType.LeftSquare => this.ForExpression(requireObjectLiteral: true), + TokenType.Identifier when current.Text == LanguageConstants.IfKeyword => this.IfCondition(ExpressionFlags.AllowComplexLiterals), + TokenType.LeftBrace => this.Object(ExpressionFlags.AllowComplexLiterals), + TokenType.LeftSquare => this.ForExpression(ExpressionFlags.AllowComplexLiterals, requireObjectLiteral: true), _ => throw new ExpectedTokenException(current, b => b.ExpectBodyStartOrIfOrLoopStart()) }; }, @@ -355,16 +371,16 @@ private Token NewLine() return Expect(TokenType.NewLine, b => b.ExpectedNewLine()); } - public SyntaxBase Expression(bool allowComplexLiterals) + public SyntaxBase Expression(ExpressionFlags expressionFlags) { - var candidate = this.BinaryExpression(allowComplexLiterals); + var candidate = this.BinaryExpression(expressionFlags); if (this.Check(TokenType.Question)) { var question = this.reader.Read(); - var trueExpression = this.Expression(allowComplexLiterals); + var trueExpression = this.Expression(WithExpressionFlag(expressionFlags, ExpressionFlags.InsideColonDelimitedContext)); var colon = this.Expect(TokenType.Colon, b => b.ExpectedCharacter(":")); - var falseExpression = this.Expression(allowComplexLiterals); + var falseExpression = this.Expression(expressionFlags); return new TernaryOperationSyntax(candidate, question, trueExpression, colon, falseExpression); } @@ -372,9 +388,9 @@ public SyntaxBase Expression(bool allowComplexLiterals) return candidate; } - private SyntaxBase BinaryExpression(bool allowComplexLiterals, int precedence = 0) + private SyntaxBase BinaryExpression(ExpressionFlags expressionFlags, int precedence = 0) { - var current = this.UnaryExpression(allowComplexLiterals); + var current = this.UnaryExpression(expressionFlags); while (true) { @@ -391,14 +407,14 @@ private SyntaxBase BinaryExpression(bool allowComplexLiterals, int precedence = this.reader.Read(); - SyntaxBase rightExpression = this.BinaryExpression(allowComplexLiterals, operatorPrecedence); + SyntaxBase rightExpression = this.BinaryExpression(expressionFlags, operatorPrecedence); current = new BinaryOperationSyntax(current, candidateOperatorToken, rightExpression); } return current; } - private SyntaxBase UnaryExpression(bool allowComplexLiterals) + private SyntaxBase UnaryExpression(ExpressionFlags expressionFlags) { Token operatorToken = this.reader.Peek(); @@ -406,16 +422,16 @@ private SyntaxBase UnaryExpression(bool allowComplexLiterals) { this.reader.Read(); - var expression = this.MemberExpression(allowComplexLiterals); + var expression = this.MemberExpression(expressionFlags); return new UnaryOperationSyntax(operatorToken, expression); } - return this.MemberExpression(allowComplexLiterals); + return this.MemberExpression(expressionFlags); } - private SyntaxBase MemberExpression(bool allowComplexLiterals) + private SyntaxBase MemberExpression(ExpressionFlags expressionFlags) { - var current = this.PrimaryExpression(allowComplexLiterals); + var current = this.PrimaryExpression(expressionFlags); while (true) { @@ -434,7 +450,7 @@ private SyntaxBase MemberExpression(bool allowComplexLiterals) } else { - SyntaxBase indexExpression = this.Expression(allowComplexLiterals); + SyntaxBase indexExpression = this.Expression(expressionFlags); Token closeSquare = this.Expect(TokenType.RightSquare, b => b.ExpectedCharacter("]")); current = new ArrayAccessSyntax(current, openSquare, indexExpression, closeSquare); @@ -452,7 +468,7 @@ private SyntaxBase MemberExpression(bool allowComplexLiterals) if (Check(TokenType.LeftParen)) { - var functionCall = FunctionCallAccess(identifier, allowComplexLiterals); + var functionCall = FunctionCallAccess(identifier, expressionFlags); // gets instance function call current = new InstanceFunctionCallSyntax( @@ -471,13 +487,32 @@ private SyntaxBase MemberExpression(bool allowComplexLiterals) continue; } + if (this.Check(TokenType.Colon) && !HasExpressionFlag(expressionFlags, ExpressionFlags.InsideColonDelimitedContext)) + { + // colon operator (nested resource lookup) + // + // We want a ternary to bind with higher precedance than a resource-access inside the "true" part + // ex: a ? b : c -> a ? (b) : (c) and NOT a ? (b:c) + // this implies that a resource-access expression will require parenthesis inside a ternary's "true" part + // + // We can't easily do this the other way because a ternary is right-associative and member/resource access + // are left-associative. + // + // The same is true of a for-expression. A colon is used as the right-delimiter. + var colon = this.reader.Read(); + var identifier = this.IdentifierOrSkip(b => b.ExpectedFunctionOrPropertyName()); + current = new ResourceAccessSyntax(current, colon, identifier); + + continue; + } + break; } return current; } - private SyntaxBase PrimaryExpression(bool allowComplexLiterals) + private SyntaxBase PrimaryExpression(ExpressionFlags expressionFlags) { Token nextToken = this.reader.Peek(); @@ -496,12 +531,12 @@ private SyntaxBase PrimaryExpression(bool allowComplexLiterals) case TokenType.MultilineString: return this.MultilineString(); - case TokenType.LeftBrace when allowComplexLiterals: - return this.Object(); + case TokenType.LeftBrace when HasExpressionFlag(expressionFlags, ExpressionFlags.AllowComplexLiterals): + return this.Object(expressionFlags); - case TokenType.LeftSquare when allowComplexLiterals: + case TokenType.LeftSquare when HasExpressionFlag(expressionFlags, ExpressionFlags.AllowComplexLiterals): return CheckKeyword(this.reader.PeekAhead(), LanguageConstants.ForKeyword) - ? this.ForExpression(requireObjectLiteral: false) + ? this.ForExpression(expressionFlags, requireObjectLiteral: false) : this.Array(); case TokenType.LeftBrace: @@ -509,32 +544,32 @@ private SyntaxBase PrimaryExpression(bool allowComplexLiterals) throw new ExpectedTokenException(nextToken, b => b.ComplexLiteralsNotAllowed()); case TokenType.LeftParen: - return this.ParenthesizedExpression(allowComplexLiterals); + return this.ParenthesizedExpression(expressionFlags); case TokenType.Identifier: - return this.FunctionCallOrVariableAccess(allowComplexLiterals); + return this.FunctionCallOrVariableAccess(expressionFlags); default: throw new ExpectedTokenException(nextToken, b => b.UnrecognizedExpression()); } } - private SyntaxBase ParenthesizedExpression(bool allowComplexLiterals) + private SyntaxBase ParenthesizedExpression(ExpressionFlags expressionFlags) { var openParen = this.Expect(TokenType.LeftParen, b => b.ExpectedCharacter("(")); - var expression = this.WithRecovery(() => this.Expression(allowComplexLiterals), RecoveryFlags.None, TokenType.RightParen, TokenType.NewLine); + var expression = this.WithRecovery(() => this.Expression(expressionFlags), RecoveryFlags.None, TokenType.RightParen, TokenType.NewLine); var closeParen = this.WithRecovery(() => this.Expect(TokenType.RightParen, b => b.ExpectedCharacter(")")), GetSuppressionFlag(expression), TokenType.NewLine); return new ParenthesizedExpressionSyntax(openParen, expression, closeParen); } - private SyntaxBase FunctionCallOrVariableAccess(bool allowComplexLiterals) + private SyntaxBase FunctionCallOrVariableAccess(ExpressionFlags expressionFlags) { var identifier = this.Identifier(b => b.ExpectedVariableOrFunctionName()); if (Check(TokenType.LeftParen)) { - var functionCall = FunctionCallAccess(identifier, allowComplexLiterals); + var functionCall = FunctionCallAccess(identifier, expressionFlags); return new FunctionCallSyntax( functionCall.Identifier, @@ -549,11 +584,11 @@ private SyntaxBase FunctionCallOrVariableAccess(bool allowComplexLiterals) /// /// Method that gets a function call identifier, its arguments plus open and close parens /// - private (IdentifierSyntax Identifier, Token OpenParen, IEnumerable ArgumentNodes, Token CloseParen) FunctionCallAccess(IdentifierSyntax functionName, bool allowComplexLiterals) + private (IdentifierSyntax Identifier, Token OpenParen, IEnumerable ArgumentNodes, Token CloseParen) FunctionCallAccess(IdentifierSyntax functionName, ExpressionFlags expressionFlags) { var openParen = this.Expect(TokenType.LeftParen, b => b.ExpectedCharacter("(")); - var argumentNodes = FunctionCallArguments(allowComplexLiterals); + var argumentNodes = FunctionCallArguments(expressionFlags); var closeParen = this.Expect(TokenType.RightParen, b => b.ExpectedCharacter(")")); @@ -566,7 +601,7 @@ private SyntaxBase FunctionCallOrVariableAccess(bool allowComplexLiterals) /// consume the right paren token. /// /// - private IEnumerable FunctionCallArguments(bool allowComplexLiterals) + private IEnumerable FunctionCallArguments(ExpressionFlags expressionFlags) { SkippedTriviaSyntax CreateDummyArgument(Token current) => new SkippedTriviaSyntax(current.ToZeroLengthSpan(), ImmutableArray.Empty, DiagnosticBuilder.ForPosition(current.ToZeroLengthSpan()).UnrecognizedExpression().AsEnumerable()); @@ -632,7 +667,7 @@ SkippedTriviaSyntax CreateDummyArgument(Token current) => throw new ExpectedTokenException(current, b => b.ExpectedCharacter(",")); } - var expression = this.Expression(allowComplexLiterals); + var expression = this.Expression(expressionFlags); arguments.Add((expression, null)); break; @@ -772,7 +807,7 @@ private SyntaxBase InterpolableString() // Look for an expression syntax inside the interpolation 'hole' (between "${" and "}"). // The lexer doesn't allow an expression contained inside an interpolation to span multiple lines, so we can safely use recovery to look for a NewLine character. // We are also blocking complex literals (arrays and objects) from inside string interpolation - var interpExpression = WithRecovery(() => Expression(allowComplexLiterals: false), RecoveryFlags.None, TokenType.StringMiddlePiece, TokenType.StringRightPiece, TokenType.NewLine); + var interpExpression = WithRecovery(() => Expression(ExpressionFlags.None), RecoveryFlags.None, TokenType.StringMiddlePiece, TokenType.StringRightPiece, TokenType.NewLine); if (!Check(TokenType.StringMiddlePiece, TokenType.StringRightPiece, TokenType.NewLine)) { // We may have successfully parsed the expression, but have not reached the end of the expression hole. Skip to the end of the hole. @@ -882,16 +917,16 @@ private SyntaxBase LiteralValue() } } - private SyntaxBase ForExpression(bool requireObjectLiteral) + private SyntaxBase ForExpression(ExpressionFlags expressionFlags, bool requireObjectLiteral) { var openBracket = this.Expect(TokenType.LeftSquare, b => b.ExpectedCharacter("[")); var forKeyword = this.ExpectKeyword(LanguageConstants.ForKeyword); var identifier = new LocalVariableSyntax(this.IdentifierWithRecovery(b => b.ExpectedLoopVariableIdentifier(), TokenType.Identifier, TokenType.RightSquare, TokenType.NewLine)); var inKeyword = this.WithRecovery(() => this.ExpectKeyword(LanguageConstants.InKeyword), GetSuppressionFlag(identifier.Name), TokenType.RightSquare, TokenType.NewLine); - var expression = this.WithRecovery(() => this.Expression(allowComplexLiterals: true), GetSuppressionFlag(inKeyword), TokenType.Colon, TokenType.RightSquare, TokenType.NewLine); + var expression = this.WithRecovery(() => this.Expression(ExpressionFlags.AllowComplexLiterals | ExpressionFlags.InsideColonDelimitedContext), GetSuppressionFlag(inKeyword), TokenType.Colon, TokenType.RightSquare, TokenType.NewLine); var colon = this.WithRecovery(() => this.Expect(TokenType.Colon, b => b.ExpectedCharacter(":")), GetSuppressionFlag(expression), TokenType.RightSquare, TokenType.NewLine); var body = this.WithRecovery( - () => requireObjectLiteral ? this.Object() : this.Expression(allowComplexLiterals: true), + () => requireObjectLiteral ? this.Object(expressionFlags) : this.Expression(WithExpressionFlag(expressionFlags, ExpressionFlags.AllowComplexLiterals)), GetSuppressionFlag(colon), TokenType.RightSquare, TokenType.NewLine); var closeBracket = this.WithRecovery(() => this.Expect(TokenType.RightSquare, b => b.ExpectedCharacter("]")), GetSuppressionFlag(body), TokenType.RightSquare, TokenType.NewLine); @@ -958,12 +993,12 @@ private SyntaxBase ArrayItem() return this.NewLine(); } - var value = this.Expression(allowComplexLiterals: true); + var value = this.Expression(ExpressionFlags.AllowComplexLiterals); return new ArrayItemSyntax(value); }, RecoveryFlags.None, TokenType.NewLine); } - private ObjectSyntax Object() + private ObjectSyntax Object(ExpressionFlags expressionFlags) { var openBrace = Expect(TokenType.LeftBrace, b => b.ExpectedCharacter("{")); @@ -974,16 +1009,16 @@ private ObjectSyntax Object() return new ObjectSyntax(openBrace, ImmutableArray.Empty, emptyCloseBrace); } - var propertiesOrTokens = new List(); + var propertiesOrResourcesTokens = new List(); while (!this.IsAtEnd() && this.reader.Peek().Type != TokenType.RightBrace) { // this produces a property node, skipped tokens node, or just a newline token - var propertyOrToken = this.ObjectProperty(); - propertiesOrTokens.Add(propertyOrToken); + var propertyOrResourceOrToken = this.ObjectProperty(expressionFlags); + propertiesOrResourcesTokens.Add(propertyOrResourceOrToken); // if skipped tokens node is returned above, the newline is not consumed // if newline token is returned, we must not expect another (could be beginning of a new property) - if (propertyOrToken is ObjectPropertySyntax) + if (propertyOrResourceOrToken is ObjectPropertySyntax) { if (Check(TokenType.Comma)) { @@ -993,24 +1028,24 @@ private ObjectSyntax Object() token.AsEnumerable(), DiagnosticBuilder.ForPosition(token.Span).UnexpectedCommaSeparator().AsEnumerable() ); - propertiesOrTokens.Add(skippedSyntax); + propertiesOrResourcesTokens.Add(skippedSyntax); } // properties must be followed by newlines var newLine = this.WithRecoveryNullable(this.NewLineOrEof, RecoveryFlags.ConsumeTerminator, TokenType.NewLine); if (newLine != null) { - propertiesOrTokens.Add(newLine); + propertiesOrResourcesTokens.Add(newLine); } } } var closeBrace = Expect(TokenType.RightBrace, b => b.ExpectedCharacter("}")); - return new ObjectSyntax(openBrace, propertiesOrTokens, closeBrace); + return new ObjectSyntax(openBrace, propertiesOrResourcesTokens, closeBrace); } - private SyntaxBase ObjectProperty() + private SyntaxBase ObjectProperty(ExpressionFlags expressionFlags) { return this.WithRecovery(() => { @@ -1021,6 +1056,20 @@ private SyntaxBase ObjectProperty() return this.NewLine(); } + // Nested resource declarations may be allowed - but we need lookahead to avoid + // treating 'resource' as a reserved property name. + if (HasExpressionFlag(expressionFlags, ExpressionFlags.AllowResourceDeclarations) && + CheckKeyword(LanguageConstants.ResourceKeyword) && + + // You are here: |resource ... + // + // If we see a non-identifier then it's not a resource declaration, + // fall back to the property parser. + Check(this.reader.PeekAhead(), TokenType.Identifier)) + { + return this.Declaration(); + } + var key = this.WithRecovery( () => ThrowIfSkipped( () => @@ -1035,18 +1084,22 @@ private SyntaxBase ObjectProperty() TokenType.Colon, TokenType.NewLine); var colon = this.WithRecovery(() => Expect(TokenType.Colon, b => b.ExpectedCharacter(":")), GetSuppressionFlag(key), TokenType.NewLine); - var value = this.WithRecovery(() => Expression(allowComplexLiterals: true), GetSuppressionFlag(colon), TokenType.NewLine); + var value = this.WithRecovery(() => Expression(ExpressionFlags.AllowComplexLiterals), GetSuppressionFlag(colon), TokenType.NewLine); return new ObjectPropertySyntax(key, colon, value); }, RecoveryFlags.None, TokenType.NewLine); } - private SyntaxBase IfCondition() + private SyntaxBase IfCondition(ExpressionFlags expressionFlags) { var keyword = this.ExpectKeyword(LanguageConstants.IfKeyword); - var conditionExpression = this.WithRecovery(() => this.ParenthesizedExpression(true), RecoveryFlags.None, TokenType.LeftBrace, TokenType.NewLine); + var conditionExpression = this.WithRecovery( + () => this.ParenthesizedExpression(WithoutExpressionFlag(expressionFlags, ExpressionFlags.AllowResourceDeclarations)), + RecoveryFlags.None, + TokenType.LeftBrace, + TokenType.NewLine); var body = this.WithRecovery( - this.Object, + () => this.Object(expressionFlags), GetSuppressionFlag(conditionExpression, conditionExpression is ParenthesizedExpressionSyntax { CloseParen: not SkippedTriviaSyntax }), TokenType.NewLine); return new IfConditionSyntax(keyword, conditionExpression, body); @@ -1176,6 +1229,16 @@ private bool Match(params TokenType[] types) return false; } + private bool Check(Token? token, params TokenType[] types) + { + if (token is null) + { + return false; + } + + return types.Contains(token.Type); + } + private bool Check(params TokenType[] types) { if (IsAtEnd()) diff --git a/src/Bicep.Core/PrettyPrint/DocumentBuildVisitor.cs b/src/Bicep.Core/PrettyPrint/DocumentBuildVisitor.cs index 800c26853fc..bcbee315c9d 100644 --- a/src/Bicep.Core/PrettyPrint/DocumentBuildVisitor.cs +++ b/src/Bicep.Core/PrettyPrint/DocumentBuildVisitor.cs @@ -151,6 +151,9 @@ public override void VisitArrayAccessSyntax(ArrayAccessSyntax syntax) => public override void VisitPropertyAccessSyntax(PropertyAccessSyntax syntax) => this.BuildWithConcat(() => base.VisitPropertyAccessSyntax(syntax)); + public override void VisitResourceAccessSyntax(ResourceAccessSyntax syntax) => + this.BuildWithConcat(() => base.VisitResourceAccessSyntax(syntax)); + public override void VisitParenthesizedExpressionSyntax(ParenthesizedExpressionSyntax syntax) => this.BuildWithConcat(() => base.VisitParenthesizedExpressionSyntax(syntax)); diff --git a/src/Bicep.Core/Resources/ResourceTypeReference.cs b/src/Bicep.Core/Resources/ResourceTypeReference.cs index 25ac8ba40a2..3fcc75ce4db 100644 --- a/src/Bicep.Core/Resources/ResourceTypeReference.cs +++ b/src/Bicep.Core/Resources/ResourceTypeReference.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Text.RegularExpressions; using Bicep.Core.Extensions; @@ -13,6 +14,8 @@ public class ResourceTypeReference { private static readonly Regex ResourceTypePattern = new Regex(@"^(?[a-z0-9][a-z0-9\.]*)(/(?[a-z0-9\-]+))+@(?(\d{4}-\d{2}-\d{2})(-(preview|alpha|beta|rc|privatepreview))?$)", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.Compiled | RegexOptions.CultureInvariant); + private static readonly Regex SingleTypePattern = new Regex(@"^(?[a-z0-9\-]+)(@(?(\d{4}-\d{2}-\d{2})(-(preview|alpha|beta|rc|privatepreview))?))?$", RegexOptions.IgnoreCase | RegexOptions.ExplicitCapture | RegexOptions.Compiled | RegexOptions.CultureInvariant); + public ResourceTypeReference(string @namespace, IEnumerable types, string apiVersion) { if (String.IsNullOrWhiteSpace(@namespace)) @@ -48,6 +51,39 @@ public ResourceTypeReference(string @namespace, IEnumerable types, strin public string FormatName() => $"{this.FullyQualifiedType}@{this.ApiVersion}"; + public bool IsParentOf(ResourceTypeReference other) + { + return + StringComparer.OrdinalIgnoreCase.Equals(this.Namespace, other.Namespace) && + + // Parent should have N types, child should have N+1, first N types should be equal + this.Types.Length + 1 == other.Types.Length && + Enumerable.SequenceEqual(this.Types, other.Types.Take(this.Types.Length), StringComparer.OrdinalIgnoreCase); + } + + public static ResourceTypeReference? TryCombine(ResourceTypeReference baseType, IEnumerable typeSegments) + { + var types = new List(baseType.Types); + + var bestVersion = baseType.ApiVersion; + foreach (var typeSegment in typeSegments) + { + if (!TryParseSingleTypeSegment(typeSegment, out var type, out var version)) + { + return null; + } + + types.Add(type); + + if (!string.IsNullOrEmpty(version)) + { + bestVersion = version; + } + } + + return new ResourceTypeReference(baseType.Namespace, types, bestVersion); + } + public static ResourceTypeReference? TryParse(string resourceType) { var match = ResourceTypePattern.Match(resourceType); @@ -65,5 +101,25 @@ public string FormatName() public static ResourceTypeReference Parse(string resourceType) => TryParse(resourceType) ?? throw new ArgumentException($"Unable to parse '{resourceType}'", nameof(resourceType)); + + public static bool TryParseSingleTypeSegment(string typeSegment, [NotNullWhen(true)] out string? type, out string? version) + { + var match = SingleTypePattern.Match(typeSegment); + if (match.Success == false) + { + type = null; + version = null; + return false; + } + + type = match.Groups["type"].Value; + version = match.Groups["version"].Value; + if (version == "") + { + version = null; + } + + return true; + } } } diff --git a/src/Bicep.Core/Semantics/Binder.cs b/src/Bicep.Core/Semantics/Binder.cs index b92de0ceaae..211163823ab 100644 --- a/src/Bicep.Core/Semantics/Binder.cs +++ b/src/Bicep.Core/Semantics/Binder.cs @@ -22,23 +22,23 @@ public Binder(SyntaxTree syntaxTree, ISymbolContext symbolContext) // TODO use lazy or some other pattern for init this.syntaxTree = syntaxTree; this.TargetScope = SyntaxHelper.GetTargetScope(syntaxTree); - var (allDeclarations, outermostScopes) = DeclarationVisitor.GetAllDeclarations(syntaxTree, symbolContext); - var uniqueDeclarations = GetUniqueDeclarations(allDeclarations); - var builtInNamespacs = GetBuiltInNamespaces(this.TargetScope); - this.bindings = GetBindings(syntaxTree, uniqueDeclarations, builtInNamespacs, outermostScopes); - this.cyclesBySymbol = GetCyclesBySymbol(syntaxTree, uniqueDeclarations, this.bindings); + var (declarations, outermostScopes) = DeclarationVisitor.GetDeclarations(syntaxTree, symbolContext); + var uniqueDeclarations = GetUniqueDeclarations(declarations); + var builtInNamespaces = GetBuiltInNamespaces(this.TargetScope); + this.bindings = GetBindings(syntaxTree, uniqueDeclarations, builtInNamespaces, outermostScopes); + this.cyclesBySymbol = GetCyclesBySymbol(syntaxTree, this.bindings); // TODO: Avoid looping 5 times? this.FileSymbol = new FileSymbol( syntaxTree.FileUri.LocalPath, syntaxTree.ProgramSyntax, - builtInNamespacs, + builtInNamespaces, outermostScopes, - allDeclarations.OfType(), - allDeclarations.OfType(), - allDeclarations.OfType(), - allDeclarations.OfType(), - allDeclarations.OfType()); + declarations.OfType(), + declarations.OfType(), + declarations.OfType(), + declarations.OfType(), + declarations.OfType()); } public ResourceScope TargetScope { get; } @@ -67,12 +67,12 @@ public IEnumerable FindReferences(Symbol symbol) => this.bindings public ImmutableArray? TryGetCycle(DeclaredSymbol declaredSymbol) => this.cyclesBySymbol.TryGetValue(declaredSymbol, out var cycle) ? cycle : null; - private static ImmutableDictionary GetUniqueDeclarations(IEnumerable allDeclarations) + private static ImmutableDictionary GetUniqueDeclarations(IEnumerable outermostDeclarations) { // in cases of duplicate declarations we will see multiple declaration symbols in the result list // for simplicitly we will bind to the first one // it may cause follow-on type errors, but there will also be errors about duplicate identifiers as well - return allDeclarations + return outermostDeclarations .ToLookup(x => x.Name, LanguageConstants.IdentifierComparer) .ToImmutableDictionary(x => x.Key, x => x.First(), LanguageConstants.IdentifierComparer); } @@ -86,21 +86,21 @@ private static ImmutableDictionary GetBuiltInNamespaces private static ImmutableDictionary GetBindings( SyntaxTree syntaxTree, - IReadOnlyDictionary uniqueDeclarations, + IReadOnlyDictionary outermostDeclarations, ImmutableDictionary builtInNamespaces, - ImmutableArray localScopes) + ImmutableArray childScopes) { // bind identifiers to declarations var bindings = new Dictionary(); - var binder = new NameBindingVisitor(uniqueDeclarations, bindings, builtInNamespaces, localScopes); + var binder = new NameBindingVisitor(outermostDeclarations, bindings, builtInNamespaces, childScopes); binder.Visit(syntaxTree.ProgramSyntax); return bindings.ToImmutableDictionary(); } - private static ImmutableDictionary> GetCyclesBySymbol(SyntaxTree syntaxTree, IReadOnlyDictionary uniqueDeclarations, IReadOnlyDictionary bindings) + private static ImmutableDictionary> GetCyclesBySymbol(SyntaxTree syntaxTree, IReadOnlyDictionary bindings) { - return CyclicCheckVisitor.FindCycles(syntaxTree.ProgramSyntax, uniqueDeclarations, bindings); + return CyclicCheckVisitor.FindCycles(syntaxTree.ProgramSyntax, bindings); } } } diff --git a/src/Bicep.Core/Semantics/DeclarationVisitor.cs b/src/Bicep.Core/Semantics/DeclarationVisitor.cs index e769e24f6b7..28213a2cf07 100644 --- a/src/Bicep.Core/Semantics/DeclarationVisitor.cs +++ b/src/Bicep.Core/Semantics/DeclarationVisitor.cs @@ -1,9 +1,9 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Bicep.Core.Extensions; using Bicep.Core.Syntax; namespace Bicep.Core.Semantics @@ -12,20 +12,21 @@ public sealed class DeclarationVisitor: SyntaxVisitor { private readonly ISymbolContext context; - private readonly IList declaredSymbols; + private readonly IList declarations; private readonly IList childScopes; private readonly Stack activeScopes = new(); - private DeclarationVisitor(ISymbolContext context, IList declaredSymbols, IList childScopes) + private DeclarationVisitor(ISymbolContext context, IList declarations, IList childScopes) { this.context = context; - this.declaredSymbols = declaredSymbols; + this.declarations = declarations; this.childScopes = childScopes; } - public static (ImmutableArray, ImmutableArray) GetAllDeclarations(SyntaxTree syntaxTree, ISymbolContext symbolContext) + // Returns the list of top level declarations as well as top level scopes. + public static (ImmutableArray, ImmutableArray) GetDeclarations(SyntaxTree syntaxTree, ISymbolContext symbolContext) { // collect declarations var declarations = new List(); @@ -41,7 +42,7 @@ public override void VisitParameterDeclarationSyntax(ParameterDeclarationSyntax base.VisitParameterDeclarationSyntax(syntax); var symbol = new ParameterSymbol(this.context, syntax.Name.IdentifierName, syntax, syntax.Modifier); - this.declaredSymbols.Add(symbol); + DeclareSymbol(symbol); } public override void VisitVariableDeclarationSyntax(VariableDeclarationSyntax syntax) @@ -49,15 +50,28 @@ public override void VisitVariableDeclarationSyntax(VariableDeclarationSyntax sy base.VisitVariableDeclarationSyntax(syntax); var symbol = new VariableSymbol(this.context, syntax.Name.IdentifierName, syntax, syntax.Value); - this.declaredSymbols.Add(symbol); + DeclareSymbol(symbol); } public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax syntax) { + // Create a scope for each resource body - this ensures that nested resources + // are contained within the appropriate scope. + // + // There may be additional scopes nested inside this between the resource declaration + // and the actual object body (for-loop). That's OK, in that case, this scope will + // be empty and we'll use the `for` scope for lookups. + var scope = new LocalScope(string.Empty, syntax, syntax.Value, ImmutableArray.Empty, ImmutableArray.Empty); + this.PushScope(scope); + base.VisitResourceDeclarationSyntax(syntax); + this.PopScope(); + + // The resource itself should be declared in the enclosing scope - it's accessible to nested + // resource, but also siblings. var symbol = new ResourceSymbol(this.context, syntax.Name.IdentifierName, syntax); - this.declaredSymbols.Add(symbol); + DeclareSymbol(symbol); } public override void VisitModuleDeclarationSyntax(ModuleDeclarationSyntax syntax) @@ -65,7 +79,7 @@ public override void VisitModuleDeclarationSyntax(ModuleDeclarationSyntax syntax base.VisitModuleDeclarationSyntax(syntax); var symbol = new ModuleSymbol(this.context, syntax.Name.IdentifierName, syntax); - this.declaredSymbols.Add(symbol); + DeclareSymbol(symbol); } public override void VisitOutputDeclarationSyntax(OutputDeclarationSyntax syntax) @@ -73,21 +87,21 @@ public override void VisitOutputDeclarationSyntax(OutputDeclarationSyntax syntax base.VisitOutputDeclarationSyntax(syntax); var symbol = new OutputSymbol(this.context, syntax.Name.IdentifierName, syntax, syntax.Value); - this.declaredSymbols.Add(symbol); + DeclareSymbol(symbol); } public override void VisitForSyntax(ForSyntax syntax) { + // create new scope without any descendants + var scope = new LocalScope(string.Empty, syntax, syntax.Body, ImmutableArray.Empty, ImmutableArray.Empty); + this.PushScope(scope); + /* * We cannot add the local symbol to the list of declarations because it will * break name binding at the global namespace level */ var itemVariable = new LocalVariableSymbol(this.context, syntax.ItemVariable.Name.IdentifierName, syntax.ItemVariable); - - // create new scope without any descendants - var scope = new LocalScope(string.Empty, syntax, syntax.Body, itemVariable.AsEnumerable(), ImmutableArray.Empty); - - this.PushScope(scope); + DeclareSymbol(itemVariable); // visit the children base.VisitForSyntax(syntax); @@ -95,12 +109,29 @@ public override void VisitForSyntax(ForSyntax syntax) this.PopScope(); } + private void DeclareSymbol(DeclaredSymbol symbol) + { + if (this.activeScopes.TryPeek(out var current)) + { + current.Locals.Add(symbol); + } + else + { + this.declarations.Add(symbol); + } + } + private void PushScope(LocalScope scope) { var item = new ScopeInfo(scope); if (this.activeScopes.TryPeek(out var current)) { + if (object.ReferenceEquals(current.Scope.BindingSyntax, scope.BindingSyntax)) + { + throw new InvalidOperationException($"Attempting to redefine the scope for {current.Scope.BindingSyntax}"); + } + // add this one to the parent current.Children.Add(item); } @@ -120,7 +151,7 @@ private void PopScope() private static LocalScope MakeImmutable(ScopeInfo info) { - return info.Scope.ReplaceChildren(info.Children.Select(MakeImmutable)); + return info.Scope.ReplaceChildren(info.Children.Select(MakeImmutable)).ReplaceLocals(info.Locals); } /// @@ -137,6 +168,8 @@ public ScopeInfo(LocalScope scope) public LocalScope Scope { get; } + public IList Locals { get; } = new List(); + public IList Children { get; } = new List(); } } diff --git a/src/Bicep.Core/Semantics/FileSymbol.cs b/src/Bicep.Core/Semantics/FileSymbol.cs index ceba3da8d39..62c364f4400 100644 --- a/src/Bicep.Core/Semantics/FileSymbol.cs +++ b/src/Bicep.Core/Semantics/FileSymbol.cs @@ -34,7 +34,7 @@ public FileSymbol(string name, this.ModuleDeclarations = moduleDeclarations.ToImmutableArray(); this.OutputDeclarations = outputDeclarations.ToImmutableArray(); - this.declarationsByName = this.AllDeclarations.ToLookup(decl => decl.Name, LanguageConstants.IdentifierComparer); + this.declarationsByName = this.Declarations.ToLookup(decl => decl.Name, LanguageConstants.IdentifierComparer); } public override IEnumerable Descendants => this.ImportedNamespaces.Values @@ -66,7 +66,7 @@ public FileSymbol(string name, /// /// Returns all the top-level declaration symbols. /// - public IEnumerable AllDeclarations => this.Descendants.OfType(); + public IEnumerable Declarations => this.Descendants.OfType(); public override void Accept(SymbolVisitor visitor) { @@ -77,6 +77,8 @@ public override void Accept(SymbolVisitor visitor) public IEnumerable GetDeclarationsByName(string name) => this.declarationsByName[name]; + public IEnumerable GetAllResourceDeclarations() => ResourceSymbolVisitor.GetAllResources(this); + private sealed class DuplicateIdentifierValidatorVisitor : SymbolVisitor { private readonly ImmutableDictionary importedNamespaces; @@ -111,8 +113,8 @@ private void ValidateScope(ILanguageScope scope) // collect duplicate identifiers at this scope // declaring a variable in a local scope hides the parent scope variables, // so we don't need to look at other levels - var outputDeclarations = scope.AllDeclarations.Where(decl => decl is OutputSymbol); - var nonOutputDeclarations = scope.AllDeclarations.Where(decl => decl is not OutputSymbol); + var outputDeclarations = scope.Declarations.Where(decl => decl is OutputSymbol); + var nonOutputDeclarations = scope.Declarations.Where(decl => decl is not OutputSymbol); // all symbols apart from outputs are in the same namespace, so check for uniqueness. this.Diagnostics.AddRange( diff --git a/src/Bicep.Core/Semantics/IBinder.cs b/src/Bicep.Core/Semantics/IBinder.cs index f5101e20762..0d2b969aece 100644 --- a/src/Bicep.Core/Semantics/IBinder.cs +++ b/src/Bicep.Core/Semantics/IBinder.cs @@ -7,14 +7,12 @@ namespace Bicep.Core.Semantics { - public interface IBinder + public interface IBinder : ISyntaxHierarchy { ResourceScope TargetScope { get; } FileSymbol FileSymbol { get; } - - SyntaxBase? GetParent(SyntaxBase syntax); - + IEnumerable FindReferences(Symbol symbol); Symbol? GetSymbolInfo(SyntaxBase syntax); diff --git a/src/Bicep.Core/Semantics/ILanguageScope.cs b/src/Bicep.Core/Semantics/ILanguageScope.cs index bcfbfb57ec2..34045049c68 100644 --- a/src/Bicep.Core/Semantics/ILanguageScope.cs +++ b/src/Bicep.Core/Semantics/ILanguageScope.cs @@ -9,6 +9,6 @@ public interface ILanguageScope { IEnumerable GetDeclarationsByName(string name); - IEnumerable AllDeclarations { get; } + IEnumerable Declarations { get; } } } \ No newline at end of file diff --git a/src/Bicep.Core/Semantics/LocalScope.cs b/src/Bicep.Core/Semantics/LocalScope.cs index 200fcd7b154..d23c4a02627 100644 --- a/src/Bicep.Core/Semantics/LocalScope.cs +++ b/src/Bicep.Core/Semantics/LocalScope.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Bicep.Core.Diagnostics; using Bicep.Core.Syntax; namespace Bicep.Core.Semantics @@ -13,7 +12,7 @@ namespace Bicep.Core.Semantics /// public class LocalScope : Symbol, ILanguageScope { - public LocalScope(string name, SyntaxBase declaringSyntax, SyntaxBase bindingSyntax, IEnumerable locals, IEnumerable childScopes) + public LocalScope(string name, SyntaxBase declaringSyntax, SyntaxBase bindingSyntax, IEnumerable locals, IEnumerable childScopes) : base(name) { this.DeclaringSyntax = declaringSyntax; @@ -33,7 +32,7 @@ public LocalScope(string name, SyntaxBase declaringSyntax, SyntaxBase bindingSyn /// Identifiers within this node will first bind to symbols in this scope. Identifiers above this node will bind to the parent scope. public SyntaxBase BindingSyntax { get; } - public ImmutableArray Locals { get; } + public ImmutableArray Locals { get; } public ImmutableArray ChildScopes { get; } @@ -43,10 +42,12 @@ public LocalScope(string name, SyntaxBase declaringSyntax, SyntaxBase bindingSyn public override IEnumerable Descendants => this.ChildScopes.Concat(this.Locals); + public LocalScope ReplaceLocals(IEnumerable newLocals) => new(this.Name, this.DeclaringSyntax, this.BindingSyntax, newLocals, this.ChildScopes); + public LocalScope ReplaceChildren(IEnumerable newChildren) => new(this.Name, this.DeclaringSyntax, this.BindingSyntax, this.Locals, newChildren); public IEnumerable GetDeclarationsByName(string name) => this.Locals.Where(symbol => symbol.NameSyntax.IsValid && string.Equals(symbol.Name, name, LanguageConstants.IdentifierComparison)).ToList(); - public IEnumerable AllDeclarations => this.Locals; + public IEnumerable Declarations => this.Locals; } } \ No newline at end of file diff --git a/src/Bicep.Core/Semantics/NameBindingVisitor.cs b/src/Bicep.Core/Semantics/NameBindingVisitor.cs index f23f12868fd..5b0b6e44a27 100644 --- a/src/Bicep.Core/Semantics/NameBindingVisitor.cs +++ b/src/Bicep.Core/Semantics/NameBindingVisitor.cs @@ -1,341 +1,410 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Diagnostics; -using System.Linq; -using Azure.Deployments.Core.Extensions; -using Bicep.Core.Diagnostics; -using Bicep.Core.Extensions; -using Bicep.Core.Syntax; -using Bicep.Core.TypeSystem; - -namespace Bicep.Core.Semantics -{ - public sealed class NameBindingVisitor : SyntaxVisitor - { - private FunctionFlags allowedFlags; - - private readonly IReadOnlyDictionary declarations; - - private readonly IDictionary bindings; - - private readonly ImmutableDictionary namespaces; - - private readonly IReadOnlyDictionary allLocalScopes; - - private readonly Stack activeScopes; - - public NameBindingVisitor(IReadOnlyDictionary declarations, IDictionary bindings, ImmutableDictionary namespaces, ImmutableArray localScopes) - { - this.declarations = declarations; - this.bindings = bindings; - this.namespaces = namespaces; - this.allLocalScopes = ScopeCollectorVisitor.Build(localScopes); - this.activeScopes = new Stack(); - } - - public override void VisitProgramSyntax(ProgramSyntax syntax) - { - base.VisitProgramSyntax(syntax); - - // create bindings for all of the declarations to their corresponding symbol - // this is needed to make find all references work correctly - // (doing this here to avoid side-effects in the constructor) - foreach (DeclaredSymbol declaredSymbol in this.declarations.Values) - { - this.bindings.Add(declaredSymbol.DeclaringSyntax, declaredSymbol); - } - - // include all the locals in the symbol table as well - // since we only allow lookups by object and not by name, - // a flat symbol table should be sufficient - foreach (var declaredSymbol in allLocalScopes.Values.SelectMany(scope => scope.AllDeclarations)) - { - this.bindings.Add(declaredSymbol.DeclaringSyntax, declaredSymbol); - } - } - - public override void VisitVariableAccessSyntax(VariableAccessSyntax syntax) - { - base.VisitVariableAccessSyntax(syntax); - - var symbol = this.LookupSymbolByName(syntax.Name, false); - - // bind what we got - the type checker will validate if it fits - this.bindings.Add(syntax, symbol); - } - - public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax syntax) - { - allowedFlags = FunctionFlags.ResoureDecorator; - this.VisitNodes(syntax.LeadingNodes); - this.Visit(syntax.Keyword); - this.Visit(syntax.Name); - this.Visit(syntax.Type); - this.Visit(syntax.ExistingKeyword); - this.Visit(syntax.Assignment); - allowedFlags = FunctionFlags.RequiresInlining; - this.Visit(syntax.Value); - allowedFlags = FunctionFlags.Default; - } - - public override void VisitModuleDeclarationSyntax(ModuleDeclarationSyntax syntax) - { - allowedFlags = FunctionFlags.ModuleDecorator; - this.VisitNodes(syntax.LeadingNodes); - this.Visit(syntax.Keyword); - this.Visit(syntax.Name); - this.Visit(syntax.Path); - this.Visit(syntax.Assignment); - allowedFlags = FunctionFlags.RequiresInlining; - this.Visit(syntax.Value); - allowedFlags = FunctionFlags.Default; - } - - public override void VisitIfConditionSyntax(IfConditionSyntax syntax) - { - this.Visit(syntax.Keyword); - allowedFlags = FunctionFlags.Default; - this.Visit(syntax.ConditionExpression); - // if-condition syntax parent is always a resource/module declaration - // this means that we have to allow the functions that are only allowed - // in resource bodies by our runtime (like reference() or listKeys()) - // TODO: Update when conditions can be composed together with loops - allowedFlags = FunctionFlags.RequiresInlining; - this.Visit(syntax.Body); - allowedFlags = FunctionFlags.Default; - } - - public override void VisitVariableDeclarationSyntax(VariableDeclarationSyntax syntax) - { - allowedFlags = FunctionFlags.VariableDecorator; - this.VisitNodes(syntax.LeadingNodes); - this.Visit(syntax.Keyword); - this.Visit(syntax.Name); - this.Visit(syntax.Assignment); - allowedFlags = FunctionFlags.RequiresInlining; - this.Visit(syntax.Value); - allowedFlags = FunctionFlags.Default; - } - - public override void VisitOutputDeclarationSyntax(OutputDeclarationSyntax syntax) - { - allowedFlags = FunctionFlags.OutputDecorator; - this.VisitNodes(syntax.LeadingNodes); - this.Visit(syntax.Keyword); - this.Visit(syntax.Name); - this.Visit(syntax.Type); - this.Visit(syntax.Assignment); - allowedFlags = FunctionFlags.RequiresInlining; - this.Visit(syntax.Value); - allowedFlags = FunctionFlags.Default; - } - - public override void VisitParameterDeclarationSyntax(ParameterDeclarationSyntax syntax) - { - allowedFlags = FunctionFlags.ParameterDecorator; - this.VisitNodes(syntax.LeadingNodes); - this.Visit(syntax.Keyword); - this.Visit(syntax.Name); - this.Visit(syntax.Type); - allowedFlags = FunctionFlags.ParamDefaultsOnly; - this.Visit(syntax.Modifier); - allowedFlags = FunctionFlags.Default; - } - - public override void VisitMissingDeclarationSyntax(MissingDeclarationSyntax syntax) - { - allowedFlags = FunctionFlags.ParameterDecorator | - FunctionFlags.VariableDecorator | - FunctionFlags.ResoureDecorator | - FunctionFlags.ModuleDecorator | - FunctionFlags.OutputDecorator; - base.VisitMissingDeclarationSyntax(syntax); - allowedFlags = FunctionFlags.Default; - } - - public override void VisitFunctionCallSyntax(FunctionCallSyntax syntax) - { - FunctionFlags currentFlags = allowedFlags; - this.Visit(syntax.Name); - this.Visit(syntax.OpenParen); - allowedFlags = allowedFlags.HasAnyDecoratorFlag() ? FunctionFlags.Default : allowedFlags; - this.VisitNodes(syntax.Arguments); - this.Visit(syntax.CloseParen); - allowedFlags = currentFlags; - - var symbol = this.LookupSymbolByName(syntax.Name, true); - - // bind what we got - the type checker will validate if it fits - this.bindings.Add(syntax, symbol); - } - - public override void VisitInstanceFunctionCallSyntax(InstanceFunctionCallSyntax syntax) - { - FunctionFlags currentFlags = allowedFlags; - this.Visit(syntax.BaseExpression); - this.Visit(syntax.Dot); - this.Visit(syntax.Name); - this.Visit(syntax.OpenParen); - allowedFlags = allowedFlags.HasAnyDecoratorFlag() ? FunctionFlags.Default : allowedFlags; - this.VisitNodes(syntax.Arguments); - this.Visit(syntax.CloseParen); - allowedFlags = currentFlags; - - if (!syntax.Name.IsValid) - { - // the parser produced an instance function calls with an invalid name - // all instance function calls must be bound to a symbol, so let's - // bind to a symbol without any errors (there's already a parse error) - this.bindings.Add(syntax, new ErrorSymbol()); - return; - } - - if (bindings.TryGetValue(syntax.BaseExpression, out var baseSymbol) && baseSymbol is NamespaceSymbol namespaceSymbol) - { - var functionSymbol = allowedFlags.HasAnyDecoratorFlag() - // Decorator functions are only valid when HasDecoratorFlag() is true which means - // the instance function call is the top level expression of a DecoratorSyntax node. - ? namespaceSymbol.Type.MethodResolver.TryGetSymbol(syntax.Name) ?? namespaceSymbol.Type.DecoratorResolver.TryGetSymbol(syntax.Name) - : namespaceSymbol.Type.MethodResolver.TryGetSymbol(syntax.Name); - - var foundSymbol = SymbolValidator.ResolveNamespaceQualifiedFunction(allowedFlags, functionSymbol, syntax.Name, namespaceSymbol); - - this.bindings.Add(syntax, foundSymbol); - } - } - - protected override void VisitInternal(SyntaxBase syntax) - { - // any node can be a binding scope - if (!this.allLocalScopes.TryGetValue(syntax, out var localScope)) - { - // not a binding scope - // visit children normally - base.VisitInternal(syntax); - return; - } - - // we are in a binding scope - // push it to the stack of active scopes - // as a result this scope will be used to resolve symbols first - // (then all the previous one and then finally the global scope) - this.activeScopes.Push(localScope); - - // visit all the children - base.VisitInternal(syntax); - - // we are leaving the loop scope - // pop the scope - no symbols will be resolved against it ever again - var lastPopped = this.activeScopes.Pop(); - Debug.Assert(ReferenceEquals(lastPopped, localScope), "ReferenceEquals(lastPopped, localScope)"); - } - - public override void VisitForSyntax(ForSyntax syntax) - { - // we must have a scope in the map for the loop body - otherwise binding won't work - Debug.Assert(this.allLocalScopes.ContainsKey(syntax.Body), "this.allLocalScopes.ContainsKey(syntax.Body)"); - - // visit all the children - base.VisitForSyntax(syntax); - } - - private Symbol LookupSymbolByName(IdentifierSyntax identifierSyntax, bool isFunctionCall) => - this.LookupLocalSymbolByName(identifierSyntax, isFunctionCall) ?? LookupGlobalSymbolByName(identifierSyntax, isFunctionCall); - - private Symbol? LookupLocalSymbolByName(IdentifierSyntax identifierSyntax, bool isFunctionCall) - { - if (isFunctionCall) - { - // functions can't be local symbols - return null; - } - - // iterating over a stack gives you the items in the same - // order as if you popped each one but without modifying the stack - foreach (var scope in activeScopes) - { - // resolve symbol against current scope - // this binds to the innermost symbol even if there exists one at the parent scope - var symbol = LookupLocalSymbolByName(scope, identifierSyntax); - if (symbol != null) - { - // found a symbol - return it - return symbol; - } - } - - return null; - } - - private static Symbol? LookupLocalSymbolByName(LocalScope scope, IdentifierSyntax identifierSyntax) => - // bind to first symbol matching the specified identifier - // (errors about duplicate identifiers are emitted elsewhere) - // loops currently are the only source of local symbols - // as a result a local scope can contain between 1 to 2 local symbols - // linear search should be fine, but this should be revisited if the above is no longer holds true - scope.AllDeclarations.FirstOrDefault(symbol => string.Equals(identifierSyntax.IdentifierName, symbol.Name, LanguageConstants.IdentifierComparison)); - - private Symbol LookupGlobalSymbolByName(IdentifierSyntax identifierSyntax, bool isFunctionCall) - { - // attempt to find name in the imported namespaces - if (this.namespaces.TryGetValue(identifierSyntax.IdentifierName, out var namespaceSymbol)) - { - // namespace symbol found - return namespaceSymbol; - } - - // declarations must not have a namespace value, namespaces are used to fully qualify a function access. - // There might be instances where a variable declaration for example uses the same name as one of the imported - // functions, in this case to differentiate a variable declaration vs a function access we check the namespace value, - // the former case must have an empty namespace value whereas the latter will have a namespace value. - if (this.declarations.TryGetValue(identifierSyntax.IdentifierName, out var globalSymbol)) - { - // we found the symbol in the global namespace - return globalSymbol; - } - - // attempt to find function in all imported namespaces - var foundSymbols = this.namespaces - .Select(kvp => allowedFlags.HasAnyDecoratorFlag() - ? kvp.Value.Type.MethodResolver.TryGetSymbol(identifierSyntax) ?? kvp.Value.Type.DecoratorResolver.TryGetSymbol(identifierSyntax) - : kvp.Value.Type.MethodResolver.TryGetSymbol(identifierSyntax)) - .Where(symbol => symbol != null) - .ToList(); - - if (foundSymbols.Count > 1) - { - // ambiguous symbol - return new ErrorSymbol(DiagnosticBuilder.ForPosition(identifierSyntax).AmbiguousSymbolReference(identifierSyntax.IdentifierName, this.namespaces.Keys)); - } - - var foundSymbol = Enumerable.FirstOrDefault(foundSymbols); - return isFunctionCall ? SymbolValidator.ResolveUnqualifiedFunction(allowedFlags, foundSymbol, identifierSyntax, namespaces.Values) : SymbolValidator.ResolveUnqualifiedSymbol(foundSymbol, identifierSyntax, namespaces.Values, declarations.Keys); - } - - private class ScopeCollectorVisitor: SymbolVisitor - { - private IDictionary ScopeMap { get; } = new Dictionary(); - - public override void VisitLocalScope(LocalScope symbol) - { - this.ScopeMap.Add(symbol.BindingSyntax, symbol); - base.VisitLocalScope(symbol); - } - - public static IReadOnlyDictionary Build(ImmutableArray outermostScopes) - { - var visitor = new ScopeCollectorVisitor(); - foreach (LocalScope outermostScope in outermostScopes) - { - visitor.Visit(outermostScope); - } - - return visitor.ScopeMap.AsReadOnly(); - } - } - } +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using Azure.Deployments.Core.Extensions; +using Bicep.Core.Diagnostics; +using Bicep.Core.Extensions; +using Bicep.Core.Syntax; +using Bicep.Core.TypeSystem; + +namespace Bicep.Core.Semantics +{ + public sealed class NameBindingVisitor : SyntaxVisitor + { + private FunctionFlags allowedFlags; + + private readonly IReadOnlyDictionary declarations; + + private readonly IDictionary bindings; + + private readonly ImmutableDictionary namespaces; + + private readonly IReadOnlyDictionary allLocalScopes; + + private readonly Stack activeScopes; + + public NameBindingVisitor(IReadOnlyDictionary declarations, IDictionary bindings, ImmutableDictionary namespaces, ImmutableArray localScopes) + { + this.declarations = declarations; + this.bindings = bindings; + this.namespaces = namespaces; + this.allLocalScopes = ScopeCollectorVisitor.Build(localScopes); + this.activeScopes = new Stack(); + } + + public override void VisitProgramSyntax(ProgramSyntax syntax) + { + base.VisitProgramSyntax(syntax); + + // create bindings for all of the declarations to their corresponding symbol + // this is needed to make find all references work correctly + // (doing this here to avoid side-effects in the constructor) + foreach (DeclaredSymbol declaredSymbol in this.declarations.Values) + { + this.bindings.Add(declaredSymbol.DeclaringSyntax, declaredSymbol); + } + + // include all the locals in the symbol table as well + // since we only allow lookups by object and not by name, + // a flat symbol table should be sufficient + foreach (var declaredSymbol in allLocalScopes.Values.SelectMany(scope => scope.Declarations)) + { + this.bindings.Add(declaredSymbol.DeclaringSyntax, declaredSymbol); + } + } + + public override void VisitVariableAccessSyntax(VariableAccessSyntax syntax) + { + base.VisitVariableAccessSyntax(syntax); + + var symbol = this.LookupSymbolByName(syntax.Name, false); + + // bind what we got - the type checker will validate if it fits + this.bindings.Add(syntax, symbol); + } + + public override void VisitResourceAccessSyntax(ResourceAccessSyntax syntax) + { + base.VisitResourceAccessSyntax(syntax); + + // we need to resolve which resource delaration the LHS is pointing to - and then + // validate that we can resolve the name. + this.bindings.TryGetValue(syntax.BaseExpression, out var symbol); + + if (symbol is ErrorSymbol) + { + this.bindings.Add(syntax, symbol); + return; + } + else if (symbol is null || symbol is not ResourceSymbol) + { + // symbol could be null in the case of an incomplete expression during parsing like `a:` + var error = new ErrorSymbol(DiagnosticBuilder.ForPosition(syntax.ResourceName).ResourceRequiredForResourceAccess(symbol?.Kind.ToString() ?? LanguageConstants.ErrorName)); + this.bindings.Add(syntax, error); + return; + } + + // This is the symbol of LHS and it's a valid resource. + var resourceSymbol = (ResourceSymbol)symbol; + var resourceBody = resourceSymbol.DeclaringResource.TryGetBody(); + if (resourceBody == null) + { + // If we have no body then there will be nothing to reference. + var error = new ErrorSymbol(DiagnosticBuilder.ForPosition(syntax.ResourceName).ResourceRequiredForResourceAccess(symbol?.Kind.ToString() ?? LanguageConstants.ErrorName)); + this.bindings.Add(syntax, error); + return; + } + + if (!this.allLocalScopes.TryGetValue(resourceBody, out var localScope)) + { + // code defect in the declaration visitor + throw new InvalidOperationException($"Local scope is missing for {syntax.GetType().Name} at {syntax.Span}"); + } + + var referencedResource = LookupResourceSymbolByName(localScope, syntax.ResourceName); + if (referencedResource is null) + { + var nestedResourceNames = localScope.Declarations.OfType().Select(r => r.Name); + var error = new ErrorSymbol(DiagnosticBuilder.ForPosition(syntax.ResourceName).NestedResourceNotFound(resourceSymbol.Name, syntax.ResourceName.IdentifierName, nestedResourceNames)); + this.bindings.Add(syntax, error); + return; + } + + // This is valid. + this.bindings.Add(syntax, referencedResource); + } + + public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax syntax) + { + allowedFlags = FunctionFlags.ResoureDecorator; + this.VisitNodes(syntax.LeadingNodes); + this.Visit(syntax.Keyword); + this.Visit(syntax.Name); + this.Visit(syntax.Type); + this.Visit(syntax.ExistingKeyword); + this.Visit(syntax.Assignment); + allowedFlags = FunctionFlags.RequiresInlining; + this.Visit(syntax.Value); + allowedFlags = FunctionFlags.Default; + } + + public override void VisitModuleDeclarationSyntax(ModuleDeclarationSyntax syntax) + { + allowedFlags = FunctionFlags.ModuleDecorator; + this.VisitNodes(syntax.LeadingNodes); + this.Visit(syntax.Keyword); + this.Visit(syntax.Name); + this.Visit(syntax.Path); + this.Visit(syntax.Assignment); + allowedFlags = FunctionFlags.RequiresInlining; + this.Visit(syntax.Value); + allowedFlags = FunctionFlags.Default; + } + + public override void VisitIfConditionSyntax(IfConditionSyntax syntax) + { + this.Visit(syntax.Keyword); + allowedFlags = FunctionFlags.Default; + this.Visit(syntax.ConditionExpression); + // if-condition syntax parent is always a resource/module declaration + // this means that we have to allow the functions that are only allowed + // in resource bodies by our runtime (like reference() or listKeys()) + // TODO: Update when conditions can be composed together with loops + allowedFlags = FunctionFlags.RequiresInlining; + this.Visit(syntax.Body); + allowedFlags = FunctionFlags.Default; + } + + public override void VisitVariableDeclarationSyntax(VariableDeclarationSyntax syntax) + { + allowedFlags = FunctionFlags.VariableDecorator; + this.VisitNodes(syntax.LeadingNodes); + this.Visit(syntax.Keyword); + this.Visit(syntax.Name); + this.Visit(syntax.Assignment); + allowedFlags = FunctionFlags.RequiresInlining; + this.Visit(syntax.Value); + allowedFlags = FunctionFlags.Default; + } + + public override void VisitOutputDeclarationSyntax(OutputDeclarationSyntax syntax) + { + allowedFlags = FunctionFlags.OutputDecorator; + this.VisitNodes(syntax.LeadingNodes); + this.Visit(syntax.Keyword); + this.Visit(syntax.Name); + this.Visit(syntax.Type); + this.Visit(syntax.Assignment); + allowedFlags = FunctionFlags.RequiresInlining; + this.Visit(syntax.Value); + allowedFlags = FunctionFlags.Default; + } + + public override void VisitParameterDeclarationSyntax(ParameterDeclarationSyntax syntax) + { + allowedFlags = FunctionFlags.ParameterDecorator; + this.VisitNodes(syntax.LeadingNodes); + this.Visit(syntax.Keyword); + this.Visit(syntax.Name); + this.Visit(syntax.Type); + allowedFlags = FunctionFlags.ParamDefaultsOnly; + this.Visit(syntax.Modifier); + allowedFlags = FunctionFlags.Default; + } + + public override void VisitMissingDeclarationSyntax(MissingDeclarationSyntax syntax) + { + allowedFlags = FunctionFlags.ParameterDecorator | + FunctionFlags.VariableDecorator | + FunctionFlags.ResoureDecorator | + FunctionFlags.ModuleDecorator | + FunctionFlags.OutputDecorator; + base.VisitMissingDeclarationSyntax(syntax); + allowedFlags = FunctionFlags.Default; + } + + public override void VisitFunctionCallSyntax(FunctionCallSyntax syntax) + { + FunctionFlags currentFlags = allowedFlags; + this.Visit(syntax.Name); + this.Visit(syntax.OpenParen); + allowedFlags = allowedFlags.HasAnyDecoratorFlag() ? FunctionFlags.Default : allowedFlags; + this.VisitNodes(syntax.Arguments); + this.Visit(syntax.CloseParen); + allowedFlags = currentFlags; + + var symbol = this.LookupSymbolByName(syntax.Name, true); + + // bind what we got - the type checker will validate if it fits + this.bindings.Add(syntax, symbol); + } + + public override void VisitInstanceFunctionCallSyntax(InstanceFunctionCallSyntax syntax) + { + FunctionFlags currentFlags = allowedFlags; + this.Visit(syntax.BaseExpression); + this.Visit(syntax.Dot); + this.Visit(syntax.Name); + this.Visit(syntax.OpenParen); + allowedFlags = allowedFlags.HasAnyDecoratorFlag() ? FunctionFlags.Default : allowedFlags; + this.VisitNodes(syntax.Arguments); + this.Visit(syntax.CloseParen); + allowedFlags = currentFlags; + + if (!syntax.Name.IsValid) + { + // the parser produced an instance function calls with an invalid name + // all instance function calls must be bound to a symbol, so let's + // bind to a symbol without any errors (there's already a parse error) + this.bindings.Add(syntax, new ErrorSymbol()); + return; + } + + if (bindings.TryGetValue(syntax.BaseExpression, out var baseSymbol) && baseSymbol is NamespaceSymbol namespaceSymbol) + { + var functionSymbol = allowedFlags.HasAnyDecoratorFlag() + // Decorator functions are only valid when HasDecoratorFlag() is true which means + // the instance function call is the top level expression of a DecoratorSyntax node. + ? namespaceSymbol.Type.MethodResolver.TryGetSymbol(syntax.Name) ?? namespaceSymbol.Type.DecoratorResolver.TryGetSymbol(syntax.Name) + : namespaceSymbol.Type.MethodResolver.TryGetSymbol(syntax.Name); + + var foundSymbol = SymbolValidator.ResolveNamespaceQualifiedFunction(allowedFlags, functionSymbol, syntax.Name, namespaceSymbol); + + this.bindings.Add(syntax, foundSymbol); + } + } + + protected override void VisitInternal(SyntaxBase syntax) + { + // any node can be a binding scope + if (!this.allLocalScopes.TryGetValue(syntax, out var localScope)) + { + // not a binding scope + // visit children normally + base.VisitInternal(syntax); + return; + } + + // we are in a binding scope + // push it to the stack of active scopes + // as a result this scope will be used to resolve symbols first + // (then all the previous one and then finally the global scope) + this.activeScopes.Push(localScope); + + // visit all the children + base.VisitInternal(syntax); + + // we are leaving the loop scope + // pop the scope - no symbols will be resolved against it ever again + var lastPopped = this.activeScopes.Pop(); + Debug.Assert(ReferenceEquals(lastPopped, localScope), "ReferenceEquals(lastPopped, localScope)"); + } + + public override void VisitForSyntax(ForSyntax syntax) + { + // we must have a scope in the map for the loop body - otherwise binding won't work + Debug.Assert(this.allLocalScopes.ContainsKey(syntax.Body), "this.allLocalScopes.ContainsKey(syntax.Body)"); + + // visit all the children + base.VisitForSyntax(syntax); + } + + private Symbol LookupSymbolByName(IdentifierSyntax identifierSyntax, bool isFunctionCall) => + this.LookupLocalSymbolByName(identifierSyntax, isFunctionCall) ?? LookupGlobalSymbolByName(identifierSyntax, isFunctionCall); + + private Symbol? LookupLocalSymbolByName(IdentifierSyntax identifierSyntax, bool isFunctionCall) + { + if (isFunctionCall) + { + // functions can't be local symbols + return null; + } + + // iterating over a stack gives you the items in the same + // order as if you popped each one but without modifying the stack + foreach (var scope in activeScopes) + { + // resolve symbol against current scope + // this binds to the innermost symbol even if there exists one at the parent scope + var symbol = LookupLocalSymbolByName(scope, identifierSyntax); + if (symbol != null) + { + // found a symbol - return it + return symbol; + } + } + + return null; + } + + private static Symbol? LookupLocalSymbolByName(LocalScope scope, IdentifierSyntax identifierSyntax) => + // bind to first symbol matching the specified identifier + // (errors about duplicate identifiers are emitted elsewhere) + // loops currently are the only source of local symbols + // as a result a local scope can contain between 1 to 2 local symbols + // linear search should be fine, but this should be revisited if the above is no longer holds true + scope.Declarations.FirstOrDefault(symbol => string.Equals(identifierSyntax.IdentifierName, symbol.Name, LanguageConstants.IdentifierComparison)); + + private static ResourceSymbol? LookupResourceSymbolByName(ILanguageScope scope, IdentifierSyntax identifierSyntax) => + scope.Declarations + .OfType() + .FirstOrDefault(symbol => string.Equals(identifierSyntax.IdentifierName, symbol.Name, LanguageConstants.IdentifierComparison)); + + private Symbol LookupGlobalSymbolByName(IdentifierSyntax identifierSyntax, bool isFunctionCall) + { + // attempt to find name in the imported namespaces + if (this.namespaces.TryGetValue(identifierSyntax.IdentifierName, out var namespaceSymbol)) + { + // namespace symbol found + return namespaceSymbol; + } + + // declarations must not have a namespace value, namespaces are used to fully qualify a function access. + // There might be instances where a variable declaration for example uses the same name as one of the imported + // functions, in this case to differentiate a variable declaration vs a function access we check the namespace value, + // the former case must have an empty namespace value whereas the latter will have a namespace value. + if (this.declarations.TryGetValue(identifierSyntax.IdentifierName, out var globalSymbol)) + { + // we found the symbol in the global namespace + return globalSymbol; + } + + // attempt to find function in all imported namespaces + var foundSymbols = this.namespaces + .Select(kvp => allowedFlags.HasAnyDecoratorFlag() + ? kvp.Value.Type.MethodResolver.TryGetSymbol(identifierSyntax) ?? kvp.Value.Type.DecoratorResolver.TryGetSymbol(identifierSyntax) + : kvp.Value.Type.MethodResolver.TryGetSymbol(identifierSyntax)) + .Where(symbol => symbol != null) + .ToList(); + + if (foundSymbols.Count > 1) + { + // ambiguous symbol + return new ErrorSymbol(DiagnosticBuilder.ForPosition(identifierSyntax).AmbiguousSymbolReference(identifierSyntax.IdentifierName, this.namespaces.Keys)); + } + + var foundSymbol = Enumerable.FirstOrDefault(foundSymbols); + return isFunctionCall ? SymbolValidator.ResolveUnqualifiedFunction(allowedFlags, foundSymbol, identifierSyntax, namespaces.Values) : SymbolValidator.ResolveUnqualifiedSymbol(foundSymbol, identifierSyntax, namespaces.Values, declarations.Keys); + } + + private class ScopeCollectorVisitor: SymbolVisitor + { + private IDictionary ScopeMap { get; } = new Dictionary(); + + + protected override void VisitInternal(Symbol node) + { + // We haven't typed checked yet, so don't visit anything that isn't a scope. + // + // Now that resources can appear in a scope, this causes problems if we visit them and try + // to get type info. + if (node is ILanguageScope) + { + base.VisitInternal(node); + } + } + + public override void VisitLocalScope(LocalScope symbol) + { + this.ScopeMap.Add(symbol.BindingSyntax, symbol); + base.VisitLocalScope(symbol); + } + + public static IReadOnlyDictionary Build(ImmutableArray outermostScopes) + { + var visitor = new ScopeCollectorVisitor(); + foreach (LocalScope outermostScope in outermostScopes) + { + visitor.Visit(outermostScope); + } + + return visitor.ScopeMap.AsReadOnly(); + } + } + } } \ No newline at end of file diff --git a/src/Bicep.Core/Semantics/ResourceAncestorGraph.cs b/src/Bicep.Core/Semantics/ResourceAncestorGraph.cs new file mode 100644 index 00000000000..3dac2367291 --- /dev/null +++ b/src/Bicep.Core/Semantics/ResourceAncestorGraph.cs @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System.Collections.Generic; +using System.Collections.Immutable; +using Bicep.Core.Diagnostics; +using Bicep.Core.Syntax; + +namespace Bicep.Core.Semantics +{ + public sealed class ResourceAncestorGraph + { + private readonly ImmutableDictionary> data; + + public ResourceAncestorGraph(ImmutableDictionary> data) + { + this.data = data; + } + + // Gets the ordered list of ancestors of this resource in order from 'oldest' to 'youngest' + // this is the same order we need to compute the name of a resource using `/` separated segments in a string. + public ImmutableArray GetAncestors(ResourceSymbol resource) + { + if (data.TryGetValue(resource, out var results)) + { + return results; + } + else + { + return ImmutableArray.Empty; + } + } + + public static ResourceAncestorGraph Compute(SyntaxTree syntaxTree, IBinder binder) + { + var visitor = new ResourceAncestorVisitor(binder); + visitor.Visit(syntaxTree.ProgramSyntax); + return new ResourceAncestorGraph(visitor.Ancestry); + } + } +} \ No newline at end of file diff --git a/src/Bicep.Core/Semantics/ResourceAncestorVisitor.cs b/src/Bicep.Core/Semantics/ResourceAncestorVisitor.cs new file mode 100644 index 00000000000..b4aa5183ee6 --- /dev/null +++ b/src/Bicep.Core/Semantics/ResourceAncestorVisitor.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Bicep.Core.Syntax; + +namespace Bicep.Core.Semantics +{ + public sealed class ResourceAncestorVisitor : SyntaxVisitor + { + private readonly IBinder binder; + private readonly Stack ancestorResources; + private readonly ImmutableDictionary>.Builder ancestry; + + public ResourceAncestorVisitor(IBinder binder) + { + this.binder = binder; + this.ancestorResources = new Stack(); + this.ancestry = ImmutableDictionary.CreateBuilder>(); + } + + public ImmutableDictionary> Ancestry + => this.ancestry.ToImmutableDictionary(); + + public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax syntax) + { + // Skip analysis for ErrorSymbol and similar cases, these are invalid cases, and won't be emitted. + var symbol = this.binder.GetSymbolInfo(syntax) as ResourceSymbol; + if (symbol is null) + { + base.VisitResourceDeclarationSyntax(syntax); + return; + } + + // We don't need to do anything here to validate types and their relationships, that was handled during type assignment. + this.ancestry.Add(symbol, ImmutableArray.CreateRange(this.ancestorResources.Reverse())); + + try + { + // This will recursively process the resource body - capture the 'current' declaration's declared resource + // type so we can validate nesting. + this.ancestorResources.Push(symbol); + base.VisitResourceDeclarationSyntax(syntax); + } + finally + { + this.ancestorResources.Pop(); + } + } + } +} \ No newline at end of file diff --git a/src/Bicep.Core/Semantics/ResourceSymbolVisitor.cs b/src/Bicep.Core/Semantics/ResourceSymbolVisitor.cs new file mode 100644 index 00000000000..262908cc014 --- /dev/null +++ b/src/Bicep.Core/Semantics/ResourceSymbolVisitor.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Bicep.Core.Semantics +{ + public class ResourceSymbolVisitor : SymbolVisitor + { + public static ImmutableArray GetAllResources(Symbol symbol) + { + var resources = new List(); + var visitor = new ResourceSymbolVisitor(resources); + visitor.Visit(symbol); + + return resources.ToImmutableArray(); + } + + private readonly List resources; + + public ResourceSymbolVisitor(List resources) + { + this.resources = resources; + } + + public override void VisitResourceSymbol(ResourceSymbol symbol) + { + resources.Add(symbol); + base.VisitResourceSymbol(symbol); + } + } +} \ No newline at end of file diff --git a/src/Bicep.Core/Semantics/SemanticModel.cs b/src/Bicep.Core/Semantics/SemanticModel.cs index 3b1f5ad9f08..babcc4daddc 100644 --- a/src/Bicep.Core/Semantics/SemanticModel.cs +++ b/src/Bicep.Core/Semantics/SemanticModel.cs @@ -15,6 +15,7 @@ public class SemanticModel { private readonly Lazy emitLimitationInfoLazy; private readonly Lazy symbolHierarchyLazy; + private readonly Lazy resourceAncestorsLazy; public SemanticModel(Compilation compilation, SyntaxTree syntaxTree) { @@ -42,6 +43,8 @@ public SemanticModel(Compilation compilation, SyntaxTree syntaxTree) return hierarchy; }); + this.resourceAncestorsLazy = new Lazy(() => ResourceAncestorGraph.Compute(syntaxTree, Binder)); + } public SyntaxTree SyntaxTree { get; } @@ -56,6 +59,8 @@ public SemanticModel(Compilation compilation, SyntaxTree syntaxTree) public EmitLimitationInfo EmitLimitationInfo => emitLimitationInfoLazy.Value; + public ResourceAncestorGraph ResourceAncestors => resourceAncestorsLazy.Value; + /// /// Gets all the parser and lexer diagnostics unsorted. Does not include diagnostics from the semantic model. /// diff --git a/src/Bicep.Core/Semantics/SymbolExtensions.cs b/src/Bicep.Core/Semantics/SymbolExtensions.cs index 6f45a0af0e0..38dd859cb4d 100644 --- a/src/Bicep.Core/Semantics/SymbolExtensions.cs +++ b/src/Bicep.Core/Semantics/SymbolExtensions.cs @@ -1,6 +1,7 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. +using System; using Bicep.Core.Syntax; namespace Bicep.Core.Semantics @@ -13,6 +14,12 @@ public static class SymbolExtensions public static SyntaxBase? SafeGetBodyPropertyValue(this ResourceSymbol resourceSymbol, string propertyName) => SafeGetBodyProperty(resourceSymbol, propertyName)?.Value; + public static ObjectPropertySyntax UnsafeGetBodyProperty(this ResourceSymbol resourceSymbol, string propertyName) + => resourceSymbol.SafeGetBodyProperty(propertyName) ?? throw new ArgumentException($"Expected resource syntax body to contain property '{propertyName}'"); + + public static SyntaxBase UnsafeGetBodyPropertyValue(this ResourceSymbol resourceSymbol, string propertyName) + => resourceSymbol.SafeGetBodyPropertyValue(propertyName) ?? throw new ArgumentException($"Expected resource syntax body to contain property '{propertyName}'"); + public static ObjectPropertySyntax? SafeGetBodyProperty(this ModuleSymbol moduleSymbol, string propertyName) => moduleSymbol.DeclaringModule.TryGetBody()?.SafeGetPropertyByName(propertyName); diff --git a/src/Bicep.Core/Syntax/ISyntaxHierarchy.cs b/src/Bicep.Core/Syntax/ISyntaxHierarchy.cs new file mode 100644 index 00000000000..454099b6740 --- /dev/null +++ b/src/Bicep.Core/Syntax/ISyntaxHierarchy.cs @@ -0,0 +1,66 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Collections.Immutable; + +namespace Bicep.Core.Syntax +{ + public interface ISyntaxHierarchy + { + /// + /// Gets the parent of the specified node. Returns null for root nodes. Throws an exception for nodes that have not been indexed. + /// + /// The node + SyntaxBase? GetParent(SyntaxBase node); + + /// + /// Gets all ancestor nodes assignable to in descending order + /// from the top of the tree. + /// + /// The syntax node. + /// The type of node to query. + /// The list of ancestors. + public ImmutableArray GetAllAncestors(SyntaxBase syntax) where TSyntax : SyntaxBase + { + var ancestors = new List(); + + SyntaxBase? next = syntax; + do + { + next = GetParent(next); + if (next is TSyntax match) + { + ancestors.Insert(0, match); + } + } + while (next is not null); + + return ancestors.ToImmutableArray(); + } + + /// + /// Gets the nearest ancestor assignable to above + /// in an ascending walk towards the root of the syntax tree. + /// + /// The syntax node. + /// The type of node to query. + /// The nearest ancestor or null. + public TSyntax? GetNearestAncestor(SyntaxBase syntax) where TSyntax : SyntaxBase + { + SyntaxBase? next = syntax; + do + { + next = GetParent(next); + if (next is TSyntax match) + { + return match; + } + } + while (next is not null); + + // Reached the top without finding a match. + return null; + } + } +} \ No newline at end of file diff --git a/src/Bicep.Core/Syntax/ISyntaxVisitor.cs b/src/Bicep.Core/Syntax/ISyntaxVisitor.cs index af41f3a25a3..8eae7ec70fe 100644 --- a/src/Bicep.Core/Syntax/ISyntaxVisitor.cs +++ b/src/Bicep.Core/Syntax/ISyntaxVisitor.cs @@ -46,6 +46,8 @@ public interface ISyntaxVisitor void VisitPropertyAccessSyntax(PropertyAccessSyntax syntax); + void VisitResourceAccessSyntax(ResourceAccessSyntax syntax); + void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax syntax); void VisitSeparatedSyntaxList(SeparatedSyntaxList syntax); diff --git a/src/Bicep.Core/Syntax/ObjectSyntax.cs b/src/Bicep.Core/Syntax/ObjectSyntax.cs index 3fc032b938b..dd07cecb40b 100644 --- a/src/Bicep.Core/Syntax/ObjectSyntax.cs +++ b/src/Bicep.Core/Syntax/ObjectSyntax.cs @@ -37,5 +37,10 @@ public override TextSpan Span /// Gets the object properties. May return duplicate properties. /// public IEnumerable Properties => this.Children.OfType(); + + /// + /// Gets the child resources of this object. + /// + public IEnumerable Resources => this.Children.OfType(); } } diff --git a/src/Bicep.Core/Syntax/ResourceAccessSyntax.cs b/src/Bicep.Core/Syntax/ResourceAccessSyntax.cs new file mode 100644 index 00000000000..f6d9a6bcc4c --- /dev/null +++ b/src/Bicep.Core/Syntax/ResourceAccessSyntax.cs @@ -0,0 +1,31 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using Bicep.Core.Navigation; +using Bicep.Core.Parsing; + +namespace Bicep.Core.Syntax +{ + public class ResourceAccessSyntax : ExpressionSyntax, ISymbolReference + { + public ResourceAccessSyntax(SyntaxBase baseExpression, Token colon, IdentifierSyntax resourceName) + { + AssertTokenType(colon, nameof(colon), TokenType.Colon); + + this.BaseExpression = baseExpression; + this.Colon = colon; + this.ResourceName = resourceName; + } + + public SyntaxBase BaseExpression { get; } + + public Token Colon { get; } + + public IdentifierSyntax ResourceName { get; } + + IdentifierSyntax ISymbolReference.Name => ResourceName; + + public override void Accept(ISyntaxVisitor visitor) => visitor.VisitResourceAccessSyntax(this); + + public override TextSpan Span => TextSpan.Between(BaseExpression, ResourceName); + } +} diff --git a/src/Bicep.Core/Syntax/ResourceDeclarationSyntax.cs b/src/Bicep.Core/Syntax/ResourceDeclarationSyntax.cs index 4f423a021b5..02a248627d1 100644 --- a/src/Bicep.Core/Syntax/ResourceDeclarationSyntax.cs +++ b/src/Bicep.Core/Syntax/ResourceDeclarationSyntax.cs @@ -8,6 +8,7 @@ using Bicep.Core.Navigation; using Bicep.Core.Parsing; using Bicep.Core.Resources; +using Bicep.Core.Semantics; using Bicep.Core.TypeSystem; namespace Bicep.Core.Syntax @@ -59,7 +60,7 @@ public ResourceDeclarationSyntax(IEnumerable leadingNodes, Token key /// Returns the same value for single resource or resource loops declarations. /// /// resource type provider - public TypeSymbol GetDeclaredType(IResourceTypeProvider resourceTypeProvider) + public TypeSymbol GetDeclaredType(IBinder binder, IResourceTypeProvider resourceTypeProvider) { var stringSyntax = this.TypeString; @@ -76,10 +77,78 @@ public TypeSymbol GetDeclaredType(IResourceTypeProvider resourceTypeProvider) return ErrorType.Create(DiagnosticBuilder.ForPosition(this.Type).InvalidResourceType()); } - var typeReference = ResourceTypeReference.TryParse(stringContent); - if (typeReference == null) + // Before we parse the type name we need to determine if it's a top level resource or not. + ResourceTypeReference? typeReference; + var ancestors = binder.GetAllAncestors(this); + if (ancestors.Length == 0) { - return ErrorType.Create(DiagnosticBuilder.ForPosition(this.Type).InvalidResourceType()); + // This is a top level resource - the type is a fully-qualified type. + typeReference = ResourceTypeReference.TryParse(stringContent); + if (typeReference == null) + { + return ErrorType.Create(DiagnosticBuilder.ForPosition(this.Type).InvalidResourceType()); + } + } + else + { + // This is a nested resource, the type name is a compound of all of the ancestor + // type names. + // + // Ex: 'My.Rp/someType@2020-01-01' -> 'someChild' -> 'someGrandchild' + + // The top-most resource must have a qualified type name. + var baseTypeStringContent = ancestors[0].TypeString?.TryGetLiteralValue(); + if (baseTypeStringContent == null) + { + return ErrorType.Create(DiagnosticBuilder.ForPosition(this.Type).InvalidAncestorResourceType(ancestors[0].Name.IdentifierName)); + } + + var baseTypeReference = ResourceTypeReference.TryParse(baseTypeStringContent); + if (baseTypeReference == null) + { + return ErrorType.Create(DiagnosticBuilder.ForPosition(this.Type).InvalidAncestorResourceType(ancestors[0].Name.IdentifierName)); + } + + // Collect each other ancestor resource's type. + var typeSegments = new List(); + for (var i = 1; i < ancestors.Length; i++) + { + var typeSegmentStringContent = ancestors[i].TypeString?.TryGetLiteralValue(); + if (typeSegmentStringContent == null) + { + return ErrorType.Create(DiagnosticBuilder.ForPosition(this.Type).InvalidAncestorResourceType(ancestors[i].Name.IdentifierName)); + } + + typeSegments.Add(typeSegmentStringContent); + } + + // Add *this* resource's type + typeSegments.Add(stringContent); + + // If this fails, let's walk through and find the root cause. This could be confusing + // for people seeing it the first time. + typeReference = ResourceTypeReference.TryCombine(baseTypeReference, typeSegments); + if (typeReference == null) + { + // We'll special case the last one since it refers to *this* resource. We don't + // want to cascade a bunch of noisy errors for parents, they get their own errors. + for (var j = 0; j < typeSegments.Count - 1; j++) + { + if (!ResourceTypeReference.TryParseSingleTypeSegment(typeSegments[j], out _, out _)) + { + return ErrorType.Create(DiagnosticBuilder.ForPosition(this.Type).InvalidAncestorResourceType(ancestors[j+1].Name.IdentifierName)); + } + } + + if (!ResourceTypeReference.TryParseSingleTypeSegment(stringContent, out _, out _)) + { + // OK this resource is the one that's wrong. + return ErrorType.Create(DiagnosticBuilder.ForPosition(this.Type).InvalidResourceTypeSegment(stringContent)); + } + + // Something went wrong, this should be unreachable. + throw new InvalidOperationException("Failed to find the root cause of an invalid compound resource type."); + } } return resourceTypeProvider.GetType(typeReference, IsExistingResource()); diff --git a/src/Bicep.Core/Syntax/SyntaxHierarchy.cs b/src/Bicep.Core/Syntax/SyntaxHierarchy.cs index ea7d77c0d4b..ac9319f57d7 100644 --- a/src/Bicep.Core/Syntax/SyntaxHierarchy.cs +++ b/src/Bicep.Core/Syntax/SyntaxHierarchy.cs @@ -2,10 +2,11 @@ // Licensed under the MIT License. using System; using System.Collections.Generic; +using System.Collections.Immutable; namespace Bicep.Core.Syntax { - public class SyntaxHierarchy + public class SyntaxHierarchy : ISyntaxHierarchy { private readonly Dictionary parentMap = new Dictionary(); @@ -36,10 +37,10 @@ public void AddRoot(SyntaxBase root) public bool IsDescendant(SyntaxBase node, SyntaxBase potentialAncestor) { var current = node; - while(current != null) + while (current != null) { current = this.GetParent(current); - if(ReferenceEquals(current,potentialAncestor)) + if (ReferenceEquals(current, potentialAncestor)) { return true; } @@ -48,7 +49,29 @@ public bool IsDescendant(SyntaxBase node, SyntaxBase potentialAncestor) return false; } - private sealed class ParentTrackingVisitor: SyntaxVisitor + /// + /// Gets all ancestor nodes assignable to in descending order + /// from the top of the tree. + /// + /// The syntax node. + /// The type of node to query. + /// The list of ancestors. + public ImmutableArray GetAllAncestors(SyntaxBase syntax) where TSyntax : SyntaxBase => + // Use default implementation + ((ISyntaxHierarchy)this).GetAllAncestors(syntax); + + /// + /// Gets the nearest ancestor assignable to above + /// in an ascending walk towards the root of the syntax tree. + /// + /// The syntax node. + /// The type of node to query. + /// The nearest ancestor or null. + public TSyntax? GetNearestAncestor(SyntaxBase syntax) where TSyntax : SyntaxBase => + // Use default implementation + ((ISyntaxHierarchy)this).GetNearestAncestor(syntax); + + private sealed class ParentTrackingVisitor : SyntaxVisitor { private readonly Dictionary parentMap; private readonly Stack currentParents = new Stack(); diff --git a/src/Bicep.Core/Syntax/SyntaxRewriteVisitor.cs b/src/Bicep.Core/Syntax/SyntaxRewriteVisitor.cs index 09ca34f53e5..776ccd578da 100644 --- a/src/Bicep.Core/Syntax/SyntaxRewriteVisitor.cs +++ b/src/Bicep.Core/Syntax/SyntaxRewriteVisitor.cs @@ -474,6 +474,21 @@ protected virtual PropertyAccessSyntax ReplacePropertyAccessSyntax(PropertyAcces } void ISyntaxVisitor.VisitPropertyAccessSyntax(PropertyAccessSyntax syntax) => ReplaceCurrent(syntax, ReplacePropertyAccessSyntax); + protected virtual ResourceAccessSyntax ReplaceResourceAccessSyntax(ResourceAccessSyntax syntax) + { + var hasChanges = Rewrite(syntax.BaseExpression, out var baseExpression); + hasChanges |= Rewrite(syntax.Colon, out var colon); + hasChanges |= Rewrite(syntax.ResourceName, out var propertyName); + + if (!hasChanges) + { + return syntax; + } + + return new ResourceAccessSyntax(baseExpression, colon, propertyName); + } + void ISyntaxVisitor.VisitResourceAccessSyntax(ResourceAccessSyntax syntax) => ReplaceCurrent(syntax, ReplaceResourceAccessSyntax); + protected virtual ParenthesizedExpressionSyntax ReplaceParenthesizedExpressionSyntax(ParenthesizedExpressionSyntax syntax) { var hasChanges = Rewrite(syntax.OpenParen, out var openParen); diff --git a/src/Bicep.Core/Syntax/SyntaxVisitor.cs b/src/Bicep.Core/Syntax/SyntaxVisitor.cs index c94854e1904..6cedf1d22e8 100644 --- a/src/Bicep.Core/Syntax/SyntaxVisitor.cs +++ b/src/Bicep.Core/Syntax/SyntaxVisitor.cs @@ -249,6 +249,13 @@ public virtual void VisitPropertyAccessSyntax(PropertyAccessSyntax syntax) this.Visit(syntax.PropertyName); } + public virtual void VisitResourceAccessSyntax(ResourceAccessSyntax syntax) + { + this.Visit(syntax.BaseExpression); + this.Visit(syntax.Colon); + this.Visit(syntax.ResourceName); + } + public virtual void VisitParenthesizedExpressionSyntax(ParenthesizedExpressionSyntax syntax) { this.Visit(syntax.OpenParen); @@ -291,9 +298,9 @@ public virtual void VisitDecoratorSyntax(DecoratorSyntax syntax) this.Visit(syntax.Expression); } - public virtual void VisitMissingDeclarationSyntax(MissingDeclarationSyntax syntax) - { - this.VisitNodes(syntax.LeadingNodes); + public virtual void VisitMissingDeclarationSyntax(MissingDeclarationSyntax syntax) + { + this.VisitNodes(syntax.LeadingNodes); } protected void VisitTokens(IEnumerable tokens) @@ -310,6 +317,6 @@ protected void VisitNodes(IEnumerable nodes) { this.Visit(node); } - } + } } } diff --git a/src/Bicep.Core/TypeSystem/CyclicCheckVisitor.cs b/src/Bicep.Core/TypeSystem/CyclicCheckVisitor.cs index 951f3ff8bea..a29e628706a 100644 --- a/src/Bicep.Core/TypeSystem/CyclicCheckVisitor.cs +++ b/src/Bicep.Core/TypeSystem/CyclicCheckVisitor.cs @@ -4,9 +4,7 @@ using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; -using Bicep.Core.Diagnostics; using Bicep.Core.Navigation; -using Bicep.Core.Parsing; using Bicep.Core.Semantics; using Bicep.Core.Syntax; using Bicep.Core.Utils; @@ -15,19 +13,17 @@ namespace Bicep.Core.TypeSystem { public sealed class CyclicCheckVisitor : SyntaxVisitor { - private readonly IReadOnlyDictionary declarations; - private readonly IReadOnlyDictionary bindings; private readonly IDictionary> declarationAccessDict; - private DeclaredSymbol? currentDeclaration; + private Stack currentDeclarations; private SyntaxBase? currentDecorator; - public static ImmutableDictionary> FindCycles(ProgramSyntax programSyntax, IReadOnlyDictionary declarations, IReadOnlyDictionary bindings) + public static ImmutableDictionary> FindCycles(ProgramSyntax programSyntax, IReadOnlyDictionary bindings) { - var visitor = new CyclicCheckVisitor(declarations, bindings); + var visitor = new CyclicCheckVisitor(bindings); visitor.Visit(programSyntax); return visitor.FindCycles(); @@ -42,34 +38,42 @@ private ImmutableDictionary> Find return CycleDetector.FindCycles(symbolGraph); } - private CyclicCheckVisitor(IReadOnlyDictionary declarations, IReadOnlyDictionary bindings) + private CyclicCheckVisitor(IReadOnlyDictionary bindings) { - this.declarations = declarations; this.bindings = bindings; this.declarationAccessDict = new Dictionary>(); + this.currentDeclarations = new Stack(); } private void VisitDeclaration(TDeclarationSyntax syntax, Action visitBaseFunc) where TDeclarationSyntax : SyntaxBase, ITopLevelNamedDeclarationSyntax { - if (!bindings.ContainsKey(syntax)) + if (!bindings.TryGetValue(syntax, out var symbol) || + symbol is not DeclaredSymbol currentDeclaration || + string.IsNullOrEmpty(currentDeclaration.Name) || + string.Equals(LanguageConstants.ErrorName, currentDeclaration.Name, StringComparison.Ordinal) || + string.Equals(LanguageConstants.MissingName, currentDeclaration.Name, StringComparison.Ordinal)) { - // If we've failed to bind the symbol, we should already have an error, and a cycle should not be possible + // If we've failed to bind the symbol to a name, we should already have an error, and a cycle should not be possible return; } - currentDeclaration = declarations[syntax.Name.IdentifierName]; + // Maintain the stack of declarations since they can be nested declarationAccessDict[currentDeclaration] = new List(); - visitBaseFunc(syntax); - currentDeclaration = null; + try + { + currentDeclarations.Push(currentDeclaration); + visitBaseFunc(syntax); + } + finally + { + currentDeclarations.Pop(); + } } public override void VisitVariableDeclarationSyntax(VariableDeclarationSyntax syntax) => VisitDeclaration(syntax, base.VisitVariableDeclarationSyntax); - public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax syntax) - => VisitDeclaration(syntax, base.VisitResourceDeclarationSyntax); - public override void VisitModuleDeclarationSyntax(ModuleDeclarationSyntax syntax) => VisitDeclaration(syntax, base.VisitModuleDeclarationSyntax); @@ -79,9 +83,35 @@ public override void VisitOutputDeclarationSyntax(OutputDeclarationSyntax syntax public override void VisitParameterDeclarationSyntax(ParameterDeclarationSyntax syntax) => VisitDeclaration(syntax, base.VisitParameterDeclarationSyntax); + public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax syntax) + { + // Push this resource onto the stack and process its body (including children). + // + // We process *this* resource using postorder because VisitDeclaration will do + // some initialization. + VisitDeclaration(syntax, base.VisitResourceDeclarationSyntax); + + // Resources are special because a lexically nested resource implies a dependency + // They are both a source of declarations and a use of them. + if (!bindings.TryGetValue(syntax, out var symbol) || symbol is not DeclaredSymbol currentDeclaration) + { + // If we've failed to bind the symbol, we should already have an error, and a cycle should not be possible + return; + } + + if (declarationAccessDict.TryGetValue(currentDeclaration, out var accesses)) + { + // Walk all ancestors and add a reference from this resource + foreach (var ancestor in currentDeclarations.OfType()) + { + accesses.Add(ancestor.DeclaringResource); + } + } + } + public override void VisitVariableAccessSyntax(VariableAccessSyntax syntax) { - if (currentDeclaration == null) + if (!currentDeclarations.TryPeek(out var currentDeclaration)) { if (currentDecorator != null) { @@ -96,21 +126,21 @@ public override void VisitVariableAccessSyntax(VariableAccessSyntax syntax) base.VisitVariableAccessSyntax(syntax); } - public override void VisitDecoratorSyntax(DecoratorSyntax syntax) - { - this.currentDecorator = syntax; - base.VisitDecoratorSyntax(syntax); + public override void VisitDecoratorSyntax(DecoratorSyntax syntax) + { + this.currentDecorator = syntax; + base.VisitDecoratorSyntax(syntax); this.currentDecorator = null; } public override void VisitFunctionCallSyntax(FunctionCallSyntax syntax) { - if (currentDeclaration == null) + if (!currentDeclarations.TryPeek(out var currentDeclaration)) { - if (currentDecorator != null) - { - // We are inside a dangling decorator. - return; + if (currentDecorator != null) + { + // We are inside a dangling decorator. + return; } throw new ArgumentException($"Function access outside of declaration or decorator"); diff --git a/src/Bicep.Core/TypeSystem/DeclaredTypeManager.cs b/src/Bicep.Core/TypeSystem/DeclaredTypeManager.cs index 1f7a409e5e5..f3efa89d38d 100644 --- a/src/Bicep.Core/TypeSystem/DeclaredTypeManager.cs +++ b/src/Bicep.Core/TypeSystem/DeclaredTypeManager.cs @@ -114,7 +114,7 @@ public DeclaredTypeManager(IResourceTypeProvider resourceTypeProvider, TypeManag private DeclaredTypeAssignment GetResourceType(ResourceDeclarationSyntax syntax) { - var declaredResourceType = syntax.GetDeclaredType(this.resourceTypeProvider); + var declaredResourceType = syntax.GetDeclaredType(this.binder, this.resourceTypeProvider); // if the value is a loop (not a condition or object), the type is an array of the declared resource type return new DeclaredTypeAssignment( diff --git a/src/Bicep.Core/TypeSystem/DeployTimeConstantVisitor.cs b/src/Bicep.Core/TypeSystem/DeployTimeConstantVisitor.cs index 74e914db509..b0c4532aab2 100644 --- a/src/Bicep.Core/TypeSystem/DeployTimeConstantVisitor.cs +++ b/src/Bicep.Core/TypeSystem/DeployTimeConstantVisitor.cs @@ -39,7 +39,7 @@ private DeployTimeConstantVisitor(SemanticModel model, IDiagnosticWriter diagnos public static void ValidateDeployTimeConstants(SemanticModel model, IDiagnosticWriter diagnosticWriter) { var deploymentTimeConstantVisitor = new DeployTimeConstantVisitor(model, diagnosticWriter); - foreach (var declaredSymbol in model.Root.ResourceDeclarations) + foreach (var declaredSymbol in model.Root.GetAllResourceDeclarations()) { deploymentTimeConstantVisitor.Visit(declaredSymbol.DeclaringSyntax); } @@ -91,13 +91,14 @@ public override void VisitObjectSyntax(ObjectSyntax syntax) return; } // Only visit the object properties if they are required to be deploy time constant. - foreach (var deployTimeIdentifier in ObjectSyntaxExtensions.ToNamedPropertyDictionary(syntax)) + foreach (var propertyName in ObjectSyntaxExtensions.ToKnownPropertyNames(syntax)) { - if (this.bodyObj.Properties.TryGetValue(deployTimeIdentifier.Key, out var propertyType) && + if (syntax.SafeGetPropertyByName(propertyName) is ObjectPropertySyntax deployTimeIdentifier && + this.bodyObj.Properties.TryGetValue(propertyName, out var propertyType) && propertyType.Flags.HasFlag(TypePropertyFlags.DeployTimeConstant)) { - this.currentProperty = deployTimeIdentifier.Key; - this.VisitObjectPropertySyntax(deployTimeIdentifier.Value); + this.currentProperty = propertyName; + this.VisitObjectPropertySyntax(deployTimeIdentifier); this.currentProperty = null; } } diff --git a/src/Bicep.Core/TypeSystem/TypeAssignmentVisitor.cs b/src/Bicep.Core/TypeSystem/TypeAssignmentVisitor.cs index 045e8899190..152538e5fae 100644 --- a/src/Bicep.Core/TypeSystem/TypeAssignmentVisitor.cs +++ b/src/Bicep.Core/TypeSystem/TypeAssignmentVisitor.cs @@ -200,7 +200,7 @@ public override void VisitForSyntax(ForSyntax syntax) public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax syntax) => AssignTypeWithDiagnostics(syntax, diagnostics => { - var declaredType = syntax.GetDeclaredType(resourceTypeProvider); + var declaredType = syntax.GetDeclaredType(binder, resourceTypeProvider); this.ValidateDecorators(syntax.Decorators, declaredType, diagnostics); @@ -772,6 +772,51 @@ public override void VisitPropertyAccessSyntax(PropertyAccessSyntax syntax) } }); + public override void VisitResourceAccessSyntax(ResourceAccessSyntax syntax) + => AssignTypeWithDiagnostics(syntax, diagnostics => { + var errors = new List(); + + var baseType = typeManager.GetTypeInfo(syntax.BaseExpression); + CollectErrors(errors, baseType); + + if (PropagateErrorType(errors, baseType)) + { + return ErrorType.Create(errors); + } + + if (baseType is not ResourceType) + { + // can only access children of resources + return ErrorType.Create(DiagnosticBuilder.ForPosition(syntax.ResourceName).ResourceRequiredForResourceAccess(baseType.Name)); + } + + if (!syntax.ResourceName.IsValid) + { + // the resource name is not valid + // there's already a parse error for it, so we don't need to add a type error as well + return ErrorType.Empty(); + } + + // Should have a symbol from name binding. + var symbol = binder.GetSymbolInfo(syntax); + if (symbol == null) + { + throw new InvalidOperationException("ResourceAccessSyntax was not assigned a symbol during name binding."); + } + + if (symbol is ErrorSymbol error) + { + return ErrorType.Create(error.GetDiagnostics()); + } + else if (symbol is not ResourceSymbol) + { + return ErrorType.Create(DiagnosticBuilder.ForPosition(syntax.ResourceName).ResourceRequiredForResourceAccess(baseType.Kind.ToString())); + } + + // This is a valid nested resource. Return its type. + return typeManager.GetTypeInfo(((ResourceSymbol)symbol).DeclaringResource); + }); + public override void VisitFunctionCallSyntax(FunctionCallSyntax syntax) => AssignType(syntax, () => { var errors = new List(); diff --git a/src/Bicep.Decompiler/Rewriters/ParentChildResourceNameRewriter.cs b/src/Bicep.Decompiler/Rewriters/ParentChildResourceNameRewriter.cs index ece2b006b89..9c4a27f9d0c 100644 --- a/src/Bicep.Decompiler/Rewriters/ParentChildResourceNameRewriter.cs +++ b/src/Bicep.Decompiler/Rewriters/ParentChildResourceNameRewriter.cs @@ -41,7 +41,7 @@ protected override ResourceDeclarationSyntax ReplaceResourceDeclarationSyntax(Re return syntax; } - foreach (var otherResourceSymbol in semanticModel.Root.ResourceDeclarations) + foreach (var otherResourceSymbol in semanticModel.Root.GetAllResourceDeclarations()) { if (otherResourceSymbol.Type is not ResourceType otherResourceType || otherResourceType.TypeReference.Types.Length != resourceType.TypeReference.Types.Length - 1 || diff --git a/src/Bicep.LangServer.IntegrationTests/Helpers/IntegrationTestHelper.cs b/src/Bicep.LangServer.IntegrationTests/Helpers/IntegrationTestHelper.cs index e25e2589988..a142e503f19 100644 --- a/src/Bicep.LangServer.IntegrationTests/Helpers/IntegrationTestHelper.cs +++ b/src/Bicep.LangServer.IntegrationTests/Helpers/IntegrationTestHelper.cs @@ -20,8 +20,8 @@ using System.Collections.Generic; using Bicep.Core.FileSystem; using Bicep.Core.UnitTests.FileSystem; -using Bicep.Core.Navigation; - +using Bicep.Core.Navigation; + namespace Bicep.LangServer.IntegrationTests { public static class IntegrationTestHelper @@ -112,17 +112,17 @@ public static async Task StartServerWithTextAsync(string text, public static Position GetPosition(ImmutableArray lineStarts, SyntaxBase syntax) { - if (syntax is InstanceFunctionCallSyntax instanceFunctionCall) + if (syntax is ISymbolReference reference) { // get identifier span otherwise syntax.Span returns the position from the starting position of the whole expression. // e.g. in an instance function call such as: az.resourceGroup(), syntax.Span position starts at 'az', // whereas instanceFunctionCall.Name.Span the position will start in resourceGroup() which is what it should be in this // case. - return PositionHelper.GetPosition(lineStarts, instanceFunctionCall.Name.Span.Position); + return PositionHelper.GetPosition(lineStarts, reference.Name.Span.Position); } - if (syntax is ITopLevelDeclarationSyntax declaration) - { + if (syntax is ITopLevelDeclarationSyntax declaration) + { return PositionHelper.GetPosition(lineStarts, declaration.Keyword.Span.Position); } diff --git a/src/Bicep.LangServer.IntegrationTests/HoverTests.cs b/src/Bicep.LangServer.IntegrationTests/HoverTests.cs index 324eac832ad..bd7bd541ddf 100644 --- a/src/Bicep.LangServer.IntegrationTests/HoverTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/HoverTests.cs @@ -62,9 +62,12 @@ public async Task HoveringOverSymbolReferencesAndDeclarationsShouldProduceHovers foreach (SyntaxBase symbolReference in symbolReferences) { - var syntaxPosition = symbolReference is ITopLevelDeclarationSyntax declaration - ? declaration.Keyword.Span.Position - : symbolReference.Span.Position; + var syntaxPosition = symbolReference switch + { + ITopLevelDeclarationSyntax d => d.Keyword.Span.Position, + ResourceAccessSyntax r => r.ResourceName.Span.Position, + _ => symbolReference.Span.Position, + }; var hover = await client.RequestHover(new HoverParams { @@ -110,8 +113,8 @@ public async Task HoveringOverSymbolReferencesAndDeclarationsShouldProduceHovers [DataTestMethod] [DynamicData(nameof(GetData), DynamicDataSourceType.Method, DynamicDataDisplayNameDeclaringType = typeof(DataSet), DynamicDataDisplayName = nameof(DataSet.GetDisplayName))] public async Task HoveringOverNonHoverableElementsShouldProduceEmptyHovers(DataSet dataSet) - { - // local function + { + // local function static bool IsNonHoverable(SyntaxBase node) => !(node is PropertyAccessSyntax propertyAccessSyntax && propertyAccessSyntax.BaseExpression is ISymbolReference) && node is not ISymbolReference && diff --git a/src/Bicep.LangServer.IntegrationTests/RenameSymbolTests.cs b/src/Bicep.LangServer.IntegrationTests/RenameSymbolTests.cs index e2eb0ddccb8..a63ea54a949 100644 --- a/src/Bicep.LangServer.IntegrationTests/RenameSymbolTests.cs +++ b/src/Bicep.LangServer.IntegrationTests/RenameSymbolTests.cs @@ -45,13 +45,13 @@ public async Task RenamingIdentifierAccessOrDeclarationShouldRenameDeclarationAn .ToLookup(pair => pair.Value, pair => pair.Key); var validVariableAccessPairs = symbolTable - .Where(pair => (pair.Key is VariableAccessSyntax || pair.Key is ITopLevelNamedDeclarationSyntax) + .Where(pair => (pair.Key is VariableAccessSyntax || pair.Key is ResourceAccessSyntax || pair.Key is ITopLevelNamedDeclarationSyntax) && pair.Value.Kind != SymbolKind.Error && pair.Value.Kind != SymbolKind.Function && pair.Value.Kind != SymbolKind.Namespace // symbols whose identifiers have parse errors will have a name like or && pair.Value.Name.Contains("<") == false); - + const string expectedNewText = "NewIdentifier"; foreach (var (syntax, symbol) in validVariableAccessPairs) { diff --git a/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs b/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs index da7553a2107..937bc254d3b 100644 --- a/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs @@ -174,6 +174,37 @@ private IEnumerable GetResourceTypeCompletions(SemanticModel mod return Enumerable.Empty(); } + // For a nested resource, we want to filter the set of types. + // + // The strategy when *can't* filter - due to errors - to fallback to the main path and offer full completions + // then once the user corrects whatever's cause the error, they will be told to simplify the type. + if (context.EnclosingDeclaration is SyntaxBase && + model.Binder.GetNearestAncestor(context.EnclosingDeclaration) is ResourceDeclarationSyntax parentSyntax && + model.GetSymbolInfo(parentSyntax) is ResourceSymbol parentSymbol && + model.GetTypeInfo(parentSyntax) is ResourceType parentType) + { + // This is more complex because we allow the API version to be omitted, so we want to make a list of unique values + // for the FQT, and then create a "no version" completion + a completion for each version. + var filtered = model.Compilation.ResourceTypeProvider.GetAvailableTypes() + .Where(rt => parentType.TypeReference.IsParentOf(rt)) + .ToLookup(rt => rt.FullyQualifiedType); + + var index = 0; + var items = new List(); + foreach (var group in filtered.OrderBy(group => group.Key, StringComparer.OrdinalIgnoreCase)) + { + // Doesn't matter which one of the group we take, we're leaving out the version. + items.Add(CreateResourceTypeSegmentCompletion(group.First(), index++, context.ReplacementRange, includeApiVersion: false, displayApiVersion: parentType.TypeReference.ApiVersion)); + + foreach (var resourceType in group.OrderByDescending(rt => rt.ApiVersion)) + { + items.Add(CreateResourceTypeSegmentCompletion(resourceType, index++, context.ReplacementRange, includeApiVersion: true, displayApiVersion: resourceType.ApiVersion)); + } + } + + return items; + } + // we need to ensure that Microsoft.Compute/virtualMachines@whatever comes before Microsoft.Compute/virtualMachines/extensions@whatever // similarly, newest api versions should be shown first return model.Compilation.ResourceTypeProvider.GetAvailableTypes() @@ -319,7 +350,7 @@ IEnumerable GetAccessibleDecoratorFunctionsWithCache(NamespaceTy { // add the non-output declarations with valid identifiers at current scope var currentScope = context.ActiveScopes[depth]; - AddSymbolCompletions(completions, currentScope.AllDeclarations.Where(decl => decl.NameSyntax.IsValid && !(decl is OutputSymbol))); + AddSymbolCompletions(completions, currentScope.Declarations.Where(decl => decl.NameSyntax.IsValid && !(decl is OutputSymbol))); } } else @@ -329,7 +360,7 @@ IEnumerable GetAccessibleDecoratorFunctionsWithCache(NamespaceTy @namespace => GetAccessibleDecoratorFunctionsWithCache(@namespace.Type).Any())); // Record the names of the non-output declarations which will be used to check name clashes later. - declaredNames.UnionWith(model.Root.AllDeclarations.Where(decl => decl.NameSyntax.IsValid && decl is not OutputSymbol).Select(decl => decl.Name)); + declaredNames.UnionWith(model.Root.Declarations.Where(decl => decl.NameSyntax.IsValid && decl is not OutputSymbol).Select(decl => decl.Name)); } // get names of functions that always require to be fully qualified due to clashes between namespaces @@ -664,6 +695,20 @@ private static CompletionItem CreateResourceTypeCompletion(ResourceTypeReference .WithSortText(index.ToString("x8")); } + private static CompletionItem CreateResourceTypeSegmentCompletion(ResourceTypeReference resourceType, int index, Range replacementRange, bool includeApiVersion, string displayApiVersion) + { + // We create one completion with and without the API version. + var insertText = includeApiVersion ? + StringUtils.EscapeBicepString($"{resourceType.Types[^1]}@{resourceType.ApiVersion}") : + StringUtils.EscapeBicepString($"{resourceType.Types[^1]}"); + return CompletionItemBuilder.Create(CompletionItemKind.Class) + .WithLabel(insertText) + .WithPlainTextEdit(replacementRange, insertText) + .WithDocumentation($"Namespace: `{resourceType.Namespace}`{MarkdownNewLine}Type: `{resourceType.TypesString}`{MarkdownNewLine}API Version: `{displayApiVersion}`") + // 8 hex digits is probably overkill :) + .WithSortText(index.ToString("x8")); + } + private static CompletionItem CreateModulePathCompletion(string name, string path, Range replacementRange, CompletionItemKind completionItemKind, CompletionPriority priority) { path = StringUtils.EscapeBicepString(path); diff --git a/src/Bicep.LangServer/Handlers/BicepDocumentSymbolHandler.cs b/src/Bicep.LangServer/Handlers/BicepDocumentSymbolHandler.cs index dd4002de90d..9b8a64aee08 100644 --- a/src/Bicep.LangServer/Handlers/BicepDocumentSymbolHandler.cs +++ b/src/Bicep.LangServer/Handlers/BicepDocumentSymbolHandler.cs @@ -52,7 +52,7 @@ private static DocumentSymbolRegistrationOptions GetSymbolRegistrationOptions() private IEnumerable GetSymbols(CompilationContext context) { - return context.Compilation.GetEntrypointSemanticModel().Root.AllDeclarations + return context.Compilation.GetEntrypointSemanticModel().Root.Declarations .OrderBy(symbol=>symbol.DeclaringSyntax.Span.Position) .Select(symbol => new SymbolInformationOrDocumentSymbol(CreateDocumentSymbol(symbol, context.LineStarts))); } diff --git a/src/Bicep.LangServer/SemanticTokenVisitor.cs b/src/Bicep.LangServer/SemanticTokenVisitor.cs index 4e976ac1ea6..a209796dbc9 100644 --- a/src/Bicep.LangServer/SemanticTokenVisitor.cs +++ b/src/Bicep.LangServer/SemanticTokenVisitor.cs @@ -132,6 +132,12 @@ public override void VisitPropertyAccessSyntax(PropertyAccessSyntax syntax) base.VisitPropertyAccessSyntax(syntax); } + public override void VisitResourceAccessSyntax(ResourceAccessSyntax syntax) + { + AddTokenType(syntax.ResourceName, SemanticTokenType.Property); + base.VisitResourceAccessSyntax(syntax); + } + public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax syntax) { AddTokenType(syntax.Keyword, SemanticTokenType.Keyword); diff --git a/src/Bicep.Wasm/LanguageHelpers/SemanticTokenVisitor.cs b/src/Bicep.Wasm/LanguageHelpers/SemanticTokenVisitor.cs index 7aca51660ab..3f347c97e4b 100644 --- a/src/Bicep.Wasm/LanguageHelpers/SemanticTokenVisitor.cs +++ b/src/Bicep.Wasm/LanguageHelpers/SemanticTokenVisitor.cs @@ -128,6 +128,12 @@ public override void VisitPropertyAccessSyntax(PropertyAccessSyntax syntax) base.VisitPropertyAccessSyntax(syntax); } + public override void VisitResourceAccessSyntax(ResourceAccessSyntax syntax) + { + AddTokenType(syntax.ResourceName, SemanticTokenType.Property); + base.VisitResourceAccessSyntax(syntax); + } + public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax syntax) { AddTokenType(syntax.Keyword, SemanticTokenType.Keyword);