diff --git a/src/Bicep.Core.IntegrationTests/ParametersTests.cs b/src/Bicep.Core.IntegrationTests/ParametersTests.cs index 5689be3fb9a..991d991937f 100644 --- a/src/Bicep.Core.IntegrationTests/ParametersTests.cs +++ b/src/Bicep.Core.IntegrationTests/ParametersTests.cs @@ -673,6 +673,219 @@ param values array }")); } + [TestMethod] + public void Base_object_spread_should_not_evaluate_unreferenced_external_input() + { + var result = CompilationHelper.CompileParams( + ("parameters.bicepparam", @" + using 'main.bicep' + extends 'shared.bicepparam' + param B = { + ...base.B + something: 'something' + } + "), + ("shared.bicepparam", @" + using none + param external = externalInput('foo', 'bar') + param B = { + parent: 'parent' + } + "), + ("main.bicep", @" + param external object + param B object + ")); + + result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics(); + result.Parameters.Should().HaveJsonAtPath("parameters.B.value", @"{ + ""parent"": ""parent"", + ""something"": ""something"" + }"); + result.Parameters.Should().HaveValueAtPath("parameters.external.expression", "[externalInputs('foo_0')]"); + result.Parameters.Should().HaveJsonAtPath("externalInputDefinitions", @"{ + ""foo_0"": { + ""kind"": ""foo"", + ""config"": ""bar"" + } + }"); + } + + [TestMethod] + public void Base_object_spread_should_support_referenced_external_input() + { + var result = CompilationHelper.CompileParams( + ("parameters.bicepparam", @" + using 'main.bicep' + extends 'shared.bicepparam' + param B = { + ...base.external + something: 'something' + } + "), + ("shared.bicepparam", @" + using none + param external = externalInput('foo', 'bar') + param B = { + parent: 'parent' + } + "), + ("main.bicep", @" + param external object + param B object + ")); + + result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics(); + result.Parameters.Should().HaveValueAtPath("parameters.B.expression", "[shallowMerge(createArray(externalInputs('foo_0'), createObject('something', 'something')))]"); + result.Parameters.Should().HaveJsonAtPath("externalInputDefinitions", @"{ + ""foo_0"": { + ""kind"": ""foo"", + ""config"": ""bar"" + } + }"); + } + + [TestMethod] + public void Base_array_spread_should_not_evaluate_unreferenced_external_input() + { + var result = CompilationHelper.CompileParams( + ("parameters.bicepparam", @" + using 'main.bicep' + extends 'shared.bicepparam' + param values = [ + ...base.values + 'child' + ] + "), + ("shared.bicepparam", @" + using none + param enabled = bool(externalInput('feature', 'enabled')) + param values = [ + 'parent' + ] + "), + ("main.bicep", @" + param enabled bool + param values array + ")); + + result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics(); + result.Parameters.Should().HaveJsonAtPath("parameters.values.value", @"[ + ""parent"", + ""child"" + ]"); + result.Parameters.Should().HaveValueAtPath("parameters.enabled.expression", "[bool(externalInputs('feature_0'))]"); + result.Parameters.Should().HaveJsonAtPath("externalInputDefinitions", @"{ + ""feature_0"": { + ""kind"": ""feature"", + ""config"": ""enabled"" + } + }"); + } + + [TestMethod] + public void Base_property_accesses_should_not_evaluate_unreferenced_external_input() + { + var result = CompilationHelper.CompileParams( + ("parameters.bicepparam", @" + using 'main.bicep' + extends 'shared.bicepparam' + param dotAccess = base.selected.value + param bracketAccess = base['selected'].value + "), + ("shared.bicepparam", @" + using none + param external = externalInput('foo') + param selected = { + value: 'parent' + } + "), + ("main.bicep", @" + param external object + param selected object + param dotAccess string + param bracketAccess string + ")); + + result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics(); + result.Parameters.Should().HaveValueAtPath("parameters.dotAccess.value", "parent"); + result.Parameters.Should().HaveValueAtPath("parameters.bracketAccess.value", "parent"); + result.Parameters.Should().HaveValueAtPath("parameters.external.expression", "[externalInputs('foo_0')]"); + } + + [TestMethod] + public void Nested_base_spreads_should_not_evaluate_unreferenced_external_input() + { + var result = CompilationHelper.CompileParams( + ("parameters.bicepparam", @" + using 'main.bicep' + extends 'middle.bicepparam' + param tags = { + ...base.tags + child: 'child' + } + "), + ("middle.bicepparam", @" + using none + extends 'base.bicepparam' + param tags = { + ...base.tags + middle: 'middle' + } + "), + ("base.bicepparam", @" + using none + param external = externalInput('foo') + param tags = { + parent: 'parent' + } + "), + ("main.bicep", @" + param external object + param tags object + ")); + + result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics(); + result.Parameters.Should().HaveJsonAtPath("parameters.tags.value", @"{ + ""parent"": ""parent"", + ""middle"": ""middle"", + ""child"": ""child"" + }"); + result.Parameters.Should().HaveValueAtPath("parameters.external.expression", "[externalInputs('foo_0')]"); + } + + [TestMethod] + public void Base_spread_should_preserve_function_bindings_in_selected_parent_parameter() + { + var result = CompilationHelper.CompileParams( + ("parameters.bicepparam", @" + using 'main.bicep' + extends 'shared.bicepparam' + param tags = { + ...base.tags + child: 'child' + } + "), + ("shared.bicepparam", @" + using none + param external = externalInput('foo') + param tags = { + parent: toLower('PARENT') + } + "), + ("main.bicep", @" + param external object + param tags object + ")); + + result.ExcludingLinterDiagnostics().Should().NotHaveAnyDiagnostics(); + result.Parameters.Should().HaveJsonAtPath("parameters.tags.value", @"{ + ""parent"": ""parent"", + ""child"": ""child"" + }"); + result.Parameters.Should().HaveValueAtPath("parameters.external.expression", "[externalInputs('foo_0')]"); + } + [TestMethod] public void Decorators_on_using_param_and_extends_statements_should_raise_errors() { diff --git a/src/Bicep.Core/Emit/InlineDependencyVisitor.cs b/src/Bicep.Core/Emit/InlineDependencyVisitor.cs index 4794acb3aaf..7754ca6856c 100644 --- a/src/Bicep.Core/Emit/InlineDependencyVisitor.cs +++ b/src/Bicep.Core/Emit/InlineDependencyVisitor.cs @@ -287,48 +287,14 @@ public override void VisitVariableAccessSyntax(VariableAccessSyntax syntax) return; } - void ProcessInlinableSymbol(DeclaredSymbol symbol, string identifierName) - { - var previousStack = this.currentStack; - if (!shouldInlineCache.TryGetValue(symbol, out var shouldInline)) - { - this.currentStack = this.currentStack?.Push(identifierName); - - // recursively visit dependent symbols - this.Visit(symbol.DeclaringSyntax); - - if (!shouldInlineCache.TryGetValue(symbol, out shouldInline)) - { - shouldInline = Decision.NotInline; - } - - if (shouldInline == Decision.Inline && this.targetVariable is not null && this.capturedSequence is null) - { - // this point is where the decision is made to inline the variable - // the variable access stack will be the deepest here - // (once captured, we will not reset because the visitor will be short-circuiting and - // unrolling the recursion which would produce shorter and inaccurate paths) - - // capture the sequence of variable accesses - this.capturedSequence = this.currentStack; - } - } - - // if we depend on a symbol that requires inlining, then we also require inlining - var newValue = shouldInlineCache[currentDeclaration] == Decision.Inline || shouldInline == Decision.Inline; - SetInlineCache(newValue); - - this.currentStack = previousStack; - } - switch (model.GetSymbolInfo(syntax)) { case VariableSymbol variableSymbol: - ProcessInlinableSymbol(variableSymbol, syntax.Name.IdentifierName); + ProcessInlinableSymbol(currentDeclaration, variableSymbol, syntax.Name.IdentifierName); return; case ParameterAssignmentSymbol parameterAssignmentSymbol: - ProcessInlinableSymbol(parameterAssignmentSymbol, syntax.Name.IdentifierName); + ProcessInlinableSymbol(currentDeclaration, parameterAssignmentSymbol, syntax.Name.IdentifierName); return; case ResourceSymbol: @@ -343,6 +309,40 @@ void ProcessInlinableSymbol(DeclaredSymbol symbol, string identifierName) } } + private void ProcessInlinableSymbol(DeclaredSymbol referencingDeclaration, DeclaredSymbol symbol, string identifierName) + { + var previousStack = this.currentStack; + if (!shouldInlineCache.TryGetValue(symbol, out var shouldInline)) + { + this.currentStack = this.currentStack?.Push(identifierName); + + // recursively visit dependent symbols + this.Visit(symbol.DeclaringSyntax); + + if (!shouldInlineCache.TryGetValue(symbol, out shouldInline)) + { + shouldInline = Decision.NotInline; + } + + if (shouldInline == Decision.Inline && this.targetVariable is not null && this.capturedSequence is null) + { + // this point is where the decision is made to inline the variable + // the variable access stack will be the deepest here + // (once captured, we will not reset because the visitor will be short-circuiting and + // unrolling the recursion which would produce shorter and inaccurate paths) + + // capture the sequence of variable accesses + this.capturedSequence = this.currentStack; + } + } + + // if we depend on a symbol that requires inlining, then we also require inlining + var newValue = shouldInlineCache[referencingDeclaration] == Decision.Inline || shouldInline == Decision.Inline; + SetInlineCache(newValue); + + this.currentStack = previousStack; + } + public override void VisitPropertyAccessSyntax(PropertyAccessSyntax syntax) { // This solution works on the assumption that all deploy-time constants are top-level properties on @@ -360,6 +360,13 @@ public override void VisitPropertyAccessSyntax(PropertyAccessSyntax syntax) return; } + if (model.GetSymbolInfo(syntax.BaseExpression) is BaseParametersSymbol baseParameters && + baseParameters.ParentAssignments.FirstOrDefault(assignment => LanguageConstants.IdentifierComparer.Equals(assignment.Name, syntax.PropertyName.IdentifierName)) is { } parentAssignment) + { + ProcessInlinableSymbol(currentDeclaration, parentAssignment, syntax.PropertyName.IdentifierName); + return; + } + bool ShouldSkipInlining(ObjectType objectType, string propertyName, ResourceSymbol? resourceSymbol = null) { if (!objectType.Properties.TryGetValue(propertyName, out var propertyType)) diff --git a/src/Bicep.Core/Intermediate/ExpressionBuilder.cs b/src/Bicep.Core/Intermediate/ExpressionBuilder.cs index d8851710da3..82ac1b91121 100644 --- a/src/Bicep.Core/Intermediate/ExpressionBuilder.cs +++ b/src/Bicep.Core/Intermediate/ExpressionBuilder.cs @@ -979,6 +979,13 @@ private Expression ConvertArrayAccess(ArrayAccessSyntax arrayAccess) } } + if (arrayAccess.IndexExpression is StringSyntax indexString && + indexString.TryGetLiteralValue() is { } propertyName && + TryGetBaseParameterAssignment(arrayAccess.BaseExpression, propertyName) is { } parentAssignment) + { + return ConvertWithoutLowering(parentAssignment.DeclaringParameterAssignment.Value); + } + var convertedBase = ConvertWithoutLowering(arrayAccess.BaseExpression); var convertedIndex = ConvertWithoutLowering(arrayAccess.IndexExpression); @@ -1030,6 +1037,11 @@ private Expression ConvertPropertyAccess(PropertyAccessSyntax propertyAccess) { var flags = GetAccessExpressionFlags(propertyAccess); + if (TryGetBaseParameterAssignment(propertyAccess.BaseExpression, propertyAccess.PropertyName.IdentifierName) is { } parentAssignment) + { + return ConvertWithoutLowering(parentAssignment.DeclaringParameterAssignment.Value); + } + // Looking for: myResource.someProp (where myResource is a resource declared in-file) if (Context.SemanticModel.ResourceMetadata.TryLookup(propertyAccess.BaseExpression) is DeclaredResourceMetadata resource) { @@ -1098,6 +1110,11 @@ moduleCollectionOutputs.BaseExpression is ArrayAccessSyntax moduleArrayAccess && flags); } + private ParameterAssignmentSymbol? TryGetBaseParameterAssignment(SyntaxBase baseExpression, string propertyName) + => Context.SemanticModel.GetSymbolInfo(baseExpression) is BaseParametersSymbol baseParameters + ? baseParameters.ParentAssignments.FirstOrDefault(assignment => LanguageConstants.IdentifierComparer.Equals(assignment.Name, propertyName)) + : null; + private Expression ConvertResourceAccess(ResourceAccessSyntax resourceAccessSyntax) { if (Context.SemanticModel.ResourceMetadata.TryLookup(resourceAccessSyntax) is { } resource)