Skip to content

Commit d51c915

Browse files
authored
implement "base" parameters support for extended bicepparam files (#17850)
## Description Fixes #14522 and #17053 This PR implements the `base` keyword to enable seamless parameter merging and inheritance in Bicep parameter files. When extending parameter files, users can now reference parent parameter values using `base.<paramName>` to create complex merging scenarios without losing the ability to override specific values. ## Example Usage ```bicep // parent.bicepparam param tags = { environment: 'dev' team: 'platform' } param foo = 'foo' // child.bicepparam extends 'parent.bicepparam' param tags = { ...base.tags // Inherit parent tags environment: 'prod' // Override specific values region: 'westus2' // Add new values } param bar = base.foo ``` ## Checklist - [x] I have read and adhere to the [contribution guide](https://github.com/Azure/bicep/blob/main/CONTRIBUTING.md). ###### Microsoft Reviewers: [Open in CodeFlow](https://microsoft.github.io/open-pr/?codeflow=https://github.com/Azure/bicep/pull/17850)
1 parent 577060f commit d51c915

File tree

10 files changed

+678
-13
lines changed

10 files changed

+678
-13
lines changed

src/Bicep.Cli.IntegrationTests/BuildParamsCommandTests.cs

Lines changed: 412 additions & 0 deletions
Large diffs are not rendered by default.

src/Bicep.Core/Diagnostics/DiagnosticBuilder.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1988,6 +1988,14 @@ public Diagnostic UsingWithClauseRequiresExperimentalFeature() => CoreError(
19881988
public Diagnostic ExpectedWithKeywordOrNewLine() => CoreError(
19891989
"BCP436",
19901990
$"Expected the \"with\" keyword or a new line character at this location.");
1991+
1992+
public Diagnostic BaseIdentifierNotAvailableWithoutExtends() => CoreError(
1993+
"BCP437",
1994+
$"The identifier '{LanguageConstants.BaseIdentifier}' is only available in parameter files that declare an '{LanguageConstants.ExtendsKeyword}' clause.");
1995+
1996+
public Diagnostic BaseIdentifierRedeclared() => CoreError(
1997+
"BCP438",
1998+
$"The identifier '{LanguageConstants.BaseIdentifier}' is reserved and cannot be declared.");
19911999
}
19922000

19932001
public static DiagnosticBuilderInternal ForPosition(TextSpan span)

src/Bicep.Core/Intermediate/ExpressionBuilder.cs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1127,6 +1127,16 @@ private Expression ConvertVariableAccess(VariableAccessSyntax variableAccessSynt
11271127
case ExtensionConfigAssignmentSymbol extensionConfigAssignmentSymbol:
11281128
return new ExtensionConfigAssignmentReferenceExpression(variableAccessSyntax, extensionConfigAssignmentSymbol);
11291129

1130+
case BaseParametersSymbol baseParamsSymbol:
1131+
var objectProperties = baseParamsSymbol.ParentAssignments
1132+
.Select(pa => new ObjectPropertyExpression(
1133+
pa.DeclaringParameterAssignment,
1134+
new StringLiteralExpression(pa.DeclaringParameterAssignment.Name, pa.Name),
1135+
ConvertWithoutLowering(pa.DeclaringParameterAssignment.Value)))
1136+
.ToImmutableArray();
1137+
1138+
return new ObjectExpression(variableAccessSyntax, objectProperties);
1139+
11301140
default:
11311141
throw new NotImplementedException($"Encountered an unexpected symbol kind '{symbol?.Kind}' and type '{symbol?.GetType().Name}' when generating a variable access expression.");
11321142

src/Bicep.Core/LanguageConstants.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ public static class LanguageConstants
9696
public const string TargetScopeTypeLocal = "local";
9797

9898
public const string CopyLoopIdentifier = "copy";
99+
public const string BaseIdentifier = "base";
99100

100101
public const string BicepConfigurationFileName = "bicepconfig.json";
101102

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
using System.Collections.Immutable;
4+
using Bicep.Core.Diagnostics;
5+
using Bicep.Core.TypeSystem;
6+
using Bicep.Core.Syntax;
7+
using Bicep.Core.TypeSystem.Types;
8+
using Bicep.Core.Text;
9+
10+
namespace Bicep.Core.Semantics
11+
{
12+
public class BaseParametersSymbol : DeclaredSymbol
13+
{
14+
public BaseParametersSymbol(ISymbolContext context, ImmutableArray<ParameterAssignmentSymbol> parentAssignments) : base(context, LanguageConstants.BaseIdentifier, new ImplicitBaseIdentifierSyntax(parentAssignments.First().DeclaringParameterAssignment), parentAssignments.First().DeclaringParameterAssignment.Name)
15+
{
16+
this.ParentAssignments = parentAssignments;
17+
this.syntheticSyntax = (ImplicitBaseIdentifierSyntax) this.DeclaringSyntax;
18+
}
19+
20+
public ImmutableArray<ParameterAssignmentSymbol> ParentAssignments { get; }
21+
22+
private readonly ImplicitBaseIdentifierSyntax syntheticSyntax;
23+
24+
public override SymbolKind Kind => SymbolKind.BaseParameters;
25+
26+
public override IEnumerable<Symbol> Descendants => ParentAssignments;
27+
28+
public override void Accept(SymbolVisitor visitor)
29+
{
30+
}
31+
32+
public ImplicitBaseIdentifierSyntax SyntheticDeclaringSyntax => syntheticSyntax;
33+
}
34+
35+
public sealed class ImplicitBaseIdentifierSyntax : SyntaxBase
36+
{
37+
private readonly ParameterAssignmentSyntax anchor;
38+
39+
public ImplicitBaseIdentifierSyntax(ParameterAssignmentSyntax anchor)
40+
{
41+
this.anchor = anchor;
42+
}
43+
44+
public override TextSpan Span => anchor.Span;
45+
46+
public override void Accept(ISyntaxVisitor visitor)
47+
{
48+
}
49+
}
50+
}

src/Bicep.Core/Semantics/Binder.cs

Lines changed: 100 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,29 +40,75 @@ public Binder(
4040
this.NamespaceResolver = NamespaceResolver.Create(namespaceResults);
4141

4242
var fileScope = DeclarationVisitor.GetDeclarations(namespaceResults, sourceFile, symbolContext);
43-
this.Bindings = NameBindingVisitor.GetBindings(sourceFile.ProgramSyntax, NamespaceResolver, fileScope);
44-
this.cyclesBySymbol = CyclicCheckVisitor.FindCycles(sourceFile.ProgramSyntax, this.Bindings);
4543

46-
var extendsDeclarations = sourceFile.ProgramSyntax.Declarations.OfType<ExtendsDeclarationSyntax>();
44+
// Process extends & synthesize 'base' BEFORE name binding so variable accesses to 'base' bind correctly.
45+
var extendsDeclarations = sourceFile.ProgramSyntax.Declarations.OfType<ExtendsDeclarationSyntax>().ToImmutableArray();
46+
bool hasExtends = extendsDeclarations.Any();
47+
var parentParameterAssignments = ImmutableArray<ParameterAssignmentSymbol>.Empty;
4748

48-
foreach (var extendsDeclaration in extendsDeclarations)
49+
if (hasExtends)
4950
{
50-
if (sourceFileLookup.TryGetSourceFile(extendsDeclaration).TryUnwrap() is { } extendedFile &&
51-
modelLookup.GetSemanticModel(extendedFile) is SemanticModel extendedModel)
51+
foreach (var extendsDeclaration in extendsDeclarations)
5252
{
53-
var parameterAssignments = ImmutableArray<ParameterAssignmentSymbol>.Empty;
54-
foreach (var assignment in extendedModel.Root.ParameterAssignments)
53+
if (!(sourceFileLookup.TryGetSourceFile(extendsDeclaration).TryUnwrap() is { } extendedFile &&
54+
modelLookup.GetSemanticModel(extendedFile) is SemanticModel extendedModel))
5555
{
56-
if (!fileScope.Locals.Any(e => e.Name == assignment.Name))
56+
continue;
57+
}
58+
59+
var allParentAssignments = extendedModel.Root.ParameterAssignments;
60+
foreach (var assignment in allParentAssignments)
61+
{
62+
if (!parentParameterAssignments.Any(a => string.Equals(a.Name, assignment.Name, LanguageConstants.IdentifierComparison)))
5763
{
58-
parameterAssignments = parameterAssignments.Add(assignment);
64+
parentParameterAssignments = parentParameterAssignments.Add(assignment);
5965
}
6066
}
6167

62-
fileScope = fileScope.ReplaceLocals(fileScope.Locals.AddRange(parameterAssignments));
68+
var parentVariables = extendedModel.Root.VariableDeclarations.OfType<VariableSymbol>().ToImmutableArray();
69+
70+
var nonConflicting = allParentAssignments.Where(a => !fileScope.Locals.Any(e => string.Equals(e.Name, a.Name, LanguageConstants.IdentifierComparison)));
71+
fileScope = fileScope.ReplaceLocals(fileScope.Locals.AddRange(nonConflicting));
72+
73+
var nonConflictingVars = parentVariables.Where(v => !fileScope.Locals.Any(e => string.Equals(e.Name, v.Name, LanguageConstants.IdentifierComparison)));
74+
fileScope = fileScope.ReplaceLocals(fileScope.Locals.AddRange(nonConflictingVars));
75+
}
76+
77+
if (parentParameterAssignments.Any())
78+
{
79+
var localsWithoutOldBase = fileScope.Locals.Where(l => l is not BaseParametersSymbol).ToImmutableArray();
80+
fileScope = fileScope.ReplaceLocals(localsWithoutOldBase.Add(new BaseParametersSymbol(symbolContext, parentParameterAssignments)));
81+
}
82+
}
83+
84+
var baseBindings = NameBindingVisitor.GetBindings(sourceFile.ProgramSyntax, NamespaceResolver, fileScope).ToBuilder();
85+
86+
if (hasExtends && parentParameterAssignments.Any())
87+
{
88+
foreach (var parentAssignment in parentParameterAssignments)
89+
{
90+
ProcessSyntaxForBinding(
91+
parentAssignment.DeclaringParameterAssignment.Value,
92+
parentAssignment.Context.Binder,
93+
baseBindings);
94+
}
95+
96+
var inheritedVariables = fileScope.Locals.OfType<VariableSymbol>()
97+
.Where(v => !ReferenceEquals(v.Context.SourceFile, sourceFile))
98+
.ToImmutableArray();
99+
100+
foreach (var inheritedVar in inheritedVariables)
101+
{
102+
ProcessSyntaxForBinding(
103+
inheritedVar.DeclaringVariable.Value,
104+
inheritedVar.Context.Binder,
105+
baseBindings);
63106
}
64107
}
65108

109+
this.Bindings = baseBindings.ToImmutable();
110+
this.cyclesBySymbol = CyclicCheckVisitor.FindCycles(sourceFile.ProgramSyntax, this.Bindings);
111+
66112
this.FileSymbol = new FileSymbol(
67113
symbolContext,
68114
sourceFile,
@@ -119,5 +165,48 @@ private ImmutableHashSet<DeclaredSymbol> CalculateReferencedSymbolClosure(Declar
119165

120166
return builder.ToImmutable();
121167
}
168+
169+
private void ProcessSyntaxForBinding(
170+
SyntaxBase rootSyntax,
171+
IBinder parentBinder,
172+
IDictionary<SyntaxBase, Symbol> baseBindings)
173+
{
174+
var stack = new Stack<SyntaxBase>();
175+
stack.Push(rootSyntax);
176+
177+
while (stack.Count > 0)
178+
{
179+
var current = stack.Pop();
180+
181+
if (current is VariableAccessSyntax || current == rootSyntax || current is PropertyAccessSyntax || current is ArrayAccessSyntax)
182+
{
183+
var parentSymbol = parentBinder.GetSymbolInfo(current);
184+
if (parentSymbol is not null && !baseBindings.ContainsKey(current))
185+
{
186+
baseBindings[current] = parentSymbol;
187+
}
188+
}
189+
190+
var childNodes = current switch
191+
{
192+
ObjectSyntax obj => obj.Properties.Select(p => p.Value),
193+
ArraySyntax arr => arr.Items.Select(i => i.Value),
194+
PropertyAccessSyntax propAccess => [propAccess.BaseExpression],
195+
ArrayAccessSyntax arrayAccess => [arrayAccess.BaseExpression, arrayAccess.IndexExpression],
196+
FunctionCallSyntaxBase funcCall => funcCall.Arguments.Select(a => a.Expression),
197+
ParenthesizedExpressionSyntax paren => [paren.Expression],
198+
TernaryOperationSyntax ternary => [ternary.ConditionExpression, ternary.TrueExpression, ternary.FalseExpression],
199+
BinaryOperationSyntax binary => [binary.LeftExpression, binary.RightExpression],
200+
UnaryOperationSyntax unary => [unary.Expression],
201+
NonNullAssertionSyntax nonNull => [nonNull.BaseExpression],
202+
_ => []
203+
};
204+
205+
foreach (var child in childNodes)
206+
{
207+
stack.Push(child);
208+
}
209+
}
210+
}
122211
}
123212
}

src/Bicep.Core/Semantics/SemanticDiagnosticVisitor.cs

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// Licensed under the MIT License.
33
using System.Collections.Immutable;
44
using Bicep.Core.Diagnostics;
5+
using Bicep.Core.SourceGraph;
56
using Bicep.Core.Syntax;
67
using Bicep.Core.TypeSystem;
78

@@ -97,6 +98,55 @@ public override void VisitFileSymbol(FileSymbol symbol)
9798
}
9899
}
99100
}
101+
102+
if (symbol.FileKind == BicepSourceFileKind.ParamsFile)
103+
{
104+
var hasExtends = extendsSyntaxes.Length == 1;
105+
106+
if (!hasExtends)
107+
{
108+
foreach (var access in FindVariableAccesses(symbol.Syntax, LanguageConstants.BaseIdentifier))
109+
{
110+
this.diagnosticWriter.Write(access.Name, x => x.BaseIdentifierNotAvailableWithoutExtends());
111+
}
112+
}
113+
else
114+
{
115+
foreach (var decl in symbol.Declarations.Where(d => string.Equals(d.Name, LanguageConstants.BaseIdentifier, LanguageConstants.IdentifierComparison) && d is not BaseParametersSymbol))
116+
{
117+
this.diagnosticWriter.Write(decl.DeclaringSyntax, x => x.BaseIdentifierRedeclared());
118+
}
119+
}
120+
}
121+
}
122+
123+
private static IEnumerable<VariableAccessSyntax> FindVariableAccesses(SyntaxBase root, string identifier)
124+
{
125+
var results = new List<VariableAccessSyntax>();
126+
var visitor = new VariableAccessCollector(identifier, results);
127+
root.Accept(visitor);
128+
return results;
129+
}
130+
131+
private sealed class VariableAccessCollector : CstVisitor
132+
{
133+
private readonly string identifier;
134+
private readonly IList<VariableAccessSyntax> results;
135+
136+
public VariableAccessCollector(string identifier, IList<VariableAccessSyntax> results)
137+
{
138+
this.identifier = identifier;
139+
this.results = results;
140+
}
141+
142+
public override void VisitVariableAccessSyntax(VariableAccessSyntax syntax)
143+
{
144+
if (syntax.Name.IdentifierName == identifier)
145+
{
146+
results.Add(syntax);
147+
}
148+
base.VisitVariableAccessSyntax(syntax);
149+
}
100150
}
101151

