From 516fecc65cb691062db89a7d4ecfd6cf48e90e6a Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Fri, 22 May 2026 09:02:22 +0200 Subject: [PATCH 1/3] Prune unreachable types from source schemas after preprocessing --- .../Fusion/src/Fusion.Composition/SchemaComposer.cs | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs index 1de9513be6b..e5019ec2c0b 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs @@ -78,6 +78,13 @@ public CompositionResult Compose() return enrichmentResult.Errors; } + // Prune unreachable definitions from each source schema before validation, so types + // stripped by @excludeByTag (or otherwise unreferenced) are not validated or merged. + foreach (var schema in schemas) + { + schema.RemoveUnreferencedDefinitions(schemas); + } + // Validate Source Schemas var validationResult = new SourceSchemaValidator(schemas, s_sourceSchemaRules, _log).Validate(); From 2255b2028adc152e2a783a2ab4c77505ae8ffc8d Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Fri, 22 May 2026 09:15:46 +0200 Subject: [PATCH 2/3] Add test --- .../SchemaComposerTests.cs | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SchemaComposerTests.cs b/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SchemaComposerTests.cs index 480d3573f14..62dbd365be7 100644 --- a/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SchemaComposerTests.cs +++ b/src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SchemaComposerTests.cs @@ -41,6 +41,76 @@ type Product { entry.Message); } + [Fact] + public void Compose_OrphanedTypeAfterTagExclusion_DoesNotProduceShareableError() + { + // arrange + // Schema A's Product is only reachable via Mutation. When Mutation is removed by + // tag exclusion, Product becomes orphaned. Without pruning, Product.name would + // collide with Schema B's Product.name and trigger an InvalidFieldSharing error. + var schemaComposer = new SchemaComposer( + [ + new SourceSchemaText( + "A", + """ + type Query { + book(id: ID!): Book @lookup + } + + type Mutation @tag(name: "internal") { + createProduct(name: String!): Product + } + + type Book { + id: ID! + title: String! + } + + type Product { + id: ID! + name: String! + } + + directive @tag(name: String!) repeatable on OBJECT + """), + new SourceSchemaText( + "B", + """ + type Query { + productById(id: ID!): Product @lookup + } + + type Product { + id: ID! + name: String! + } + """) + ], + new SchemaComposerOptions + { + Merger = { AddFusionDefinitions = false }, + SourceSchemas = + { + ["A"] = new SourceSchemaOptions + { + Preprocessor = new SourceSchemaPreprocessorOptions + { + ExcludeByTag = ["internal"] + } + } + } + }, + new CompositionLog()); + + // act + var result = schemaComposer.Compose(); + + // assert + Assert.True(result.IsSuccess); + Assert.False(result.Value.Types.ContainsName("Mutation")); + Assert.True(result.Value.Types.ContainsName("Product")); + } + [Fact] public void Compose_WithExtensions_AppliesExtensions() { From 9b05c1f17bd343fa6c1cc3cad4cf3e924033476f Mon Sep 17 00:00:00 2001 From: tobias-tengler <45513122+tobias-tengler@users.noreply.github.com> Date: Fri, 22 May 2026 10:02:26 +0200 Subject: [PATCH 3/3] Cleanup --- .../Extensions/MutableSchemaDefinitionExtensions.cs | 7 +++---- .../Fusion/src/Fusion.Composition/SchemaComposer.cs | 9 +++++++-- .../Fusion/src/Fusion.Composition/SourceSchemaMerger.cs | 3 ++- 3 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/Extensions/MutableSchemaDefinitionExtensions.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/Extensions/MutableSchemaDefinitionExtensions.cs index fd9da00d0de..cdb3a1dc2b7 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/Extensions/MutableSchemaDefinitionExtensions.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/Extensions/MutableSchemaDefinitionExtensions.cs @@ -136,7 +136,7 @@ public static List GetPossibleFusionLookupDirectivesById( public static void RemoveUnreferencedDefinitions( this MutableSchemaDefinition schema, - ImmutableSortedSet sourceSchemas) + IReadOnlySet preservedTypeNames) { var touchedDefinitions = new HashSet(); var backlog = new Stack(); @@ -161,8 +161,6 @@ public static void RemoveUnreferencedDefinitions( backlog.Push(schema.SubscriptionType); } - var preservedTypeNames = GetPreservedTypeNames(sourceSchemas); - foreach (var typeName in preservedTypeNames) { if (schema.Types.TryGetType(typeName, out var inputType)) @@ -260,7 +258,8 @@ private static List GetFusionLookupDirectives( /// Returns a list of type names for types that must be preserved in the merged schema /// even if they are not directly referenced. /// - private static HashSet GetPreservedTypeNames(ImmutableSortedSet sourceSchemas) + public static HashSet GetPreservedTypeNames( + ImmutableSortedSet sourceSchemas) { var preservedTypeNames = new HashSet(); diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs index e5019ec2c0b..16c9a451208 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs @@ -80,9 +80,14 @@ public CompositionResult Compose() // Prune unreachable definitions from each source schema before validation, so types // stripped by @excludeByTag (or otherwise unreferenced) are not validated or merged. - foreach (var schema in schemas) + if (_schemaComposerOptions.Merger.RemoveUnreferencedDefinitions) { - schema.RemoveUnreferencedDefinitions(schemas); + var preservedTypeNames = MutableSchemaDefinitionExtensions.GetPreservedTypeNames(schemas); + + foreach (var schema in schemas) + { + schema.RemoveUnreferencedDefinitions(preservedTypeNames); + } } // Validate Source Schemas diff --git a/src/HotChocolate/Fusion/src/Fusion.Composition/SourceSchemaMerger.cs b/src/HotChocolate/Fusion/src/Fusion.Composition/SourceSchemaMerger.cs index 0d89e1e78d0..aa149d250e3 100644 --- a/src/HotChocolate/Fusion/src/Fusion.Composition/SourceSchemaMerger.cs +++ b/src/HotChocolate/Fusion/src/Fusion.Composition/SourceSchemaMerger.cs @@ -107,7 +107,8 @@ public CompositionResult Merge() // Remove unreferenced definitions. if (_options.RemoveUnreferencedDefinitions) { - mergedSchema.RemoveUnreferencedDefinitions(_schemas); + mergedSchema.RemoveUnreferencedDefinitions( + MutableSchemaDefinitionExtensions.GetPreservedTypeNames(_schemas)); } // Add Fusion definitions.