Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
213 changes: 213 additions & 0 deletions src/Bicep.Core.IntegrationTests/ParametersTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
{
Expand Down
79 changes: 43 additions & 36 deletions src/Bicep.Core/Emit/InlineDependencyVisitor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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
Expand All @@ -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))
Expand Down
17 changes: 17 additions & 0 deletions src/Bicep.Core/Intermediate/ExpressionBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
Expand Down
Loading