From 9133b4c0286ae7787efd3646a6df175bdcd5a1d8 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 In progress for #1363 This is an early preview of the work for allowing lexical scoping (nesting) of child resources. This changeset doesn't totally match the current proposal. It will need to be updated based on decisions tracker there and the loops/scoping work that is happening right now. --- .../ResourceTests.cs | 187 ++++++++++++++++++ .../TypeSystem/TypeValidationTests.cs | 48 +++++ .../Files/Resources_CRLF/main.bicep | 19 ++ .../Resources_CRLF/main.diagnostics.bicep | 22 +++ .../Files/Resources_CRLF/main.formatted.bicep | 18 ++ .../Files/Resources_CRLF/main.json | 24 +++ .../Files/Resources_CRLF/main.symbols.bicep | 23 +++ .../Files/Resources_CRLF/main.syntax.bicep | 130 ++++++++++++ .../Files/Resources_CRLF/main.tokens.bicep | 86 ++++++++ .../DiagnosticCollectionExtensions.cs | 8 + .../Utils/ParserHelper.cs | 2 +- .../Diagnostics/DiagnosticBuilder.cs | 5 + .../Emit/EmitLimitationCalculator.cs | 2 +- src/Bicep.Core/Emit/ExpressionConverter.cs | 27 ++- src/Bicep.Core/Emit/ExpressionEmitter.cs | 5 + .../Emit/ResourceDependencyVisitor.cs | 4 +- src/Bicep.Core/Emit/ScopeHelper.cs | 2 +- src/Bicep.Core/Emit/TemplateWriter.cs | 63 +++++- src/Bicep.Core/Parsing/Parser.cs | 86 ++++---- .../Resources/ResourceTypeReference.cs | 12 ++ src/Bicep.Core/Semantics/Binder.cs | 36 ++-- .../Semantics/DeclarationVisitor.cs | 60 ++++-- src/Bicep.Core/Semantics/FileSymbol.cs | 10 +- src/Bicep.Core/Semantics/ILanguageScope.cs | 2 +- src/Bicep.Core/Semantics/LocalScope.cs | 13 +- .../Semantics/NameBindingVisitor.cs | 58 ++++-- .../Semantics/ResourceAncestorGraph.cs | 40 ++++ .../Semantics/ResourceAncestorVisitor.cs | 87 ++++++++ .../Semantics/ResourceSymbolVisitor.cs | 32 +++ src/Bicep.Core/Semantics/SemanticModel.cs | 6 + src/Bicep.Core/Semantics/SymbolVisitor.cs | 1 - .../Syntax/ObjectSyntaxExtensions.cs | 72 ++++--- .../TypeSystem/CyclicCheckVisitor.cs | 81 +++++--- .../TypeSystem/DeployTimeConstantVisitor.cs | 11 +- .../ParentChildResourceNameRewriter.cs | 2 +- .../Completions/BicepCompletionProvider.cs | 2 +- .../Handlers/BicepDocumentSymbolHandler.cs | 2 +- 37 files changed, 1116 insertions(+), 172 deletions(-) create mode 100644 src/Bicep.Core.IntegrationTests/ResourceTests.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 diff --git a/src/Bicep.Core.IntegrationTests/ResourceTests.cs b/src/Bicep.Core.IntegrationTests/ResourceTests.cs new file mode 100644 index 00000000000..bc76c4591c5 --- /dev/null +++ b/src/Bicep.Core.IntegrationTests/ResourceTests.cs @@ -0,0 +1,187 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +using System.Linq; +using Bicep.Core.Diagnostics; +using Bicep.Core.Semantics; +using Bicep.Core.TypeSystem; +using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.Utils; +using FluentAssertions; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Bicep.Core.IntegrationTests +{ + [TestClass] + public class ResourceTests + { + [TestMethod] + public void NestedResources_symbols_are_bound() + { + var program = @" +resource parent 'My.RP/parentType@2020-01-01' = { + name: 'parent' + properties: { + size: 'large' + } + + resource child 'My.RP/parentType/childType@2020-01-01' = { + name: 'child' + properties: { + style: 'very cool' + } + } + + resource sibling 'My.RP/parentType/childType@2020-01-01' = { + 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-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_child_cannot_be_referenced_outside_of_scope() + { + 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' + } + } +} + +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_cycle_is_detected_correctly() + { + var program = @" +resource parent 'My.RP/parentType@2020-01-01' = { + name: 'parent' + properties: { + style: child.properties.style + } + + resource child 'My.RP/parentType/childType@2020-01-01' = { + 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 'My.RP/parentType/childType@2020-01-01' = { + name: 'child' + properties: { + } + + resource grandchild 'My.RP/parentType/childType/grandchildType@2020-01-01' = { + 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 'My.RP/parentType/childType@2020-01-01' = { + name: 'child' + properties: { + } + + resource grandchild 'My.RP/parentType/childType/grandchildType@2020-01-01' = { + 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 + } + } +} \ No newline at end of file diff --git a/src/Bicep.Core.IntegrationTests/TypeSystem/TypeValidationTests.cs b/src/Bicep.Core.IntegrationTests/TypeSystem/TypeValidationTests.cs index f807c3e3593..3b37046261c 100644 --- a/src/Bicep.Core.IntegrationTests/TypeSystem/TypeValidationTests.cs +++ b/src/Bicep.Core.IntegrationTests/TypeSystem/TypeValidationTests.cs @@ -347,5 +347,53 @@ public void Type_validation_narrowing_on_discriminated_object_types(TypeSymbolVa ); } } + + [TestMethod] + public void Type_validation_prevents_invalid_nesting() + { + var customTypes = new [] + { + ResourceTypeProviderHelper.CreateCustomResourceType("My.Rp/nonNestedType", "2020-01-01", TypeSymbolValidationFlags.Default), + ResourceTypeProviderHelper.CreateCustomResourceType("My.Rp/nestedType", "2020-01-01", TypeSymbolValidationFlags.Default), + ResourceTypeProviderHelper.CreateCustomResourceType("My.Rp/nestedType/childType", "2020-01-01", TypeSymbolValidationFlags.Default), + ResourceTypeProviderHelper.CreateCustomResourceType("My.Rp/nestedType/childType/grandchildType", "2020-01-01", TypeSymbolValidationFlags.Default), + }; + + // Nesting inside the wrong type + var program = @" +resource parent 'My.Rp/nonNestedType@2020-01-01' = { + name: 'top' + properties: { } + + resource child 'My.Rp/nestedType/childType@2020-01-01' = { + name: 'child' + properties: { } + } +} +"; + + var model = GetSemanticModelForTest(program, customTypes); + model.GetAllDiagnostics().Should().SatisfyRespectively( + x => x.Should().HaveCodeAndSeverity("BCP139", DiagnosticLevel.Error).And.HaveMessage("The resource type \"My.Rp/nestedType/childType\" cannot be nested inside a resource of type \"My.Rp/nonNestedType\".") + ); + + // Nesting more than one level deep + program = @" +resource parent 'My.Rp/nestedType@2020-01-01' = { + name: 'top' + properties: { } + + resource child 'My.Rp/nestedType/childType/grandchildType@2020-01-01' = { + name: 'child' + properties: { } + } +} +"; + + model = GetSemanticModelForTest(program, customTypes); + model.GetAllDiagnostics().Should().SatisfyRespectively( + x => x.Should().HaveCodeAndSeverity("BCP139", DiagnosticLevel.Error).And.HaveMessage("The resource type \"My.Rp/nestedType/childType/grandchildType\" cannot be nested inside a resource of type \"My.Rp/nestedType\".") + ); + } } } \ No newline at end of file diff --git a/src/Bicep.Core.Samples/Files/Resources_CRLF/main.bicep b/src/Bicep.Core.Samples/Files/Resources_CRLF/main.bicep index e5831399467..40becf941df 100644 --- a/src/Bicep.Core.Samples/Files/Resources_CRLF/main.bicep +++ b/src/Bicep.Core.Samples/Files/Resources_CRLF/main.bicep @@ -265,4 +265,23 @@ resource existing2 'Mock.Rp/existingExtensionResource@2020-01-01' existing = { resource extension3 'My.Rp/extensionResource@2020-12-01' = { name: 'extension3' scope: existing1 +} + +param shouldDeployChildAndGrandChild bool = true +resource nestedA 'My.Rp/nestedResource@2020-01-01' existing = { + name: 'nestedA' + + resource nestedB 'My.Rp/nestedResource/childType@2020-01-01' = if (shouldDeployChildAndGrandChild) { + name: 'nestedB' + + resource nestedC 'My.Rp/nestedResource/childType/grandchildType@2020-01-01' = { + name: 'nestedC' + properties: { + style: nestedA.properties.style + } + } + + properties: { + } + } } \ No newline at end of file diff --git a/src/Bicep.Core.Samples/Files/Resources_CRLF/main.diagnostics.bicep b/src/Bicep.Core.Samples/Files/Resources_CRLF/main.diagnostics.bicep index 9ece22b1dd4..45ec7d0f032 100644 --- a/src/Bicep.Core.Samples/Files/Resources_CRLF/main.diagnostics.bicep +++ b/src/Bicep.Core.Samples/Files/Resources_CRLF/main.diagnostics.bicep @@ -282,3 +282,25 @@ resource extension3 'My.Rp/extensionResource@2020-12-01' = { name: 'extension3' scope: existing1 } + +param shouldDeployChildAndGrandChild bool = true +resource nestedA 'My.Rp/nestedResource@2020-01-01' existing = { +//@[17:50) [BCP081 (Warning)] Resource type "My.Rp/nestedResource@2020-01-01" does not have types available. |'My.Rp/nestedResource@2020-01-01'| + name: 'nestedA' + + resource nestedB 'My.Rp/nestedResource/childType@2020-01-01' = if (shouldDeployChildAndGrandChild) { +//@[19:62) [BCP081 (Warning)] Resource type "My.Rp/nestedResource/childType@2020-01-01" does not have types available. |'My.Rp/nestedResource/childType@2020-01-01'| + name: 'nestedB' + + resource nestedC 'My.Rp/nestedResource/childType/grandchildType@2020-01-01' = { +//@[21:79) [BCP081 (Warning)] Resource type "My.Rp/nestedResource/childType/grandchildType@2020-01-01" does not have types available. |'My.Rp/nestedResource/childType/grandchildType@2020-01-01'| + name: 'nestedC' + properties: { + style: nestedA.properties.style + } + } + + properties: { + } + } +} diff --git a/src/Bicep.Core.Samples/Files/Resources_CRLF/main.formatted.bicep b/src/Bicep.Core.Samples/Files/Resources_CRLF/main.formatted.bicep index 5e26386af6a..e986f4a2c52 100644 --- a/src/Bicep.Core.Samples/Files/Resources_CRLF/main.formatted.bicep +++ b/src/Bicep.Core.Samples/Files/Resources_CRLF/main.formatted.bicep @@ -265,3 +265,21 @@ resource extension3 'My.Rp/extensionResource@2020-12-01' = { name: 'extension3' scope: existing1 } + +param shouldDeployChildAndGrandChild bool = true +resource nestedA 'My.Rp/nestedResource@2020-01-01' existing = { + name: 'nestedA' + + resource nestedB 'My.Rp/nestedResource/childType@2020-01-01' = if (shouldDeployChildAndGrandChild) { + name: 'nestedB' + + resource nestedC 'My.Rp/nestedResource/childType/grandchildType@2020-01-01' = { + name: 'nestedC' + properties: { + style: nestedA.properties.style + } + } + + properties: {} + } +} diff --git a/src/Bicep.Core.Samples/Files/Resources_CRLF/main.json b/src/Bicep.Core.Samples/Files/Resources_CRLF/main.json index 7cd0d5b5482..3df0687f4a0 100644 --- a/src/Bicep.Core.Samples/Files/Resources_CRLF/main.json +++ b/src/Bicep.Core.Samples/Files/Resources_CRLF/main.json @@ -21,6 +21,10 @@ "shouldDeployVm": { "type": "bool", "defaultValue": true + }, + "shouldDeployChildAndGrandChild": { + "type": "bool", + "defaultValue": true } }, "functions": [], @@ -37,6 +41,26 @@ "myInterpKey": "abc" }, "resources": [ + { + "condition": "[parameters('shouldDeployChildAndGrandChild')]", + "type": "My.Rp/nestedResource/childType/grandchildType", + "apiVersion": "2020-01-01", + "name": "[format('{0}/{1}', 'nestedA', 'nestedB')]", + "properties": { + "style": "[reference(resourceId('My.Rp/nestedResource', 'nestedA'), '2020-01-01').style]" + }, + "dependsOn": [ + "[resourceId('My.Rp/nestedResource/childType', split(format('{0}', 'nestedA'), '/')[0], split(format('{0}', 'nestedA'), '/')[1])]" + ] + }, + { + "condition": "[parameters('shouldDeployChildAndGrandChild')]", + "type": "My.Rp/nestedResource/childType", + "apiVersion": "2020-01-01", + "name": "[format('{0}', 'nestedA')]", + "properties": {}, + "dependsOn": [] + }, { "type": "Microsoft.Storage/storageAccounts", "apiVersion": "2019-06-01", diff --git a/src/Bicep.Core.Samples/Files/Resources_CRLF/main.symbols.bicep b/src/Bicep.Core.Samples/Files/Resources_CRLF/main.symbols.bicep index 38b6434f8d8..ea94802c6c3 100644 --- a/src/Bicep.Core.Samples/Files/Resources_CRLF/main.symbols.bicep +++ b/src/Bicep.Core.Samples/Files/Resources_CRLF/main.symbols.bicep @@ -307,3 +307,26 @@ resource extension3 'My.Rp/extensionResource@2020-12-01' = { name: 'extension3' scope: existing1 } + +param shouldDeployChildAndGrandChild bool = true +//@[6:36) Parameter shouldDeployChildAndGrandChild. Type: bool. Declaration start char: 0, length: 48 +resource nestedA 'My.Rp/nestedResource@2020-01-01' existing = { +//@[9:16) Resource nestedA. Type: My.Rp/nestedResource@2020-01-01. Declaration start char: 0, length: 433 + name: 'nestedA' + + resource nestedB 'My.Rp/nestedResource/childType@2020-01-01' = if (shouldDeployChildAndGrandChild) { +//@[11:18) Resource nestedB. Type: My.Rp/nestedResource/childType@2020-01-01. Declaration start char: 2, length: 342 + name: 'nestedB' + + resource nestedC 'My.Rp/nestedResource/childType/grandchildType@2020-01-01' = { +//@[13:20) Resource nestedC. Type: My.Rp/nestedResource/childType/grandchildType@2020-01-01. Declaration start char: 4, length: 180 + name: 'nestedC' + properties: { + style: nestedA.properties.style + } + } + + properties: { + } + } +} diff --git a/src/Bicep.Core.Samples/Files/Resources_CRLF/main.syntax.bicep b/src/Bicep.Core.Samples/Files/Resources_CRLF/main.syntax.bicep index 5a291f38a7d..5cf5e9a01f7 100644 --- a/src/Bicep.Core.Samples/Files/Resources_CRLF/main.syntax.bicep +++ b/src/Bicep.Core.Samples/Files/Resources_CRLF/main.syntax.bicep @@ -1968,4 +1968,134 @@ resource extension3 'My.Rp/extensionResource@2020-12-01' = { //@[18:20) NewLine |\r\n| } //@[0:1) RightBrace |}| +//@[1:5) NewLine |\r\n\r\n| + +param shouldDeployChildAndGrandChild bool = true +//@[0:48) ParameterDeclarationSyntax +//@[0:5) Identifier |param| +//@[6:36) IdentifierSyntax +//@[6:36) Identifier |shouldDeployChildAndGrandChild| +//@[37:41) TypeSyntax +//@[37:41) Identifier |bool| +//@[42:48) ParameterDefaultValueSyntax +//@[42:43) Assignment |=| +//@[44:48) BooleanLiteralSyntax +//@[44:48) TrueKeyword |true| +//@[48:50) NewLine |\r\n| +resource nestedA 'My.Rp/nestedResource@2020-01-01' existing = { +//@[0:433) ResourceDeclarationSyntax +//@[0:8) Identifier |resource| +//@[9:16) IdentifierSyntax +//@[9:16) Identifier |nestedA| +//@[17:50) StringSyntax +//@[17:50) StringComplete |'My.Rp/nestedResource@2020-01-01'| +//@[51:59) Identifier |existing| +//@[60:61) Assignment |=| +//@[62:433) ObjectSyntax +//@[62:63) LeftBrace |{| +//@[63:65) NewLine |\r\n| + name: 'nestedA' +//@[2:17) ObjectPropertySyntax +//@[2:6) IdentifierSyntax +//@[2:6) Identifier |name| +//@[6:7) Colon |:| +//@[8:17) StringSyntax +//@[8:17) StringComplete |'nestedA'| +//@[17:21) NewLine |\r\n\r\n| + + resource nestedB 'My.Rp/nestedResource/childType@2020-01-01' = if (shouldDeployChildAndGrandChild) { +//@[2:344) ResourceDeclarationSyntax +//@[2:10) Identifier |resource| +//@[11:18) IdentifierSyntax +//@[11:18) Identifier |nestedB| +//@[19:62) StringSyntax +//@[19:62) StringComplete |'My.Rp/nestedResource/childType@2020-01-01'| +//@[63:64) Assignment |=| +//@[65:344) IfConditionSyntax +//@[65:67) Identifier |if| +//@[68:100) ParenthesizedExpressionSyntax +//@[68:69) LeftParen |(| +//@[69:99) VariableAccessSyntax +//@[69:99) IdentifierSyntax +//@[69:99) Identifier |shouldDeployChildAndGrandChild| +//@[99:100) RightParen |)| +//@[101:344) ObjectSyntax +//@[101:102) LeftBrace |{| +//@[102:104) NewLine |\r\n| + name: 'nestedB' +//@[4:19) ObjectPropertySyntax +//@[4:8) IdentifierSyntax +//@[4:8) Identifier |name| +//@[8:9) Colon |:| +//@[10:19) StringSyntax +//@[10:19) StringComplete |'nestedB'| +//@[19:23) NewLine |\r\n\r\n| + + resource nestedC 'My.Rp/nestedResource/childType/grandchildType@2020-01-01' = { +//@[4:184) ResourceDeclarationSyntax +//@[4:12) Identifier |resource| +//@[13:20) IdentifierSyntax +//@[13:20) Identifier |nestedC| +//@[21:79) StringSyntax +//@[21:79) StringComplete |'My.Rp/nestedResource/childType/grandchildType@2020-01-01'| +//@[80:81) Assignment |=| +//@[82:184) ObjectSyntax +//@[82:83) LeftBrace |{| +//@[83:85) NewLine |\r\n| + name: 'nestedC' +//@[6:21) ObjectPropertySyntax +//@[6:10) IdentifierSyntax +//@[6:10) Identifier |name| +//@[10:11) Colon |:| +//@[12:21) StringSyntax +//@[12:21) StringComplete |'nestedC'| +//@[21:23) NewLine |\r\n| + properties: { +//@[6:69) ObjectPropertySyntax +//@[6:16) IdentifierSyntax +//@[6:16) Identifier |properties| +//@[16:17) Colon |:| +//@[18:69) ObjectSyntax +//@[18:19) LeftBrace |{| +//@[19:21) NewLine |\r\n| + style: nestedA.properties.style +//@[8:39) ObjectPropertySyntax +//@[8:13) IdentifierSyntax +//@[8:13) Identifier |style| +//@[13:14) Colon |:| +//@[15:39) PropertyAccessSyntax +//@[15:33) PropertyAccessSyntax +//@[15:22) VariableAccessSyntax +//@[15:22) IdentifierSyntax +//@[15:22) Identifier |nestedA| +//@[22:23) Dot |.| +//@[23:33) IdentifierSyntax +//@[23:33) Identifier |properties| +//@[33:34) Dot |.| +//@[34:39) IdentifierSyntax +//@[34:39) Identifier |style| +//@[39:41) NewLine |\r\n| + } +//@[6:7) RightBrace |}| +//@[7:9) NewLine |\r\n| + } +//@[4:5) RightBrace |}| +//@[5:9) NewLine |\r\n\r\n| + + properties: { +//@[4:24) ObjectPropertySyntax +//@[4:14) IdentifierSyntax +//@[4:14) Identifier |properties| +//@[14:15) Colon |:| +//@[16:24) ObjectSyntax +//@[16:17) LeftBrace |{| +//@[17:19) NewLine |\r\n| + } +//@[4:5) RightBrace |}| +//@[5:7) NewLine |\r\n| + } +//@[2:3) RightBrace |}| +//@[3:5) NewLine |\r\n| +} +//@[0:1) RightBrace |}| //@[1:1) EndOfFile || diff --git a/src/Bicep.Core.Samples/Files/Resources_CRLF/main.tokens.bicep b/src/Bicep.Core.Samples/Files/Resources_CRLF/main.tokens.bicep index 8b7ea09bad6..e515507aab8 100644 --- a/src/Bicep.Core.Samples/Files/Resources_CRLF/main.tokens.bicep +++ b/src/Bicep.Core.Samples/Files/Resources_CRLF/main.tokens.bicep @@ -1257,4 +1257,90 @@ resource extension3 'My.Rp/extensionResource@2020-12-01' = { //@[18:20) NewLine |\r\n| } //@[0:1) RightBrace |}| +//@[1:5) NewLine |\r\n\r\n| + +param shouldDeployChildAndGrandChild bool = true +//@[0:5) Identifier |param| +//@[6:36) Identifier |shouldDeployChildAndGrandChild| +//@[37:41) Identifier |bool| +//@[42:43) Assignment |=| +//@[44:48) TrueKeyword |true| +//@[48:50) NewLine |\r\n| +resource nestedA 'My.Rp/nestedResource@2020-01-01' existing = { +//@[0:8) Identifier |resource| +//@[9:16) Identifier |nestedA| +//@[17:50) StringComplete |'My.Rp/nestedResource@2020-01-01'| +//@[51:59) Identifier |existing| +//@[60:61) Assignment |=| +//@[62:63) LeftBrace |{| +//@[63:65) NewLine |\r\n| + name: 'nestedA' +//@[2:6) Identifier |name| +//@[6:7) Colon |:| +//@[8:17) StringComplete |'nestedA'| +//@[17:21) NewLine |\r\n\r\n| + + resource nestedB 'My.Rp/nestedResource/childType@2020-01-01' = if (shouldDeployChildAndGrandChild) { +//@[2:10) Identifier |resource| +//@[11:18) Identifier |nestedB| +//@[19:62) StringComplete |'My.Rp/nestedResource/childType@2020-01-01'| +//@[63:64) Assignment |=| +//@[65:67) Identifier |if| +//@[68:69) LeftParen |(| +//@[69:99) Identifier |shouldDeployChildAndGrandChild| +//@[99:100) RightParen |)| +//@[101:102) LeftBrace |{| +//@[102:104) NewLine |\r\n| + name: 'nestedB' +//@[4:8) Identifier |name| +//@[8:9) Colon |:| +//@[10:19) StringComplete |'nestedB'| +//@[19:23) NewLine |\r\n\r\n| + + resource nestedC 'My.Rp/nestedResource/childType/grandchildType@2020-01-01' = { +//@[4:12) Identifier |resource| +//@[13:20) Identifier |nestedC| +//@[21:79) StringComplete |'My.Rp/nestedResource/childType/grandchildType@2020-01-01'| +//@[80:81) Assignment |=| +//@[82:83) LeftBrace |{| +//@[83:85) NewLine |\r\n| + name: 'nestedC' +//@[6:10) Identifier |name| +//@[10:11) Colon |:| +//@[12:21) StringComplete |'nestedC'| +//@[21:23) NewLine |\r\n| + properties: { +//@[6:16) Identifier |properties| +//@[16:17) Colon |:| +//@[18:19) LeftBrace |{| +//@[19:21) NewLine |\r\n| + style: nestedA.properties.style +//@[8:13) Identifier |style| +//@[13:14) Colon |:| +//@[15:22) Identifier |nestedA| +//@[22:23) Dot |.| +//@[23:33) Identifier |properties| +//@[33:34) Dot |.| +//@[34:39) Identifier |style| +//@[39:41) NewLine |\r\n| + } +//@[6:7) RightBrace |}| +//@[7:9) NewLine |\r\n| + } +//@[4:5) RightBrace |}| +//@[5:9) NewLine |\r\n\r\n| + + properties: { +//@[4:14) Identifier |properties| +//@[14:15) Colon |:| +//@[16:17) LeftBrace |{| +//@[17:19) NewLine |\r\n| + } +//@[4:5) RightBrace |}| +//@[5:7) NewLine |\r\n| + } +//@[2:3) RightBrace |}| +//@[3:5) NewLine |\r\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/Utils/ParserHelper.cs b/src/Bicep.Core.UnitTests/Utils/ParserHelper.cs index 9939d294fed..368d623e94b 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, bool allowComplexLiterals = true) => new Parser(text).Expression(allowComplexLiterals, allowResourceDeclarations: false); } } diff --git a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs index 470a3eb6ea1..c13671cc459 100644 --- a/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs +++ b/src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs @@ -793,6 +793,11 @@ public Diagnostic RuntimePropertyNotAllowed(string property, IEnumerable TextSpan, "BCP138", "Loops are not currently supported."); + + public ErrorDiagnostic InvalidResourceNesting(ResourceTypeReference parent, ResourceTypeReference child) => new( + TextSpan, + "BCP139", + $"The resource type \"{child.FullyQualifiedType}\" cannot be nested inside a resource of type \"{parent.FullyQualifiedType}\"."); } public static DiagnosticBuilderInternal ForPosition(TextSpan span) diff --git a/src/Bicep.Core/Emit/EmitLimitationCalculator.cs b/src/Bicep.Core/Emit/EmitLimitationCalculator.cs index c0b83c7c167..26d48cc1709 100644 --- a/src/Bicep.Core/Emit/EmitLimitationCalculator.cs +++ b/src/Bicep.Core/Emit/EmitLimitationCalculator.cs @@ -81,7 +81,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()) { diff --git a/src/Bicep.Core/Emit/ExpressionConverter.cs b/src/Bicep.Core/Emit/ExpressionConverter.cs index aa8442c5beb..0cc224fc16f 100644 --- a/src/Bicep.Core/Emit/ExpressionConverter.cs +++ b/src/Bicep.Core/Emit/ExpressionConverter.cs @@ -7,6 +7,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; @@ -151,11 +152,33 @@ public LanguageExpression ConvertExpression(SyntaxBase expression) return (moduleSymbol, propertyAccess.PropertyName.IdentifierName); } - private LanguageExpression GetResourceNameExpression(ResourceSymbol resourceSymbol) + public LanguageExpression GetResourceNameExpression(ResourceSymbol resourceSymbol) { // this condition should have already been validated by the type checker var nameValueSyntax = resourceSymbol.SafeGetBodyPropertyValue(LanguageConstants.ResourceNamePropertyName) ?? throw new ArgumentException($"Expected resource syntax body to contain property 'name'"); - return ConvertExpression(nameValueSyntax); + + // 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); + } + + var segments = new List(); + foreach (var ancestor in ancestors) + { + var segment = ancestor.SafeGetBodyPropertyValue(LanguageConstants.ResourceNamePropertyName) ?? throw new ArgumentException($"Expected resource syntax body to contain property 'name'"); + segments.Add(segment); + } + + // Build an expression like '${parent.name}/${child.name}' + var tokens = new List(); + tokens.Add(SyntaxFactory.CreateToken(TokenType.StringLeftPiece, "")); + tokens.AddRange(Enumerable.Range(0, segments.Count - 1).Select(_ => SyntaxFactory.CreateToken(TokenType.StringMiddlePiece, "/"))); + tokens.Add(SyntaxFactory.CreateToken(TokenType.StringRightPiece, "")); + + var format = new StringSyntax(tokens, segments, tokens.Select(t => t.Text)); + return ConvertExpression(format); } private LanguageExpression GetModuleNameExpression(ModuleSymbol moduleSymbol) diff --git a/src/Bicep.Core/Emit/ExpressionEmitter.cs b/src/Bicep.Core/Emit/ExpressionEmitter.cs index c827b1e5b80..d7d5a8fdb64 100644 --- a/src/Bicep.Core/Emit/ExpressionEmitter.cs +++ b/src/Bicep.Core/Emit/ExpressionEmitter.cs @@ -115,6 +115,11 @@ public void EmitResourceIdReference(ModuleSymbol moduleSymbol) writer.WriteValue(serialized); } + public LanguageExpression GetResourceNameExpression(ResourceSymbol resourceSymbol) + { + return converter.GetResourceNameExpression(resourceSymbol); + } + public LanguageExpression GetManagementGroupResourceId(SyntaxBase managementGroupNameProperty, bool fullyQualified) => converter.GenerateManagementGroupResourceId(managementGroupNameProperty, fullyQualified); diff --git a/src/Bicep.Core/Emit/ResourceDependencyVisitor.cs b/src/Bicep.Core/Emit/ResourceDependencyVisitor.cs index 76b4f98395e..c15e3f02bd9 100644 --- a/src/Bicep.Core/Emit/ResourceDependencyVisitor.cs +++ b/src/Bicep.Core/Emit/ResourceDependencyVisitor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; using System.Collections.Immutable; +using System.Linq; using Bicep.Core.Semantics; using Bicep.Core.Syntax; @@ -24,7 +25,8 @@ public static ImmutableDictionary(); - foreach (var resourceSymbol in semanticModel.Root.ResourceDeclarations) + foreach (var resourceSymbol in semanticModel.Root.GetAllResourceDeclarations()) { if (resourceSymbol.Type is not ResourceType resourceType) { diff --git a/src/Bicep.Core/Emit/TemplateWriter.cs b/src/Bicep.Core/Emit/TemplateWriter.cs index c32803895c5..6c9ddab4492 100644 --- a/src/Bicep.Core/Emit/TemplateWriter.cs +++ b/src/Bicep.Core/Emit/TemplateWriter.cs @@ -2,10 +2,12 @@ // Licensed under the MIT License. using System; +using System.Collections.Generic; using System.Collections.Immutable; using System.Linq; 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; @@ -33,6 +35,7 @@ public class TemplateWriter private static ImmutableHashSet ResourcePropertiesToOmit = new [] { LanguageConstants.ResourceScopePropertyName, LanguageConstants.ResourceDependsOnPropertyName, + LanguageConstants.ResourceNamePropertyName, }.ToImmutableHashSet(); private static ImmutableHashSet ModulePropertiesToOmit = new [] { @@ -247,7 +250,7 @@ private void EmitResources() writer.WritePropertyName("resources"); writer.WriteStartArray(); - foreach (var resourceSymbol in this.context.SemanticModel.Root.ResourceDeclarations) + foreach (var resourceSymbol in this.context.SemanticModel.Root.GetAllResourceDeclarations()) { if (resourceSymbol.DeclaringResource.IsExistingResource()) { @@ -271,10 +274,59 @@ private void EmitResource(ResourceSymbol resourceSymbol) var typeReference = EmitHelpers.GetTypeReference(resourceSymbol); var body = resourceSymbol.DeclaringResource.Value; - if (body is IfConditionSyntax ifCondition) + + + var ancestors = this.context.SemanticModel.ResourceAncestors.GetAncestors(resourceSymbol); + + var conditions = new List(); + foreach (var ancestor in ancestors) { - body = ifCondition.Body; - this.emitter.EmitProperty("condition", ifCondition.ConditionExpression); + if (ancestor.DeclaringResource.Value is IfConditionSyntax ifCondition) + { + conditions.Add(ifCondition.ConditionExpression); + } + } + + // Unwrap the 'real' resource body if there's a condition + { + if (body is IfConditionSyntax ifCondition) + { + body = ifCondition.Body; + conditions.Add(ifCondition.ConditionExpression); + } + } + + if (conditions.Count == 1) + { + this.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]); + } + + this.emitter.EmitProperty("condition", @operator); + } + + var names = new List(); + foreach (var ancestor in ancestors) + { + var nameValueSyntax = ancestor.SafeGetBodyPropertyValue(LanguageConstants.ResourceNamePropertyName) ?? throw new ArgumentException($"Expected resource syntax body to contain property 'name'"); + names.Add(nameValueSyntax); + } + + { + var nameValueSyntax = resourceSymbol.SafeGetBodyPropertyValue(LanguageConstants.ResourceNamePropertyName) ?? throw new ArgumentException($"Expected resource syntax body to contain property 'name'"); + names.Add(nameValueSyntax); } this.emitter.EmitProperty("type", typeReference.FullyQualifiedType); @@ -283,6 +335,9 @@ private void EmitResource(ResourceSymbol resourceSymbol) { this.emitter.EmitProperty("scope", () => this.emitter.EmitUnqualifiedResourceId(scopeResource)); } + + this.emitter.EmitProperty("name", this.emitter.GetResourceNameExpression(resourceSymbol)); + this.emitter.EmitObjectProperties((ObjectSyntax)body, ResourcePropertiesToOmit); // dependsOn is currently not allowed as a top-level resource property in bicep diff --git a/src/Bicep.Core/Parsing/Parser.cs b/src/Bicep.Core/Parsing/Parser.cs index 717f999412a..db58539c2b6 100644 --- a/src/Bicep.Core/Parsing/Parser.cs +++ b/src/Bicep.Core/Parsing/Parser.cs @@ -165,7 +165,7 @@ 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(allowComplexLiterals: true, allowResourceDeclarations: false), RecoveryFlags.None, TokenType.NewLine); return new TargetScopeSyntax(leadingNodes, keyword, assignment, value); } @@ -246,7 +246,7 @@ private SyntaxBase ParameterDeclaration(IEnumerable leadingNodes) TokenType.Assignment => this.ParameterDefaultValue(), // modifier is specified - TokenType.LeftBrace => this.Object(), + TokenType.LeftBrace => this.Object(allowResourceDeclarations: false), _ => throw new ExpectedTokenException(current, b => b.ExpectedParameterContinuation()) }; @@ -260,7 +260,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(allowComplexLiterals: true, allowResourceDeclarations: false), RecoveryFlags.None, TokenType.NewLine); return new ParameterDefaultValueSyntax(assignmentToken, defaultValue); } @@ -270,7 +270,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(allowComplexLiterals: true, allowResourceDeclarations: false), GetSuppressionFlag(assignment), TokenType.NewLine); return new VariableDeclarationSyntax(leadingNodes, keyword, name, assignment, value); } @@ -281,7 +281,7 @@ private SyntaxBase OutputDeclaration(IEnumerable leadingNodes) var name = this.IdentifierWithRecovery(b => b.ExpectedOutputIdentifier(), TokenType.Identifier, TokenType.NewLine); var type = this.WithRecovery(() => Type(b => b.ExpectedParameterType()), 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(allowComplexLiterals: true, allowResourceDeclarations: false), GetSuppressionFlag(assignment), TokenType.NewLine); return new OutputDeclarationSyntax(leadingNodes, keyword, name, type, assignment, value); } @@ -311,9 +311,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(), + TokenType.Identifier when current.Text == LanguageConstants.IfKeyword => this.IfCondition(allowResourceDeclarations: true), + TokenType.LeftBrace => this.Object(allowResourceDeclarations: true), + TokenType.LeftSquare => this.ForExpression(allowResourceDeclarations: true), _ => throw new ExpectedTokenException(current, b => b.ExpectBodyStartOrIfOrLoopStart()) }; }, @@ -341,8 +341,8 @@ private SyntaxBase ModuleDeclaration(IEnumerable leadingNodes) return current.Type switch { TokenType.Identifier when current.Text == LanguageConstants.IfKeyword => this.IfCondition(), - TokenType.LeftBrace => this.Object(), - TokenType.LeftSquare => this.ForExpression(), + TokenType.LeftBrace => this.Object(allowResourceDeclarations: false), + TokenType.LeftSquare => this.ForExpression(allowResourceDeclarations: false), _ => throw new ExpectedTokenException(current, b => b.ExpectBodyStartOrIfOrLoopStart()) }; }, @@ -368,16 +368,16 @@ private Token NewLine() return Expect(TokenType.NewLine, b => b.ExpectedNewLine()); } - public SyntaxBase Expression(bool allowComplexLiterals) + public SyntaxBase Expression(bool allowComplexLiterals, bool allowResourceDeclarations) { - var candidate = this.BinaryExpression(allowComplexLiterals); + var candidate = this.BinaryExpression(allowComplexLiterals, allowResourceDeclarations); if (this.Check(TokenType.Question)) { var question = this.reader.Read(); - var trueExpression = this.Expression(allowComplexLiterals); + var trueExpression = this.Expression(allowComplexLiterals, allowResourceDeclarations); var colon = this.Expect(TokenType.Colon, b => b.ExpectedCharacter(":")); - var falseExpression = this.Expression(allowComplexLiterals); + var falseExpression = this.Expression(allowComplexLiterals, allowResourceDeclarations); return new TernaryOperationSyntax(candidate, question, trueExpression, colon, falseExpression); } @@ -385,9 +385,9 @@ public SyntaxBase Expression(bool allowComplexLiterals) return candidate; } - private SyntaxBase BinaryExpression(bool allowComplexLiterals, int precedence = 0) + private SyntaxBase BinaryExpression(bool allowComplexLiterals, bool allowResourceDeclarations, int precedence = 0) { - var current = this.UnaryExpression(allowComplexLiterals); + var current = this.UnaryExpression(allowComplexLiterals, allowResourceDeclarations); while (true) { @@ -404,14 +404,14 @@ private SyntaxBase BinaryExpression(bool allowComplexLiterals, int precedence = this.reader.Read(); - SyntaxBase rightExpression = this.BinaryExpression(allowComplexLiterals, operatorPrecedence); + SyntaxBase rightExpression = this.BinaryExpression(allowComplexLiterals, allowResourceDeclarations, operatorPrecedence); current = new BinaryOperationSyntax(current, candidateOperatorToken, rightExpression); } return current; } - private SyntaxBase UnaryExpression(bool allowComplexLiterals) + private SyntaxBase UnaryExpression(bool allowComplexLiterals, bool allowResourceDeclarations) { Token operatorToken = this.reader.Peek(); @@ -419,16 +419,16 @@ private SyntaxBase UnaryExpression(bool allowComplexLiterals) { this.reader.Read(); - var expression = this.MemberExpression(allowComplexLiterals); + var expression = this.MemberExpression(allowComplexLiterals, allowResourceDeclarations); return new UnaryOperationSyntax(operatorToken, expression); } - return this.MemberExpression(allowComplexLiterals); + return this.MemberExpression(allowComplexLiterals, allowResourceDeclarations); } - private SyntaxBase MemberExpression(bool allowComplexLiterals) + private SyntaxBase MemberExpression(bool allowComplexLiterals, bool allowResourceDeclarations) { - var current = this.PrimaryExpression(allowComplexLiterals); + var current = this.PrimaryExpression(allowComplexLiterals, allowResourceDeclarations); while (true) { @@ -447,7 +447,7 @@ private SyntaxBase MemberExpression(bool allowComplexLiterals) } else { - SyntaxBase indexExpression = this.Expression(allowComplexLiterals); + SyntaxBase indexExpression = this.Expression(allowComplexLiterals, allowResourceDeclarations); Token closeSquare = this.Expect(TokenType.RightSquare, b => b.ExpectedCharacter("]")); current = new ArrayAccessSyntax(current, openSquare, indexExpression, closeSquare); @@ -490,7 +490,7 @@ private SyntaxBase MemberExpression(bool allowComplexLiterals) return current; } - private SyntaxBase PrimaryExpression(bool allowComplexLiterals) + private SyntaxBase PrimaryExpression(bool allowComplexLiterals, bool allowResourceDeclarations) { Token nextToken = this.reader.Peek(); @@ -507,11 +507,11 @@ private SyntaxBase PrimaryExpression(bool allowComplexLiterals) return this.InterpolableString(); case TokenType.LeftBrace when allowComplexLiterals: - return this.Object(); + return this.Object(allowResourceDeclarations); case TokenType.LeftSquare when allowComplexLiterals: return CheckKeyword(this.reader.PeekAhead(), LanguageConstants.ForKeyword) - ? this.ForExpression() + ? this.ForExpression(allowResourceDeclarations) : this.Array(); case TokenType.LeftBrace: @@ -532,7 +532,7 @@ private SyntaxBase PrimaryExpression(bool allowComplexLiterals) private SyntaxBase ParenthesizedExpression(bool allowComplexLiterals) { 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(allowComplexLiterals, allowResourceDeclarations: false), 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); @@ -642,7 +642,7 @@ SkippedTriviaSyntax CreateDummyArgument(Token current) => throw new ExpectedTokenException(current, b => b.ExpectedCharacter(",")); } - var expression = this.Expression(allowComplexLiterals); + var expression = this.Expression(allowComplexLiterals, allowResourceDeclarations: false); arguments.Add((expression, null)); break; @@ -769,7 +769,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(allowComplexLiterals: false, allowResourceDeclarations: false), 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. @@ -879,15 +879,15 @@ private SyntaxBase LiteralValue() } } - private SyntaxBase ForExpression() + private SyntaxBase ForExpression(bool allowResourceDeclarations) { 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(allowComplexLiterals: true, allowResourceDeclarations: false), 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(() => this.Expression(allowComplexLiterals: true), GetSuppressionFlag(colon), TokenType.RightSquare, TokenType.NewLine); + var body = this.WithRecovery(() => this.Expression(allowComplexLiterals: true, allowResourceDeclarations), GetSuppressionFlag(colon), TokenType.RightSquare, TokenType.NewLine); var closeBracket = this.WithRecovery(() => this.Expect(TokenType.RightSquare, b => b.ExpectedCharacter("]")), GetSuppressionFlag(body), TokenType.RightSquare, TokenType.NewLine); return new ForSyntax(openBracket, forKeyword, identifier, inKeyword, expression, colon, body, closeBracket); @@ -952,12 +952,12 @@ private SyntaxBase ArrayItem() return this.NewLine(); } - var value = this.Expression(allowComplexLiterals: true); + var value = this.Expression(allowComplexLiterals: true, allowResourceDeclarations: false); return new ArrayItemSyntax(value); }, RecoveryFlags.None, TokenType.NewLine); } - private ObjectSyntax Object() + private ObjectSyntax Object(bool allowResourceDeclarations) { var openBrace = Expect(TokenType.LeftBrace, b => b.ExpectedCharacter("{")); @@ -972,7 +972,7 @@ private ObjectSyntax Object() 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(); + var propertyOrToken = this.ObjectProperty(allowResourceDeclarations); propertiesOrTokens.Add(propertyOrToken); // if skipped tokens node is returned above, the newline is not consumed @@ -1004,7 +1004,7 @@ private ObjectSyntax Object() return new ObjectSyntax(openBrace, propertiesOrTokens, closeBrace); } - private SyntaxBase ObjectProperty() + private SyntaxBase ObjectProperty(bool allowResourceDeclarations = false) { return this.WithRecovery(() => { @@ -1015,6 +1015,14 @@ private SyntaxBase ObjectProperty() return this.NewLine(); } + // Nested resource declarations may be allowed + if (allowResourceDeclarations && + (current.Type == TokenType.At || + (current.Type == TokenType.Identifier && current.Text == LanguageConstants.ResourceKeyword))) + { + return Declaration(); + } + var key = this.WithRecovery( () => ThrowIfSkipped( () => @@ -1029,18 +1037,18 @@ 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(allowComplexLiterals: true, allowResourceDeclarations: false), GetSuppressionFlag(colon), TokenType.NewLine); return new ObjectPropertySyntax(key, colon, value); }, RecoveryFlags.None, TokenType.NewLine); } - private SyntaxBase IfCondition() + private SyntaxBase IfCondition(bool allowResourceDeclarations = false) { var keyword = this.ExpectKeyword(LanguageConstants.IfKeyword); var conditionExpression = this.WithRecovery(() => this.ParenthesizedExpression(true), RecoveryFlags.None, TokenType.LeftBrace, TokenType.NewLine); var body = this.WithRecovery( - this.Object, + () => this.Object(allowResourceDeclarations), GetSuppressionFlag(conditionExpression, conditionExpression is ParenthesizedExpressionSyntax { CloseParen: not SkippedTriviaSyntax }), TokenType.NewLine); return new IfConditionSyntax(keyword, conditionExpression, body); diff --git a/src/Bicep.Core/Resources/ResourceTypeReference.cs b/src/Bicep.Core/Resources/ResourceTypeReference.cs index 25ac8ba40a2..1b7fb39d228 100644 --- a/src/Bicep.Core/Resources/ResourceTypeReference.cs +++ b/src/Bicep.Core/Resources/ResourceTypeReference.cs @@ -48,6 +48,18 @@ 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) && + + StringComparer.OrdinalIgnoreCase.Equals(this.ApiVersion, other.ApiVersion); + } + public static ResourceTypeReference? TryParse(string resourceType) { var match = ResourceTypePattern.Match(resourceType); diff --git a/src/Bicep.Core/Semantics/Binder.cs b/src/Bicep.Core/Semantics/Binder.cs index b92de0ceaae..b7238ecb949 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, GetUniqueDeclarations(declarations), 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 a874c53b904..67712b96dbd 100644 --- a/src/Bicep.Core/Semantics/DeclarationVisitor.cs +++ b/src/Bicep.Core/Semantics/DeclarationVisitor.cs @@ -3,7 +3,6 @@ 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 +11,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(); @@ -36,12 +36,17 @@ public static (ImmutableArray, ImmutableArray) GetAl return (declarations.ToImmutableArray(), childScopes.Select(MakeImmutable).ToImmutableArray()); } + public override void VisitProgramSyntax(ProgramSyntax syntax) + { + base.VisitProgramSyntax(syntax); + } + public override void VisitParameterDeclarationSyntax(ParameterDeclarationSyntax syntax) { 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 +54,24 @@ 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 - this ensures that nested resources + // are contained within the appropriate scope. + var scope = new LocalScope(string.Empty, syntax, 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, 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, itemVariable.AsEnumerable(), ImmutableArray.Empty); - - this.PushScope(scope); + DeclareSymbol(itemVariable); // visit the children base.VisitForSyntax(syntax); @@ -95,6 +109,18 @@ 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); @@ -120,7 +146,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 +163,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 ff617d3ea81..b351801d12e 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,7 +113,7 @@ 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 - this.Diagnostics.AddRange(scope.AllDeclarations + this.Diagnostics.AddRange(scope.Declarations .Where(decl => decl.NameSyntax.IsValid) .GroupBy(decl => decl.Name, LanguageConstants.IdentifierComparer) .Where(group => group.Count() > 1) @@ -121,7 +123,7 @@ private void ValidateScope(ILanguageScope scope) // imported namespaces are reserved in all the scopes // otherwise the user could accidentally hide a namespace which would remove the ability // to fully qualify a function - this.Diagnostics.AddRange(scope.AllDeclarations + this.Diagnostics.AddRange(scope.Declarations .Where(decl => decl.NameSyntax.IsValid && this.importedNamespaces.ContainsKey(decl.Name)) .Select(reservedSymbol => DiagnosticBuilder.ForPosition(reservedSymbol.NameSyntax).SymbolicNameCannotUseReservedNamespaceName(reservedSymbol.Name, this.importedNamespaces.Keys))); } 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 e8a1f24e277..ffed58a3308 100644 --- a/src/Bicep.Core/Semantics/LocalScope.cs +++ b/src/Bicep.Core/Semantics/LocalScope.cs @@ -13,7 +13,7 @@ namespace Bicep.Core.Semantics /// public class LocalScope : Symbol, ILanguageScope { - public LocalScope(string name, SyntaxBase enclosingSyntax, IEnumerable locals, IEnumerable childScopes) + public LocalScope(string name, SyntaxBase enclosingSyntax, IEnumerable locals, IEnumerable childScopes) : base(name) { this.EnclosingSyntax = enclosingSyntax; @@ -23,7 +23,7 @@ public LocalScope(string name, SyntaxBase enclosingSyntax, IEnumerable Locals { get; } + public ImmutableArray Locals { get; } public ImmutableArray ChildScopes { get; } @@ -33,16 +33,21 @@ public LocalScope(string name, SyntaxBase enclosingSyntax, IEnumerable Descendants => this.ChildScopes.Concat(this.Locals); + public LocalScope ReplaceLocals(IEnumerable newLocals) => new(this.Name, this.EnclosingSyntax, newLocals, this.ChildScopes); + public LocalScope ReplaceChildren(IEnumerable newChildren) => new(this.Name, this.EnclosingSyntax, 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; public override IEnumerable GetDiagnostics() { // TODO: Remove when loops codegen is done. - yield return DiagnosticBuilder.ForPosition(((ForSyntax) this.EnclosingSyntax).ForKeyword).LoopsNotSupported(); + if (this.EnclosingSyntax is ForSyntax forSyntax) + { + yield return DiagnosticBuilder.ForPosition(forSyntax.ForKeyword).LoopsNotSupported(); + } } } } \ No newline at end of file diff --git a/src/Bicep.Core/Semantics/NameBindingVisitor.cs b/src/Bicep.Core/Semantics/NameBindingVisitor.cs index 37ef39bd7a6..c32924915c8 100644 --- a/src/Bicep.Core/Semantics/NameBindingVisitor.cs +++ b/src/Bicep.Core/Semantics/NameBindingVisitor.cs @@ -18,7 +18,7 @@ public sealed class NameBindingVisitor : SyntaxVisitor { private FunctionFlags allowedFlags; - private readonly IReadOnlyDictionary declarations; + private readonly IReadOnlyDictionary outermostDeclarations; private readonly IDictionary bindings; @@ -28,12 +28,16 @@ public sealed class NameBindingVisitor : SyntaxVisitor private readonly Stack activeScopes; - public NameBindingVisitor(IReadOnlyDictionary declarations, IDictionary bindings, ImmutableDictionary namespaces, ImmutableArray localScopes) + public NameBindingVisitor( + IReadOnlyDictionary outermostDeclarations, + IDictionary bindings, + ImmutableDictionary namespaces, + ImmutableArray childScopes) { - this.declarations = declarations; + this.outermostDeclarations = outermostDeclarations; this.bindings = bindings; this.namespaces = namespaces; - this.allLocalScopes = ScopeCollectorVisitor.Build(localScopes); + this.allLocalScopes = ScopeCollectorVisitor.Build(childScopes); this.activeScopes = new Stack(); } @@ -44,7 +48,7 @@ public override void VisitProgramSyntax(ProgramSyntax 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) + foreach (DeclaredSymbol declaredSymbol in this.outermostDeclarations.Values) { this.bindings.Add(declaredSymbol.DeclaringSyntax, declaredSymbol); } @@ -52,9 +56,9 @@ public override void VisitProgramSyntax(ProgramSyntax syntax) // 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)) + foreach (var declaredSymbol in allLocalScopes.Values.SelectMany(scope => scope.Declarations)) { - this.bindings.Add(declaredSymbol.DeclaringSyntax, declaredSymbol); + this.bindings.TryAdd(declaredSymbol.DeclaringSyntax, declaredSymbol); } } @@ -77,9 +81,27 @@ public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax sy this.Visit(syntax.Type); this.Visit(syntax.ExistingKeyword); this.Visit(syntax.Assignment); + + if (!this.allLocalScopes.TryGetValue(syntax, out var localScope)) + { + // code defect in the declaration visitor + throw new InvalidOperationException($"Local scope is missing for {syntax.GetType().Name} at {syntax.Span}"); + } + + // 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 the body allowedFlags = FunctionFlags.RequiresInlining; this.Visit(syntax.Value); allowedFlags = FunctionFlags.Default; + + // 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 VisitModuleDeclarationSyntax(ModuleDeclarationSyntax syntax) @@ -247,13 +269,13 @@ private Symbol LookupSymbolByName(IdentifierSyntax identifierSyntax, bool isFunc return null; } - private static Symbol? LookupLocalSymbolByName(LocalScope scope, IdentifierSyntax identifierSyntax) => + private static Symbol? LookupLocalSymbolByName(ILanguageScope 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)); + scope.Declarations.FirstOrDefault(symbol => string.Equals(identifierSyntax.IdentifierName, symbol.Name, LanguageConstants.IdentifierComparison)); private Symbol LookupGlobalSymbolByName(IdentifierSyntax identifierSyntax, bool isFunctionCall) { @@ -268,7 +290,7 @@ private Symbol LookupGlobalSymbolByName(IdentifierSyntax identifierSyntax, bool // 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)) + if (this.outermostDeclarations.TryGetValue(identifierSyntax.IdentifierName, out var globalSymbol)) { // we found the symbol in the global namespace return globalSymbol; @@ -289,10 +311,10 @@ private Symbol LookupGlobalSymbolByName(IdentifierSyntax identifierSyntax, bool } var foundSymbol = Enumerable.FirstOrDefault(foundSymbols); - return isFunctionCall ? SymbolValidator.ResolveUnqualifiedFunction(allowedFlags, foundSymbol, identifierSyntax, namespaces.Values) : SymbolValidator.ResolveUnqualifiedSymbol(foundSymbol, identifierSyntax, namespaces.Values, declarations.Keys); + return isFunctionCall ? SymbolValidator.ResolveUnqualifiedFunction(allowedFlags, foundSymbol, identifierSyntax, namespaces.Values) : SymbolValidator.ResolveUnqualifiedSymbol(foundSymbol, identifierSyntax, namespaces.Values, outermostDeclarations.Keys); } - private class ScopeCollectorVisitor: SymbolVisitor + private class ScopeCollectorVisitor : SymbolVisitor { private IDictionary ScopeMap { get; } = new Dictionary(); @@ -302,10 +324,20 @@ public override void VisitLocalScope(LocalScope symbol) base.VisitLocalScope(symbol); } + protected override void VisitInternal(Symbol symbol) + { + // We have to special case JUST the scope types here ... WHY? because things like modules + // and resources need access to the type info to be visited. + if (symbol is ILanguageScope) + { + base.VisitInternal(symbol); + } + } + public static IReadOnlyDictionary Build(ImmutableArray outermostScopes) { var visitor = new ScopeCollectorVisitor(); - foreach (LocalScope outermostScope in outermostScopes) + foreach (var outermostScope in outermostScopes) { visitor.Visit(outermostScope); } diff --git a/src/Bicep.Core/Semantics/ResourceAncestorGraph.cs b/src/Bicep.Core/Semantics/ResourceAncestorGraph.cs new file mode 100644 index 00000000000..dd16c3494d4 --- /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 IEnumerable diagnostics; + private readonly ImmutableDictionary> data; + + public ResourceAncestorGraph(IEnumerable diagnostics, ImmutableDictionary> data) + { + this.diagnostics = diagnostics; + this.data = data; + } + + public IEnumerable GetAllDiagnostics() + { + return this.diagnostics; + } + + // 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) + { + return data[resource]; + } + + public static ResourceAncestorGraph Compute(SyntaxTree syntaxTree, IBinder binder) + { + var visitor = new ResourceAncestorVisitor(binder); + visitor.Visit(syntaxTree.ProgramSyntax); + return new ResourceAncestorGraph(visitor.Diagnostics, 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..24736ccd947 --- /dev/null +++ b/src/Bicep.Core/Semantics/ResourceAncestorVisitor.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using Bicep.Core.Diagnostics; +using Bicep.Core.Resources; +using Bicep.Core.Syntax; +using Bicep.Core.TypeSystem; + +namespace Bicep.Core.Semantics +{ + public sealed class ResourceAncestorVisitor : SyntaxVisitor + { + private readonly IBinder binder; + private readonly Stack ancestorResources; + private readonly List diagnostics; + private readonly ImmutableDictionary>.Builder ancestry; + + public ResourceAncestorVisitor(IBinder binder) + { + this.binder = binder; + this.ancestorResources = new Stack(); + this.diagnostics = new List(); + this.ancestry = ImmutableDictionary.CreateBuilder>(); + } + + public IEnumerable Diagnostics => this.diagnostics; + + public ImmutableDictionary> Ancestry + => this.ancestry.ToImmutableDictionary(); + + public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax syntax) + { + var symbol = this.binder.GetSymbolInfo(syntax); + var resourceTypeReference = GetResourceTypeReference(symbol); + + // Skip analysis for ErrorSymbol and similar cases, we need the type information to validate the nesting + // and we use the ancestry info for emit. + if (symbol is ResourceSymbol resourceSymbol && resourceTypeReference is ResourceTypeReference) + { + // We only support 'direct' nesting - My.RP/TypeA can contain My.RP/TypeA/TypeB, but cannot contain My.RP/TypeA/TypeB/TypeC directly. + if (this.ancestorResources.Count >= 1 && + GetResourceTypeReference(this.ancestorResources.Peek()) is ResourceTypeReference parentTypeReference) + { + // We're inside a nested context, the immediate type above us should be the direct parent. + if (!parentTypeReference.IsParentOf(resourceTypeReference)) + { + diagnostics.Add(DiagnosticBuilder.ForPosition(syntax.Type).InvalidResourceNesting(parentTypeReference, resourceTypeReference)); + } + + } + else if (this.ancestorResources.Count >= 1 && this.ancestorResources.Peek() is null) + { + // If we get here this means we're in a nested context but don't have information about the parent + // type, likely due to error or unknown type. Don't add an extra diagnostic in that case. + } + + // Make a record of this symbol's ancestor resources (including the empty set). + this.ancestry.Add(resourceSymbol, ImmutableArray.CreateRange(this.ancestorResources.OfType().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(); + } + } + + private static ResourceTypeReference? GetResourceTypeReference(Symbol? symbol) + { + if (symbol is ResourceSymbol resourceSymbol && resourceSymbol.Type is ResourceType resourceType) + { + return resourceType.TypeReference; + } + + return null; + } + } +} \ 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 48e5b6e1a15..27ed899f6e8 100644 --- a/src/Bicep.Core/Semantics/SemanticModel.cs +++ b/src/Bicep.Core/Semantics/SemanticModel.cs @@ -14,6 +14,7 @@ namespace Bicep.Core.Semantics public class SemanticModel { private readonly Lazy emitLimitationInfoLazy; + private readonly Lazy resourceAncestorsLazy; public SemanticModel(Compilation compilation, SyntaxTree syntaxTree) { @@ -34,6 +35,8 @@ public SemanticModel(Compilation compilation, SyntaxTree syntaxTree) symbolContext.Unlock(); this.emitLimitationInfoLazy = new Lazy(() => EmitLimitationCalculator.Calculate(this)); + this.resourceAncestorsLazy = new Lazy(() => ResourceAncestorGraph.Compute(syntaxTree, Binder)); + } public SyntaxTree SyntaxTree { get; } @@ -48,6 +51,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. /// @@ -68,6 +73,7 @@ public IReadOnlyList GetSemanticDiagnostics() diagnosticWriter.WriteMultiple(typeValidationDiagnostics); diagnosticWriter.WriteMultiple(EmitLimitationInfo.Diagnostics); + diagnosticWriter.WriteMultiple(ResourceAncestors.GetAllDiagnostics()); return diagnosticWriter.GetDiagnostics(); } diff --git a/src/Bicep.Core/Semantics/SymbolVisitor.cs b/src/Bicep.Core/Semantics/SymbolVisitor.cs index a55a44091e3..b957a71eb9b 100644 --- a/src/Bicep.Core/Semantics/SymbolVisitor.cs +++ b/src/Bicep.Core/Semantics/SymbolVisitor.cs @@ -20,7 +20,6 @@ public virtual void VisitFileSymbol(FileSymbol symbol) { VisitDescendants(symbol); } - public virtual void VisitParameterSymbol(ParameterSymbol symbol) { VisitDescendants(symbol); diff --git a/src/Bicep.Core/Syntax/ObjectSyntaxExtensions.cs b/src/Bicep.Core/Syntax/ObjectSyntaxExtensions.cs index adf14a4a966..1a6fd9bde89 100644 --- a/src/Bicep.Core/Syntax/ObjectSyntaxExtensions.cs +++ b/src/Bicep.Core/Syntax/ObjectSyntaxExtensions.cs @@ -27,8 +27,18 @@ public static ImmutableDictionary ToKnownPropertyValueDictio /// Converts a syntactically valid object syntax node to a dictionary mapping property name strings to property syntax nodes. Will throw if you provide a node with duplicate properties. /// /// The object syntax node - public static ImmutableDictionary ToNamedPropertyDictionary(this ObjectSyntax syntax) => - syntax.Properties.ToImmutableDictionaryExcludingNull(p => p.TryGetKeyText(), LanguageConstants.IdentifierComparer); + public static ImmutableDictionary ToNamedPropertyDictionary(this ObjectSyntax syntax) + { + try + { + return syntax.Properties.ToImmutableDictionaryExcludingNull(p => p.TryGetKeyText(), LanguageConstants.IdentifierComparer); + } + catch (System.Exception) + { + throw; + } + } + /// /// Returns the specified property by name on any valid or invalid object syntax node if there is exactly one property by that name. @@ -63,21 +73,21 @@ public static ImmutableDictionary ToNamedPropertyD return result; } - public static ObjectSyntax MergeProperty(this ObjectSyntax? syntax, string propertyName, string propertyValue) => - syntax.MergeProperty(propertyName, SyntaxFactory.CreateStringLiteral(propertyValue)); - + public static ObjectSyntax MergeProperty(this ObjectSyntax? syntax, string propertyName, string propertyValue) => + syntax.MergeProperty(propertyName, SyntaxFactory.CreateStringLiteral(propertyValue)); + public static ObjectSyntax MergeProperty(this ObjectSyntax? syntax, string propertyName, SyntaxBase propertyValue) { - if (syntax == null) - { + if (syntax == null) + { return SyntaxFactory.CreateObject(SyntaxFactory.CreateObjectProperty(propertyName, propertyValue).AsEnumerable()); } var properties = syntax.Properties.ToList(); int matchingIndex = 0; - while (matchingIndex < properties.Count) - { + while (matchingIndex < properties.Count) + { if (string.Equals(properties[matchingIndex].TryGetKeyText(), propertyName, LanguageConstants.IdentifierComparison)) { break; @@ -86,34 +96,34 @@ public static ObjectSyntax MergeProperty(this ObjectSyntax? syntax, string prope matchingIndex++; } - if (matchingIndex < properties.Count) - { - // If both property values are objects, merge them. Otherwise, replace the matching property value. - SyntaxBase mergedValue = properties[matchingIndex].Value is ObjectSyntax sourceObject && propertyValue is ObjectSyntax targetObject - ? sourceObject.DeepMerge(targetObject) - : propertyValue; - - properties[matchingIndex] = SyntaxFactory.CreateObjectProperty(propertyName, mergedValue); + if (matchingIndex < properties.Count) + { + // If both property values are objects, merge them. Otherwise, replace the matching property value. + SyntaxBase mergedValue = properties[matchingIndex].Value is ObjectSyntax sourceObject && propertyValue is ObjectSyntax targetObject + ? sourceObject.DeepMerge(targetObject) + : propertyValue; + + properties[matchingIndex] = SyntaxFactory.CreateObjectProperty(propertyName, mergedValue); } - else - { - properties.Add(SyntaxFactory.CreateObjectProperty(propertyName, propertyValue)); + else + { + properties.Add(SyntaxFactory.CreateObjectProperty(propertyName, propertyValue)); } return SyntaxFactory.CreateObject(properties); } - public static ObjectSyntax DeepMerge(this ObjectSyntax? sourceObject, ObjectSyntax targetObject) - { - if (sourceObject == null) - { - return targetObject; - } - - return targetObject.Properties.Aggregate(sourceObject, (mergedObject, property) => - property.TryGetKeyText() is string propertyName - ? mergedObject.MergeProperty(propertyName, property.Value) - : mergedObject); + public static ObjectSyntax DeepMerge(this ObjectSyntax? sourceObject, ObjectSyntax targetObject) + { + if (sourceObject == null) + { + return targetObject; + } + + return targetObject.Properties.Aggregate(sourceObject, (mergedObject, property) => + property.TryGetKeyText() is string propertyName + ? mergedObject.MergeProperty(propertyName, property.Value) + : mergedObject); } } } \ No newline at end of file diff --git a/src/Bicep.Core/TypeSystem/CyclicCheckVisitor.cs b/src/Bicep.Core/TypeSystem/CyclicCheckVisitor.cs index 951f3ff8bea..cbbf107d8c3 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,32 @@ 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; + } + + // Walk all ancestors and add a reference from this resource + foreach (var ancestor in currentDeclarations.OfType()) + { + declarationAccessDict[currentDeclaration].Add(ancestor.DeclaringResource); + } + } + public override void VisitVariableAccessSyntax(VariableAccessSyntax syntax) { - if (currentDeclaration == null) + if (!currentDeclarations.TryPeek(out var currentDeclaration)) { if (currentDecorator != null) { @@ -96,21 +123,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/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.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/Completions/BicepCompletionProvider.cs b/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs index 5c4c34b9c47..6fcefffd15e 100644 --- a/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs +++ b/src/Bicep.LangServer/Completions/BicepCompletionProvider.cs @@ -287,7 +287,7 @@ void AddSymbolCompletions(IDictionary result, IEnumerabl AddSymbolCompletions(completions, model.Root.ImportedNamespaces.Values); // add the non-output declarations with valid identifiers - AddSymbolCompletions(completions, model.Root.AllDeclarations.Where(decl => decl.NameSyntax.IsValid && !(decl is OutputSymbol))); + AddSymbolCompletions(completions, model.Root.Declarations.Where(decl => decl.NameSyntax.IsValid && !(decl is OutputSymbol))); // get names of functions that always require to be fully qualified due to clashes between namespaces var alwaysFullyQualifiedNames = model.Root.ImportedNamespaces 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))); }