[BUG] Cross-scope `existing` resource reference via symbolic name (languageVersion 2.0) resolves to a malformed Mock, breaking concat()/map() during pre-flight expansion
Maintainers usually reply within 3 days
Assessment
- Difficulty
- 3/5
- Estimated time
- 1-2 days
- Newbie friendliness
- 75/100
Research direction
Start with Arm/Symbols/*, Arm/Deployments/TemplateContext.cs, and Arm/Expressions/Functions.cs, then run the existing Arm/Deployments and Arm/Expressions unit test suites. Trace how symbolic existing resources are resolved during reference() evaluation and add coverage for cross-scope languageVersion 2.0 references; done means their properties resolve correctly through concat() and map() without malformed mocks.
Written by the indexing model from the issue text.
Description
Describe the bug
When a Bicep-compiled ARM template uses languageVersion: "2.0" (symbolic name codegen — triggered e.g. by any compile-time import statement) and contains a cross-scope existing resource, reference() calls against that resource use the bare symbolic name as the first argument (e.g. reference('vnet').addressSpace.addressPrefixes) instead of reference(extensionResourceId(...), apiVersion, 'full').
This is valid, standard ARM syntax for languageVersion 2.0 templates — real Azure deployments resolve it fine because ARM evaluates symbolic references against the resource's declared scope.
However, PSRule's offline expansion (used by GetBicepParamResources / pre-flight bicep(param) expansion, e.g. via ps-rule-assert@2) fails to resolve this form. Downstream use of the result in a strict-typed function such as concat()/map() then throws:
An error occurred evaluating expression '[map(concat(reference('resourceA', ...), reference('resourceB', ...)), ...)]' ... The function 'map' failed. The function 'concat' failed. The arguments for 'Concat' are not in the expected format or type.
Scalar property assignments elsewhere in the same template hit the identical broken resolution but don't surface as hard errors, since plain string/object fields tolerate an unresolved/mock value — making this easy to miss until it feeds an array function.
Root cause (traced in source)
DeploymentVisitor.Emit()(src/PSRule.Rules.Azure/Arm/Deployments/DeploymentVisitor.cs) explicitly skips registering existing resources:protected virtual void Emit(TemplateContext context, IResourceValue resource) { if (resource == null || resource.IsExisting()) return; ... context.AddResource(resource); }- For the symbolic-name (
languageVersion 2.0) resources-as-object form,DeploymentVisitor.Reference(context, symbolicName, resource)creates anExistingResourceValueand registers it only as a symbol (context.AddSymbol(symbol)), never viacontext.AddResource(...). So the resource's id is never added toTemplateContext._ResourceIds. TemplateContext.TryGetResource(nameOrResourceId, ...)resolves a symbolic name to an id viasymbol.GetId(0), then looks that id up in_ResourceIds— which never contains it for existing resources, so the lookup fails.Functions.Reference(context, args)then falls back to a placeholder:return context.TryGetResource(resourceId, out var resourceValue) ? GetReferenceResult(resourceValue, full) : full ? new Mock.MockResource(resourceId) : new Mock.MockResource(resourceId)["properties"];Mock.MockResource(resourceId)(src/PSRule.Rules.Azure/Arm/Mocks/Mock.cs) expectsresourceIdto be a full ARM resource ID and parses it viaResourceHelper.TryResourceIdComponents(...)to buildid/type/name/properties. WhenresourceIdis actually just the bare symbolic name (e.g."vnet"), that parsing produces a malformed/incomplete mock object whose nested property values are not usable as arrays/strings by strict functions likeconcat()/map().
Note that ObjectDeploymentSymbol/ArrayDeploymentSymbol already retain a reference to the real IResourceValue via Configure(resource) — it's discarded after computing the id string, which is what forces the failed _ResourceIds round-trip in step 3.
To Reproduce
other.bicep
@export()
var unused = 'unused'
main.bicep
targetScope = 'resourceGroup'
import { unused } from 'other.bicep'
resource vnet 'Microsoft.Network/virtualNetworks@2023-11-01' existing = {
scope: resourceGroup('other-rg')
name: 'vnet1'
}
output addressPrefixes array = vnet.properties.addressSpace.addressPrefixes
bicep build main.bicep --stdout (with the import present) produces languageVersion 2.0 output using the bare symbolic form:
"addressPrefixes": {
"type": "array",
"value": "[reference('vnet').addressSpace.addressPrefixes]"
}
Running this through PSRule's pre-flight expansion (Assert-PSRule/ps-rule-assert@2 against the compiled template, or directly via GetBicepParamResources) fails to resolve addressPrefixes correctly and can throw when the value feeds concat()/map() elsewhere in the template.
Removing the import line removes the trigger for languageVersion 2.0 and restores the extensionResourceId(...)-based codegen, which — while still ultimately routed through the same Mock.MockResource fallback for existing resources — receives a well-formed resource ID and therefore avoids the malformed mock.
Expected behavior
PSRule's expansion of existing cross-scope resources should resolve consistently regardless of whether the compiled template uses the resourceId(...)/extensionResourceId(...) form or the bare symbolic-name form used by languageVersion 2.0 templates, so that property access (including through concat()/map()) does not fail or silently produce malformed values.
Suggested fix direction
Rather than round-tripping symbolic names through TemplateContext._ResourceIds (which by design never contains existing resources), TryGetResource could resolve the actual IResourceValue directly from the matched symbol — e.g. by extending IDeploymentSymbol with a GetResource(int index) accessor alongside the existing GetId(int index), and having ObjectDeploymentSymbol/ArrayDeploymentSymbol return the IResourceValue they already hold via Configure(...). Functions.Reference/Functions.References would then get the real ExistingResourceValue (which already computes scope-aware Id/Name) instead of falling back to Mock.MockResource with an unparsable identifier.
This looks like a contained change scoped to Arm/Symbols/*, Arm/Deployments/TemplateContext.cs, and Arm/Expressions/Functions.cs, testable with the existing Arm/Deployments/Arm/Expressions unit test suites — not a large architectural change.
Environment
- Discovered while investigating https://github.com/Azure/bicep/issues/20321 (closed as not-a-bicep-bug; codegen for
languageVersion 2.0symbolic references is correct/standard ARM behavior). - Observed downstream in Azure Pipelines'
ps-rule-assert@2task.
- Dominant language
- PowerShell
- Stars
- 449
- Forks
- 110
- Avg merge
- 6d 1h
- Merged PRs (30d)
- 9
Getting set up
Starts the project's dev container in your browser, under your own GitHub account.
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from Azure/PSRule.Rules.Azure
-
[BUG] Azure.AppService.PlanInstanceCount incorrectly applies to Flex Consumption (FC1) plansPossibly taken @bwateratmsft claimed this 18 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
Azure/PSRule.Rules.Azure#3929 · 1 comment ·
Maintainers usually reply within 3 days
-
Export-AzRuleData fails with HTTP 400 for DefenderForStorageSettings due to deprecated API versionPossibly taken @saryn17 claimed this 25 days ago. Openbug feature: in-flight-export
Difficulty 1/5 Under an hour Newbie friendliness 90/100
Azure/PSRule.Rules.Azure#3865 · 1 comment · 1 reaction ·
Maintainers usually reply within 3 days
-
[FEATURE] Behaviour documentation for PR #3908Possibly taken @ElinoraWhite claimed this 1 day ago. Opendocumentation
Azure/PSRule.Rules.Azure#3944 · 1 reaction · 2 assignees ·
Maintainers usually reply within 3 days
-
[BUG] Expansion fails with "Hash must be finalized before the hash value is retrieved" when using an object output from a cross-scope modulePossibly taken @oWretch claimed this 36 days ago. Open
Difficulty 4/5 3-5 days Newbie friendliness 62/100
Azure/PSRule.Rules.Azure#3909 ·
Maintainers usually reply within 3 days
-
[BUG] cidrHost/cidrSubnet fail during Bicep expansion when address prefixes are assigned outside the templatePossibly taken A pull request linked to this issue is open or already merged. Openbug feature: bicep-language feature: pre-flight-expansion
Difficulty 4/5 3-5 days Newbie friendliness 55/100
Azure/PSRule.Rules.Azure#3907 ·
Maintainers usually reply within 3 days
All issues in Azure/PSRule.Rules.Azure
Similar issues
-
Difficulty 1/5 Under an hour Newbie friendliness 85/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 85/100
kubernetes-sigs/headlamp#7950 ·
Maintainers usually reply within 3 days
-
kind/bug
Difficulty 1/5 1-3 hours Newbie friendliness 85/100
kubernetes-sigs/vsphere-csi-driver#4351 ·
Maintainers usually reply within 1 day
-
component/gpu-kubelet-plugin component/helm needs-triage
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
kubernetes-sigs/dra-driver-nvidia-gpu#1559 ·
Maintainers usually reply within 1 day
-
OSCI'26
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
GauravKarakoti/SecureFlow#1301 ·
Maintainers usually reply within 1 day