private LanguageExpression ConvertResourcePropertyAccess()

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()}");
            }
        }