diff --git a/src/Bicep.Cli.IntegrationTests/BuildParamsCommandTests.cs b/src/Bicep.Cli.IntegrationTests/BuildParamsCommandTests.cs index abf9af83488..b572a4b1365 100644 --- a/src/Bicep.Cli.IntegrationTests/BuildParamsCommandTests.cs +++ b/src/Bicep.Cli.IntegrationTests/BuildParamsCommandTests.cs @@ -327,6 +327,99 @@ param vnetConfigs array paramsObject.Should().HaveValueAtPath("parameters.sharedGroupName.value", "rg-search-foo-nonprod"); } + [TestMethod] + public async Task Build_params_for_expression_variable_should_succeed() + { + var outputPath = FileHelper.GetUniqueTestOutputPath(TestContext); + + _ = FileHelper.SaveResultFile( + TestContext, + "main.bicep", + """ + type FleetConfig = { + namePrefix: string + sku: string + capacity: int + clusteringPolicy: string + } + + param testMatrix FleetConfig[] + """, + outputPath); + + var paramsPath = FileHelper.SaveResultFile( + TestContext, + "main.bicepparam", + """ + using './main.bicep' + + var matrix = [ + { + namePrefix: 'e10impactx4' + sku: 'Enterprise_E10' + capacity: 4 + } + { + namePrefix: 'e10impact' + sku: 'Enterprise_E10' + capacity: 2 + } + ] + + var type1 = [for item in matrix: { + namePrefix: item.namePrefix + sku: item.sku + capacity: item.capacity + clusteringPolicy: 'EnterpriseCluster' + }] + + var type2 = [for item in matrix: { + namePrefix: '${item.namePrefix}-ent' + sku: item.sku + capacity: item.capacity + clusteringPolicy: 'OSSCluster' + }] + + param testMatrix = concat(type1, type2) + """, + outputPath); + + var result = await Bicep(CreateDefaultSettings(), "build-params", paramsPath, "--stdout"); + + result.Should().Succeed(); + + var parametersStdout = result.Stdout.FromJson(); + var paramsObject = parametersStdout.parametersJson.FromJson(); + paramsObject.Should().HaveValueAtPath("parameters.testMatrix.value", JToken.Parse(""" + [ + { + "namePrefix": "e10impactx4", + "sku": "Enterprise_E10", + "capacity": 4, + "clusteringPolicy": "EnterpriseCluster" + }, + { + "namePrefix": "e10impact", + "sku": "Enterprise_E10", + "capacity": 2, + "clusteringPolicy": "EnterpriseCluster" + }, + { + "namePrefix": "e10impactx4-ent", + "sku": "Enterprise_E10", + "capacity": 4, + "clusteringPolicy": "OSSCluster" + }, + { + "namePrefix": "e10impact-ent", + "sku": "Enterprise_E10", + "capacity": 2, + "clusteringPolicy": "OSSCluster" + } + ] + """)); + } + [TestMethod] public async Task Build_params_extends_variable_uses_base_params_not_overridden() { diff --git a/src/Bicep.Core.UnitTests/Emit/ParameterAssignmentEvaluatorTests.cs b/src/Bicep.Core.UnitTests/Emit/ParameterAssignmentEvaluatorTests.cs new file mode 100644 index 00000000000..c7e1e7130ce --- /dev/null +++ b/src/Bicep.Core.UnitTests/Emit/ParameterAssignmentEvaluatorTests.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using Bicep.Core.UnitTests.Assertions; +using Bicep.Core.UnitTests.Utils; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Newtonsoft.Json.Linq; + +namespace Bicep.Core.UnitTests.Emit; + +[TestClass] +public class ParameterAssignmentEvaluatorTests +{ + [TestMethod] + public void BuildParams_ForExpressionVariable_EvaluatesToValue() + { + var services = new ServiceBuilder().WithEmptyAzResources(); + + var result = CompilationHelper.CompileParams( + services, + ("main.bicep", "param p int[]"), + ("parameters.bicepparam", """ + using 'main.bicep' + + var x = [for item in [1, 2]: item * 2] + param p = x + """)); + + result.Should().NotHaveAnyDiagnostics(); + result.Parameters.Should().HaveValueAtPath("parameters.p.value", JToken.Parse("[2, 4]")); + } +} diff --git a/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs b/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs index ff4b246d06d..7eb561a57c6 100644 --- a/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs +++ b/src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs @@ -25,6 +25,24 @@ namespace Bicep.Core.Emit; public class ParameterAssignmentEvaluator { + private sealed class ForLoopIndexRewriter : ExpressionRewriteVisitor + { + private readonly long index; + + private ForLoopIndexRewriter(long index) + { + this.index = index; + } + + public static Expression Rewrite(Expression expression, long index) + { + return new ForLoopIndexRewriter(index).Replace(expression); + } + + public override Expression ReplaceCopyIndexExpression(CopyIndexExpression expression) => new IntegerLiteralExpression(expression.SourceSyntax, index); + + public override Expression ReplaceForLoopExpression(ForLoopExpression expression) => expression; + } private class ParameterAssignmentEvaluationContext : IEvaluationContext { private readonly TemplateExpressionEvaluationHelper evaluationHelper; @@ -419,7 +437,7 @@ public Result EvaluateParameter(ParameterAssignmentSymbol parameter) try { - return Result.For(parameterConverter.ConvertExpression(intermediate).EvaluateExpression(context)); + return Result.For(EvaluateExpression(parameterConverter, intermediate, context)); } catch (Exception ex) { @@ -478,7 +496,7 @@ public ImmutableDictionary EvaluateExtensionConfigAssignment(Ext { try { - propertyResult = Result.For(converter.ConvertExpression(intermediate).EvaluateExpression(context)); + propertyResult = Result.For(EvaluateExpression(converter, intermediate, context)); } catch (Exception ex) { @@ -542,7 +560,7 @@ private Result EvaluateVariable(VariableSymbol variable) var variableConverter = GetConverterForVariable(variable); var intermediate = variableConverter.ConvertToIntermediateExpression(variable.DeclaringVariable.Value); - return Result.For(variableConverter.ConvertExpression(intermediate).EvaluateExpression(context)); + return Result.For(EvaluateExpression(variableConverter, intermediate, context)); } catch (Exception ex) { @@ -560,7 +578,7 @@ private Result EvaluateSynthesizeVariableExpression(string name, Expression expr { var evalContext = GetExpressionEvaluationContextForModel(model); var exprConverter = converterCache.GetOrAdd(model, m => new ExpressionConverter(new EmitterContext(m))); - return Result.For(exprConverter.ConvertExpression(expression).EvaluateExpression(evalContext)); + return Result.For(EvaluateExpression(exprConverter, expression, evalContext)); } catch (Exception e) { @@ -790,7 +808,7 @@ public ExpressionEvaluationResult EvaluateExpression(SyntaxBase expressionSyntax { var context = GetExpressionEvaluationContext(); var intermediate = converter.ConvertToIntermediateExpression(expressionSyntax); - var result = converter.ConvertExpression(intermediate).EvaluateExpression(context); + var result = EvaluateExpression(converter, intermediate, context); return ExpressionEvaluationResult.For(result); } catch (Exception ex) @@ -800,6 +818,33 @@ public ExpressionEvaluationResult EvaluateExpression(SyntaxBase expressionSyntax } } + private JToken EvaluateExpression(ExpressionConverter expressionConverter, Expression expression, ParameterAssignmentEvaluationContext context) + { + if (expression is ForLoopExpression forLoop) + { + return EvaluateForLoopExpression(expressionConverter, forLoop, context); + } + + return expressionConverter.ConvertExpression(expression).EvaluateExpression(context); + } + + private JToken EvaluateForLoopExpression(ExpressionConverter expressionConverter, ForLoopExpression forLoop, ParameterAssignmentEvaluationContext context) + { + var source = EvaluateExpression(expressionConverter, forLoop.Expression, context); + if (source is not JArray sourceArray) + { + throw new InvalidOperationException("For-expression source must be an array."); + } + + var results = new JArray(); + for (var i = 0; i < sourceArray.Count; i++) + { + results.Add(EvaluateExpression(expressionConverter, ForLoopIndexRewriter.Rewrite(forLoop.Body, i), context)); + } + + return results; + } + /// /// Rewrites the external input function calls to use the externalInputs function with the index of the external input. /// e.g. externalInput('sys.cli', 'foo') becomes externalInputs('0')