Skip to content

Commit 38a91a1

Browse files
authored
feat: add support for loadTextContent function in extendable parameter files (#18424)
Fixes #18370 ## Description The fix ensures that when evaluating parameters from extended files, the evaluator uses the correct `ExpressionConverter` that corresponds to the semantic model of the file where the parameter was originally declared. This allows functions like `loadTextContent()` to resolve file paths correctly relative to their defining file. ## Example Usage ```bicep // example.txt This is an example txt file. // main.bicep param foo object // base.bicepparam using none param foo = { bar: loadTextContent('./example.txt') } // main.bicepparam using './main.bicep' extends './base.bicepparam' ``` To build the `main.bicepparam`, use the following command; ```shell dotnet run --project ./src/Bicep.Cli/Bicep.Cli.csproj -- build-params ./main.bicepparam ``` With the fixes in this PR building the bicepparam will succeed. _PS : `Build_params_with_extends_and_loadTextContent_in_base_succeeds` test is to ensure the fix works 👍_ ## 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/18424)
1 parent 5d4eca9 commit 38a91a1

File tree

3 files changed

+152
-5
lines changed

3 files changed

+152
-5
lines changed

src/Bicep.Cli.IntegrationTests/BuildParamsCommandTests.cs

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1283,5 +1283,71 @@ param organizationProfile resourceInput<'Microsoft.DevOpsInfrastructure/pools@20
12831283
var kindValue = parametersObject["parameters"]?["organizationProfile"]?["value"]?["kind"]?.ToString();
12841284
kindValue.Should().Be("AzureDevOps");
12851285
}
1286+
1287+
[TestMethod]
1288+
public async Task Build_params_with_extends_and_loadTextContent_in_base_succeeds()
1289+
{
1290+
var outputPath = FileHelper.GetUniqueTestOutputPath(TestContext);
1291+
1292+
var exampleTxtFile = FileHelper.SaveResultFile(
1293+
TestContext,
1294+
"example.txt",
1295+
"This is an example txt file.",
1296+
outputPath);
1297+
1298+
var baseParamsFile = FileHelper.SaveResultFile(
1299+
TestContext,
1300+
"base.bicepparam",
1301+
"""
1302+
using none
1303+
1304+
param foo = {
1305+
bar: loadTextContent('./example.txt')
1306+
}
1307+
""",
1308+
outputPath);
1309+
1310+
var mainParamsFile = FileHelper.SaveResultFile(
1311+
TestContext,
1312+
"main.bicepparam",
1313+
"""
1314+
using './main.bicep'
1315+
extends './base.bicepparam'
1316+
""",
1317+
outputPath);
1318+
1319+
FileHelper.SaveResultFile(
1320+
TestContext,
1321+
"main.bicep",
1322+
"""
1323+
#disable-next-line no-unused-params
1324+
param foo object
1325+
""",
1326+
outputPath);
1327+
1328+
FileHelper.SaveResultFile(
1329+
TestContext,
1330+
"bicepconfig.json",
1331+
"""
1332+
{
1333+
"experimentalFeaturesEnabled": {
1334+
"extendableParamFiles": true
1335+
}
1336+
}
1337+
""",
1338+
outputPath);
1339+
1340+
var settings = CreateDefaultSettings();
1341+
var result = await Bicep(settings, "build-params", mainParamsFile, "--stdout");
1342+
1343+
result.Should().Succeed();
1344+
1345+
var parametersStdout = result.Stdout.FromJson<BuildParamsStdout>();
1346+
parametersStdout.Should().NotBeNull();
1347+
1348+
var parametersObject = JObject.Parse(parametersStdout!.parametersJson);
1349+
var bar = parametersObject["parameters"]?["foo"]?["value"]?["bar"]?.ToString();
1350+
bar.Should().Be("This is an example txt file.");
1351+
}
12861352
}
12871353
}

src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs

