Skip to content

Commit

Permalink
Lexically scoped child resources
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
rynowak committed Feb 16, 2021
1 parent 3350d6b commit 9133b4c
Show file tree
Hide file tree
Showing 37 changed files with 1,116 additions and 172 deletions.
187 changes: 187 additions & 0 deletions src/Bicep.Core.IntegrationTests/ResourceTests.cs
Original file line number Diff line number Diff line change
@@ -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
}
}
}
48 changes: 48 additions & 0 deletions src/Bicep.Core.IntegrationTests/TypeSystem/TypeValidationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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\".")
);
}
}
}
19 changes: 19 additions & 0 deletions src/Bicep.Core.Samples/Files/Resources_CRLF/main.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
}
}
}
22 changes: 22 additions & 0 deletions src/Bicep.Core.Samples/Files/Resources_CRLF/main.diagnostics.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
}
}
}
18 changes: 18 additions & 0 deletions src/Bicep.Core.Samples/Files/Resources_CRLF/main.formatted.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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: {}
}
}
24 changes: 24 additions & 0 deletions src/Bicep.Core.Samples/Files/Resources_CRLF/main.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
"shouldDeployVm": {
"type": "bool",
"defaultValue": true
},
"shouldDeployChildAndGrandChild": {
"type": "bool",
"defaultValue": true
}
},
"functions": [],
Expand All @@ -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",
Expand Down
23 changes: 23 additions & 0 deletions src/Bicep.Core.Samples/Files/Resources_CRLF/main.symbols.bicep
Original file line number Diff line number Diff line change
Expand Up @@ -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: {
}
}
}
Loading

0 comments on commit 9133b4c

Please sign in to comment.