in src/Bicep.Core/Emit/ExpressionConverter.cs [305:481]
private LanguageExpression ConvertResourcePropertyAccess(ResourceReferenceExpression reference, PropertyAccessExpression expression)
{
var resource = reference.Metadata;
var indexContext = reference.IndexContext;
var propertyName = expression.PropertyName;
var safeAccess = expression.Flags.HasFlag(AccessExpressionFlags.SafeAccess);
if (!resource.IsAzResource)
{
// For an extensible resource, always generate a 'reference' statement.
// User-defined properties appear inside "properties", so use a non-full reference.
var refExpression = GetReferenceExpression(resource, indexContext, false);
return expression.Flags.HasFlag(AccessExpressionFlags.SafeAccess)
? new FunctionExpression("tryGet", [refExpression, new JTokenExpression(propertyName)], [])
: AppendProperties(refExpression, new JTokenExpression(propertyName));
}
// creates an expression like: `last(split(<resource id>, '/'))`
LanguageExpression NameFromIdExpression(LanguageExpression idExpression) => new FunctionExpression("last",
[
new FunctionExpression("split",
[
idExpression,
new JTokenExpression("/"),
],
[]),
],
[]);
// The cases for a parameter resource are much simpler and can be handled up front. These do not
// support symbolic names they are somewhat different from the declared resource case since we just have an
// ID and type.
if (resource is ParameterResourceMetadata parameter)
{
switch (propertyName)
{
case "id":
return GetFullyQualifiedResourceId(parameter);
case "type":
return new JTokenExpression(resource.TypeReference.FormatType());
case "apiVersion":
var apiVersion = resource.TypeReference.ApiVersion ?? throw new UnreachableException();
return new JTokenExpression(apiVersion);
case "name":
return NameFromIdExpression(GetFullyQualifiedResourceId(parameter));
case string when expression.Flags.HasFlag(AccessExpressionFlags.SafeAccess):
return new FunctionExpression(
"tryGet",
[GetReferenceExpression(resource, indexContext, true), new JTokenExpression(propertyName)],
[]);
case "properties":
// use the reference() overload without "full" to generate a shorter expression
// this is dependent on the name expression which could involve locals in case of a resource collection
return GetReferenceExpression(resource, indexContext, false);
default:
return AppendProperties(GetReferenceExpression(resource, indexContext, true), new JTokenExpression(propertyName));
}
}
else if (resource is ModuleOutputResourceMetadata output)
{
// there are some slight variations if a safe dereference operator was used on the output itself, e.g., `mod.outputs.?myResource.<prop>`
var shortCircuitableResourceRef = reference.SourceSyntax is AccessExpressionSyntax accessExpression && accessExpression.IsSafeAccess;
switch (propertyName)
{
case "id" when shortCircuitableResourceRef:
return new FunctionExpression(
"tryGet",
[
AppendProperties(GetModuleReferenceExpression(output.Module, null, true), new JTokenExpression("outputs")),
new JTokenExpression(output.OutputName),
new JTokenExpression("value"),
],
[]);
case "id":
return AppendProperties(
GetModuleReferenceExpression(output.Module, null, true),
new JTokenExpression("outputs"),
new JTokenExpression(output.OutputName),
new JTokenExpression("value"));
case "type":
return new JTokenExpression(resource.TypeReference.FormatType());
case "apiVersion":
var apiVersion = resource.TypeReference.ApiVersion ?? throw new UnreachableException();
return new JTokenExpression(apiVersion);
case "name" when shortCircuitableResourceRef:
// this expression will execute a `reference` expression against the module twice (once to make sure the named output exists, then again to
// retrieve the value of that output), but this inefficiency is unavoidable since passing `null` to `split` will cause the deployment to fail
return new FunctionExpression("if",
[
new FunctionExpression("contains",
[
AppendProperties(GetModuleReferenceExpression(output.Module, null, true), new JTokenExpression("outputs")),
new JTokenExpression(output.OutputName),
],
[]),
NameFromIdExpression(GetFullyQualifiedResourceId(output)),
new FunctionExpression("null", [], []),
],
[]);
case "name":
return NameFromIdExpression(GetFullyQualifiedResourceId(output));
default:
// this would have been blocked by EmitLimitationCalculator
throw new InvalidOperationException($"Unsupported module output resource property '{propertyName}'.");
}
}
else if (resource is DeclaredResourceMetadata declaredResource)
{
if (context.SemanticModel.Features.ResourceInfoCodegenEnabled)
{
// Use simplified "resourceInfo" code generation.
LanguageExpression getResourceInfoExpression()
{
var symbolExpression = GenerateSymbolicReference(declaredResource, indexContext);
return AppendProperties(CreateFunction("resourceInfo", symbolExpression), new JTokenExpression(propertyName));
}
switch (propertyName)
{
case AzResourceTypeProvider.ResourceIdPropertyName:
case AzResourceTypeProvider.ResourceTypePropertyName:
case AzResourceTypeProvider.ResourceApiVersionPropertyName:
return getResourceInfoExpression();
case AzResourceTypeProvider.ResourceNamePropertyName:
var nameExpression = getResourceInfoExpression();
if (declaredResource.Parent is { })
{
// resourceInfo('foo').name will always return a fully-qualified name, whereas using "foo.name" in Bicep
// will give different results depending on whether the resource has a parent (either syntactically, or with the 'parent' property) or not.
// We must preserve this behavior by using "last(split(..., '/'))" to convert from qualified -> unqualified, to avoid this being a breaking change.
return CreateFunction(
"last",
CreateFunction(
"split",
nameExpression,
new JTokenExpression("/")));
}
return nameExpression;
}
}
switch (propertyName)
{
case AzResourceTypeProvider.ResourceIdPropertyName:
// the ID is dependent on the name expression which could involve locals in case of a resource collection
return GetFullyQualifiedResourceId(resource);
case AzResourceTypeProvider.ResourceNamePropertyName:
// the name is dependent on the name expression which could involve locals in case of a resource collection
// Note that we don't want to return the fully-qualified resource name in the case of name property access.
// we should return whatever the user has set as the value of the 'name' property for a predictable user experience.
return ConvertExpression(declaredResource.NameSyntax);
case AzResourceTypeProvider.ResourceTypePropertyName:
return new JTokenExpression(resource.TypeReference.FormatType());
case AzResourceTypeProvider.ResourceApiVersionPropertyName:
var apiVersion = resource.TypeReference.ApiVersion ?? throw new InvalidOperationException($"Expected resource type {resource.TypeReference.FormatName()} to contain version");
return new JTokenExpression(apiVersion);
case string when expression.Flags.HasFlag(AccessExpressionFlags.SafeAccess):
return new FunctionExpression(
"tryGet",
[GetReferenceExpression(resource, indexContext, true), new JTokenExpression(propertyName)],
[]);
case "properties":
// use the reference() overload without "full" to generate a shorter expression
// this is dependent on the name expression which could involve locals in case of a resource collection
return GetReferenceExpression(resource, indexContext, false);
default:
return AppendProperties(GetReferenceExpression(resource, indexContext, true), new JTokenExpression(propertyName));
}
}
else
{
throw new InvalidOperationException($"Unsupported resource metadata type: {resource.GetType()}");
}
}