Skip to content

Commit b63f44f

Browse files
authored
External input bug fixes (#19256)
## Description - Handle fully-evaluable variable, params, and UDFs (including imports) references used within parameter assignments containing external inputs - Do not crash when analyzing external inputs if there are parsing errors (e.g. invalid external input syntax) For #19228, #19226, #19212 ## 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/19256)
1 parent 171a4f9 commit b63f44f

19 files changed

+1202
-31
lines changed

src/Bicep.Core.IntegrationTests/ParameterFileTests.cs

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -344,6 +344,94 @@ public void ExternalInput_parameter_with_variable_references_compiles_successful
344344
});
345345
}
346346

347+
[TestMethod]
348+
public void ExternalInput_parameter_with_non_external_input_variable_references_compiles_successfully()
349+
{
350+
var result = CompilationHelper.CompileParams(
351+
("parameters.bicepparam", @"
352+
using none
353+
var foo = 'foo'
354+
param foo2 = '${foo}-${externalInput('sys.cli', 'foo2')}'
355+
"));
356+
357+
result.Should().NotHaveAnyDiagnostics();
358+
var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
359+
parameters["foo2"].Value.Should().BeNull();
360+
parameters["foo2"].Expression.Should().DeepEqual("""[format('{0}-{1}', 'foo', externalInputs('sys_cli_0'))]""");
361+
362+
var externalInputs = TemplateHelper.ConvertAndAssertExternalInputs(result.Parameters);
363+
externalInputs["sys_cli_0"].Should().DeepEqual(new JObject
364+
{
365+
["kind"] = "sys.cli",
366+
["config"] = "foo2",
367+
});
368+
}
369+
370+
[TestMethod]
371+
public void ExternalInput_parameter_with_unevaluable_imported_variable_references_returns_diagnostic()
372+
{
373+
var result = CompilationHelper.CompileParams(
374+
("parameters.bicepparam", @"
375+
using none
376+
import { foo } from 'main.bicep'
377+
param foo2 = '${foo}-${externalInput('sys.cli', 'foo2')}'
378+
"),
379+
("main.bicep", @"
380+
@export()
381+
var foo = resourceGroup().location // cannot be evaluated in bicepparam file
382+
"));
383+
result.Should().OnlyContainDiagnostic(
384+
"BCP338",
385+
DiagnosticLevel.Error,
386+
"Failed to evaluate parameter \"foo2\": Failed to evaluate variable \"foo\": The template function 'RESOURCEGROUP' is not expected at this location. Please see https://aka.ms/arm-functions for usage details.");
387+
}
388+
389+
[TestMethod]
390+
public void ExternalInput_parameter_with_imported_function_compiles_successfully()
391+
{
392+
var result = CompilationHelper.CompileParams(
393+
("parameters.bicepparam", @"
394+
using none
395+
import { foo } from 'main.bicep'
396+
param foo2 = '${foo()}-${externalInput('sys.cli', 'foo2')}'
397+
"),
398+
("main.bicep", @"
399+
@export()
400+
func foo() string => 'Hello foo'
401+
"));
402+
403+
result.Should().NotHaveAnyDiagnostics();
404+
var parameters = TemplateHelper.ConvertAndAssertParameters(result.Parameters);
405+
parameters["foo2"].Value.Should().BeNull();
406+
parameters["foo2"].Expression.Should().DeepEqual("""[format('{0}-{1}', 'Hello foo', externalInputs('sys_cli_0'))]""");
407+
408+
var externalInputs = TemplateHelper.ConvertAndAssertExternalInputs(result.Parameters);
409+
externalInputs["sys_cli_0"].Should().DeepEqual(new JObject
410+
{
411+
["kind"] = "sys.cli",
412+
["config"] = "foo2",
413+
});
414+
}
415+
416+
[TestMethod]
417+
public void ExternalInput_parameter_with_unevaluable_imported_function_returns_diagnostics()
418+
{
419+
var result = CompilationHelper.CompileParams(
420+
("parameters.bicepparam", @"
421+
using none
422+
import { foo } from 'main.bicep'
423+
param foo2 = '${foo()}-${externalInput('sys.cli', 'foo2')}'
424+
"),
425+
("main.bicep", @"
426+
@export()
427+
func foo() string => resourceGroup().location // cannot be evaluated in bicepparam file
428+
"));
429+
result.Should().OnlyContainDiagnostic(
430+
"BCP338",
431+
DiagnosticLevel.Error,
432+
"Failed to evaluate parameter \"foo2\": The template function 'RESOURCEGROUP' is not expected at this location. Please see https://aka.ms/arm-functions for usage details.");
433+
}
434+
347435
[TestMethod]
348436
public void ExternalInput_parameter_with_param_references_compiles_successfully()
349437
{

src/Bicep.Core.IntegrationTests/ScenarioTests.cs

Lines changed: 180 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7571,7 +7571,7 @@ public void Test_Issue18520()
75717571
secureStrings: [for i in range(0, 10): mod[i]!.outputs.foo]
75727572
}
75737573
}
7574-
7574+
75757575
"""),
75767576
("mod.bicep", """
75777577
@secure()
@@ -7636,8 +7636,8 @@ public void Syntactically_nested_existing_resources_with_explicit_dependencies_s
76367636
]
76377637
}
76387638
}
7639-
7640-
7639+
7640+
76417641
resource c 'Microsoft.Network/dnsZones@2018-05-01' = {
76427642
name: 'zone'
76437643
location: resourceGroup().location
@@ -7648,4 +7648,181 @@ public void Syntactically_nested_existing_resources_with_explicit_dependencies_s
76487648
result.Template.Should().NotBeNull();
76497649
result.Template.Should().HaveValueAtPath("languageVersion", "2.0");
76507650
}
7651+
7652+
[TestMethod]
7653+
public void Test_Issue19226_externalInputs_with_variable_references()
7654+
{
7655+
var result = CompilationHelper.CompileParams(
7656+
("main.bicep", """
7657+
param foo object[]
7658+
"""),
7659+
("import.bicep", """
7660+
@export()
7661+
type KeyVaultAccessPolicyPermissions = {
7662+
keys: string[]
7663+
secrets: string[]
7664+
certificates: string[]
7665+
storage: string[]
7666+
}
7667+
7668+
@export()
7669+
var KeyVaultFullAccessPermissions KeyVaultAccessPolicyPermissions = {
7670+
keys: [
7671+
'get'
7672+
'delete'
7673+
'list'
7674+
'create'
7675+
'import'
7676+
'update'
7677+
'recover'
7678+
'backup'
7679+
'restore'
7680+
'sign'
7681+
'verify'
7682+
'wrapKey'
7683+
'unwrapKey'
7684+
'encrypt'
7685+
'decrypt'
7686+
'purge'
7687+
]
7688+
secrets: [
7689+
'get'
7690+
'delete'
7691+
'list'
7692+
'set'
7693+
'recover'
7694+
'backup'
7695+
'restore'
7696+
'purge'
7697+
]
7698+
certificates: [
7699+
'get'
7700+
'delete'
7701+
'list'
7702+
'create'
7703+
'import'
7704+
'update'
7705+
'deleteissuers'
7706+
'getissuers'
7707+
'listissuers'
7708+
'managecontacts'
7709+
'manageissuers'
7710+
'setissuers'
7711+
'recover'
7712+
'purge'
7713+
]
7714+
storage: [
7715+
'delete'
7716+
'deletesas'
7717+
'get'
7718+
'getsas'
7719+
'list'
7720+
'listsas'
7721+
'regeneratekey'
7722+
'set'
7723+
'setsas'
7724+
'update'
7725+
'recover'
7726+
'backup'
7727+
'restore'
7728+
'purge'
7729+
]
7730+
}
7731+
"""),
7732+
("parameters.bicepparam", """
7733+
using 'main.bicep'
7734+
7735+
import { KeyVaultFullAccessPermissions } from 'import.bicep'
7736+
7737+
var principals = externalInput('t', 'c')
7738+
7739+
param foo = [
7740+
...map(principals, objectId => {
7741+
objectId: objectId
7742+
permissions: KeyVaultFullAccessPermissions
7743+
})
7744+
]
7745+
"""));
7746+
7747+
result.Should().NotHaveAnyDiagnostics();
7748+
result.Parameters.Should().HaveValueAtPath(
7749+
"parameters.foo.expression",
7750+
"[flatten(createArray(map(externalInputs('t_0'), lambda('objectId', createObject('objectId', lambdaVariables('objectId'), 'permissions', createObject('keys', createArray('get', 'delete', 'list', 'create', 'import', 'update', 'recover', 'backup', 'restore', 'sign', 'verify', 'wrapKey', 'unwrapKey', 'encrypt', 'decrypt', 'purge'), 'secrets', createArray('get', 'delete', 'list', 'set', 'recover', 'backup', 'restore', 'purge'), 'certificates', createArray('get', 'delete', 'list', 'create', 'import', 'update', 'deleteissuers', 'getissuers', 'listissuers', 'managecontacts', 'manageissuers', 'setissuers', 'recover', 'purge'), 'storage', createArray('delete', 'deletesas', 'get', 'getsas', 'list', 'listsas', 'regeneratekey', 'set', 'setsas', 'update', 'recover', 'backup', 'restore', 'purge')))))))]");
7751+
7752+
result.Parameters.Should().HaveJsonAtPath(
7753+
"externalInputDefinitions",
7754+
"""
7755+
{
7756+
"t_0": {
7757+
"kind": "t",
7758+
"config": "c"
7759+
}
7760+
}
7761+
"""
7762+
);
7763+
}
7764+
7765+
[TestMethod]
7766+
public void Test_Issue19212_externalInputs_with_variables()
7767+
{
7768+
var result = CompilationHelper.CompileParams(
7769+
("parameters.bicepparam", """
7770+
using none
7771+
7772+
var example = 'example'
7773+
7774+
param varWithLiteral = [
7775+
example
7776+
'literal'
7777+
]
7778+
7779+
param varWithExternalInput = [
7780+
example
7781+
externalInput('foo.bar', 'baz')
7782+
]
7783+
"""));
7784+
7785+
result.Should().NotHaveAnyDiagnostics();
7786+
result.Parameters.Should().HaveJsonAtPath(
7787+
"parameters.varWithLiteral.value",
7788+
"""
7789+
[
7790+
"example",
7791+
"literal"
7792+
]
7793+
""");
7794+
result.Parameters.Should().HaveValueAtPath(
7795+
"parameters.varWithExternalInput.expression",
7796+
"[createArray('example', externalInputs('foo_bar_0'))]");
7797+
7798+
result.Parameters.Should().HaveJsonAtPath(
7799+
"externalInputDefinitions",
7800+
"""
7801+
{
7802+
"foo_bar_0": {
7803+
"kind": "foo.bar",
7804+
"config": "baz"
7805+
}
7806+
}
7807+
"""
7808+
);
7809+
}
7810+
7811+
[TestMethod]
7812+
public void Test_Issue19228_externalInputs_analysis_should_not_cause_crash_for_invalid_syntax()
7813+
{
7814+
var result = CompilationHelper.CompileParams(
7815+
("parameters.bicepparam", """
7816+
using 'main.bicep'
7817+
7818+
param foo = externalInput('t',)
7819+
"""),
7820+
("main.bicep", """
7821+
param foo string
7822+
"""));
7823+
result.Should().OnlyContainDiagnostic(
7824+
"BCP009",
7825+
DiagnosticLevel.Error,
7826+
"Expected a literal value, an array, an object, a parenthesized expression, or a function call at this location.");
7827+
}
76517828
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
@export()
2+
var person = {
3+
name: 'Alice'
4+
age: 42
5+
}
6+
7+
@export()
8+
func getPerson(name string, age int) {
9+
name: string
10+
age: int
11+
} => {
12+
name: name
13+
age: age
14+
}
15+
16+
@export()
17+
func getDefaultPerson() {
18+
name: string
19+
age: int
20+
} => getPerson('John Doe', 0)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
@export()
2+
var vmConfig VmConfig = {
3+
name: 'beefyVM'
4+
size: 'Standard_D2s_v7'
5+
}
6+
7+
@export()
8+
var storageConfig StorageConfig = {
9+
name: 'beefyStorage'
10+
sku: 'Standard_ZRS'
11+
}
12+
13+
14+
type StorageConfig = {
15+
name: string
16+
sku: string
17+
}
18+
19+
type VmConfig = {
20+
name: string
21+
size: string
22+
}
23+
24+
@export()
25+
type InfraConfig = {
26+
storage: StorageConfig
27+
vm: VmConfig
28+
tag: string
29+
}

src/Bicep.Core.Samples/Files/baselines_bicepparam/External_Inputs/parameters.bicepparam

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,3 +50,35 @@ param objectBody = {
5050
bar: externalInput('custom.binding', 'bar')
5151
baz: 'blah'
5252
}
53+
54+
var poodle = 'toy'
55+
param retriever = 'golden'
56+
param concat = '${poodle}-${retriever}-${externalInput('sys.cli', 'foo')}'
57+
58+
import * as main2 from 'main2.bicep'
59+
import { person, getPerson, getDefaultPerson } from 'main.bicep'
60+
61+
param principalIds = externalInput('sys.cli', 'principalIds')
62+
63+
var anotherPerson = {
64+
name: 'John'
65+
age: 21
66+
}
67+
param varPeople = [
68+
...map(principalIds, id => {
69+
objectId: id
70+
})
71+
person
72+
anotherPerson
73+
getPerson('Bob', 30)
74+
getDefaultPerson()
75+
externalInput('custom.binding', 'foo')
76+
]
77+
78+
var infra main2.InfraConfig = {
79+
storage: main2.storageConfig
80+
vm: main2.vmConfig
81+
tag: externalInput('custom.binding', 'bar')
82+
}
83+
84+
param infraParam = infra

0 commit comments

Comments
 (0)