Skip to content

Commit

Permalink
Allow function imports in .bicepparam files (#13459)
Browse files Browse the repository at this point in the history
  • Loading branch information
anthony-c-martin committed Feb 27, 2024
1 parent f316152 commit 98d54b4
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 5 deletions.
38 changes: 38 additions & 0 deletions src/Bicep.Core.IntegrationTests/ScenarioTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5626,6 +5626,44 @@ public void Test_Issue12908()
});
}

[TestMethod]
public void Functions_can_be_imported_in_bicepparam_files()
{
var result = CompilationHelper.CompileParams(
("parameters.bicepparam", """
using 'main.bicep'

import * as func from 'func.bicep'
import { greetMultiple } from 'func.bicep'

param foo = func.greet('Anthony')
param foo2 = greetMultiple(['Evie', 'Casper'])
"""),
("func.bicep", """
@export()
@description('Say hi to someone')
func greet(name string) string => 'Hi, ${name}!'

@export()
func greetMultiple(names string[]) string[] => map(names, name => greet(name))
"""),
("main.bicep", """
param foo string
param foo2 string[]
"""),
("bicepconfig.json", """
{
"experimentalFeaturesEnabled": {
"userDefinedFunctions": true
}
}
"""));

result.Should().NotHaveAnyDiagnostics();
result.Parameters.Should().HaveValueAtPath("parameters.foo.value", "Hi, Anthony!");
result.Parameters.Should().HaveValueAtPath("parameters.foo2.value", JToken.Parse("""["Hi, Evie!", "Hi, Casper!"]"""));
}

// https://github.com/Azure/bicep/issues/12347
[TestMethod]
public void Test_Issue12347()
Expand Down
72 changes: 68 additions & 4 deletions src/Bicep.Core/Emit/ParameterAssignmentEvaluator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@
using System.Diagnostics;
using Azure.Deployments.Core.Definitions.Schema;
using Azure.Deployments.Core.Diagnostics;
using Azure.Deployments.Core.ErrorResponses;
using Azure.Deployments.Expression.Expressions;
using Azure.Deployments.Templates.Engines;
using Azure.Deployments.Templates.Expressions;
using Azure.Deployments.Templates.Extensions;
using Bicep.Core.Diagnostics;
using Bicep.Core.Emit.CompileTimeImports;
using Bicep.Core.Intermediate;
using Bicep.Core.Semantics;
using Bicep.Core.Semantics.Metadata;
using Microsoft.WindowsAzure.ResourceStack.Common.Extensions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

Expand Down Expand Up @@ -49,7 +52,7 @@ private Result(JToken? value, ParameterKeyVaultReferenceExpression? keyVaultRefe
private readonly ConcurrentDictionary<SemanticModel, ResultWithDiagnostic<Template>> templateResults = new();
private readonly ImmutableDictionary<string, ParameterAssignmentSymbol> paramsByName;
private readonly ImmutableDictionary<string, VariableSymbol> variablesByName;
private readonly ImmutableDictionary<string, ImportedVariableSymbol> importsByName;
private readonly ImmutableDictionary<string, ImportedSymbol> importsByName;
private readonly ImmutableDictionary<string, WildcardImportPropertyReference> wildcardImportPropertiesByName;
private readonly ImmutableDictionary<string, Expression> synthesizedVariableValuesByName;
private readonly ExpressionConverter converter;
Expand All @@ -66,7 +69,6 @@ public ParameterAssignmentEvaluator(SemanticModel model)
EmitterContext context = new(model);
this.converter = new(context);
this.importsByName = context.ImportClosureInfo.ImportedSymbolNames.Keys
.OfType<ImportedVariableSymbol>()
.Select(importedVariable => (context.ImportClosureInfo.ImportedSymbolNames[importedVariable], importedVariable))
.GroupBy(x => x.Item1, LanguageConstants.IdentifierComparer)
.ToImmutableDictionary(x => x.Key, x => x.First().importedVariable, LanguageConstants.IdentifierComparer);
Expand Down Expand Up @@ -137,6 +139,16 @@ private Result EvaluateSynthesizeVariableExpression(string name, Expression expr
}
});