Lines changed: 85 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
using Bicep.Core.Intermediate;
1616
using Bicep.Core.Semantics;
1717
using Bicep.Core.Semantics.Metadata;
18+
using Bicep.Core.SourceGraph;
1819
using Bicep.Core.Syntax;
1920
using Microsoft.WindowsAzure.ResourceStack.Common.Extensions;
2021
using Newtonsoft.Json;
@@ -149,7 +150,9 @@ private Result(JToken? value, Expression? expression, ParameterKeyVaultReference
149150
private readonly ConcurrentDictionary<Expression, Result> synthesizedVariableResults = new();
150151
private readonly ConcurrentDictionary<SemanticModel, ResultWithDiagnosticBuilder<Template>> templateResults = new();
151152
private readonly ConcurrentDictionary<Template, TemplateVariablesEvaluator> armEvaluators = new();
153+
private readonly ConcurrentDictionary<SemanticModel, ExpressionConverter> converterCache = new();
152154
private readonly ImmutableDictionary<string, ParameterAssignmentSymbol> paramsByName;
155+
private readonly SemanticModel semanticModel;
153156
private readonly ImmutableDictionary<string, VariableSymbol> variablesByName;
154157
private readonly ImmutableDictionary<string, DeclaredFunctionSymbol> functionsByName;
155158
private readonly ImmutableDictionary<string, ImportedSymbol> importsByName;
@@ -158,8 +161,70 @@ private Result(JToken? value, Expression? expression, ParameterKeyVaultReference
158161
private readonly ExternalInputReferences externalInputReferences;
159162
private readonly ExpressionConverter converter;
160163

164+
private static IEnumerable<FunctionVariable> CollectFunctionVariablesIncludingExtends(SemanticModel model, EmitterContext context)
165+
{
166+
foreach (var functionVariable in context.FunctionVariables.Values)
167+
{
168+
yield return functionVariable;
169+
}
170+
171+
if (model.SourceFile is not BicepParamFile)
172+
{
173+
yield break;
174+
}
175+
176+
var visitedModels = new HashSet<ISemanticModel>();
177+
178+
foreach (var extendsDeclaration in model.SourceFile.ProgramSyntax.Declarations.OfType<ExtendsDeclarationSyntax>())
179+
{
180+
foreach (var functionVariable in CollectFunctionVariablesFromExtendedModel(model, extendsDeclaration, visitedModels))
181+
{
182+
yield return functionVariable;
183+
}
184+
}
185+
}
186+
187+
private static IEnumerable<FunctionVariable> CollectFunctionVariablesFromExtendedModel(SemanticModel currentModel, ExtendsDeclarationSyntax extendsDeclaration, HashSet<ISemanticModel> visitedModels)
188+
{
189+
if (!currentModel.TryGetReferencedModel(extendsDeclaration).IsSuccess(out var extendedModel))
190+
{
191+
yield break;
192+
}
193+
194+
if (!visitedModels.Add(extendedModel))
195+
{
196+
yield break;
197+
}
198+
199+
if (extendedModel is not SemanticModel extendedSemanticModel)
200+
{
201+
yield break;
202+
}
203+
204+
var extendedContext = new EmitterContext(extendedSemanticModel);
205+
206+
foreach (var functionVariable in extendedContext.FunctionVariables.Values)
207+
{
208+
yield return functionVariable;
209+
}
210+
211+
if (extendedSemanticModel.SourceFile is BicepParamFile)
212+
{
213+
var nestedExtendsDeclarations = extendedSemanticModel.SourceFile.ProgramSyntax.Declarations.OfType<ExtendsDeclarationSyntax>();
214+
215+
foreach (var nestedExtendsDeclaration in nestedExtendsDeclarations)
216+
{
217+
foreach (var functionVariable in CollectFunctionVariablesFromExtendedModel(extendedSemanticModel, nestedExtendsDeclaration, visitedModels))
218+
{
219+
yield return functionVariable;
220+
}
221+
}
222+
}
223+
}
224+
161225
public ParameterAssignmentEvaluator(SemanticModel model)
162226
{
227+
this.semanticModel = model;
163228
this.paramsByName = model.Root.ParameterAssignments
164229
.GroupBy(x => x.Name, LanguageConstants.IdentifierComparer)
165230
.ToImmutableDictionary(x => x.Key, x => x.First(), LanguageConstants.IdentifierComparer);
@@ -172,20 +237,36 @@ public ParameterAssignmentEvaluator(SemanticModel model)
172237

173238
EmitterContext context = new(model);
174239
this.converter = new(context);
240+
this.converterCache[model] = this.converter;
175241
this.importsByName = context.SemanticModel.ImportClosureInfo.ImportedSymbolNames.Keys
176242
.Select(importedVariable => (context.SemanticModel.ImportClosureInfo.ImportedSymbolNames[importedVariable], importedVariable))
177243
.GroupBy(x => x.Item1, LanguageConstants.IdentifierComparer)
178244
.ToImmutableDictionary(x => x.Key, x => x.First().importedVariable, LanguageConstants.IdentifierComparer);
179245
this.wildcardImportPropertiesByName = context.SemanticModel.ImportClosureInfo.WildcardImportPropertyNames
180246
.GroupBy(x => x.Value, LanguageConstants.IdentifierComparer)
181247
.ToImmutableDictionary(x => x.Key, x => x.First().Key, LanguageConstants.IdentifierComparer);
182-
this.synthesizedVariableValuesByName = context.FunctionVariables.Values
248+
this.synthesizedVariableValuesByName = CollectFunctionVariablesIncludingExtends(model, context)
183249
.GroupBy(result => result.Name)
184250
.ToImmutableDictionary(x => x.Key, x => x.First().Value);
185251

186252
this.externalInputReferences = context.ExternalInputReferences;
187253
}
188254

255+
private ExpressionConverter GetConverterForParameter(ParameterAssignmentSymbol parameter)
256+
{
257+
if (ReferenceEquals(parameter.Context.SourceFile, semanticModel.SourceFile))
258+
{
259+
return converter;
260+
}
261+
262+
if (parameter.Context.ModelLookup.GetSemanticModel(parameter.Context.SourceFile) is SemanticModel parameterModel)
263+
{
264+
return converterCache.GetOrAdd(parameterModel, model => new ExpressionConverter(new EmitterContext(model)));
265+
}
266+
267+
return converter;
268+
}
269+
189270
public Result EvaluateParameter(ParameterAssignmentSymbol parameter)
190271
=> results.GetOrAdd(
191272
parameter,
@@ -195,7 +276,8 @@ public Result EvaluateParameter(ParameterAssignmentSymbol parameter)
195276

196277
var declaringParam = parameter.DeclaringParameterAssignment;
197278

198-
var intermediate = converter.ConvertToIntermediateExpression(declaringParam.Value);
279+
var parameterConverter = GetConverterForParameter(parameter);
280+
var intermediate = parameterConverter.ConvertToIntermediateExpression(declaringParam.Value);
199281

200282
if (this.externalInputReferences.ParametersReferences.Contains(parameter))
201283
{
@@ -212,7 +294,7 @@ public Result EvaluateParameter(ParameterAssignmentSymbol parameter)
212294

213295
try
214296
{
215-
return Result.For(converter.ConvertExpression(intermediate).EvaluateExpression(context));
297+
return Result.For(parameterConverter.ConvertExpression(intermediate).EvaluateExpression(context));
216298
}
217299
catch (Exception ex)
218300
{

src/Bicep.Core/Intermediate/ExpressionBuilder.cs

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -884,8 +884,7 @@ [.. method.Arguments.Select(a => ConvertWithoutLowering(a.Expression))]),
884884

885885
private Expression ConvertFunction(FunctionCallSyntaxBase functionCall)
886886
{
887-
if (Context.Settings.FileKind == BicepSourceFileKind.BicepFile &&
888-
Context.FunctionVariables.GetValueOrDefault(functionCall) is { } functionVariable)
887+
if (Context.FunctionVariables.GetValueOrDefault(functionCall) is { } functionVariable)
889888
{
890889
return new SynthesizedVariableReferenceExpression(functionCall, functionVariable.Name);
891890
}

0 commit comments

Comments
 (0)