diff --git a/src/Bicep.Core.IntegrationTests/ParametersTests.cs b/src/Bicep.Core.IntegrationTests/ParametersTests.cs index 5689be3fb9a..1f1ce6f97d3 100644 --- a/src/Bicep.Core.IntegrationTests/ParametersTests.cs +++ b/src/Bicep.Core.IntegrationTests/ParametersTests.cs @@ -673,6 +673,185 @@ 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_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/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)