private TypeSymbol? NarrowObjectAssignmentType()

in src/Bicep.Core/TypeSystem/TypeValidator.cs [1044:1285]


        private TypeSymbol? NarrowObjectAssignmentType(TypeValidatorConfig config, SyntaxBase expression, TypeSymbol expressionType, ObjectType targetType)
        {
            static (TypeSymbol type, bool typeWasPreserved) AddImplicitNull(TypeSymbol propertyType, TypePropertyFlags propertyFlags)
            {
                bool preserveType = propertyFlags.HasFlag(TypePropertyFlags.Required) || !propertyFlags.HasFlag(TypePropertyFlags.AllowImplicitNull);
                return (preserveType ? propertyType : TypeHelper.CreateTypeUnion(propertyType, LanguageConstants.Null), preserveType);
            }

            static TypeSymbol RemoveImplicitNull(TypeSymbol type, bool typeWasPreserved)
            {
                return typeWasPreserved || type is not UnionType unionType
                    ? type
                    : TypeHelper.CreateTypeUnion(unionType.Members.Where(m => m != LanguageConstants.Null));
            }

            if (expression is VariableAccessSyntax variableAccess && DeclaringSyntax(variableAccess) is SyntaxBase declaringSyntax)
            {
                var newConfig = config with { OriginSyntax = variableAccess };
                return NarrowObjectAssignmentType(newConfig, declaringSyntax, expressionType, targetType);
            }

            // TODO: Short-circuit on any object to avoid unnecessary processing?
            // TODO: Consider doing the schema check even if there are parse errors
            // if we have parse errors, there's no point to check assignability
            // we should not return the parse errors however because they will get double collected
            if (this.parsingErrorLookup.Contains(expression))
            {
                return targetType;
            }

            if (expressionType is ObjectType expressionObjectType)
            {
                var missingRequiredProperties = expressionObjectType.AdditionalProperties is not null
                    // if the assigned value allows additional properties, we can't know if it's missing any
                    ? []
                    // otherwise, look for required properties on the target for which there is no declared counterpart on the assigned value
                    : targetType.Properties.Values
                        .Where(p => p.Flags.HasFlag(TypePropertyFlags.Required) &&
                            !AreTypesAssignable(LanguageConstants.Null, p.TypeReference.Type) &&
                            !expressionObjectType.Properties.ContainsKey(p.Name))
                        .OrderBy(p => p.Name)
                        .ToImmutableArray();

                if (missingRequiredProperties.Length > 0)
                {
                    var (positionable, blockName) = GetMissingPropertyContext(expression);

                    diagnosticWriter.Write(
                        config.OriginSyntax ?? positionable,
                        x => x.MissingRequiredProperties(
                            warnInsteadOfError: (config.IsResourceDeclaration && missingRequiredProperties.All(p => !p.Flags.HasFlag(TypePropertyFlags.SystemProperty))) ||
                                ShouldWarnForPropertyMismatch(targetType),
                            TryGetSourceDeclaration(config),
                            expression as ObjectSyntax,
                            missingRequiredProperties.Select(p => p.Name).ToList(),
                            blockName,
                            config.IsResourceDeclaration && missingRequiredProperties.Any(p => !p.Flags.HasFlag(TypePropertyFlags.SystemProperty)),
                            parsingErrorLookup));
                }

                var narrowedProperties = new List<NamedTypeProperty>();
                foreach (var declaredProperty in targetType.Properties.Values)
                {
                    if (expressionObjectType.Properties.TryGetValue(declaredProperty.Name, out var expressionTypeProperty))
                    {
                        var declaredPropertySyntax = (expression as ObjectSyntax)?.TryGetPropertyByName(declaredProperty.Name);
                        var skipConstantCheckForProperty = config.SkipConstantCheck;

                        // is the property marked as requiring compile-time constants and has the parent already validated this?
                        if (skipConstantCheckForProperty == false && declaredProperty.Flags.HasFlag(TypePropertyFlags.Constant))
                        {
                            // validate that values are compile-time constants
                            GetCompileTimeConstantViolation(declaredPropertySyntax?.Value ?? expression, diagnosticWriter);

                            // disable compile-time constant validation for children
                            skipConstantCheckForProperty = true;
                        }

                        if (declaredProperty.Flags.HasFlag(TypePropertyFlags.ReadOnly))
                        {
                            var diagnosticTarget = config.OriginSyntax ?? declaredPropertySyntax?.Key ?? expression;
                            // the declared property is read-only
                            // value cannot be assigned to a read-only property
                            bool? isExistingResource = binder.GetParent(expression) switch
                            {
                                ResourceDeclarationSyntax rds => rds.IsExistingResource(),
                                // we previously "unwrapped" an if condition body, so there may be one more ancestor to check
                                IfConditionSyntax ifCondition => (binder.GetParent(ifCondition) as ResourceDeclarationSyntax)?.IsExistingResource(),
                                _ => null,
                            };
                            if (isExistingResource is true)
                            {
                                diagnosticWriter.Write(diagnosticTarget, x => x.CannotUsePropertyInExistingResource(declaredProperty.Name));
                            }
                            else if (!expressionTypeProperty.Flags.HasFlag(TypePropertyFlags.ReadOnly))
                            {
                                var resourceTypeInaccuracy = !declaredProperty.Flags.HasFlag(TypePropertyFlags.SystemProperty) && config.IsResourceDeclaration;
                                diagnosticWriter.Write(diagnosticTarget, x => x.CannotAssignToReadOnlyProperty(resourceTypeInaccuracy || ShouldWarnForPropertyMismatch(targetType), declaredProperty.Name, resourceTypeInaccuracy));
                            }

                            narrowedProperties.Add(declaredProperty);
                            continue;
                        }

                        if (declaredProperty.Flags.HasFlag(TypePropertyFlags.FallbackProperty))
                        {
                            diagnosticWriter.Write(config.OriginSyntax ?? declaredPropertySyntax?.Key ?? expression,
                                x => x.FallbackPropertyUsed(shouldDowngrade: false, declaredProperty.Name));
                        }

                        var newConfig = config with
                        {
                            SkipConstantCheck = skipConstantCheckForProperty,
                            SkipTypeErrors = true,
                            DisallowAny = declaredProperty.Flags.HasFlag(TypePropertyFlags.DisallowAny),
                            OnTypeMismatch = GetPropertyMismatchDiagnosticWriter(
                                config: config,
                                shouldWarn: (config.IsResourceDeclaration && !declaredProperty.Flags.HasFlag(TypePropertyFlags.SystemProperty)) || ShouldWarn(declaredProperty.TypeReference.Type),
                                propertyName: declaredProperty.Name,
                                showTypeInaccuracyClause: config.IsResourceDeclaration && !declaredProperty.Flags.HasFlag(TypePropertyFlags.SystemProperty)),
                        };

                        var propertyExpression = declaredPropertySyntax?.Value ?? expression;
                        TypeSymbol propertyTargetType = declaredProperty.TypeReference.Type;
                        TypeSymbol propertyExpressionType = expressionTypeProperty.TypeReference.Type;

                        TypeSymbol GetNarrowedPropertyType()
                        {
                            // append "| null" to the property type for non-required properties
                            var (propertyAssignmentType, typeWasPreserved) = AddImplicitNull(propertyTargetType, declaredProperty.Flags);

                            var narrowedType = NarrowType(newConfig, propertyExpression, propertyExpressionType, propertyAssignmentType);
                            return RemoveImplicitNull(narrowedType, typeWasPreserved);
                        }

                        // In the case of a recursive type, eager narrowing can lead to infinite recursion. If we've
                        // already narrowed this (expressionSyntax, expressionType, targetType) triple, then all
                        // relevant diagnostics have already been raised. Use a deferred type reference to stop eagerly
                        // comparing and narrowing types from this point forward.
                        ITypeReference narrowedPropertyType = config.currentlyProcessing.Add((propertyExpression, propertyExpressionType, propertyTargetType))
                            ? GetNarrowedPropertyType()
                            : new DeferredTypeReference(GetNarrowedPropertyType);

                        narrowedProperties.Add(new NamedTypeProperty(declaredProperty.Name, narrowedPropertyType, declaredProperty.Flags));
                    }
                    else
                    {
                        // TODO should this be narrowed against expressionObjectType.AdditionalPropertiesType ?
                        narrowedProperties.Add(declaredProperty);
                    }
                }

                // find properties that are specified on in the expression object but not declared in the schema
                var extraProperties = expressionObjectType.Properties
                    .Where(p => !targetType.Properties.ContainsKey(p.Key));

                // extra properties should raise a diagnostic if the target does not allow additional properties OR the additional properties schema on the target is a "fallback"
                // No diagnostic should be raised if the receiver accepts but discourages additional properties and the assigned value is not an object literal
                if (targetType.AdditionalProperties is null || (expression is ObjectSyntax && targetType.AdditionalProperties.Flags.HasFlag(TypePropertyFlags.FallbackProperty)))
                {
                    var shouldWarn = (targetType.AdditionalProperties is not null && targetType.AdditionalProperties.Flags.HasFlag(TypePropertyFlags.FallbackProperty)) || ShouldWarnForPropertyMismatch(targetType);
                    var validUnspecifiedProperties = targetType.Properties.Values
                        .Where(p => !p.Flags.HasFlag(TypePropertyFlags.ReadOnly) &&
                            !p.Flags.HasFlag(TypePropertyFlags.FallbackProperty) &&
                            !expressionObjectType.Properties.ContainsKey(p.Name))
                        .Select(p => p.Name)
                        .OrderBy(x => x)
                        .ToList();

                    foreach (var extraProperty in extraProperties)
                    {
                        var extraPropertySyntax = (expression as ObjectSyntax)?.TryGetPropertyByName(extraProperty.Key);
                        diagnosticWriter.Write(config.OriginSyntax ?? extraPropertySyntax?.Key ?? expression, x =>
                        {
                            var sourceDeclaration = TryGetSourceDeclaration(config);

                            if (sourceDeclaration is null && SpellChecker.GetSpellingSuggestion(extraProperty.Key, validUnspecifiedProperties) is { } suggestedKeyName)
                            {
                                // only look up suggestions if we're not sourcing this type from another declaration.
                                return x.DisallowedPropertyWithSuggestion(shouldWarn, extraProperty.Key, targetType, suggestedKeyName);
                            }

                            return x.DisallowedProperty(shouldWarn, sourceDeclaration, extraProperty.Key, targetType, validUnspecifiedProperties, config.IsResourceDeclaration);
                        });
                    }

                    foreach (var unknownProperty in (expression as ObjectSyntax)?.Properties.Where(p => p.TryGetKeyText() is null) ?? [])
                    {
                        diagnosticWriter.Write(DiagnosticBuilder.ForPosition(unknownProperty.Key).DisallowedInterpolatedKeyProperty(shouldWarn,
                            TryGetSourceDeclaration(config),
                            targetType,
                            validUnspecifiedProperties));
                    }
                }
                else
                {
                    // extra properties must be assignable to the right type
                    foreach (var extraProperty in extraProperties)
                    {
                        var skipConstantCheckForProperty = config.SkipConstantCheck;
                        var extraPropertySyntax = (expression as ObjectSyntax)?.TryGetPropertyByName(extraProperty.Key);

                        // is the property marked as requiring compile-time constants and has the parent already validated this?
                        if (skipConstantCheckForProperty == false && targetType.AdditionalProperties.Flags.HasFlag(TypePropertyFlags.Constant))
                        {
                            // validate that values are compile-time constants
                            GetCompileTimeConstantViolation(extraPropertySyntax?.Value ?? expression, diagnosticWriter);

                            // disable compile-time constant validation for children
                            skipConstantCheckForProperty = true;
                        }

                        var newConfig = config with
                        {
                            SkipConstantCheck = skipConstantCheckForProperty,
                            SkipTypeErrors = true,
                            DisallowAny = targetType.AdditionalProperties.Flags.HasFlag(TypePropertyFlags.DisallowAny),
                            OnTypeMismatch = GetPropertyMismatchDiagnosticWriter(config, ShouldWarn(targetType.AdditionalProperties.TypeReference.Type), extraProperty.Key, false),
                        };

                        // append "| null" to the type on non-required properties
                        var (additionalPropertiesAssignmentType, _) = AddImplicitNull(targetType.AdditionalProperties.TypeReference.Type, targetType.AdditionalProperties.Flags);

                        // although we don't use the result here, it's important to call NarrowType to collect diagnostics
                        if (config.currentlyProcessing.Add((extraPropertySyntax?.Value ?? expression, extraProperty.Value.TypeReference.Type, additionalPropertiesAssignmentType)))
                        {
                            var narrowedType = NarrowType(newConfig, extraPropertySyntax?.Value ?? expression, extraProperty.Value.TypeReference.Type, additionalPropertiesAssignmentType);
                        }

                        // TODO should we try and narrow the additional properties type? May be difficult
                    }
                }

                var narrowedObject = new ObjectType(targetType.Name, targetType.ValidationFlags, narrowedProperties, targetType.AdditionalProperties, targetType.MethodResolver.CopyToObject);

                return config.IsResourceDeclaration
                    ? TypeHelper.RemovePropertyFlagsRecursively(narrowedObject, TypePropertyFlags.ReadOnly)
                    : narrowedObject;
            }

            return null;
        }