private ResultWithDiagnostic<Template> GetTemplateWithCaching(ISemanticModel model)
=> model switch
{
SemanticModel bicepModel => templateResults.GetOrAdd(bicepModel, GetTemplate),
ArmTemplateSemanticModel armModel when armModel.SourceFile.Template is Template template => new(template),
TemplateSpecSemanticModel tsModel when tsModel.SourceFile.MainTemplateFile.Template is Template template => new(template),
ArmTemplateSemanticModel or TemplateSpecSemanticModel => new(x => x.ReferencedArmTemplateHasErrors()),
_ => throw new UnreachableException(),
};

private Result EvaluateImport(ImportedVariableSymbol import) => importResults.GetOrAdd(import, import =>
{
if (import.Kind == SymbolKind.Variable)
Expand Down Expand Up @@ -251,9 +263,9 @@ private ExpressionEvaluationContext GetExpressionEvaluationContext()
return EvaluateVariable(variable).Value ?? throw new InvalidOperationException($"Variable {name} has an invalid value");
}
if (importsByName.TryGetValue(name, out var imported))
if (importsByName.TryGetValue(name, out var imported) && imported is ImportedVariableSymbol importedVariable)
{
return EvaluateImport(imported).Value ?? throw new InvalidOperationException($"Imported variable {name} has an invalid value");
return EvaluateImport(importedVariable).Value ?? throw new InvalidOperationException($"Imported variable {name} has an invalid value");
}
if (wildcardImportPropertiesByName.TryGetValue(name, out var wildcardImportProperty))
Expand All @@ -275,9 +287,61 @@ private ExpressionEvaluationContext GetExpressionEvaluationContext()
return EvaluateParameter(paramsByName[name]).Value ?? throw new InvalidOperationException($"Parameter {name} has an invalid value");
};

var defaultEvaluateFunction = helper.EvaluationContext.EvaluateFunction;
helper.EvaluationContext.EvaluateFunction = (expression, parameters, additionalProperties) => {
if (TemplateFunction.IsTemplateFunction(expression.Function))
{
return EvaluateTemplateFunction(expression, parameters, additionalProperties);
}
return defaultEvaluateFunction(expression, parameters, additionalProperties);
};

return helper.EvaluationContext;
}

private JToken EvaluateTemplateFunction(FunctionExpression expression, FunctionArgument[] parameters, TemplateErrorAdditionalInfo additionalProperties)
{
JToken evaluateFunction(Template template, string originalFunctionName)
{
var functionsLookup = template.GetFunctionDefinitions().ToOrdinalInsensitiveDictionary(x => x.Key, x => x.Function);

var rewrittenExpression = new FunctionExpression(
$"{EmitConstants.UserDefinedFunctionsNamespace}.{originalFunctionName}",
expression.Parameters,
expression.Properties);

// we must explicitly ensure the evaluation takes place in the context of the referenced template,
// so that accessing scoped functions works as expected
var helper = new TemplateExpressionEvaluationHelper
{
OnGetFunction = (name, _) => functionsLookup[name],
ValidationContext = SchemaValidationContext.ForTemplate(template),
};

return helper.EvaluationContext.EvaluateFunction(rewrittenExpression, parameters, additionalProperties);
}

if (expression.Function.StartsWith($"{EmitConstants.UserDefinedFunctionsNamespace}.") &&
expression.Function.Substring($"{EmitConstants.UserDefinedFunctionsNamespace}.".Length) is {} functionName &&
importsByName.TryGetValue(functionName, out var imported) &&
imported.OriginalSymbolName is {} originalSymbolName)
{
var template = GetTemplateWithCaching(imported.SourceModel).Unwrap();

return evaluateFunction(template, originalSymbolName);
}

if (wildcardImportPropertiesByName.TryGetValue(expression.Function, out var wildcardImportProperty))
{
var template = GetTemplateWithCaching(wildcardImportProperty.WildcardImport.SourceModel).Unwrap();

return evaluateFunction(template, wildcardImportProperty.PropertyName);
}

throw new InvalidOperationException($"Function {expression.Function} not found");
}

private static ResultWithDiagnostic<Template> GetTemplate(SemanticModel model)
{
if (model.HasErrors())
Expand Down
3 changes: 2 additions & 1 deletion src/Bicep.Core/Semantics/ImportedSymbol.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@ ExportMetadataKind.Variable or
},
BicepParamFile => ExportMetadata.Kind switch
{
ExportMetadataKind.Variable => true,
ExportMetadataKind.Variable or
ExportMetadataKind.Function => true,
_ => false,
},
_ => false,
Expand Down

0 comments on commit 98d54b4

Please sign in to comment.