102152
public override void VisitVariableSymbol(VariableSymbol symbol)

src/Bicep.Core/Semantics/SymbolKind.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public enum SymbolKind
2323
ParameterAssignment,
2424
Metadata,
2525
TypeAlias,
26-
ExtensionConfigAssignment
26+
ExtensionConfigAssignment,
27+
BaseParameters
2728
}
2829
}

src/Bicep.Core/TypeSystem/TypeAssignmentVisitor.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2430,6 +2430,9 @@ public override void VisitVariableAccessSyntax(VariableAccessSyntax syntax)
24302430
case WildcardImportSymbol wildcardImport:
24312431
return wildcardImport.Type;
24322432

2433+
case BaseParametersSymbol baseParameters:
2434+
return new DeferredTypeReference(() => VisitDeclaredSymbol(syntax, baseParameters));
2435+
24332436
default:
24342437
return ErrorType.Create(DiagnosticBuilder.ForPosition(syntax.Name.Span).SymbolicNameIsNotAVariableOrParameter(syntax.Name.IdentifierName));
24352438
}

src/Bicep.Core/TypeSystem/TypeManager.cs

Lines changed: 42 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
3+
using System.Collections.Concurrent;
4+
35
using Bicep.Core.Diagnostics;
46
using Bicep.Core.Intermediate;
57
using Bicep.Core.Semantics;
68
using Bicep.Core.Syntax;
9+
using Bicep.Core.TypeSystem.Types;
710

