diff --git a/dictionary.txt b/dictionary.txt
index fdebf7dec77..0cdce2781d9 100644
--- a/dictionary.txt
+++ b/dictionary.txt
@@ -136,6 +136,7 @@ Rgba
Rhai
Roslynator
runbooks
+Satisfiability
Senn
shoooe
Skywalker
diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props
index 22a48f4fd9f..8018a15419c 100644
--- a/src/Directory.Packages.props
+++ b/src/Directory.Packages.props
@@ -60,8 +60,8 @@
-
+
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionContext.cs
new file mode 100644
index 00000000000..a356991c860
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/CompositionContext.cs
@@ -0,0 +1,20 @@
+using System.Collections.Immutable;
+using HotChocolate.Fusion.Logging.Contracts;
+using HotChocolate.Skimmed;
+
+namespace HotChocolate.Fusion;
+
+internal sealed class CompositionContext(
+ ImmutableArray schemaDefinitions,
+ ICompositionLog compositionLog)
+{
+ ///
+ /// Gets the schema definitions.
+ ///
+ public ImmutableArray SchemaDefinitions { get; } = schemaDefinitions;
+
+ ///
+ /// Gets the composition log.
+ ///
+ public ICompositionLog Log { get; } = compositionLog;
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Errors/CompositionError.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Errors/CompositionError.cs
new file mode 100644
index 00000000000..c4081105cd5
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Errors/CompositionError.cs
@@ -0,0 +1,6 @@
+namespace HotChocolate.Fusion.Errors;
+
+public sealed class CompositionError(string message)
+{
+ public string Message { get; } = message;
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Errors/ErrorHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Errors/ErrorHelper.cs
new file mode 100644
index 00000000000..4841f244835
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Errors/ErrorHelper.cs
@@ -0,0 +1,10 @@
+using HotChocolate.Fusion.PreMergeValidation.Contracts;
+using static HotChocolate.Fusion.Properties.CompositionResources;
+
+namespace HotChocolate.Fusion.Errors;
+
+internal static class ErrorHelper
+{
+ public static CompositionError PreMergeValidationRuleFailed(IPreMergeValidationRule rule)
+ => new(string.Format(ErrorHelper_PreMergeValidationRuleFailed, rule.GetType().Name));
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Extensions/TypeDefinitionExtensions.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Extensions/TypeDefinitionExtensions.cs
new file mode 100644
index 00000000000..741b60e4fd1
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Extensions/TypeDefinitionExtensions.cs
@@ -0,0 +1,16 @@
+using HotChocolate.Skimmed;
+
+namespace HotChocolate.Fusion.Extensions;
+
+internal static class TypeDefinitionExtensions
+{
+ public static ITypeDefinition InnerNullableType(this ITypeDefinition type)
+ {
+ return type switch
+ {
+ ListTypeDefinition listType => listType.ElementType.NullableType(),
+ NonNullTypeDefinition nonNullType => nonNullType.NullableType,
+ _ => type
+ };
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj
index f11759cfaec..a7d4025054b 100644
--- a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/HotChocolate.Fusion.Composition.csproj
@@ -1,8 +1,31 @@
-
+
HotChocolate.Fusion.Composition
HotChocolate.Fusion
+
+
+
+
+
+
+
+
+
+
+ ResXFileCodeGenerator
+ CompositionResources.Designer.cs
+
+
+
+
+
+ True
+ True
+ CompositionResources.resx
+
+
+
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs
new file mode 100644
index 00000000000..2f9cc4b9cea
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/CompositionLog.cs
@@ -0,0 +1,27 @@
+using System.Collections;
+using HotChocolate.Fusion.Logging.Contracts;
+
+namespace HotChocolate.Fusion.Logging;
+
+public sealed class CompositionLog : ICompositionLog, IEnumerable
+{
+ public bool IsEmpty => _entries.Count == 0;
+
+ private readonly List _entries = [];
+
+ public void Write(LogEntry entry)
+ {
+ ArgumentNullException.ThrowIfNull(entry);
+
+ _entries.Add(entry);
+ }
+
+ public ILoggingSession CreateSession()
+ {
+ return new LoggingSession(this);
+ }
+
+ public IEnumerator GetEnumerator() => _entries.GetEnumerator();
+
+ IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs
new file mode 100644
index 00000000000..1251c6adf64
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ICompositionLog.cs
@@ -0,0 +1,21 @@
+namespace HotChocolate.Fusion.Logging.Contracts;
+
+///
+/// Defines an interface for logging composition information, warnings, and errors.
+///
+public interface ICompositionLog
+{
+ bool IsEmpty { get; }
+
+ ///
+ /// Writes the specified to the log.
+ ///
+ /// The to write.
+ void Write(LogEntry entry);
+
+ ///
+ /// Creates a new logging session that keeps track of the number of info, warning, and error
+ /// entries logged.
+ ///
+ ILoggingSession CreateSession();
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ILoggingSession.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ILoggingSession.cs
new file mode 100644
index 00000000000..56f1b5b7547
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/Contracts/ILoggingSession.cs
@@ -0,0 +1,12 @@
+namespace HotChocolate.Fusion.Logging.Contracts;
+
+public interface ILoggingSession
+{
+ int InfoCount { get; }
+
+ int WarningCount { get; }
+
+ int ErrorCount { get; }
+
+ void Write(LogEntry entry);
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntry.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntry.cs
new file mode 100644
index 00000000000..cad3c9a3d28
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntry.cs
@@ -0,0 +1,71 @@
+using HotChocolate.Skimmed;
+
+namespace HotChocolate.Fusion.Logging;
+
+///
+/// Represents an entry in a composition log that describes an issue encountered during the
+/// composition process.
+///
+public sealed record LogEntry
+{
+ ///
+ /// Initializes a new instance of the record with the specified values.
+ ///
+ public LogEntry(
+ string message,
+ string code,
+ LogSeverity severity = LogSeverity.Error,
+ SchemaCoordinate? coordinate = null,
+ ITypeSystemMemberDefinition? member = null,
+ SchemaDefinition? schema = null,
+ object? extension = null)
+ {
+ ArgumentNullException.ThrowIfNull(message);
+ ArgumentNullException.ThrowIfNull(code);
+
+ Message = message;
+ Code = code;
+ Severity = severity;
+ Coordinate = coordinate;
+ Member = member;
+ Schema = schema;
+ Extension = extension;
+ }
+
+ ///
+ /// Gets the message associated with this log entry.
+ ///
+ public string Message { get; }
+
+ ///
+ /// Gets the code associated with this log entry.
+ ///
+ public string Code { get; }
+
+ ///
+ /// Gets the severity of this log entry.
+ ///
+ public LogSeverity Severity { get; }
+
+ ///
+ /// Gets the schema coordinate associated with this log entry.
+ ///
+ public SchemaCoordinate? Coordinate { get; }
+
+ ///
+ /// Gets the type system member associated with this log entry.
+ ///
+ public ITypeSystemMemberDefinition? Member { get; }
+
+ ///
+ /// Gets the schema associated with this log entry.
+ ///
+ public SchemaDefinition? Schema { get; }
+
+ ///
+ /// Gets the extension object associated with this log entry.
+ ///
+ public object? Extension { get; }
+
+ public override string ToString() => Message;
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs
new file mode 100644
index 00000000000..298f766cf33
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryCodes.cs
@@ -0,0 +1,7 @@
+namespace HotChocolate.Fusion.Logging;
+
+public static class LogEntryCodes
+{
+ public const string DisallowedInaccessible = "DISALLOWED_INACCESSIBLE";
+ public const string OutputFieldTypesNotMergeable = "OUTPUT_FIELD_TYPES_NOT_MERGEABLE";
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs
new file mode 100644
index 00000000000..f678870839e
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogEntryHelper.cs
@@ -0,0 +1,88 @@
+using HotChocolate.Skimmed;
+using static HotChocolate.Fusion.Properties.CompositionResources;
+
+namespace HotChocolate.Fusion.Logging;
+
+internal static class LogEntryHelper
+{
+ public static LogEntry DisallowedInaccessibleScalar(
+ ScalarTypeDefinition scalar,
+ SchemaDefinition schema)
+ => new(
+ string.Format(LogEntryHelper_DisallowedInaccessibleScalar, scalar.Name),
+ LogEntryCodes.DisallowedInaccessible,
+ LogSeverity.Error,
+ new SchemaCoordinate(scalar.Name),
+ scalar,
+ schema);
+
+ public static LogEntry DisallowedInaccessibleIntrospectionType(
+ INamedTypeDefinition type,
+ SchemaDefinition schema)
+ => new(
+ string.Format(LogEntryHelper_DisallowedInaccessibleIntrospectionType, type.Name),
+ LogEntryCodes.DisallowedInaccessible,
+ LogSeverity.Error,
+ new SchemaCoordinate(type.Name),
+ type,
+ schema);
+
+ public static LogEntry DisallowedInaccessibleIntrospectionField(
+ OutputFieldDefinition field,
+ string typeName,
+ SchemaDefinition schema)
+ => new(
+ string.Format(
+ LogEntryHelper_DisallowedInaccessibleIntrospectionField,
+ field.Name,
+ typeName),
+ LogEntryCodes.DisallowedInaccessible,
+ LogSeverity.Error,
+ new SchemaCoordinate(typeName, field.Name),
+ field,
+ schema);
+
+ public static LogEntry DisallowedInaccessibleIntrospectionArgument(
+ InputFieldDefinition argument,
+ string fieldName,
+ string typeName,
+ SchemaDefinition schema)
+ {
+ var coordinate = new SchemaCoordinate(typeName, fieldName, argument.Name);
+
+ return new LogEntry(
+ string.Format(
+ LogEntryHelper_DisallowedInaccessibleIntrospectionArgument,
+ argument.Name,
+ coordinate),
+ LogEntryCodes.DisallowedInaccessible,
+ LogSeverity.Error,
+ coordinate,
+ argument,
+ schema);
+ }
+
+ public static LogEntry DisallowedInaccessibleDirectiveArgument(
+ InputFieldDefinition argument,
+ string directiveName,
+ SchemaDefinition schema)
+ => new(
+ string.Format(
+ LogEntryHelper_DisallowedInaccessibleDirectiveArgument,
+ argument.Name,
+ directiveName),
+ LogEntryCodes.DisallowedInaccessible,
+ LogSeverity.Error,
+ new SchemaCoordinate(directiveName, argumentName: argument.Name, ofDirective: true),
+ schema: schema);
+
+ public static LogEntry OutputFieldTypesNotMergeable(string typeName, string fieldName)
+ => new(
+ string.Format(
+ LogEntryHelper_OutputFieldTypesNotMergeable,
+ typeName,
+ fieldName),
+ LogEntryCodes.OutputFieldTypesNotMergeable,
+ LogSeverity.Error,
+ new SchemaCoordinate(typeName, fieldName));
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogSeverity.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogSeverity.cs
new file mode 100644
index 00000000000..70cc9994b49
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LogSeverity.cs
@@ -0,0 +1,22 @@
+namespace HotChocolate.Fusion.Logging;
+
+///
+/// Defines the severity of the log entry.
+///
+public enum LogSeverity
+{
+ ///
+ /// The entry contains an informational message.
+ ///
+ Info,
+
+ ///
+ /// The entry contains a warning message.
+ ///
+ Warning,
+
+ ///
+ /// The entry contains an error message.
+ ///
+ Error
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LoggingSession.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LoggingSession.cs
new file mode 100644
index 00000000000..6ee1b23df63
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Logging/LoggingSession.cs
@@ -0,0 +1,37 @@
+using HotChocolate.Fusion.Logging.Contracts;
+
+namespace HotChocolate.Fusion.Logging;
+
+public sealed class LoggingSession(ICompositionLog compositionLog) : ILoggingSession
+{
+ public int InfoCount { get; private set; }
+
+ public int WarningCount { get; private set; }
+
+ public int ErrorCount { get; private set; }
+
+ public void Write(LogEntry entry)
+ {
+ ArgumentNullException.ThrowIfNull(entry);
+
+ switch (entry.Severity)
+ {
+ case LogSeverity.Info:
+ InfoCount++;
+ break;
+
+ case LogSeverity.Warning:
+ WarningCount++;
+ break;
+
+ case LogSeverity.Error:
+ ErrorCount++;
+ break;
+
+ default:
+ throw new InvalidOperationException();
+ }
+
+ compositionLog.Write(entry);
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidation/PostMergeValidator.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidation/PostMergeValidator.cs
new file mode 100644
index 00000000000..fb5234a33b3
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PostMergeValidation/PostMergeValidator.cs
@@ -0,0 +1,13 @@
+using HotChocolate.Fusion.Results;
+using HotChocolate.Skimmed;
+
+namespace HotChocolate.Fusion.PostMergeValidation;
+
+internal sealed class PostMergeValidator
+{
+ public CompositionResult Validate(SchemaDefinition _)
+ {
+ // FIXME: Implement.
+ return CompositionResult.Success();
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Contracts/IPreMergeValidationRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Contracts/IPreMergeValidationRule.cs
new file mode 100644
index 00000000000..a9a60320a91
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Contracts/IPreMergeValidationRule.cs
@@ -0,0 +1,8 @@
+using HotChocolate.Fusion.Results;
+
+namespace HotChocolate.Fusion.PreMergeValidation.Contracts;
+
+internal interface IPreMergeValidationRule
+{
+ CompositionResult Run(PreMergeValidationContext context);
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs
new file mode 100644
index 00000000000..538df65d0ca
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidationContext.cs
@@ -0,0 +1,77 @@
+using System.Collections.Immutable;
+using HotChocolate.Fusion.Logging.Contracts;
+using HotChocolate.Skimmed;
+
+namespace HotChocolate.Fusion.PreMergeValidation;
+
+internal sealed class PreMergeValidationContext(CompositionContext context)
+{
+ public ImmutableArray SchemaDefinitions => context.SchemaDefinitions;
+ public ICompositionLog Log => context.Log;
+ public ImmutableArray OutputTypeInfo = [];
+
+ public void Initialize()
+ {
+ InitializeOutputTypeInfo();
+ }
+
+ ///
+ /// Initializes a structure that makes it easier to access combined output types, fields, and
+ /// arguments for validation purposes.
+ ///
+ private void InitializeOutputTypeInfo()
+ {
+ OutputTypeInfo =
+ [
+ .. SchemaDefinitions
+ .SelectMany(s => s.Types)
+ .Where(t => t.IsOutputType())
+ .OfType()
+ .GroupBy(
+ t => t.Name,
+ (typeName, types) =>
+ {
+ types = types.ToImmutableArray();
+
+ var fieldInfo = types
+ .SelectMany(t => t.Fields)
+ .GroupBy(
+ f => f.Name,
+ (fieldName, fields) =>
+ {
+ fields = fields.ToImmutableArray();
+
+ var argumentInfo = fields
+ .SelectMany(f => f.Arguments)
+ .GroupBy(
+ a => a.Name,
+ (argumentName, arguments) =>
+ new OutputArgumentInfo(
+ argumentName,
+ [.. arguments]));
+
+ return new OutputFieldInfo(
+ fieldName,
+ [.. fields],
+ [.. argumentInfo]);
+ });
+
+ return new OutputTypeInfo(typeName, [.. types], [.. fieldInfo]);
+ })
+ ];
+ }
+}
+
+internal record OutputTypeInfo(
+ string TypeName,
+ ImmutableArray Types,
+ ImmutableArray FieldInfo);
+
+internal record OutputFieldInfo(
+ string FieldName,
+ ImmutableArray Fields,
+ ImmutableArray Arguments);
+
+internal record OutputArgumentInfo(
+ string ArgumentName,
+ ImmutableArray Arguments);
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs
new file mode 100644
index 00000000000..1d812523f12
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/PreMergeValidator.cs
@@ -0,0 +1,36 @@
+using System.Collections.Immutable;
+using HotChocolate.Fusion.Errors;
+using HotChocolate.Fusion.PreMergeValidation.Contracts;
+using HotChocolate.Fusion.PreMergeValidation.Rules;
+using HotChocolate.Fusion.Results;
+
+namespace HotChocolate.Fusion.PreMergeValidation;
+
+internal sealed class PreMergeValidator
+{
+ private readonly ImmutableArray _validationRules =
+ [
+ new DisallowedInaccessibleElementsRule(),
+ new OutputFieldTypesMergeableRule()
+ ];
+
+ public CompositionResult Validate(CompositionContext compositionContext)
+ {
+ var preMergeValidationContext = new PreMergeValidationContext(compositionContext);
+ preMergeValidationContext.Initialize();
+
+ var errors = new List();
+
+ foreach (var validationRule in _validationRules)
+ {
+ var result = validationRule.Run(preMergeValidationContext);
+
+ if (result.IsFailure)
+ {
+ errors.AddRange(result.Errors);
+ }
+ }
+
+ return errors;
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs
new file mode 100644
index 00000000000..b01d1f11c78
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/DisallowedInaccessibleElementsRule.cs
@@ -0,0 +1,94 @@
+using HotChocolate.Fusion.Errors;
+using HotChocolate.Fusion.PreMergeValidation.Contracts;
+using HotChocolate.Fusion.Results;
+using HotChocolate.Skimmed;
+using static HotChocolate.Fusion.Logging.LogEntryHelper;
+
+namespace HotChocolate.Fusion.PreMergeValidation.Rules;
+
+///
+/// This rule ensures that certain essential elements of a GraphQL schema, particularly built-in
+/// scalars, directive arguments, and introspection types, cannot be marked as @inaccessible. These
+/// types are fundamental to GraphQL. Making these elements inaccessible would break core GraphQL
+/// functionality.
+///
+///
+/// Specification
+///
+internal sealed class DisallowedInaccessibleElementsRule : IPreMergeValidationRule
+{
+ public CompositionResult Run(PreMergeValidationContext context)
+ {
+ var loggingSession = context.Log.CreateSession();
+
+ foreach (var schema in context.SchemaDefinitions)
+ {
+ foreach (var type in schema.Types)
+ {
+ if (type is ScalarTypeDefinition { IsSpecScalar: true } scalar
+ && !ValidationHelper.IsAccessible(type))
+ {
+ loggingSession.Write(DisallowedInaccessibleScalar(scalar, schema));
+ }
+
+ if (type.IsIntrospectionType)
+ {
+ if (!ValidationHelper.IsAccessible(type))
+ {
+ loggingSession.Write(DisallowedInaccessibleIntrospectionType(type, schema));
+ }
+
+ if (type is ComplexTypeDefinition complexType)
+ {
+ foreach (var field in complexType.Fields)
+ {
+ if (!ValidationHelper.IsAccessible(field))
+ {
+ loggingSession.Write(
+ DisallowedInaccessibleIntrospectionField(
+ field,
+ type.Name,
+ schema));
+ }
+
+ foreach (var argument in field.Arguments)
+ {
+ if (!ValidationHelper.IsAccessible(argument))
+ {
+ loggingSession.Write(
+ DisallowedInaccessibleIntrospectionArgument(
+ argument,
+ field.Name,
+ type.Name,
+ schema));
+ }
+ }
+ }
+ }
+ }
+ }
+
+ foreach (var directive in schema.DirectiveDefinitions)
+ {
+ if (BuiltIns.IsBuiltInDirective(directive.Name))
+ {
+ foreach (var argument in directive.Arguments)
+ {
+ if (!ValidationHelper.IsAccessible(argument))
+ {
+ loggingSession.Write(
+ DisallowedInaccessibleDirectiveArgument(
+ argument,
+ directive.Name,
+ schema));
+ }
+ }
+ }
+ }
+ }
+
+ return loggingSession.ErrorCount == 0
+ ? CompositionResult.Success()
+ : ErrorHelper.PreMergeValidationRuleFailed(this);
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs
new file mode 100644
index 00000000000..e93c6e91b14
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/PreMergeValidation/Rules/OutputFieldTypesMergeableRule.cs
@@ -0,0 +1,39 @@
+using HotChocolate.Fusion.Errors;
+using HotChocolate.Fusion.Logging;
+using HotChocolate.Fusion.PreMergeValidation.Contracts;
+using HotChocolate.Fusion.Results;
+
+namespace HotChocolate.Fusion.PreMergeValidation.Rules;
+
+///
+/// Fields on objects or interfaces that have the same name are considered semantically equivalent
+/// and mergeable when they have a mergeable field type.
+///
+///
+/// Specification
+///
+internal sealed class OutputFieldTypesMergeableRule : IPreMergeValidationRule
+{
+ public CompositionResult Run(PreMergeValidationContext context)
+ {
+ var loggingSession = context.Log.CreateSession();
+
+ foreach (var outputTypeInfo in context.OutputTypeInfo)
+ {
+ foreach (var fieldInfo in outputTypeInfo.FieldInfo)
+ {
+ if (!ValidationHelper.FieldsAreMergeable(fieldInfo.Fields))
+ {
+ loggingSession.Write(
+ LogEntryHelper.OutputFieldTypesNotMergeable(
+ fieldInfo.FieldName,
+ outputTypeInfo.TypeName));
+ }
+ }
+ }
+
+ return loggingSession.ErrorCount == 0
+ ? CompositionResult.Success()
+ : ErrorHelper.PreMergeValidationRuleFailed(this);
+ }
+}
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
new file mode 100644
index 00000000000..b3a9cb1b02d
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.Designer.cs
@@ -0,0 +1,125 @@
+//------------------------------------------------------------------------------
+//
+// This code was generated by a tool.
+//
+// Changes to this file may cause incorrect behavior and will be lost if
+// the code is regenerated.
+//
+//------------------------------------------------------------------------------
+
+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()]
+ internal class CompositionResources {
+
+ private static global::System.Resources.ResourceManager resourceMan;
+
+ private static global::System.Globalization.CultureInfo resourceCulture;
+
+ [global::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 {
+ get {
+ if (object.ReferenceEquals(resourceMan, null)) {
+ global::System.Resources.ResourceManager temp = new global::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 {
+ get {
+ return resourceCulture;
+ }
+ set {
+ resourceCulture = value;
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Pre-merge validation rule '{0}' failed. View the composition log for details..
+ ///
+ internal static string ErrorHelper_PreMergeValidationRuleFailed {
+ get {
+ return ResourceManager.GetString("ErrorHelper_PreMergeValidationRuleFailed", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The argument '{0}' on built-in directive type '{1}' is not accessible..
+ ///
+ internal static string LogEntryHelper_DisallowedInaccessibleDirectiveArgument {
+ get {
+ return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleDirectiveArgument", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The introspection argument '{0}' with schema coordinate '{1}' is not accessible..
+ ///
+ internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionArgument {
+ get {
+ return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionArgument", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The introspection field '{0}' on type '{1}' is not accessible..
+ ///
+ internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionField {
+ get {
+ return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionField", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The introspection type '{0}' is not accessible..
+ ///
+ internal static string LogEntryHelper_DisallowedInaccessibleIntrospectionType {
+ get {
+ return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleIntrospectionType", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to The built-in scalar type '{0}' is not accessible..
+ ///
+ internal static string LogEntryHelper_DisallowedInaccessibleScalar {
+ get {
+ return ResourceManager.GetString("LogEntryHelper_DisallowedInaccessibleScalar", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Field '{0}' on type '{1}' is not mergeable..
+ ///
+ internal static string LogEntryHelper_OutputFieldTypesNotMergeable {
+ get {
+ return ResourceManager.GetString("LogEntryHelper_OutputFieldTypesNotMergeable", 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
new file mode 100644
index 00000000000..d291d0f232a
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Properties/CompositionResources.resx
@@ -0,0 +1,42 @@
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 1.3
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Pre-merge validation rule '{0}' failed. View the composition log for details.
+
+
+ The built-in scalar type '{0}' is not accessible.
+
+
+ The introspection type '{0}' is not accessible.
+
+
+ The introspection field '{0}' on type '{1}' is not accessible.
+
+
+ The introspection argument '{0}' with schema coordinate '{1}' is not accessible.
+
+
+ The argument '{0}' on built-in directive type '{1}' is not accessible.
+
+
+ Field '{0}' on type '{1}' is not mergeable.
+
+
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs
new file mode 100644
index 00000000000..4bbe1f88e62
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult.cs
@@ -0,0 +1,67 @@
+using System.Collections.Immutable;
+using HotChocolate.Fusion.Errors;
+
+namespace HotChocolate.Fusion.Results;
+
+public readonly record struct CompositionResult
+{
+ public bool IsFailure { get; }
+
+ public bool IsSuccess { get; }
+
+ public ImmutableArray Errors { get; } = [];
+
+ public CompositionResult()
+ {
+ IsSuccess = true;
+ }
+
+ private CompositionResult(CompositionError error)
+ {
+ Errors = [error];
+ IsFailure = true;
+ }
+
+ private CompositionResult(ImmutableArray errors)
+ {
+ if (errors.Length == 0)
+ {
+ IsSuccess = true;
+ }
+ else
+ {
+ Errors = errors;
+ IsFailure = true;
+ }
+ }
+
+ public static CompositionResult Success() => new();
+
+ ///
+ /// Creates a from a composition error.
+ ///
+ public static implicit operator CompositionResult(CompositionError error)
+ {
+ ArgumentNullException.ThrowIfNull(error);
+
+ return new CompositionResult(error);
+ }
+
+ ///
+ /// Creates a from an array of composition errors.
+ ///
+ public static implicit operator CompositionResult(ImmutableArray errors)
+ {
+ return new CompositionResult(errors);
+ }
+
+ ///
+ /// Creates a from a list of composition errors.
+ ///
+ public static implicit operator CompositionResult(List errors)
+ {
+ ArgumentNullException.ThrowIfNull(errors);
+
+ return new CompositionResult([.. errors]);
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs
new file mode 100644
index 00000000000..6e801fac65c
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/Results/CompositionResult~1.cs
@@ -0,0 +1,79 @@
+using System.Collections.Immutable;
+using HotChocolate.Fusion.Errors;
+
+namespace HotChocolate.Fusion.Results;
+
+public readonly record struct CompositionResult
+{
+ public bool IsFailure { get; }
+
+ public bool IsSuccess { get; }
+
+ public ImmutableArray Errors { get; } = [];
+
+ public TValue Value => IsSuccess
+ ? _value
+ : throw new Exception("Value may not be accessed on an unsuccessful result.");
+
+ private readonly TValue _value = default!;
+
+ private CompositionResult(TValue value)
+ {
+ _value = value;
+ IsSuccess = true;
+ }
+
+ private CompositionResult(CompositionError error)
+ {
+ Errors = [error];
+ IsFailure = true;
+ }
+
+ private CompositionResult(ImmutableArray errors)
+ {
+ if (errors.Length == 0)
+ {
+ IsSuccess = true;
+ }
+ else
+ {
+ Errors = errors;
+ IsFailure = true;
+ }
+ }
+
+ ///
+ /// Creates a from a value.
+ ///
+ public static implicit operator CompositionResult(TValue value)
+ {
+ return new CompositionResult(value);
+ }
+
+ ///
+ /// Creates a from a composition error.
+ ///
+ public static implicit operator CompositionResult(CompositionError error)
+ {
+ ArgumentNullException.ThrowIfNull(error);
+
+ return new CompositionResult(error);
+ }
+
+ ///
+ /// Creates a from an array of composition errors.
+ ///
+ public static implicit operator CompositionResult(
+ ImmutableArray errors)
+ {
+ return new CompositionResult(errors);
+ }
+
+ ///
+ /// Creates a from a .
+ ///
+ public static implicit operator CompositionResult(CompositionResult result)
+ {
+ return new CompositionResult(result.Errors);
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SatisfiabilityValidator.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SatisfiabilityValidator.cs
new file mode 100644
index 00000000000..70be0ec4862
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SatisfiabilityValidator.cs
@@ -0,0 +1,13 @@
+using HotChocolate.Fusion.Results;
+using HotChocolate.Skimmed;
+
+namespace HotChocolate.Fusion;
+
+internal sealed class SatisfiabilityValidator
+{
+ public CompositionResult Validate(SchemaDefinition _)
+ {
+ // FIXME: Implement.
+ return CompositionResult.Success();
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs
new file mode 100644
index 00000000000..e4525fba52c
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SchemaComposer.cs
@@ -0,0 +1,44 @@
+using HotChocolate.Fusion.Logging.Contracts;
+using HotChocolate.Fusion.Results;
+using HotChocolate.Skimmed;
+
+namespace HotChocolate.Fusion;
+
+public sealed class SchemaComposer
+{
+ public CompositionResult Compose(
+ IEnumerable schemaDefinitions,
+ ICompositionLog compositionLog)
+ {
+ ArgumentNullException.ThrowIfNull(schemaDefinitions);
+ ArgumentNullException.ThrowIfNull(compositionLog);
+
+ var context = new CompositionContext([.. schemaDefinitions], compositionLog);
+
+ // Validate Source Schemas
+ var validationResult = new SourceSchemaValidator().Validate(context);
+
+ if (validationResult.IsFailure)
+ {
+ return validationResult;
+ }
+
+ // Merge Source Schemas
+ var mergeResult = new SourceSchemaMerger().Merge(context);
+
+ if (mergeResult.IsFailure)
+ {
+ return mergeResult;
+ }
+
+ // Validate Satisfiability
+ var satisfiabilityResult = new SatisfiabilityValidator().Validate(mergeResult.Value);
+
+ if (satisfiabilityResult.IsFailure)
+ {
+ return satisfiabilityResult;
+ }
+
+ return mergeResult;
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs
new file mode 100644
index 00000000000..eb2266bc711
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaMerger.cs
@@ -0,0 +1,44 @@
+using HotChocolate.Fusion.PostMergeValidation;
+using HotChocolate.Fusion.PreMergeValidation;
+using HotChocolate.Fusion.Results;
+using HotChocolate.Skimmed;
+
+namespace HotChocolate.Fusion;
+
+internal sealed class SourceSchemaMerger
+{
+ public CompositionResult Merge(CompositionContext context)
+ {
+ // Pre Merge Validation
+ var preMergeValidationResult = new PreMergeValidator().Validate(context);
+
+ if (preMergeValidationResult.IsFailure)
+ {
+ return preMergeValidationResult;
+ }
+
+ // Merge
+ var mergeResult = MergeSchemaDefinitions(context);
+
+ if (mergeResult.IsFailure)
+ {
+ return mergeResult;
+ }
+
+ // Post Merge Validation
+ var postMergeValidationResult = new PostMergeValidator().Validate(mergeResult.Value);
+
+ if (postMergeValidationResult.IsFailure)
+ {
+ return postMergeValidationResult;
+ }
+
+ return mergeResult;
+ }
+
+ private CompositionResult MergeSchemaDefinitions(CompositionContext _)
+ {
+ // FIXME: Implement.
+ return new SchemaDefinition();
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaValidator.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaValidator.cs
new file mode 100644
index 00000000000..67e91bc4258
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/SourceSchemaValidator.cs
@@ -0,0 +1,12 @@
+using HotChocolate.Fusion.Results;
+
+namespace HotChocolate.Fusion;
+
+internal sealed class SourceSchemaValidator
+{
+ public CompositionResult Validate(CompositionContext _)
+ {
+ // FIXME: Implement.
+ return CompositionResult.Success();
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs
new file mode 100644
index 00000000000..85d98039087
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/ValidationHelper.cs
@@ -0,0 +1,73 @@
+using System.Collections.Immutable;
+using HotChocolate.Skimmed;
+
+namespace HotChocolate.Fusion;
+
+internal sealed class ValidationHelper
+{
+ public static bool FieldsAreMergeable(ImmutableArray fields)
+ {
+ for (var i = 0; i < fields.Length - 1; i++)
+ {
+ var typeA = fields[i].Type;
+ var typeB = fields[i + 1].Type;
+
+ if (!SameTypeShape(typeA, typeB))
+ {
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public static bool IsAccessible(IDirectivesProvider type)
+ {
+ return !type.Directives.ContainsName(WellKnownDirectiveNames.Inaccessible);
+ }
+
+ public static bool SameTypeShape(ITypeDefinition typeA, ITypeDefinition typeB)
+ {
+ while (true)
+ {
+ if (typeA is NonNullTypeDefinition && typeB is not NonNullTypeDefinition)
+ {
+ typeA = typeA.InnerType();
+
+ continue;
+ }
+
+ if (typeB is NonNullTypeDefinition && typeA is not NonNullTypeDefinition)
+ {
+ typeB = typeB.InnerType();
+
+ continue;
+ }
+
+ if (typeA is ListTypeDefinition || typeB is ListTypeDefinition)
+ {
+ if (typeA is not ListTypeDefinition || typeB is not ListTypeDefinition)
+ {
+ return false;
+ }
+
+ typeA = typeA.InnerType();
+ typeB = typeB.InnerType();
+
+ continue;
+ }
+
+ if (typeA.Kind != typeB.Kind)
+ {
+ return false;
+ }
+
+ if (typeA.NamedType().Name != typeB.NamedType().Name)
+ {
+ return false;
+ }
+
+ return true;
+ }
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/WellKnownDirectiveNames.cs b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/WellKnownDirectiveNames.cs
new file mode 100644
index 00000000000..b4cf8adf77c
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/src/Fusion.Composition/WellKnownDirectiveNames.cs
@@ -0,0 +1,6 @@
+namespace HotChocolate.Fusion;
+
+internal static class WellKnownDirectiveNames
+{
+ public const string Inaccessible = "inaccessible";
+}
diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/HotChocolate.Fusion.Composition.Tests.csproj b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/HotChocolate.Fusion.Composition.Tests.csproj
index b2fc6a7f637..7c9e08f086f 100644
--- a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/HotChocolate.Fusion.Composition.Tests.csproj
+++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/HotChocolate.Fusion.Composition.Tests.csproj
@@ -1,4 +1,4 @@
-
+
HotChocolate.Fusion.Composition.Tests
@@ -11,4 +11,8 @@
+
+
+
+
diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs
new file mode 100644
index 00000000000..b46fee89348
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/DisallowedInaccessibleElementsRuleTests.cs
@@ -0,0 +1,130 @@
+using HotChocolate.Fusion;
+using HotChocolate.Fusion.Logging;
+using HotChocolate.Fusion.PreMergeValidation;
+using HotChocolate.Fusion.PreMergeValidation.Rules;
+using HotChocolate.Skimmed.Serialization;
+
+namespace HotChocolate.Composition.PreMergeValidation.Rules;
+
+public sealed class DisallowedInaccessibleElementsRuleTests
+{
+ [Test]
+ [MethodDataSource(nameof(ValidExamplesData))]
+ public async Task Examples_Valid(string[] sdl)
+ {
+ // arrange
+ var log = new CompositionLog();
+ var context = new PreMergeValidationContext(
+ new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log));
+
+ context.Initialize();
+
+ // act
+ var result = new DisallowedInaccessibleElementsRule().Run(context);
+
+ // assert
+ await Assert.That(result.IsSuccess).IsTrue();
+ await Assert.That(log.IsEmpty).IsTrue();
+ }
+
+ [Test]
+ [MethodDataSource(nameof(InvalidExamplesData))]
+ public async Task Examples_Invalid(string[] sdl)
+ {
+ // arrange
+ var log = new CompositionLog();
+ var context = new PreMergeValidationContext(
+ new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log));
+
+ context.Initialize();
+
+ // act
+ var result = new DisallowedInaccessibleElementsRule().Run(context);
+
+ // assert
+ await Assert.That(result.IsFailure).IsTrue();
+ await Assert.That(log.Count()).IsEqualTo(1);
+ await Assert.That(log.First().Code).IsEqualTo("DISALLOWED_INACCESSIBLE");
+ await Assert.That(log.First().Severity).IsEqualTo(LogSeverity.Error);
+ }
+
+ public static IEnumerable> ValidExamplesData()
+ {
+ return
+ [
+ // Here, the String type is not marked as @inaccessible, which adheres to the rule.
+ () =>
+ [
+ """
+ type Product {
+ price: Float
+ name: String
+ }
+ """
+ ]
+ ];
+ }
+
+ public static IEnumerable> InvalidExamplesData()
+ {
+ return
+ [
+ // In this example, the String scalar is marked as @inaccessible. This violates the rule
+ // because String is a required built-in type that cannot be inaccessible.
+ () =>
+ [
+ """
+ scalar String @inaccessible
+
+ type Product {
+ price: Float
+ name: String
+ }
+ """
+ ],
+ // In this example, the introspection type __Type is marked as @inaccessible. This
+ // violates the rule because introspection types must remain accessible for GraphQL
+ // introspection queries to work.
+ () =>
+ [
+ """
+ type __Type @inaccessible {
+ kind: __TypeKind!
+ name: String
+ fields(includeDeprecated: Boolean = false): [__Field!]
+ }
+ """
+ ],
+ // Inaccessible introspection field.
+ () =>
+ [
+ """
+ type __Type {
+ kind: __TypeKind! @inaccessible
+ name: String
+ fields(includeDeprecated: Boolean = false): [__Field!]
+ }
+ """
+ ],
+ // Inaccessible introspection argument.
+ () =>
+ [
+ """
+ type __Type {
+ kind: __TypeKind!
+ name: String
+ fields(includeDeprecated: Boolean = false @inaccessible): [__Field!]
+ }
+ """
+ ],
+ // Inaccessible built-in directive argument.
+ () =>
+ [
+ """
+ directive @skip(if: Boolean! @inaccessible)
+ on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
+ """
+ ]
+ ];
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs
new file mode 100644
index 00000000000..998ee86ef0d
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/PreMergeValidation/Rules/OutputFieldTypesMergeableRuleTests.cs
@@ -0,0 +1,144 @@
+using HotChocolate.Fusion;
+using HotChocolate.Fusion.Logging;
+using HotChocolate.Fusion.PreMergeValidation;
+using HotChocolate.Fusion.PreMergeValidation.Rules;
+using HotChocolate.Skimmed.Serialization;
+
+namespace HotChocolate.Composition.PreMergeValidation.Rules;
+
+public sealed class OutputFieldTypesMergeableRuleTests
+{
+ [Test]
+ [MethodDataSource(nameof(ValidExamplesData))]
+ public async Task Examples_Valid(string[] sdl)
+ {
+ // arrange
+ var log = new CompositionLog();
+ var context = new PreMergeValidationContext(
+ new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log));
+
+ context.Initialize();
+
+ // act
+ var result = new OutputFieldTypesMergeableRule().Run(context);
+
+ // assert
+ await Assert.That(result.IsSuccess).IsTrue();
+ await Assert.That(log.IsEmpty).IsTrue();
+ }
+
+ [Test]
+ [MethodDataSource(nameof(InvalidExamplesData))]
+ public async Task Examples_Invalid(string[] sdl)
+ {
+ // arrange
+ var log = new CompositionLog();
+ var context = new PreMergeValidationContext(
+ new CompositionContext([.. sdl.Select(SchemaParser.Parse)], log));
+
+ context.Initialize();
+
+ // act
+ var result = new OutputFieldTypesMergeableRule().Run(context);
+
+ // assert
+ await Assert.That(result.IsFailure).IsTrue();
+ await Assert.That(log.Count()).IsEqualTo(1);
+ await Assert.That(log.First().Code).IsEqualTo("OUTPUT_FIELD_TYPES_NOT_MERGEABLE");
+ await Assert.That(log.First().Severity).IsEqualTo(LogSeverity.Error);
+ }
+
+ public static IEnumerable> ValidExamplesData()
+ {
+ return
+ [
+ // Fields with the same type are mergeable.
+ () =>
+ [
+ """
+ type User {
+ birthdate: String
+ }
+ """,
+ """
+ type User {
+ birthdate: String
+ }
+ """
+ ],
+ // Fields with different nullability are mergeable, resulting in a merged field with a
+ // nullable type.
+ () =>
+ [
+ """
+ type User {
+ birthdate: String!
+ }
+ """,
+ """
+ type User {
+ birthdate: String
+ }
+ """
+ ],
+ () =>
+ [
+ """
+ type User {
+ tags: [String!]
+ }
+ """,
+ """
+ type User {
+ tags: [String]!
+ }
+ """,
+ """
+ type User {
+ tags: [String]
+ }
+ """
+ ]
+ ];
+ }
+
+ public static IEnumerable> InvalidExamplesData()
+ {
+ return
+ [
+ // Fields are not mergeable if the named types are different in kind or name.
+ () =>
+ [
+ """
+ type User {
+ birthdate: String!
+ }
+ """,
+ """
+ type User {
+ birthdate: DateTime!
+ }
+ """
+ ],
+ () =>
+ [
+ """
+ type User {
+ tags: [Tag]
+ }
+
+ type Tag {
+ value: String
+ }
+ """,
+ """
+ type User {
+ tags: [Tag]
+ }
+
+ scalar Tag
+ """
+ ]
+ ];
+ }
+}
diff --git a/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/ValidationHelperTests.cs b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/ValidationHelperTests.cs
new file mode 100644
index 00000000000..26ee73906b3
--- /dev/null
+++ b/src/HotChocolate/Fusion-vnext/test/Fusion.Composition.Tests/ValidationHelperTests.cs
@@ -0,0 +1,78 @@
+using HotChocolate.Fusion;
+using HotChocolate.Skimmed;
+using HotChocolate.Skimmed.Serialization;
+
+namespace HotChocolate.Composition;
+
+public sealed class ValidationHelperTests
+{
+ [Test]
+ [Arguments("Int", "Int")]
+ [Arguments("[Int]", "[Int]")]
+ [Arguments("[[Int]]", "[[Int]]")]
+ // Different nullability.
+ [Arguments("Int", "Int!")]
+ [Arguments("[Int]", "[Int!]")]
+ [Arguments("[Int]", "[Int!]!")]
+ [Arguments("[[Int]]", "[[Int!]]")]
+ [Arguments("[[Int]]", "[[Int!]!]")]
+ [Arguments("[[Int]]", "[[Int!]!]!")]
+ public async Task SameTypeShape_True(string sdlTypeA, string sdlTypeB)
+ {
+ // arrange
+ var schema1 = SchemaParser.Parse($$"""type Test { field: {{sdlTypeA}} }""");
+ var schema2 = SchemaParser.Parse($$"""type Test { field: {{sdlTypeB}} }""");
+ var typeA = ((ObjectTypeDefinition)schema1.Types["Test"]).Fields["field"].Type;
+ var typeB = ((ObjectTypeDefinition)schema2.Types["Test"]).Fields["field"].Type;
+
+ // act
+ var result = ValidationHelper.SameTypeShape(typeA, typeB);
+
+ // assert
+ await Assert.That(result).IsTrue();
+ }
+
+ [Test]
+ // Different type kind.
+ [Arguments("Tag", "Tag")]
+ [Arguments("[Tag]", "[Tag]")]
+ [Arguments("[[Tag]]", "[[Tag]]")]
+ // Different type name.
+ [Arguments("String", "DateTime")]
+ [Arguments("[String]", "[DateTime]")]
+ [Arguments("[[String]]", "[[DateTime]]")]
+ // Different depth.
+ [Arguments("String", "[String]")]
+ [Arguments("String", "[[String]]")]
+ [Arguments("[String]", "[[String]]")]
+ [Arguments("[[String]]", "[[[String]]]")]
+ // Different depth and nullability.
+ [Arguments("String", "[String!]")]
+ [Arguments("String", "[String!]!")]
+ public async Task SameTypeShape_False(string sdlTypeA, string sdlTypeB)
+ {
+ // arrange
+ var schema1 = SchemaParser.Parse(
+ $$"""
+ type Test { field: {{sdlTypeA}} }
+
+ type Tag { value: String }
+ """);
+
+ var schema2 = SchemaParser.Parse(
+ $$"""
+ type Test { field: {{sdlTypeB}} }
+
+ scalar Tag
+ """);
+
+ var typeA = ((ObjectTypeDefinition)schema1.Types["Test"]).Fields["field"].Type;
+ var typeB = ((ObjectTypeDefinition)schema2.Types["Test"]).Fields["field"].Type;
+
+ // act
+ var result = ValidationHelper.SameTypeShape(typeA, typeB);
+
+ // assert
+ await Assert.That(result).IsFalse();
+ }
+}
diff --git a/src/HotChocolate/Skimmed/src/Skimmed/Contracts/ITypeDefinition.cs b/src/HotChocolate/Skimmed/src/Skimmed/Contracts/ITypeDefinition.cs
index 26b0aca7ec5..5c0bd5e02ff 100644
--- a/src/HotChocolate/Skimmed/src/Skimmed/Contracts/ITypeDefinition.cs
+++ b/src/HotChocolate/Skimmed/src/Skimmed/Contracts/ITypeDefinition.cs
@@ -12,6 +12,11 @@ public interface ITypeDefinition : IEquatable
///
TypeKind Kind { get; }
+ ///
+ /// Gets a value indicating whether the type is an introspection type.
+ ///
+ bool IsIntrospectionType => this is INamedTypeDefinition type && type.Name.StartsWith("__");
+
///
/// Indicates whether the current object is equal to another object of the same type.
///
diff --git a/src/HotChocolate/Skimmed/src/Skimmed/Extensions/TypeExtensions.cs b/src/HotChocolate/Skimmed/src/Skimmed/Extensions/TypeExtensions.cs
index 7b8fa0de483..a1a081157a9 100644
--- a/src/HotChocolate/Skimmed/src/Skimmed/Extensions/TypeExtensions.cs
+++ b/src/HotChocolate/Skimmed/src/Skimmed/Extensions/TypeExtensions.cs
@@ -48,6 +48,15 @@ public static ITypeDefinition InnerType(this ITypeDefinition type)
_ => type,
};
+ public static ITypeDefinition NullableType(this ITypeDefinition type)
+ {
+ ArgumentNullException.ThrowIfNull(type);
+
+ return type.Kind == TypeKind.NonNull
+ ? ((NonNullTypeDefinition)type).NullableType
+ : type;
+ }
+
public static INamedTypeDefinition NamedType(this ITypeDefinition type)
{
while (true)
diff --git a/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaParser.cs b/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaParser.cs
index 28640e6c5eb..d3aa0da048b 100644
--- a/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaParser.cs
+++ b/src/HotChocolate/Skimmed/src/Skimmed/Serialization/SchemaParser.cs
@@ -38,19 +38,17 @@ private static void DiscoverDirectives(SchemaDefinition schema, DocumentNode doc
{
if (definition is DirectiveDefinitionNode def)
{
- if (BuiltIns.IsBuiltInDirective(def.Name.Value))
- {
- // If a built-in directive is redefined in the schema, we just ignore it.
- continue;
- }
-
if (schema.DirectiveDefinitions.ContainsName(def.Name.Value))
{
// TODO : parsing error
throw new Exception("duplicate");
}
- schema.DirectiveDefinitions.Add(new DirectiveDefinition(def.Name.Value));
+ schema.DirectiveDefinitions.Add(
+ new DirectiveDefinition(def.Name.Value)
+ {
+ IsSpecDirective = BuiltIns.IsBuiltInDirective(def.Name.Value)
+ });
}
}
}
@@ -61,11 +59,6 @@ private static void DiscoverTypes(SchemaDefinition schema, DocumentNode document
{
if (definition is ITypeDefinitionNode typeDef)
{
- if (BuiltIns.IsBuiltInScalar(typeDef.Name.Value))
- {
- continue;
- }
-
if (schema.Types.ContainsName(typeDef.Name.Value))
{
// TODO : parsing error
@@ -91,7 +84,11 @@ private static void DiscoverTypes(SchemaDefinition schema, DocumentNode document
break;
case ScalarTypeDefinitionNode:
- schema.Types.Add(new ScalarTypeDefinition(typeDef.Name.Value));
+ schema.Types.Add(
+ new ScalarTypeDefinition(typeDef.Name.Value)
+ {
+ IsSpecScalar = BuiltIns.IsBuiltInScalar(typeDef.Name.Value)
+ });
break;
case UnionTypeDefinitionNode:
@@ -196,11 +193,6 @@ private static void BuildTypes(SchemaDefinition schema, DocumentNode document)
break;
case ScalarTypeDefinitionNode typeDef:
- if (BuiltIns.IsBuiltInScalar(typeDef.Name.Value))
- {
- continue;
- }
-
BuildScalarType(
schema,
(ScalarTypeDefinition)schema.Types[typeDef.Name.Value],
@@ -545,11 +537,6 @@ private static void BuildDirectiveTypes(SchemaDefinition schema, DocumentNode do
{
if (definition is DirectiveDefinitionNode directiveDef)
{
- if (BuiltIns.IsBuiltInDirective(directiveDef.Name.Value))
- {
- continue;
- }
-
BuildDirectiveType(
schema,
schema.DirectiveDefinitions[directiveDef.Name.Value],
diff --git a/src/HotChocolate/Skimmed/test/Skimmed.Tests/SchemaParserTests.cs b/src/HotChocolate/Skimmed/test/Skimmed.Tests/SchemaParserTests.cs
index f827677511c..1149a6bbb62 100644
--- a/src/HotChocolate/Skimmed/test/Skimmed.Tests/SchemaParserTests.cs
+++ b/src/HotChocolate/Skimmed/test/Skimmed.Tests/SchemaParserTests.cs
@@ -1,5 +1,6 @@
using System.Text;
using HotChocolate.Skimmed.Serialization;
+using HotChocolate.Types;
namespace HotChocolate.Skimmed;
@@ -118,4 +119,46 @@ extend type Foo {
});
});
}
+
+ [Fact]
+ public void Parse_With_Custom_BuiltIn_Scalar_Type()
+ {
+ // arrange
+ var sdl =
+ """
+ "Custom description"
+ scalar String @custom
+ """;
+
+ // act
+ var schema = SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+ var scalar = schema.Types["String"];
+
+ // assert
+ Assert.Equal("Custom description", scalar.Description);
+ Assert.True(scalar.Directives.ContainsName("custom"));
+ }
+
+ [Fact]
+ public void Parse_With_Custom_BuiltIn_Directive()
+ {
+ // arrange
+ var sdl =
+ """
+ "Custom description"
+ directive @skip("Custom argument description" ifCustom: String! @custom) on ENUM_VALUE
+ """;
+
+ // act
+ var schema = SchemaParser.Parse(Encoding.UTF8.GetBytes(sdl));
+ var directive = schema.DirectiveDefinitions["skip"];
+ var argument = directive.Arguments["ifCustom"];
+
+ // assert
+ Assert.Equal("Custom description", directive.Description);
+ Assert.Equal("Custom argument description", argument.Description);
+ Assert.Equal("String", argument.Type.NamedType().Name);
+ Assert.True(argument.Directives.ContainsName("custom"));
+ Assert.Equal(DirectiveLocation.EnumValue, directive.Locations);
+ }
}