Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parent property implementation #1800

Merged
merged 20 commits into from
Mar 17, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
cc56e22
MVP for parent property
anthony-c-martin Mar 9, 2021
2ad9838
Use name as-is for nested/parent resource property access
anthony-c-martin Mar 9, 2021
8577272
Add some assertions for parent/nested resources & scopes
anthony-c-martin Mar 10, 2021
73aef17
Merge remote-tracking branch 'origin/main' into antmarti/parent_property
anthony-c-martin Mar 11, 2021
e73f3c0
Merge remote-tracking branch 'origin/main' into antmarti/parent_property
anthony-c-martin Mar 11, 2021
20081f3
Add some scope validations
anthony-c-martin Mar 11, 2021
bfac1ae
Inherit scope from oldest ancestor
anthony-c-martin Mar 11, 2021
008dead
Fix dependsOn generation for resource access syntax
anthony-c-martin Mar 11, 2021
aba5af0
Update test baselines
Mar 11, 2021
b023eb2
Merge remote-tracking branch 'origin/main' into antmarti/parent_property
anthony-c-martin Mar 12, 2021
bcc737b
Merge remote-tracking branch 'origin/main' into antmarti/parent_property
anthony-c-martin Mar 12, 2021
9b1084c
Make parent writable; fix tests
anthony-c-martin Mar 12, 2021
3b4b760
Add validation for literal resource names
anthony-c-martin Mar 12, 2021
a613559
Merge remote-tracking branch 'origin/main' into antmarti/parent_property
anthony-c-martin Mar 12, 2021
61f16bb
Add loop tests
anthony-c-martin Mar 12, 2021
efe088e
Loops implementation
anthony-c-martin Mar 13, 2021
c81ad77
Merge remote-tracking branch 'origin/main' into antmarti/parent_property
anthony-c-martin Mar 13, 2021
54466fd
Add handling & tests for parent property cycles
anthony-c-martin Mar 16, 2021
709a41f
Merge remote-tracking branch 'origin/main' into antmarti/parent_property
anthony-c-martin Mar 16, 2021
016c843
Reword error messages
anthony-c-martin Mar 17, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 68 additions & 0 deletions src/Bicep.Core.IntegrationTests/ParentResourceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
using System.IO;
using System.Linq;
using System.Text;
using Bicep.Core.Diagnostics;
using Bicep.Core.Emit;
using Bicep.Core.Semantics;
using Bicep.Core.TypeSystem;
using Bicep.Core.UnitTests;
using Bicep.Core.UnitTests.Assertions;
using Bicep.Core.UnitTests.Utils;
using FluentAssertions;
using FluentAssertions.Execution;
using Microsoft.VisualStudio.TestTools.UnitTesting;

