private static IEnumerable <ErrorDiagnostic> GetDiscriminatedObjectAssignmentDiagnostics(ITypeManager typeManager, ObjectSyntax expression, DiscriminatedObjectType targetType, bool skipConstantCheck) { // 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 (expression.HasParseErrors()) { yield break; } var propertyMap = expression.ToPropertyDictionary(); if (!propertyMap.TryGetValue(targetType.DiscriminatorKey, out var discriminatorProperty)) { // object doesn't contain the discriminator field yield return(DiagnosticBuilder.ForPosition(expression).MissingRequiredProperty(targetType.DiscriminatorKey, targetType.DiscriminatorKeysUnionType)); yield break; } // At some point in the future we may want to relax the expectation of a string literal key, and allow a generic string. // In this case, the best we can do is validate against the union of all the settable properties. // Let's not do this just yet, and see if a use-case arises. var discriminatorType = typeManager.GetTypeInfo(discriminatorProperty.Value); if (!(discriminatorType is StringLiteralType stringLiteralDiscriminator)) { yield return(DiagnosticBuilder.ForPosition(expression).PropertyTypeMismatch(targetType.DiscriminatorKey, targetType.DiscriminatorKeysUnionType, discriminatorType)); yield break; } if (!targetType.UnionMembersByKey.TryGetValue(stringLiteralDiscriminator.Name, out var selectedObjectReference)) { // no matches yield return(DiagnosticBuilder.ForPosition(discriminatorProperty.Value).PropertyTypeMismatch(targetType.DiscriminatorKey, targetType.DiscriminatorKeysUnionType, discriminatorType)); yield break; } if (!(selectedObjectReference.Type is ObjectType selectedObjectType)) { throw new InvalidOperationException($"Discriminated type {targetType.Name} contains non-object member"); } // we have a match! foreach (var diagnostic in GetObjectAssignmentDiagnostics(typeManager, expression, selectedObjectType, skipConstantCheck)) { yield return(diagnostic); } yield break; }
private static void GetObjectAssignmentDiagnostics(ITypeManager typeManager, ObjectSyntax expression, ObjectType targetType, IList <Diagnostic> diagnostics, bool skipConstantCheck) { // 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 (expression.HasParseErrors()) { return; } var propertyMap = expression.ToPropertyDictionary(); var missingRequiredProperties = targetType.Properties.Values .Where(p => p.Flags.HasFlag(TypePropertyFlags.Required) && propertyMap.ContainsKey(p.Name) == false) .Select(p => p.Name) .OrderBy(p => p) .ConcatString(LanguageConstants.ListSeparator); if (string.IsNullOrEmpty(missingRequiredProperties) == false) { diagnostics.Add(DiagnosticBuilder.ForPosition(expression).MissingRequiredProperties(missingRequiredProperties)); } foreach (var declaredProperty in targetType.Properties.Values) { if (propertyMap.TryGetValue(declaredProperty.Name, out var declaredPropertySyntax)) { bool skipConstantCheckForProperty = skipConstantCheck; // is the property marked as requiring compile-time constants and has the parent already validated this? if (skipConstantCheck == false && declaredProperty.Flags.HasFlag(TypePropertyFlags.Constant)) { // validate that values are compile-time constants diagnostics.AddRange(GetCompileTimeConstantViolation(declaredPropertySyntax.Value)); // disable compile-time constant validation for children skipConstantCheckForProperty = true; } if (declaredProperty.Flags.HasFlag(TypePropertyFlags.ReadOnly)) { // the declared property is read-only // value cannot be assigned to a read-only property diagnostics.Add(DiagnosticBuilder.ForPosition(declaredPropertySyntax.Key).CannotAssignToReadOnlyProperty(declaredProperty.Name)); } // declared property is specified in the value object // validate type GetExpressionAssignmentDiagnosticsInternal( typeManager, declaredPropertySyntax.Value, declaredProperty.TypeReference.Type, diagnostics, (expectedType, actualType, errorExpression) => DiagnosticBuilder.ForPosition(errorExpression).PropertyTypeMismatch(declaredProperty.Name, expectedType, actualType), skipConstantCheckForProperty, skipTypeErrors: true); } } // find properties that are specified on in the expression object but not declared in the schema var extraProperties = expression.Properties .Select(p => p.GetKeyText()) .Except(targetType.Properties.Values.Select(p => p.Name), LanguageConstants.IdentifierComparer) .Select(name => propertyMap[name]); if (targetType.AdditionalPropertiesType == null) { var validUnspecifiedProperties = targetType.Properties.Values .Where(p => !p.Flags.HasFlag(TypePropertyFlags.ReadOnly)) .Select(p => p.Name) .Except(expression.Properties.Select(p => p.GetKeyText()), LanguageConstants.IdentifierComparer) .OrderBy(x => x); // extra properties are not allowed by the type foreach (var extraProperty in extraProperties) { var error = validUnspecifiedProperties.Any() ? DiagnosticBuilder.ForPosition(extraProperty.Key).DisallowedPropertyWithPermissibleProperties(extraProperty.GetKeyText(), targetType.Name, validUnspecifiedProperties) : DiagnosticBuilder.ForPosition(extraProperty.Key).DisallowedProperty(extraProperty.GetKeyText(), targetType.Name); diagnostics.AddRange(error.AsEnumerable()); } } else { // extra properties must be assignable to the right type foreach (ObjectPropertySyntax extraProperty in extraProperties) { bool skipConstantCheckForProperty = skipConstantCheck; // is the property marked as requiring compile-time constants and has the parent already validated this? if (skipConstantCheckForProperty == false && targetType.AdditionalPropertiesFlags.HasFlag(TypePropertyFlags.Constant)) { // validate that values are compile-time constants diagnostics.AddRange(GetCompileTimeConstantViolation(extraProperty.Value)); // disable compile-time constant validation for children skipConstantCheckForProperty = true; } GetExpressionAssignmentDiagnosticsInternal( typeManager, extraProperty.Value, targetType.AdditionalPropertiesType.Type, diagnostics, (expectedType, actualType, errorExpression) => DiagnosticBuilder.ForPosition(errorExpression).PropertyTypeMismatch(extraProperty.GetKeyText(), expectedType, actualType), skipConstantCheckForProperty, skipTypeErrors: true); } } }