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;
}