811
namespace Bicep.Core.TypeSystem
912
{
@@ -12,6 +15,7 @@ public class TypeManager : ITypeManager
1215
// stores results of type checks
1316
private readonly TypeAssignmentVisitor typeAssignmentVisitor;
1417
private readonly DeclaredTypeManager declaredTypeManager;
18+
private readonly IBinder binder;
1519

1620
public TypeManager(SemanticModel model, IBinder binder)
1721
{
@@ -20,10 +24,47 @@ public TypeManager(SemanticModel model, IBinder binder)
2024
// (using the IReadOnlyDictionary to prevent accidental mutation)
2125
this.typeAssignmentVisitor = new(this, model);
2226
this.declaredTypeManager = new(this, binder, model.Features);
27+
this.binder = binder;
2328
}
2429

2530
public TypeSymbol GetTypeInfo(SyntaxBase syntax)
26-
=> typeAssignmentVisitor.GetTypeInfo(syntax);
31+
{
32+
if (syntax is ImplicitBaseIdentifierSyntax synthetic)
33+
{
34+
return GetSyntheticBaseParametersType(synthetic);
35+
}
36+
37+
return typeAssignmentVisitor.GetTypeInfo(syntax);
38+
}
39+
40+
private readonly ConcurrentDictionary<ImplicitBaseIdentifierSyntax, TypeSymbol> syntheticBaseTypes = new();
41+
42+
private TypeSymbol GetSyntheticBaseParametersType(ImplicitBaseIdentifierSyntax syntax)
43+
{
44+
return syntheticBaseTypes.GetOrAdd(syntax, key =>
45+
{
46+
if (this.declaredTypeManager is null)
47+
{
48+
return LanguageConstants.Any;
49+
}
50+
51+
if (this.binder.FileSymbol.Declarations.OfType<BaseParametersSymbol>().FirstOrDefault(s => ReferenceEquals(s.DeclaringSyntax, key)) is not { } baseSymbol)
52+
{
53+
return LanguageConstants.Any;
54+
}
55+
56+
var namedProperties = baseSymbol.ParentAssignments
57+
.GroupBy(pa => pa.Name, LanguageConstants.IdentifierComparer)
58+
.Select(group => group.First())
59+
.Select(pa => new NamedTypeProperty(pa.Name, GetTypeInfo(pa.DeclaringParameterAssignment.Value), TypePropertyFlags.ReadOnly));
60+
61+
return new ObjectType(
62+
name: "baseParameters",
63+
validationFlags: TypeSymbolValidationFlags.Default,
64+
properties: namedProperties,
65+
additionalProperties: null);
66+
});
67+
}
2768

2869
public TypeSymbol? GetDeclaredType(SyntaxBase syntax)
2970
=> declaredTypeManager.GetDeclaredType(syntax);

0 commit comments

Comments
 (0)