namespace Bicep.Core.IntegrationTests
{
[TestClass]
public class ParentPropertyResourceTests
{
[TestMethod]
public void NestedResources_symbols_are_bound()
{
var program = @"
resource vnet 'Microsoft.Network/virtualNetworks@2020-06-01' = {
location: resourceGroup().location
name: 'myVnet'
properties: {
addressSpace: {
addressPrefixes: [
'10.0.0.0/20'
]
}
}
}

resource subnet1 'Microsoft.Network/virtualNetworks/subnets@2020-06-01' = {
parent: vnet
name: 'subnet1'
properties: {
addressPrefix: '10.0.0.0/24'
}
}

resource subnet2 'Microsoft.Network/virtualNetworks/subnets@2020-06-01' = {
parent: vnet
name: 'subnet2'
properties: {
addressPrefix: '10.0.1.0/24'
}
}
";

var (template, diags, _) = CompilationHelper.Compile(program);

using (new AssertionScope())
{
template!.Should().NotBeNull();
diags.Should().BeEmpty();

template!.SelectToken("$.resources[0].name")!.Should().DeepEqual("myVnet");
template!.SelectToken("$.resources[1].name")!.Should().DeepEqual("[format('{0}/{1}', 'myVnet', 'subnet1')]");
template!.SelectToken("$.resources[2].name")!.Should().DeepEqual("[format('{0}/{1}', 'myVnet', 'subnet2')]");
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public static AndConstraint<JTokenAssertions> DeepEqual(this JTokenAssertions in
Execute.Assertion
.BecauseOf(because, becauseArgs)
.ForCondition(JToken.DeepEquals(instance.Subject, expected))
.FailWith("Expected '{0}' but got '{1}'", expected?.ToString(), instance.Subject?.ToString());
.FailWith("Expected {0} but got {1}", expected?.ToString(), instance.Subject?.ToString());

return new AndConstraint<JTokenAssertions>(instance);
}
Expand Down
10 changes: 10 additions & 0 deletions src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -918,6 +918,16 @@ public Diagnostic RuntimePropertyNotAllowed(string property, IEnumerable<string>
"BCP161",
"Parameter modifiers are deprecated and will be removed in a future release. Use decorators instead (see https://aka.ms/BicepSpecParams for examples).",
DiagnosticLabel.Deprecated);

public ErrorDiagnostic ParentResourceTypeHasErrors(string resourceName) => new(
TextSpan,
"BCP162",
$"The resource type cannot be validated due to an error in parent resource \"{resourceName}\".");

public ErrorDiagnostic ResourceTypeIsNotValidParent(string resourceType, string parentResourceType) => new(
TextSpan,
"BCP163",
$"Resource type \"{resourceType}\" is not a valid child resource of parent \"{parentResourceType}\".");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

parent [](start = 84, length = 6)

Should the message also say what the permissible types would be? (I haven't yet read how this is used.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Personally I don't think we should list the available child types in the error - the inconsistency is detected via prefix matching (using the type string to check if it's a child), so it feels like it could be inconsistent or potentially misleading to try and detect possible types from the type system.

}

public static DiagnosticBuilderInternal ForPosition(TextSpan span)
Expand Down
46 changes: 30 additions & 16 deletions src/Bicep.Core/Emit/ExpressionConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Azure.Deployments.Core.Extensions;
Expand Down Expand Up @@ -295,6 +296,32 @@ grandChildArrayAccess.BaseExpression is VariableAccessSyntax grandGrandChildVari
new JTokenExpression(propertyAccess.PropertyName.IdentifierName));
}

public IEnumerable<LanguageExpression> GetResourceNameSegments(ResourceSymbol resourceSymbol, ResourceTypeReference typeReference)
{
var ancestors = this.context.SemanticModel.ResourceAncestors.GetAncestors(resourceSymbol);
if (ancestors.Length > 0)
{
Debug.Assert(ancestors.Length + 1 == typeReference.Types.Length, "ancestors.Length + 1 == typeReference.Types.Length");

return ancestors.Concat(resourceSymbol.AsEnumerable())
.Select(x => GetResourceNameSyntax(x))
.Select(x => ConvertExpression(x));
}

var nameSyntax = GetResourceNameSyntax(resourceSymbol);
var nameExpression = ConvertExpression(nameSyntax);

if (typeReference.Types.Length == 1)
{
return nameExpression.AsEnumerable();
}

return typeReference.Types.Select(
(type, i) => AppendProperties(
CreateFunction("split", nameExpression, new JTokenExpression("/")),
new JTokenExpression(i)));
}

public LanguageExpression GetResourceNameExpression(ResourceSymbol resourceSymbol)
{
var nameValueSyntax = GetResourceNameSyntax(resourceSymbol);
Expand Down Expand Up @@ -348,22 +375,6 @@ public static SyntaxBase GetModuleNameSyntax(ModuleSymbol moduleSymbol)
return moduleSymbol.SafeGetBodyPropertyValue(LanguageConstants.ResourceNamePropertyName) ?? throw new ArgumentException($"Expected module syntax body to contain property 'name'");
}

public IEnumerable<LanguageExpression> GetResourceNameSegments(ResourceSymbol resourceSymbol, ResourceTypeReference typeReference)
{
if (typeReference.Types.Length == 1)
{
return GetResourceNameExpression(resourceSymbol).AsEnumerable();
}

return typeReference.Types.Select(
(type, i) => AppendProperties(
CreateFunction(
"split",
GetResourceNameExpression(resourceSymbol),
new JTokenExpression("/")),
new JTokenExpression(i)));
}

public LanguageExpression GetUnqualifiedResourceId(ResourceSymbol resourceSymbol)
{
var typeReference = EmitHelpers.GetTypeReference(resourceSymbol);
Expand Down Expand Up @@ -499,6 +510,9 @@ private LanguageExpression ConvertVariableAccess(VariableAccessSyntax variableAc
return GetReferenceExpression(resourceSymbol, typeReference, true);

case ModuleSymbol moduleSymbol:
// referencing a module directly should be blocked at an earlier stage - there is nothing great we can codegen here.
Debug.Fail("Found direct module variable access");

return GetModuleOutputsReferenceExpression(moduleSymbol);

case LocalVariableSymbol localVariableSymbol:
Expand Down
1 change: 1 addition & 0 deletions src/Bicep.Core/Emit/TemplateWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ public class TemplateWriter

private static readonly ImmutableHashSet<string> ResourcePropertiesToOmit = new [] {
LanguageConstants.ResourceScopePropertyName,
LanguageConstants.ResourceParentPropertyName,
LanguageConstants.ResourceDependsOnPropertyName,
LanguageConstants.ResourceNamePropertyName,
}.ToImmutableHashSet();
Expand Down
1 change: 1 addition & 0 deletions src/Bicep.Core/LanguageConstants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ public static class LanguageConstants
public const string ResourceTypePropertyName = "type";
public const string ResourceApiVersionPropertyName = "apiVersion";
public const string ResourceScopePropertyName = "scope";
public const string ResourceParentPropertyName = "parent";
public const string ResourceDependsOnPropertyName = "dependsOn";

public static readonly StringComparer IdentifierComparer = StringComparer.Ordinal;
Expand Down
2 changes: 2 additions & 0 deletions src/Bicep.Core/Resources/ResourceTypeReference.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,8 @@ public ResourceTypeReference(string @namespace, IEnumerable<string> types, strin
public string FormatName()
=> $"{this.FullyQualifiedType}@{this.ApiVersion}";

public bool IsRootType => Types.Length == 1;

public bool IsParentOf(ResourceTypeReference other)
{
return
Expand Down
18 changes: 17 additions & 1 deletion src/Bicep.Core/Semantics/ResourceAncestorGraph.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// Licensed under the MIT License.
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Linq;
using Bicep.Core.Diagnostics;
using Bicep.Core.Syntax;

Expand Down Expand Up @@ -30,11 +31,26 @@ public ImmutableArray<ResourceSymbol> GetAncestors(ResourceSymbol resource)
}
}

private static IEnumerable<ResourceSymbol> GetAncestorsYoungestToOldest(ImmutableDictionary<ResourceSymbol, ResourceSymbol> hierarchy, ResourceSymbol resource)
{
while (hierarchy.TryGetValue(resource, out var parentResource))
{
yield return parentResource;

resource = parentResource;
}
}

public static ResourceAncestorGraph Compute(SyntaxTree syntaxTree, IBinder binder)
{
var visitor = new ResourceAncestorVisitor(binder);
visitor.Visit(syntaxTree.ProgramSyntax);
return new ResourceAncestorGraph(visitor.Ancestry);

var ancestry = visitor.Ancestry.Keys.ToImmutableDictionary(
child => child,
child => GetAncestorsYoungestToOldest(visitor.Ancestry, child).Reverse().ToImmutableArray());

return new ResourceAncestorGraph(ancestry);
}
}
}
35 changes: 19 additions & 16 deletions src/Bicep.Core/Semantics/ResourceAncestorVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,15 @@ namespace Bicep.Core.Semantics
public sealed class ResourceAncestorVisitor : SyntaxVisitor
{
private readonly IBinder binder;
private readonly Stack<ResourceSymbol> ancestorResources;
private readonly ImmutableDictionary<ResourceSymbol, ImmutableArray<ResourceSymbol>>.Builder ancestry;
private readonly ImmutableDictionary<ResourceSymbol, ResourceSymbol>.Builder ancestry;

public ResourceAncestorVisitor(IBinder binder)
{
this.binder = binder;
this.ancestorResources = new Stack<ResourceSymbol>();
this.ancestry = ImmutableDictionary.CreateBuilder<ResourceSymbol, ImmutableArray<ResourceSymbol>>();
this.ancestry = ImmutableDictionary.CreateBuilder<ResourceSymbol, ResourceSymbol>();
}

public ImmutableDictionary<ResourceSymbol, ImmutableArray<ResourceSymbol>> Ancestry
public ImmutableDictionary<ResourceSymbol, ResourceSymbol> Ancestry
=> this.ancestry.ToImmutableDictionary();

public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax syntax)
Expand All @@ -33,21 +31,26 @@ public override void VisitResourceDeclarationSyntax(ResourceDeclarationSyntax sy
base.VisitResourceDeclarationSyntax(syntax);
return;
}

// We don't need to do anything here to validate types and their relationships, that was handled during type assignment.
this.ancestry.Add(symbol, ImmutableArray.CreateRange(this.ancestorResources.Reverse()));

try

if (this.binder.GetNearestAncestor<ResourceDeclarationSyntax>(syntax) is {} nestedParentSyntax)
{
// 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);
// nested resource parent syntax
if (this.binder.GetSymbolInfo(nestedParentSyntax) is ResourceSymbol parentSymbol)
{
this.ancestry.Add(symbol, parentSymbol);
}
}
finally
else if (symbol.SafeGetBodyPropertyValue(LanguageConstants.ResourceParentPropertyName) is {} referenceParentSyntax)
{
this.ancestorResources.Pop();
// parent property reference syntax
if (this.binder.GetSymbolInfo(referenceParentSyntax) is ResourceSymbol parentSymbol)
{
this.ancestry.Add(symbol, parentSymbol);
}
}

base.VisitResourceDeclarationSyntax(syntax);
return;
}
}
}
21 changes: 20 additions & 1 deletion src/Bicep.Core/Syntax/ResourceDeclarationSyntax.cs
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,32 @@ public TypeSymbol GetDeclaredType(IBinder binder, IResourceTypeProvider resource
ResourceTypeReference? typeReference;
var ancestors = binder.GetAllAncestors<ResourceDeclarationSyntax>(this);
if (ancestors.Length == 0)
{
{
anthony-c-martin marked this conversation as resolved.
Show resolved Hide resolved
// This is a top level resource - the type is a fully-qualified type.
typeReference = ResourceTypeReference.TryParse(stringContent);
if (typeReference == null)
{
return ErrorType.Create(DiagnosticBuilder.ForPosition(this.Type).InvalidResourceType());
}

if (binder.GetSymbolInfo(this) is ResourceSymbol resourceSymbol &&
resourceSymbol.SafeGetBodyPropertyValue(LanguageConstants.ResourceParentPropertyName) is {} referenceParentSyntax &&
binder.GetSymbolInfo(referenceParentSyntax) is ResourceSymbol parentResourceSymbol)
anthony-c-martin marked this conversation as resolved.
Show resolved Hide resolved
{
var parentType = parentResourceSymbol.DeclaringResource.GetDeclaredType(binder, resourceTypeProvider);
if (parentType is not ResourceType parentResourceType)
{
// TODO should we raise an error, or just rely on the error on the parent?
return ErrorType.Create(DiagnosticBuilder.ForPosition(referenceParentSyntax).ParentResourceTypeHasErrors(parentResourceSymbol.DeclaringResource.Name.IdentifierName));
}

if (!parentResourceType.TypeReference.IsParentOf(typeReference))
{
return ErrorType.Create(DiagnosticBuilder.ForPosition(referenceParentSyntax).ResourceTypeIsNotValidParent(
typeReference.FullyQualifiedType,
parentResourceType.TypeReference.FullyQualifiedType));
}
}
}
else
{
Expand Down
16 changes: 13 additions & 3 deletions src/Bicep.Core/TypeSystem/Az/AzResourceTypeProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -69,12 +69,12 @@ public static ResourceType SetBicepResourceProperties(ResourceType resourceType,
switch (bodyType)
{
case ObjectType bodyObjectType:
bodyType = SetBicepResourceProperties(bodyObjectType, resourceType.ValidParentScopes, isExistingResource);
bodyType = SetBicepResourceProperties(bodyObjectType, resourceType.ValidParentScopes, resourceType.TypeReference, isExistingResource);
break;
case DiscriminatedObjectType bodyDiscriminatedType:
var bodyTypes = bodyDiscriminatedType.UnionMembersByKey.Values.ToList()
.Select(x => x.Type as ObjectType ?? throw new ArgumentException($"Resource {resourceType.Name} has unexpected body type {bodyType.GetType()}"));
bodyTypes = bodyTypes.Select(x => SetBicepResourceProperties(x, resourceType.ValidParentScopes, isExistingResource));
bodyTypes = bodyTypes.Select(x => SetBicepResourceProperties(x, resourceType.ValidParentScopes, resourceType.TypeReference, isExistingResource));
bodyType = new DiscriminatedObjectType(
bodyDiscriminatedType.Name,
bodyDiscriminatedType.ValidationFlags,
Expand All @@ -90,7 +90,7 @@ public static ResourceType SetBicepResourceProperties(ResourceType resourceType,
return new ResourceType(resourceType.TypeReference, resourceType.ValidParentScopes, bodyType);
}

private static ObjectType SetBicepResourceProperties(ObjectType objectType, ResourceScope validParentScopes, bool isExistingResource)
private static ObjectType SetBicepResourceProperties(ObjectType objectType, ResourceScope validParentScopes, ResourceTypeReference typeReference, bool isExistingResource)
{
var properties = objectType.Properties;

Expand Down Expand Up @@ -119,6 +119,16 @@ private static ObjectType SetBicepResourceProperties(ObjectType objectType, Reso
properties = properties.SetItem(LanguageConstants.ResourceScopePropertyName, new TypeProperty(LanguageConstants.ResourceScopePropertyName, scopeReference, scopePropertyFlags));
}
}

// add the 'parent' property for child resource types
if (!typeReference.IsRootType)
{
var parentType = LanguageConstants.CreateResourceScopeReference(ResourceScope.Resource);
var parentFlags = TypePropertyFlags.WriteOnly | TypePropertyFlags.DeployTimeConstant;

properties = properties.SetItem(LanguageConstants.ResourceParentPropertyName, new TypeProperty(LanguageConstants.ResourceParentPropertyName, parentType, parentFlags));
}

// Deployments RP
if (StringComparer.OrdinalIgnoreCase.Equals(objectType.Name, ResourceTypeDeployments))
{
Expand Down