From abeddfdac5bac93fdf4b5d326c9103053e7e4a4e Mon Sep 17 00:00:00 2001 From: Daniel Reynolds Date: Tue, 4 Feb 2025 21:09:58 +0000 Subject: [PATCH 1/4] [Fusion] Added post-merge validation rule InputFieldReferencesInaccessibleTypeRule --- .../Logging/LogEntryCodes.cs | 1 + .../Logging/LogEntryHelper.cs | 21 ++ ...nputFieldReferencesInaccessibleTypeRule.cs | 60 +++++ .../CompositionResources.Designer.cs | 210 +++--------------- .../Properties/CompositionResources.resx | 3 + .../src/Fusion.Composition/SchemaComposer.cs | 1 + ...ieldReferencesInaccessibleTypeRuleTests.cs | 132 +++++++++++ 7 files changed, 244 insertions(+), 184 deletions(-) create mode 100644 src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs create mode 100644 src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRuleTests.cs diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs index 1499ef8ebfc..15d3d587d0d 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs @@ -14,6 +14,7 @@ public static class LogEntryCodes public const string ExternalUnused = "EXTERNAL_UNUSED"; public const string FieldArgumentTypesNotMergeable = "FIELD_ARGUMENT_TYPES_NOT_MERGEABLE"; public const string InputFieldDefaultMismatch = "INPUT_FIELD_DEFAULT_MISMATCH"; + public const string InputFieldReferencesInaccessibleType = "INPUT_FIELD_REFERENCES_INACCESSIBLE_TYPE"; public const string InputFieldTypesNotMergeable = "INPUT_FIELD_TYPES_NOT_MERGEABLE"; public const string InputWithMissingRequiredFields = "INPUT_WITH_MISSING_REQUIRED_FIELDS"; public const string InvalidGraphQL = "INVALID_GRAPHQL"; diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs index 55ae2f8afdf..efcc6078b4f 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs @@ -279,6 +279,27 @@ public static LogEntry InputFieldDefaultMismatch( schemaA); } + public static LogEntry InputFieldReferencesInaccessibleType( + InputFieldDefinition field, + string typeName, + string referenceTypeName, + SchemaDefinition schema) + { + var coordinate = new SchemaCoordinate(typeName, field.Name); + + return new LogEntry( + string.Format( + LogEntryHelper_InputFieldReferencesInaccessibleType, + field.Name, + typeName, + referenceTypeName), + LogEntryCodes.InputFieldReferencesInaccessibleType, + LogSeverity.Error, + coordinate, + field, + schema); + } + public static LogEntry InputFieldTypesNotMergeable( InputFieldDefinition field, string typeName, diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs new file mode 100644 index 00000000000..32231794e6b --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs @@ -0,0 +1,60 @@ +using System.Collections.Immutable; +using System.Diagnostics; +using HotChocolate.Fusion.Events; +using HotChocolate.Fusion.Events.Contracts; +using HotChocolate.Fusion.Extensions; +using HotChocolate.Skimmed; +using static HotChocolate.Fusion.Logging.LogEntryHelper; + +namespace HotChocolate.Fusion.PostMergeValidationRules; + +/// +/// In a composed schema, a field within an input type must only reference types that are exposed. +/// This requirement guarantees that public types do not reference inaccessible structures +/// which are intended for internal use. +/// +/// +/// Specification +/// +internal sealed class InputFieldReferencesInaccessibleTypeRule + : IEventHandler, IEventHandler +{ + private ImmutableHashSet? _inaccessibleTypes; + + public void Handle(SchemaEvent @event, CompositionContext context) + { + var builder = ImmutableHashSet.CreateBuilder(); + foreach (var type in @event.Schema.Types) + { + if (type.HasInaccessibleDirective()) + { + builder.Add(type.Name); + } + } + + _inaccessibleTypes = builder.ToImmutable(); + } + + public void Handle(InputFieldEvent @event, CompositionContext context) + { + Debug.Assert(_inaccessibleTypes is not null); + + var (field, type, schema) = @event; + + if (field.HasInaccessibleDirective()) + { + return; + } + + var fieldTypeName = field.Type.NamedType().Name; + if (_inaccessibleTypes.Contains(fieldTypeName)) + { + context.Log.Write( + InputFieldReferencesInaccessibleType( + field, + type.Name, + fieldTypeName, + schema)); + } + } +} diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs index b2898318a2e..1b9ecf371bd 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs @@ -11,46 +11,32 @@ namespace HotChocolate.Fusion.Properties { using System; - /// - /// A strongly-typed resource class, for looking up localized strings, etc. - /// - // This class was auto-generated by the StronglyTypedResourceBuilder - // class via a tool like ResGen or Visual Studio. - // To add or remove a member, edit your .ResX file then rerun ResGen - // with the /str option, or rebuild your VS project. - [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] - [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] - [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + [System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [System.Diagnostics.DebuggerNonUserCodeAttribute()] + [System.Runtime.CompilerServices.CompilerGeneratedAttribute()] internal class CompositionResources { - private static global::System.Resources.ResourceManager resourceMan; + private static System.Resources.ResourceManager resourceMan; - private static global::System.Globalization.CultureInfo resourceCulture; + private static System.Globalization.CultureInfo resourceCulture; - [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] internal CompositionResources() { } - /// - /// Returns the cached ResourceManager instance used by this class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Resources.ResourceManager ResourceManager { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Resources.ResourceManager ResourceManager { get { - if (object.ReferenceEquals(resourceMan, null)) { - global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("HotChocolate.Fusion.Properties.CompositionResources", typeof(CompositionResources).Assembly); + if (object.Equals(null, resourceMan)) { + System.Resources.ResourceManager temp = new System.Resources.ResourceManager("HotChocolate.Fusion.Properties.CompositionResources", typeof(CompositionResources).Assembly); resourceMan = temp; } return resourceMan; } } - /// - /// Overrides the current thread's CurrentUICulture property for all - /// resource lookups using this strongly typed resource class. - /// - [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] - internal static global::System.Globalization.CultureInfo Culture { + [System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Advanced)] + internal static System.Globalization.CultureInfo Culture { get { return resourceCulture; } @@ -59,450 +45,306 @@ internal CompositionResources() { } } - /// - /// Looks up a localized string similar to Post-merge validation failed. View the composition log for details.. - /// internal static string ErrorHelper_PostMergeValidationFailed { get { return ResourceManager.GetString("ErrorHelper_PostMergeValidationFailed", resourceCulture); } } - /// - /// Looks up a localized string similar to Pre-merge validation failed. View the composition log for details.. - /// internal static string ErrorHelper_PreMergeValidationFailed { get { return ResourceManager.GetString("ErrorHelper_PreMergeValidationFailed", resourceCulture); } } - /// - /// Looks up a localized string similar to Source schema parsing failed. View the composition log for details.. - /// internal static string ErrorHelper_SourceSchemaParsingFailed { get { return ResourceManager.GetString("ErrorHelper_SourceSchemaParsingFailed", resourceCulture); } } - /// - /// Looks up a localized string similar to Source schema validation failed. View the composition log for details.. - /// internal static string ErrorHelper_SourceSchemaValidationFailed { get { return ResourceManager.GetString("ErrorHelper_SourceSchemaValidationFailed", resourceCulture); } } - /// - /// Looks up a localized string similar to The built-in scalar type '{0}' in schema '{1}' is not accessible.. - /// internal static string LogEntryHelper_DisallowedInaccessibleBuiltInScalar { get { return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleBuiltInScalar", resourceCulture); } } - /// - /// Looks up a localized string similar to The built-in directive argument '{0}' in schema '{1}' is not accessible.. - /// - internal static string LogEntryHelper_DisallowedInaccessibleDirectiveArgument { + internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionType { get { - return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleDirectiveArgument", resourceCulture); + return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionType", resourceCulture); } } - /// - /// Looks up a localized string similar to The introspection argument '{0}' in schema '{1}' is not accessible.. - /// - internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionArgument { + internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionField { get { - return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionArgument", resourceCulture); + return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionField", resourceCulture); } } - /// - /// Looks up a localized string similar to The introspection field '{0}' in schema '{1}' is not accessible.. - /// - internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionField { + internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionArgument { get { - return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionField", resourceCulture); + return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionArgument", resourceCulture); } } - /// - /// Looks up a localized string similar to The introspection type '{0}' in schema '{1}' is not accessible.. - /// - internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionType { + internal static string LogEntryHelper_DisallowedInaccessibleDirectiveArgument { get { - return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionType", resourceCulture); + return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleDirectiveArgument", resourceCulture); } } - /// - /// Looks up a localized string similar to The merged enum type '{0}' is empty.. - /// internal static string LogEntryHelper_EmptyMergedEnumType { get { return ResourceManager.GetString("LogEntryHelper_EmptyMergedEnumType", resourceCulture); } } - /// - /// Looks up a localized string similar to The merged interface type '{0}' is empty.. - /// internal static string LogEntryHelper_EmptyMergedInterfaceType { get { return ResourceManager.GetString("LogEntryHelper_EmptyMergedInterfaceType", resourceCulture); } } - /// - /// Looks up a localized string similar to The merged object type '{0}' is empty.. - /// internal static string LogEntryHelper_EmptyMergedObjectType { get { return ResourceManager.GetString("LogEntryHelper_EmptyMergedObjectType", resourceCulture); } } - /// - /// Looks up a localized string similar to The merged union type '{0}' is empty.. - /// internal static string LogEntryHelper_EmptyMergedUnionType { get { return ResourceManager.GetString("LogEntryHelper_EmptyMergedUnionType", resourceCulture); } } - /// - /// Looks up a localized string similar to The enum type '{0}' in schema '{1}' must define the value '{2}'.. - /// internal static string LogEntryHelper_EnumValuesMismatch { get { return ResourceManager.GetString("LogEntryHelper_EnumValuesMismatch", resourceCulture); } } - /// - /// Looks up a localized string similar to The argument with schema coordinate '{0}' has inconsistent default values.. - /// internal static string LogEntryHelper_ExternalArgumentDefaultMismatch { get { return ResourceManager.GetString("LogEntryHelper_ExternalArgumentDefaultMismatch", resourceCulture); } } - /// - /// Looks up a localized string similar to The external field '{0}' in schema '{1}' is not defined (non-external) in any other schema.. - /// internal static string LogEntryHelper_ExternalMissingOnBase { get { return ResourceManager.GetString("LogEntryHelper_ExternalMissingOnBase", resourceCulture); } } - /// - /// Looks up a localized string similar to The interface field '{0}' in schema '{1}' must not be marked as external.. - /// internal static string LogEntryHelper_ExternalOnInterface { get { return ResourceManager.GetString("LogEntryHelper_ExternalOnInterface", resourceCulture); } } - /// - /// Looks up a localized string similar to The external field '{0}' in schema '{1}' is not referenced by a @provides directive in the schema.. - /// internal static string LogEntryHelper_ExternalUnused { get { return ResourceManager.GetString("LogEntryHelper_ExternalUnused", resourceCulture); } } - /// - /// Looks up a localized string similar to The argument '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'.. - /// internal static string LogEntryHelper_FieldArgumentTypesNotMergeable { get { return ResourceManager.GetString("LogEntryHelper_FieldArgumentTypesNotMergeable", resourceCulture); } } - /// - /// Looks up a localized string similar to The default value '{0}' of input field '{1}' in schema '{2}' differs from the default value of '{3}' in schema '{4}'.. - /// internal static string LogEntryHelper_InputFieldDefaultMismatch { get { return ResourceManager.GetString("LogEntryHelper_InputFieldDefaultMismatch", resourceCulture); } } - /// - /// Looks up a localized string similar to The input field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'.. - /// + internal static string LogEntryHelper_InputFieldReferencesInaccessibleType { + get { + return ResourceManager.GetString("LogEntryHelper_InputFieldReferencesInaccessibleType", resourceCulture); + } + } + internal static string LogEntryHelper_InputFieldTypesNotMergeable { get { return ResourceManager.GetString("LogEntryHelper_InputFieldTypesNotMergeable", resourceCulture); } } - /// - /// Looks up a localized string similar to The input type '{0}' in schema '{1}' must define the required field '{2}'.. - /// internal static string LogEntryHelper_InputWithMissingRequiredFields { get { return ResourceManager.GetString("LogEntryHelper_InputWithMissingRequiredFields", resourceCulture); } } - /// - /// Looks up a localized string similar to Invalid GraphQL in source schema. Exception message: {0}.. - /// internal static string LogEntryHelper_InvalidGraphQL { get { return ResourceManager.GetString("LogEntryHelper_InvalidGraphQL", resourceCulture); } } - /// - /// Looks up a localized string similar to The interface field '{0}' in schema '{1}' must not be marked as shareable.. - /// internal static string LogEntryHelper_InvalidShareableUsage { get { return ResourceManager.GetString("LogEntryHelper_InvalidShareableUsage", resourceCulture); } } - /// - /// Looks up a localized string similar to A @key directive on type '{0}' in schema '{1}' references field '{2}', which must not include directive applications.. - /// internal static string LogEntryHelper_KeyDirectiveInFieldsArgument { get { return ResourceManager.GetString("LogEntryHelper_KeyDirectiveInFieldsArgument", resourceCulture); } } - /// - /// Looks up a localized string similar to A @key directive on type '{0}' in schema '{1}' references field '{2}', which must not have arguments.. - /// internal static string LogEntryHelper_KeyFieldsHasArguments { get { return ResourceManager.GetString("LogEntryHelper_KeyFieldsHasArguments", resourceCulture); } } - /// - /// Looks up a localized string similar to A @key directive on type '{0}' in schema '{1}' references field '{2}', which must not be a list, interface, or union type.. - /// internal static string LogEntryHelper_KeyFieldsSelectInvalidType { get { return ResourceManager.GetString("LogEntryHelper_KeyFieldsSelectInvalidType", resourceCulture); } } - /// - /// Looks up a localized string similar to A @key directive on type '{0}' in schema '{1}' references field '{2}', which does not exist.. - /// internal static string LogEntryHelper_KeyInvalidFields { get { return ResourceManager.GetString("LogEntryHelper_KeyInvalidFields", resourceCulture); } } - /// - /// Looks up a localized string similar to A @key directive on type '{0}' in schema '{1}' must specify a string value for the 'fields' argument.. - /// internal static string LogEntryHelper_KeyInvalidFieldsType { get { return ResourceManager.GetString("LogEntryHelper_KeyInvalidFieldsType", resourceCulture); } } - /// - /// Looks up a localized string similar to A @key directive on type '{0}' in schema '{1}' contains invalid syntax in the 'fields' argument.. - /// internal static string LogEntryHelper_KeyInvalidSyntax { get { return ResourceManager.GetString("LogEntryHelper_KeyInvalidSyntax", resourceCulture); } } - /// - /// Looks up a localized string similar to The lookup field '{0}' in schema '{1}' must not return a list.. - /// internal static string LogEntryHelper_LookupReturnsList { get { return ResourceManager.GetString("LogEntryHelper_LookupReturnsList", resourceCulture); } } - /// - /// Looks up a localized string similar to The lookup field '{0}' in schema '{1}' should return a nullable type.. - /// internal static string LogEntryHelper_LookupReturnsNonNullableType { get { return ResourceManager.GetString("LogEntryHelper_LookupReturnsNonNullableType", resourceCulture); } } - /// - /// Looks up a localized string similar to The merged query type has no accessible fields.. - /// internal static string LogEntryHelper_NoQueries { get { return ResourceManager.GetString("LogEntryHelper_NoQueries", resourceCulture); } } - /// - /// Looks up a localized string similar to The output field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'.. - /// internal static string LogEntryHelper_OutputFieldTypesNotMergeable { get { return ResourceManager.GetString("LogEntryHelper_OutputFieldTypesNotMergeable", resourceCulture); } } - /// - /// Looks up a localized string similar to The @override directive on field '{0}' in schema '{1}' must not reference the same schema.. - /// internal static string LogEntryHelper_OverrideFromSelf { get { return ResourceManager.GetString("LogEntryHelper_OverrideFromSelf", resourceCulture); } } - /// - /// Looks up a localized string similar to The interface field '{0}' in schema '{1}' must not be annotated with the @override directive.. - /// internal static string LogEntryHelper_OverrideOnInterface { get { return ResourceManager.GetString("LogEntryHelper_OverrideOnInterface", resourceCulture); } } - /// - /// Looks up a localized string similar to The @provides directive on field '{0}' in schema '{1}' references field '{2}', which must not include directive applications.. - /// internal static string LogEntryHelper_ProvidesDirectiveInFieldsArgument { get { return ResourceManager.GetString("LogEntryHelper_ProvidesDirectiveInFieldsArgument", resourceCulture); } } - /// - /// Looks up a localized string similar to The @provides directive on field '{0}' in schema '{1}' references field '{2}', which must not have arguments.. - /// internal static string LogEntryHelper_ProvidesFieldsHasArguments { get { return ResourceManager.GetString("LogEntryHelper_ProvidesFieldsHasArguments", resourceCulture); } } - /// - /// Looks up a localized string similar to The @provides directive on field '{0}' in schema '{1}' references field '{2}', which must be marked as external.. - /// internal static string LogEntryHelper_ProvidesFieldsMissingExternal { get { return ResourceManager.GetString("LogEntryHelper_ProvidesFieldsMissingExternal", resourceCulture); } } - /// - /// Looks up a localized string similar to The @provides directive on field '{0}' in schema '{1}' must specify a string value for the 'fields' argument.. - /// internal static string LogEntryHelper_ProvidesInvalidFieldsType { get { return ResourceManager.GetString("LogEntryHelper_ProvidesInvalidFieldsType", resourceCulture); } } - /// - /// Looks up a localized string similar to The @provides directive on field '{0}' in schema '{1}' contains invalid syntax in the 'fields' argument.. - /// internal static string LogEntryHelper_ProvidesInvalidSyntax { get { return ResourceManager.GetString("LogEntryHelper_ProvidesInvalidSyntax", resourceCulture); } } - /// - /// Looks up a localized string similar to The field '{0}' in schema '{1}' includes a @provides directive, but does not return a composite type.. - /// internal static string LogEntryHelper_ProvidesOnNonCompositeField { get { return ResourceManager.GetString("LogEntryHelper_ProvidesOnNonCompositeField", resourceCulture); } } - /// - /// Looks up a localized string similar to The root query type in schema '{0}' must be accessible.. - /// internal static string LogEntryHelper_QueryRootTypeInaccessible { get { return ResourceManager.GetString("LogEntryHelper_QueryRootTypeInaccessible", resourceCulture); } } - /// - /// Looks up a localized string similar to The @require directive on argument '{0}' in schema '{1}' references field '{2}', which must not include directive applications.. - /// internal static string LogEntryHelper_RequireDirectiveInFieldsArgument { get { return ResourceManager.GetString("LogEntryHelper_RequireDirectiveInFieldsArgument", resourceCulture); } } - /// - /// Looks up a localized string similar to The @require directive on argument '{0}' in schema '{1}' must specify a string value for the 'fields' argument.. - /// internal static string LogEntryHelper_RequireInvalidFieldsType { get { return ResourceManager.GetString("LogEntryHelper_RequireInvalidFieldsType", resourceCulture); } } - /// - /// Looks up a localized string similar to The @require directive on argument '{0}' in schema '{1}' contains invalid syntax in the 'fields' argument.. - /// internal static string LogEntryHelper_RequireInvalidSyntax { get { return ResourceManager.GetString("LogEntryHelper_RequireInvalidSyntax", resourceCulture); } } - /// - /// Looks up a localized string similar to The root mutation type in schema '{0}' must be named 'Mutation'.. - /// internal static string LogEntryHelper_RootMutationUsed { get { return ResourceManager.GetString("LogEntryHelper_RootMutationUsed", resourceCulture); } } - /// - /// Looks up a localized string similar to The root query type in schema '{0}' must be named 'Query'.. - /// internal static string LogEntryHelper_RootQueryUsed { get { return ResourceManager.GetString("LogEntryHelper_RootQueryUsed", resourceCulture); } } - /// - /// Looks up a localized string similar to The root subscription type in schema '{0}' must be named 'Subscription'.. - /// internal static string LogEntryHelper_RootSubscriptionUsed { get { return ResourceManager.GetString("LogEntryHelper_RootSubscriptionUsed", resourceCulture); } } - /// - /// Looks up a localized string similar to The type '{0}' has a different kind in schema '{1}' ({2}) than it does in schema '{3}' ({4}).. - /// internal static string LogEntryHelper_TypeKindMismatch { get { return ResourceManager.GetString("LogEntryHelper_TypeKindMismatch", resourceCulture); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx index f01e7691ff6..59c5edd9890 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx @@ -78,6 +78,9 @@ The default value '{0}' of input field '{1}' in schema '{2}' differs from the default value of '{3}' in schema '{4}'. + + The merged input field '{0}' in type '{1}' cannot reference the inaccessible type '{2}'. + The input field '{0}' has a different type shape in schema '{1}' than it does in schema '{2}'. diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs index fae6c94d73c..2d0365a6cf9 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs @@ -124,6 +124,7 @@ public CompositionResult Compose() new EmptyMergedInterfaceTypeRule(), new EmptyMergedObjectTypeRule(), new EmptyMergedUnionTypeRule(), + new InputFieldReferencesInaccessibleTypeRule(), new NoQueriesRule() ]; } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRuleTests.cs new file mode 100644 index 00000000000..5268fb9161e --- /dev/null +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRuleTests.cs @@ -0,0 +1,132 @@ +using System.Collections.Immutable; +using HotChocolate.Fusion.Logging; + +namespace HotChocolate.Fusion.PostMergeValidationRules; + +public sealed class InputFieldReferencesInaccessibleTypeRuleTests : CompositionTestBase +{ + private static readonly object s_rule = new InputFieldReferencesInaccessibleTypeRule(); + private static readonly ImmutableArray s_rules = [s_rule]; + private readonly CompositionLog _log = new(); + + [Theory] + [MemberData(nameof(ValidExamplesData))] + public void Examples_Valid(string[] sdl) + { + // arrange + var schemas = CreateSchemaDefinitions(sdl); + var merger = new SourceSchemaMerger(schemas); + var mergeResult = merger.Merge(); + var validator = new PostMergeValidator(mergeResult.Value, s_rules, schemas, _log); + + // act + var result = validator.Validate(); + + // assert + Assert.True(result.IsSuccess); + Assert.True(_log.IsEmpty); + } + + [Theory] + [MemberData(nameof(InvalidExamplesData))] + public void Examples_Invalid(string[] sdl, string[] errorMessages) + { + // arrange + var schemas = CreateSchemaDefinitions(sdl); + var merger = new SourceSchemaMerger(schemas); + var mergeResult = merger.Merge(); + var validator = new PostMergeValidator(mergeResult.Value, s_rules, schemas, _log); + + // act + var result = validator.Validate(); + + // assert + Assert.True(result.IsFailure); + Assert.Equal(errorMessages, _log.Select(e => e.Message).ToArray()); + Assert.True(_log.All(e => e.Code == "INPUT_FIELD_REFERENCES_INACCESSIBLE_TYPE")); + Assert.True(_log.All(e => e.Severity == LogSeverity.Error)); + } + + public static TheoryData ValidExamplesData() + { + return new TheoryData + { + // A valid case where a public input field references another public input type. + { + [ + """ + # Schema A + input Input1 { + field1: String! + field2: Input2 + } + + input Input2 { + field3: String + } + """, + """ + # Schema B + input Input2 { + field3: String + } + """ + ] + }, + // Another valid case is where the field is not exposed in the composed schema. + { + [ + """ + # Schema A + input Input1 { + field1: String! + field2: Input2 @inaccessible + } + + input Input2 { + field3: String + } + """, + """ + # Schema B + input Input2 @inaccessible { + field3: String + } + """ + ] + } + }; + } + + public static TheoryData InvalidExamplesData() + { + return new TheoryData + { + // An invalid case is when an input field references an inaccessible type. + { + [ + """ + # Schema A + input Input1 { + field1: String! + field2: Input2! + } + + input Input2 { + field3: String + } + """, + """ + # Schema B + input Input2 @inaccessible { + field3: String + } + """ + ], + [ + "The merged input field 'field2' in type 'Input1' cannot reference the inaccessible type 'Input2'." + ] + } + }; + } +} From f99dc0f9fe7bdfbb198b57eea4a567de23baf19c Mon Sep 17 00:00:00 2001 From: Glen Date: Thu, 6 Feb 2025 14:13:36 +0200 Subject: [PATCH 2/4] Refactored --- ...nputFieldReferencesInaccessibleTypeRule.cs | 30 ++++--------------- ...ieldReferencesInaccessibleTypeRuleTests.cs | 3 +- 2 files changed, 7 insertions(+), 26 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs index 32231794e6b..4a965fdd574 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs @@ -1,5 +1,3 @@ -using System.Collections.Immutable; -using System.Diagnostics; using HotChocolate.Fusion.Events; using HotChocolate.Fusion.Events.Contracts; using HotChocolate.Fusion.Extensions; @@ -16,29 +14,10 @@ namespace HotChocolate.Fusion.PostMergeValidationRules; /// /// Specification /// -internal sealed class InputFieldReferencesInaccessibleTypeRule - : IEventHandler, IEventHandler +internal sealed class InputFieldReferencesInaccessibleTypeRule : IEventHandler { - private ImmutableHashSet? _inaccessibleTypes; - - public void Handle(SchemaEvent @event, CompositionContext context) - { - var builder = ImmutableHashSet.CreateBuilder(); - foreach (var type in @event.Schema.Types) - { - if (type.HasInaccessibleDirective()) - { - builder.Add(type.Name); - } - } - - _inaccessibleTypes = builder.ToImmutable(); - } - public void Handle(InputFieldEvent @event, CompositionContext context) { - Debug.Assert(_inaccessibleTypes is not null); - var (field, type, schema) = @event; if (field.HasInaccessibleDirective()) @@ -46,14 +25,15 @@ public void Handle(InputFieldEvent @event, CompositionContext context) return; } - var fieldTypeName = field.Type.NamedType().Name; - if (_inaccessibleTypes.Contains(fieldTypeName)) + var namedType = field.Type.NamedType(); + + if (namedType.HasInaccessibleDirective()) { context.Log.Write( InputFieldReferencesInaccessibleType( field, type.Name, - fieldTypeName, + namedType.Name, schema)); } } diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRuleTests.cs index 5268fb9161e..8b2c883004c 100644 --- a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRuleTests.cs +++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRuleTests.cs @@ -124,7 +124,8 @@ input Input2 @inaccessible { """ ], [ - "The merged input field 'field2' in type 'Input1' cannot reference the inaccessible type 'Input2'." + "The merged input field 'field2' in type 'Input1' cannot reference the " + + "inaccessible type 'Input2'." ] } }; From 9abdacb8893982ef26fa8f1b3e88e44eea10d82f Mon Sep 17 00:00:00 2001 From: Glen Date: Thu, 13 Mar 2025 18:14:47 +0200 Subject: [PATCH 3/4] Updated code (Skimmed -> Mutable) --- .../src/Fusion.Composition/Logging/LogEntryHelper.cs | 4 ++-- .../InputFieldReferencesInaccessibleTypeRule.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs index 18553d5af06..a4bfbbf6ab7 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs @@ -311,10 +311,10 @@ public static LogEntry InputFieldDefaultMismatch( } public static LogEntry InputFieldReferencesInaccessibleType( - InputFieldDefinition field, + MutableInputFieldDefinition field, string typeName, string referenceTypeName, - SchemaDefinition schema) + MutableSchemaDefinition schema) { var coordinate = new SchemaCoordinate(typeName, field.Name); diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs index 4a965fdd574..a34f01a8943 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs @@ -1,7 +1,7 @@ using HotChocolate.Fusion.Events; using HotChocolate.Fusion.Events.Contracts; using HotChocolate.Fusion.Extensions; -using HotChocolate.Skimmed; +using HotChocolate.Types; using static HotChocolate.Fusion.Logging.LogEntryHelper; namespace HotChocolate.Fusion.PostMergeValidationRules; @@ -25,15 +25,15 @@ public void Handle(InputFieldEvent @event, CompositionContext context) return; } - var namedType = field.Type.NamedType(); + var fieldType = field.Type.AsTypeDefinition(); - if (namedType.HasInaccessibleDirective()) + if (fieldType.HasFusionInaccessibleDirective()) { context.Log.Write( InputFieldReferencesInaccessibleType( field, type.Name, - namedType.Name, + fieldType.Name, schema)); } } From efdb7d51332cb35e7b35b1caeab0bba04dc4636e Mon Sep 17 00:00:00 2001 From: Glen Date: Thu, 13 Mar 2025 19:21:36 +0200 Subject: [PATCH 4/4] Fixed test --- .../InputFieldReferencesInaccessibleTypeRule.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs index a34f01a8943..99f52afa8db 100644 --- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs +++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidationRules/InputFieldReferencesInaccessibleTypeRule.cs @@ -20,7 +20,7 @@ public void Handle(InputFieldEvent @event, CompositionContext context) { var (field, type, schema) = @event; - if (field.HasInaccessibleDirective()) + if (field.HasFusionInaccessibleDirective()) { return; }