diff --git a/docs/CHANGELOG-v1.md b/docs/CHANGELOG-v1.md
index 2bfce58112b..48d95ab4c8d 100644
--- a/docs/CHANGELOG-v1.md
+++ b/docs/CHANGELOG-v1.md
@@ -29,7 +29,7 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
## Unreleased
-What's changed since v1.39.1:
+What's changed since v1.39.2:
- Engineering:
- Migrated Azure samples into PSRule for Azure by @BernieWhite.
@@ -37,6 +37,16 @@ What's changed since v1.39.1:
- Quality updates to rule documentation by @BernieWhite.
[#3102](https://github.com/Azure/PSRule.Rules.Azure/issues/3102)
+## v1.39.2
+
+What's changed since v1.39.1:
+
+- Bug fixes:
+ - Fixed user-defined function reference to exported variable by @BernieWhite.
+ [#3120](https://github.com/Azure/PSRule.Rules.Azure/issues/3120)
+ - Fixed name expand of existing resource references by @BernieWhite.
+ [#3123](https://github.com/Azure/PSRule.Rules.Azure/issues/3123)
+
## v1.39.1
What's changed since v1.39.0:
diff --git a/src/PSRule.Rules.Azure/Data/Template/ExistingResourceValue.cs b/src/PSRule.Rules.Azure/Data/Template/ExistingResourceValue.cs
index a5a543fbc1e..dc82fc88a7f 100644
--- a/src/PSRule.Rules.Azure/Data/Template/ExistingResourceValue.cs
+++ b/src/PSRule.Rules.Azure/Data/Template/ExistingResourceValue.cs
@@ -70,8 +70,7 @@ public string Name
///
private string GetId()
{
- if (!Value.TryResourceScope(_Context, out var scopeId))
- throw new TemplateSymbolException(SymbolicName);
+ if (!Value.TryResourceScope(_Context, out var scopeId)) throw new TemplateSymbolException(SymbolicName);
return _Id = ResourceHelper.ResourceId(Type, Name, scopeId);
}
@@ -81,10 +80,9 @@ private string GetId()
///
private string GetName()
{
- if (!Value.TryResourceName(out var name) || string.IsNullOrEmpty(name))
- throw new TemplateSymbolException(SymbolicName);
+ if (!Value.TryResourceName(out var name) || string.IsNullOrEmpty(name)) throw new TemplateSymbolException(SymbolicName);
- return _Name = name.IsExpressionString() ? name.ToString() : name;
+ return _Name = ExpandString(_Context, name);
}
}
diff --git a/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs b/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs
index 1a23332d928..4437093ccfb 100644
--- a/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs
+++ b/src/PSRule.Rules.Azure/Data/Template/TemplateVisitor.cs
@@ -769,37 +769,6 @@ internal void TrackDependencies(IResourceValue resource, string[] dependencies)
}
}
- internal sealed class UserDefinedFunctionContext : NestedTemplateContext
- {
- private readonly Dictionary _Parameters;
-
- public UserDefinedFunctionContext(ITemplateContext context)
- : base(context)
- {
- _Parameters = new Dictionary(StringComparer.OrdinalIgnoreCase);
- }
-
- public override bool TryParameter(string parameterName, out object value)
- {
- return _Parameters.TryGetValue(parameterName, out value);
- }
-
- public override bool TryVariable(string variableName, out object value)
- {
- value = null;
- return false;
- }
-
- internal void SetParameters(JObject[] parameters, object[] args)
- {
- if (parameters == null || parameters.Length == 0 || args == null || args.Length == 0)
- return;
-
- for (var i = 0; i < parameters.Length; i++)
- _Parameters.Add(parameters[i]["name"].Value(), args[i]);
- }
- }
-
internal interface IParameterValue
{
string Name { get; }
@@ -1169,7 +1138,6 @@ protected virtual void Function(TemplateContext context, string ns, string name,
return;
TryArrayProperty(function, PROPERTY_PARAMETERS, out var parameters);
- //var outputFn = context.Expression.Build(outputValue);
ExpressionFn fn = (ctx, args) =>
{
var fnContext = new UserDefinedFunctionContext(ctx);
diff --git a/src/PSRule.Rules.Azure/Data/Template/UserDefinedFunctionContext.cs b/src/PSRule.Rules.Azure/Data/Template/UserDefinedFunctionContext.cs
new file mode 100644
index 00000000000..f74386db43c
--- /dev/null
+++ b/src/PSRule.Rules.Azure/Data/Template/UserDefinedFunctionContext.cs
@@ -0,0 +1,48 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+using System;
+using System.Collections.Generic;
+using Newtonsoft.Json.Linq;
+
+namespace PSRule.Rules.Azure.Data.Template;
+
+#nullable enable
+
+///
+/// A nested context for user-defined functions.
+///
+/// The parent context.
+internal sealed class UserDefinedFunctionContext(ITemplateContext context) : NestedTemplateContext(context)
+{
+ private const string PROPERTY_NAME = "name";
+
+ private readonly Dictionary _Parameters = new(StringComparer.OrdinalIgnoreCase);
+
+ public override bool TryParameter(string parameterName, out object? value)
+ {
+ return _Parameters.TryGetValue(parameterName, out value);
+ }
+
+ public override bool TryVariable(string variableName, out object? value)
+ {
+ return base.TryVariable(variableName, out value);
+ }
+
+ internal void SetParameters(JObject[] parameters, object[]? args)
+ {
+ if (parameters == null || parameters.Length == 0 || args == null || args.Length == 0)
+ return;
+
+ for (var i = 0; i < parameters.Length; i++)
+ {
+ var name = parameters[i][PROPERTY_NAME]?.Value();
+ if (!string.IsNullOrEmpty(name))
+ {
+ _Parameters.Add(name!, args[i]);
+ }
+ }
+ }
+}
+
+#nullable restore
diff --git a/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.1.bicep b/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.1.bicep
index 38cebc4c143..e699262fc2a 100644
--- a/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.1.bicep
+++ b/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.1.bicep
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/2922
+// Based on work contributed by @GABRIELNGBTUC
module child_loop './Tests.Bicep.1.child.bicep' = [
for (item, index) in range(0, 2): {
diff --git a/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.3.bicep b/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.3.bicep
new file mode 100644
index 00000000000..ca1ee35451e
--- /dev/null
+++ b/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.3.bicep
@@ -0,0 +1,24 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3123
+// Contributed by @CharlesToniolo
+
+var webAppsNames = [
+ 'example1'
+ 'example2'
+]
+
+resource webApps 'Microsoft.Web/sites@2022-09-01' existing = [
+ for webAppName in webAppsNames: {
+ name: webAppName
+ }
+]
+
+#disable-next-line no-unused-existing-resources
+resource webAppSettings 'Microsoft.Web/sites/config@2022-09-01' existing = [
+ for i in range(0, length(webAppsNames)): {
+ name: 'appsettings'
+ parent: webApps[i]
+ }
+]
diff --git a/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.3.json b/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.3.json
new file mode 100644
index 00000000000..eea0827c710
--- /dev/null
+++ b/tests/PSRule.Rules.Azure.Tests/Bicep/SymbolicNameTestCases/Tests.Bicep.3.json
@@ -0,0 +1,43 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "languageVersion": "2.0",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.30.23.60470",
+ "templateHash": "3757483382650491065"
+ }
+ },
+ "variables": {
+ "webAppsNames": [
+ "example1",
+ "example2"
+ ]
+ },
+ "resources": {
+ "webApps": {
+ "copy": {
+ "name": "webApps",
+ "count": "[length(variables('webAppsNames'))]"
+ },
+ "existing": true,
+ "type": "Microsoft.Web/sites",
+ "apiVersion": "2022-09-01",
+ "name": "[variables('webAppsNames')[copyIndex()]]"
+ },
+ "webAppSettings": {
+ "copy": {
+ "name": "webAppSettings",
+ "count": "[length(range(0, length(variables('webAppsNames'))))]"
+ },
+ "existing": true,
+ "type": "Microsoft.Web/sites/config",
+ "apiVersion": "2022-09-01",
+ "name": "[format('{0}/{1}', variables('webAppsNames')[range(0, length(variables('webAppsNames')))[copyIndex()]], 'appsettings')]",
+ "dependsOn": [
+ "[format('webApps[{0}]', range(0, length(variables('webAppsNames')))[copyIndex()])]"
+ ]
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.bicep b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.bicep
new file mode 100644
index 00000000000..8f98a5f9dad
--- /dev/null
+++ b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.bicep
@@ -0,0 +1,20 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3120
+// Based on work contributed by @GABRIELNGBTUC
+
+import * as child from './Tests.Bicep.1.child.bicep'
+
+var v1 = []
+var v2 = [
+ 2
+]
+
+func getV3() array => union(v1, v2)
+
+output o1 array = getV3()
+output o2 array = child.getV3()
+output o3 array = union(child.v1, child.v2)
+output o4 array = union(v2, child.v2)
+output o5 array = child.getV4()
diff --git a/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.child.bicep b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.child.bicep
new file mode 100644
index 00000000000..5eb32fe19a2
--- /dev/null
+++ b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.child.bicep
@@ -0,0 +1,18 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+@export()
+var v1 = []
+
+@export()
+var v2 = [
+ 1
+]
+
+@export()
+func getV3() array => union(v1, v2)
+
+import * as child2 from './Tests.Bicep.1.child2.bicep'
+
+@export()
+func getV4() array => union(child2.v1, child2.v2)
diff --git a/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.child2.bicep b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.child2.bicep
new file mode 100644
index 00000000000..096347dcbfe
--- /dev/null
+++ b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.child2.bicep
@@ -0,0 +1,10 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+@export()
+var v1 = []
+
+@export()
+var v2 = [
+ 3
+]
diff --git a/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.json b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.json
new file mode 100644
index 00000000000..31c436d8804
--- /dev/null
+++ b/tests/PSRule.Rules.Azure.Tests/Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.json
@@ -0,0 +1,92 @@
+{
+ "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
+ "languageVersion": "2.0",
+ "contentVersion": "1.0.0.0",
+ "metadata": {
+ "_generator": {
+ "name": "bicep",
+ "version": "0.30.23.60470",
+ "templateHash": "3104036790319433669"
+ }
+ },
+ "functions": [
+ {
+ "namespace": "__bicep",
+ "members": {
+ "getV3": {
+ "parameters": [],
+ "output": {
+ "type": "array",
+ "value": "[union(variables('v1'), variables('v2'))]"
+ }
+ }
+ }
+ },
+ {
+ "namespace": "_1",
+ "members": {
+ "getV3": {
+ "parameters": [],
+ "output": {
+ "type": "array",
+ "value": "[union(variables('_1.v1'), variables('_1.v2'))]"
+ },
+ "metadata": {
+ "__bicep_imported_from!": {
+ "sourceTemplate": "Tests.Bicep.1.child.bicep"
+ }
+ }
+ },
+ "getV4": {
+ "parameters": [],
+ "output": {
+ "type": "array",
+ "value": "[union(variables('_2.v1'), variables('_2.v2'))]"
+ },
+ "metadata": {
+ "__bicep_imported_from!": {
+ "sourceTemplate": "Tests.Bicep.1.child.bicep"
+ }
+ }
+ }
+ }
+ }
+ ],
+ "variables": {
+ "v1": [],
+ "v2": [
+ 2
+ ],
+ "_1.v1": [],
+ "_1.v2": [
+ 1
+ ],
+ "_2.v1": [],
+ "_2.v2": [
+ 3
+ ]
+ },
+ "resources": {},
+ "outputs": {
+ "o1": {
+ "type": "array",
+ "value": "[__bicep.getV3()]"
+ },
+ "o2": {
+ "type": "array",
+ "value": "[_1.getV3()]"
+ },
+ "o3": {
+ "type": "array",
+ "value": "[union(variables('_1.v1'), variables('_1.v2'))]"
+ },
+ "o4": {
+ "type": "array",
+ "value": "[union(variables('v2'), variables('_1.v2'))]"
+ },
+ "o5": {
+ "type": "array",
+ "value": "[_1.getV4()]"
+ }
+ }
+}
\ No newline at end of file
diff --git a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj
index c1f502c73c2..89e3ac05b67 100644
--- a/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj
+++ b/tests/PSRule.Rules.Azure.Tests/PSRule.Rules.Azure.Tests.csproj
@@ -293,6 +293,12 @@
PreserveNewest
+
+ PreserveNewest
+
+
+ PreserveNewest
+
diff --git a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs
index 39e0fc24097..846829ff55b 100644
--- a/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs
+++ b/tests/PSRule.Rules.Azure.Tests/TemplateVisitorTests.cs
@@ -1307,6 +1307,15 @@ public void ProcessTemplate_WhenConditionalExistingReference_IgnoresExpand()
Assert.Equal("02041802-66a9-0a85-7330-8186e16422c7", actual["name"].Value());
}
+ ///
+ /// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3123
+ ///
+ [Fact]
+ public void ProcessTemplate_WhenExistingReferenceNameUsesExpression_ShouldExpandExpression()
+ {
+ var resources = ProcessTemplate(GetSourcePath("Bicep/SymbolicNameTestCases/Tests.Bicep.3.json"), null, out _);
+ }
+
[Fact]
public void ProcessTemplate_WhenParented_ShouldReturnExpectedScope()
{
@@ -1384,6 +1393,30 @@ public void ProcessTemplate_WhenConditionalSecretParameter_ShouldReturnSecretsPl
Assert.Equal("placeholder", actual["properties"]["value"].Value());
}
+ ///
+ /// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/3120
+ ///
+ [Fact]
+ public void ProcessTemplate_WhenUserDefinedFunctionReferencesExportedVariables_ShouldFindVariable()
+ {
+ _ = ProcessTemplate(GetSourcePath("Bicep/UserDefinedFunctionTestCases/Tests.Bicep.1.json"), null, out var templateContext);
+
+ Assert.True(templateContext.RootDeployment.TryOutput("o1", out JObject o1));
+ Assert.Equal([2], o1["value"].Values());
+
+ Assert.True(templateContext.RootDeployment.TryOutput("o2", out JObject o2));
+ Assert.Equal([1], o2["value"].Values());
+
+ Assert.True(templateContext.RootDeployment.TryOutput("o3", out JObject o3));
+ Assert.Equal([1], o3["value"].Values());
+
+ Assert.True(templateContext.RootDeployment.TryOutput("o4", out JObject o4));
+ Assert.Equal([2, 1], o4["value"].Values());
+
+ Assert.True(templateContext.RootDeployment.TryOutput("o5", out JObject o5));
+ Assert.Equal([3], o5["value"].Values());
+ }
+
#region Helper methods
private static string GetSourcePath(string fileName)
diff --git a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.38.bicep b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.38.bicep
index 45ec9c80a9b..873c5c71ab5 100644
--- a/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.38.bicep
+++ b/tests/PSRule.Rules.Azure.Tests/Tests.Bicep.38.bicep
@@ -2,6 +2,7 @@
// Licensed under the MIT License.
// Test case for https://github.com/Azure/PSRule.Rules.Azure/issues/2850
+// Based on work contributed by @maythamfahmi
// Handle from subscription scope.
targetScope = 'subscription'