-
Notifications
You must be signed in to change notification settings - Fork 759
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Support for user defined functions (#10465)
See https://github.com/Azure/bicep/blob/ant/exp/func/src/Bicep.Core.Samples/Files/Functions_LF/main.bicep for a syntax example. Notes/limitations: * UDFs in JSON require declaring the return type, so I've modified the spec to require the user to declare the return type. * User-defined types in UDF params & outputs are unsupported. * UDFs are currently quite limited - they can't call UDFs, or access outer scoped variables. * This is currently behind an experimental feature flag ("userDefinedFunctions") Closes #447 Closes #9239 ###### Microsoft Reviewers: [Open in CodeFlow](https://portal.fabricbot.ms/api/codeflow?pullrequest=https://github.com/Azure/bicep/pull/10465)
- Loading branch information
1 parent
adeab53
commit 23ebbef
Showing
117 changed files
with
3,439 additions
and
230 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
129 changes: 129 additions & 0 deletions
129
src/Bicep.Core.IntegrationTests/UserDefinedFunctionTests.cs
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
// Copyright (c) Microsoft Corporation. | ||
// Licensed under the MIT License. | ||
using System.Diagnostics.CodeAnalysis; | ||
using Bicep.Core.Diagnostics; | ||
using Bicep.Core.UnitTests; | ||
using Bicep.Core.UnitTests.Assertions; | ||
using Bicep.Core.UnitTests.Utils; | ||
using Microsoft.VisualStudio.TestTools.UnitTesting; | ||
|
||
namespace Bicep.Core.IntegrationTests; | ||
|
||
[TestClass] | ||
public class UserDefinedFunctionTests | ||
{ | ||
private ServiceBuilder Services => new ServiceBuilder() | ||
.WithFeatureOverrides(new(TestContext, UserDefinedFunctionsEnabled: true)); | ||
|
||
[NotNull] public TestContext? TestContext { get; set; } | ||
|
||
[TestMethod] | ||
public void User_defined_functions_basic_case() | ||
{ | ||
var result = CompilationHelper.Compile(Services, @" | ||
func buildUrl(https bool, hostname string, path string) string => '${https ? 'https' : 'http'}://${hostname}${empty(path) ? '' : '/${path}'}' | ||
output foo string = buildUrl(true, 'google.com', 'search') | ||
"); | ||
|
||
result.Should().NotHaveAnyDiagnostics(); | ||
var evaluated = TemplateEvaluator.Evaluate(result.Template); | ||
|
||
evaluated.Should().HaveValueAtPath("$.outputs['foo'].value", "https://google.com/search"); | ||
} | ||
|
||
[TestMethod] | ||
public void Outer_scope_symbolic_references_are_blocked() | ||
{ | ||
var result = CompilationHelper.Compile(Services, @" | ||
param foo string | ||
var bar = 'abc' | ||
func getBaz() string => 'baz' | ||
func testFunc(baz string) string => '${foo}-${bar}-${baz}-${getBaz()}' | ||
"); | ||
|
||
result.ExcludingLinterDiagnostics().Should().HaveDiagnostics(new [] { | ||
("BCP057", DiagnosticLevel.Error, """The name "foo" does not exist in the current context."""), | ||
("BCP057", DiagnosticLevel.Error, """The name "bar" does not exist in the current context."""), | ||
("BCP057", DiagnosticLevel.Error, """The name "getBaz" does not exist in the current context."""), | ||
}); | ||
} | ||
|
||
[TestMethod] | ||
public void Functions_can_have_descriptions_applied() | ||
{ | ||
var result = CompilationHelper.Compile(Services, @" | ||
@description('Returns foo') | ||
func returnFoo() string => 'foo' | ||
output outputFoo string = returnFoo() | ||
"); | ||
|
||
result.Should().NotHaveAnyDiagnostics(); | ||
var evaluated = TemplateEvaluator.Evaluate(result.Template); | ||
|
||
evaluated.Should().HaveValueAtPath("$.outputs['outputFoo'].value", "foo"); | ||
} | ||
|
||
[TestMethod] | ||
public void User_defined_functions_cannot_reference_each_other() | ||
{ | ||
var result = CompilationHelper.Compile(Services, @" | ||
func getAbc() string => 'abc' | ||
func getAbcDef() string => '${getAbc()}def' | ||
"); | ||
|
||
result.Should().HaveDiagnostics(new [] { | ||
("BCP057", DiagnosticLevel.Error, "The name \"getAbc\" does not exist in the current context."), | ||
}); | ||
} | ||
|
||
[TestMethod] | ||
public void User_defined_functions_unsupported_custom_types() | ||
{ | ||
var services = new ServiceBuilder().WithFeatureOverrides(new(UserDefinedTypesEnabled: true, UserDefinedFunctionsEnabled: true)); | ||
|
||
var result = CompilationHelper.Compile(services, @" | ||
func getAOrB(aOrB ('a' | 'b')) bool => (aOrB == 'a') | ||
"); | ||
|
||
result.Should().HaveDiagnostics(new [] { | ||
("BCP342", DiagnosticLevel.Error, "User-defined types are not supported in user-defined function parameters or outputs."), | ||
}); | ||
|
||
result = CompilationHelper.Compile(services, @" | ||
func getAOrB(aOrB bool) ('a' | 'b') => aOrB ? 'a' : 'b' | ||
"); | ||
|
||
result.Should().HaveDiagnostics(new [] { | ||
("BCP342", DiagnosticLevel.Error, "User-defined types are not supported in user-defined function parameters or outputs."), | ||
}); | ||
} | ||
|
||
[TestMethod] | ||
public void User_defined_functions_unsupported_runtime_functions() | ||
{ | ||
var result = CompilationHelper.Compile(Services, @" | ||
func useRuntimeFunction() string => reference('foo').bar | ||
"); | ||
|
||
result.Should().HaveDiagnostics(new [] { | ||
("BCP341", DiagnosticLevel.Error, "This expression is being used inside a function declaration, which requires a value that can be calculated at the start of the deployment."), | ||
}); | ||
} | ||
|
||
[TestMethod] | ||
public void User_defined_functions_requires_experimental_feature_enabled() | ||
{ | ||
var services = new ServiceBuilder().WithFeatureOverrides(new()); | ||
|
||
var result = CompilationHelper.Compile(services, @" | ||
func useRuntimeFunction() string => 'test' | ||
"); | ||
|
||
result.Should().HaveDiagnostics(new [] { | ||
("BCP343", DiagnosticLevel.Error, "Using a func declaration statement requires enabling EXPERIMENTAL feature \"UserDefinedFunctions\"."), | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
{ | ||
"experimentalFeaturesEnabled": { | ||
"userDefinedFunctions": true | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
func buildUrl(https bool, hostname string, path string) string => '${https ? 'https' : 'http'}://${hostname}${empty(path) ? '' : '/${path}'}' | ||
|
||
output foo string = buildUrl(true, 'google.com', 'search') | ||
|
||
func sayHello(name string) string => 'Hi ${name}!' | ||
|
||
output hellos array = map(['Evie', 'Casper'], name => sayHello(name)) | ||
|
||
func objReturnType(name string) object => { | ||
hello: 'Hi ${name}!' | ||
} | ||
|
||
func arrayReturnType(name string) array => [ | ||
name | ||
] | ||
|
||
func asdf(name string) array => [ | ||
'asdf' | ||
name | ||
] |
21 changes: 21 additions & 0 deletions
21
src/Bicep.Core.Samples/Files/Functions_LF/main.diagnostics.bicep
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
func buildUrl(https bool, hostname string, path string) string => '${https ? 'https' : 'http'}://${hostname}${empty(path) ? '' : '/${path}'}' | ||
|
||
output foo string = buildUrl(true, 'google.com', 'search') | ||
|
||
func sayHello(name string) string => 'Hi ${name}!' | ||
|
||
output hellos array = map(['Evie', 'Casper'], name => sayHello(name)) | ||
|
||
func objReturnType(name string) object => { | ||
hello: 'Hi ${name}!' | ||
} | ||
|
||
func arrayReturnType(name string) array => [ | ||
name | ||
] | ||
|
||
func asdf(name string) array => [ | ||
'asdf' | ||
name | ||
] | ||
|
20 changes: 20 additions & 0 deletions
20
src/Bicep.Core.Samples/Files/Functions_LF/main.formatted.bicep
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
func buildUrl(https bool, hostname string, path string) string => '${https ? 'https' : 'http'}://${hostname}${empty(path) ? '' : '/${path}'}' | ||
|
||
output foo string = buildUrl(true, 'google.com', 'search') | ||
|
||
func sayHello(name string) string => 'Hi ${name}!' | ||
|
||
output hellos array = map([ 'Evie', 'Casper' ], name => sayHello(name)) | ||
|
||
func objReturnType(name string) object => { | ||
hello: 'Hi ${name}!' | ||
} | ||
|
||
func arrayReturnType(name string) array => [ | ||
name | ||
] | ||
|
||
func asdf(name string) array => [ | ||
'asdf' | ||
name | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,69 @@ | ||
func buildUrl(https bool, hostname string, path string) string => '${https ? 'https' : 'http'}://${hostname}${empty(path) ? '' : '/${path}'}' | ||
//@[000:503) ProgramExpression | ||
//@[000:141) ├─DeclaredFunctionExpression { Name = buildUrl } | ||
//@[013:141) | └─LambdaExpression | ||
//@[066:141) | └─InterpolatedStringExpression | ||
//@[069:093) | ├─TernaryExpression | ||
//@[069:074) | | ├─LambdaVariableReferenceExpression { Variable = https } | ||
//@[077:084) | | ├─StringLiteralExpression { Value = https } | ||
//@[087:093) | | └─StringLiteralExpression { Value = http } | ||
//@[099:107) | ├─LambdaVariableReferenceExpression { Variable = hostname } | ||
//@[110:139) | └─TernaryExpression | ||
//@[110:121) | ├─FunctionCallExpression { Name = empty } | ||
//@[116:120) | | └─LambdaVariableReferenceExpression { Variable = path } | ||
//@[124:126) | ├─StringLiteralExpression { Value = } | ||
//@[129:139) | └─InterpolatedStringExpression | ||
//@[133:137) | └─LambdaVariableReferenceExpression { Variable = path } | ||
|
||
output foo string = buildUrl(true, 'google.com', 'search') | ||
//@[000:058) ├─DeclaredOutputExpression { Name = foo } | ||
//@[020:058) | └─UserDefinedFunctionCallExpression { Name = buildUrl } | ||
//@[029:033) | ├─BooleanLiteralExpression { Value = True } | ||
//@[035:047) | ├─StringLiteralExpression { Value = google.com } | ||
//@[049:057) | └─StringLiteralExpression { Value = search } | ||
|
||
func sayHello(name string) string => 'Hi ${name}!' | ||
//@[000:050) ├─DeclaredFunctionExpression { Name = sayHello } | ||
//@[013:050) | └─LambdaExpression | ||
//@[037:050) | └─InterpolatedStringExpression | ||
//@[043:047) | └─LambdaVariableReferenceExpression { Variable = name } | ||
|
||
output hellos array = map(['Evie', 'Casper'], name => sayHello(name)) | ||
//@[000:069) └─DeclaredOutputExpression { Name = hellos } | ||
//@[022:069) └─FunctionCallExpression { Name = map } | ||
//@[026:044) ├─ArrayExpression | ||
//@[027:033) | ├─StringLiteralExpression { Value = Evie } | ||
//@[035:043) | └─StringLiteralExpression { Value = Casper } | ||
//@[046:068) └─LambdaExpression | ||
//@[054:068) └─UserDefinedFunctionCallExpression { Name = sayHello } | ||
//@[063:067) └─LambdaVariableReferenceExpression { Variable = name } | ||
|
||
func objReturnType(name string) object => { | ||
//@[000:068) ├─DeclaredFunctionExpression { Name = objReturnType } | ||
//@[018:068) | └─LambdaExpression | ||
//@[042:068) | └─ObjectExpression | ||
hello: 'Hi ${name}!' | ||
//@[002:022) | └─ObjectPropertyExpression | ||
//@[002:007) | ├─StringLiteralExpression { Value = hello } | ||
//@[009:022) | └─InterpolatedStringExpression | ||
//@[015:019) | └─LambdaVariableReferenceExpression { Variable = name } | ||
} | ||
|
||
func arrayReturnType(name string) array => [ | ||
//@[000:053) ├─DeclaredFunctionExpression { Name = arrayReturnType } | ||
//@[020:053) | └─LambdaExpression | ||
//@[043:053) | └─ArrayExpression | ||
name | ||
//@[002:006) | └─LambdaVariableReferenceExpression { Variable = name } | ||
] | ||
|
||
func asdf(name string) array => [ | ||
//@[000:051) ├─DeclaredFunctionExpression { Name = asdf } | ||
//@[009:051) | └─LambdaExpression | ||
//@[032:051) | └─ArrayExpression | ||
'asdf' | ||
//@[002:008) | ├─StringLiteralExpression { Value = asdf } | ||
name | ||
//@[002:006) | └─LambdaVariableReferenceExpression { Variable = name } | ||
] | ||
|
Oops, something went wrong.