Skip to content

Commit 39e9f92

Browse files
[Fusion] Prune unreachable types from source schemas after preprocessing (#9767)
1 parent 1f0b160 commit 39e9f92

4 files changed

Lines changed: 87 additions & 5 deletions

File tree

src/HotChocolate/Fusion/src/Fusion.Composition/Extensions/MutableSchemaDefinitionExtensions.cs

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ public static List<IDirective> GetPossibleFusionLookupDirectivesById(
136136

137137
public static void RemoveUnreferencedDefinitions(
138138
this MutableSchemaDefinition schema,
139-
ImmutableSortedSet<MutableSchemaDefinition> sourceSchemas)
139+
IReadOnlySet<string> preservedTypeNames)
140140
{
141141
var touchedDefinitions = new HashSet<ITypeSystemMember>();
142142
var backlog = new Stack<ITypeSystemMember>();
@@ -161,8 +161,6 @@ public static void RemoveUnreferencedDefinitions(
161161
backlog.Push(schema.SubscriptionType);
162162
}
163163

164-
var preservedTypeNames = GetPreservedTypeNames(sourceSchemas);
165-
166164
foreach (var typeName in preservedTypeNames)
167165
{
168166
if (schema.Types.TryGetType(typeName, out var inputType))
@@ -260,7 +258,8 @@ private static List<IDirective> GetFusionLookupDirectives(
260258
/// Returns a list of type names for types that must be preserved in the merged schema
261259
/// even if they are not directly referenced.
262260
/// </summary>
263-
private static HashSet<string> GetPreservedTypeNames(ImmutableSortedSet<MutableSchemaDefinition> sourceSchemas)
261+
public static HashSet<string> GetPreservedTypeNames(
262+
ImmutableSortedSet<MutableSchemaDefinition> sourceSchemas)
264263
{
265264
var preservedTypeNames = new HashSet<string>();
266265

src/HotChocolate/Fusion/src/Fusion.Composition/SchemaComposer.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,18 @@ public CompositionResult<MutableSchemaDefinition> Compose()
7878
return enrichmentResult.Errors;
7979
}
8080

81+
// Prune unreachable definitions from each source schema before validation, so types
82+
// stripped by @excludeByTag (or otherwise unreferenced) are not validated or merged.
83+
if (_schemaComposerOptions.Merger.RemoveUnreferencedDefinitions)
84+
{
85+
var preservedTypeNames = MutableSchemaDefinitionExtensions.GetPreservedTypeNames(schemas);
86+
87+
foreach (var schema in schemas)
88+
{
89+
schema.RemoveUnreferencedDefinitions(preservedTypeNames);
90+
}
91+
}
92+
8193
// Validate Source Schemas
8294
var validationResult =
8395
new SourceSchemaValidator(schemas, s_sourceSchemaRules, _log).Validate();

src/HotChocolate/Fusion/src/Fusion.Composition/SourceSchemaMerger.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,8 @@ public CompositionResult<MutableSchemaDefinition> Merge()
107107
// Remove unreferenced definitions.
108108
if (_options.RemoveUnreferencedDefinitions)
109109
{
110-
mergedSchema.RemoveUnreferencedDefinitions(_schemas);
110+
mergedSchema.RemoveUnreferencedDefinitions(
111+
MutableSchemaDefinitionExtensions.GetPreservedTypeNames(_schemas));
111112
}
112113

113114
// Add Fusion definitions.

src/HotChocolate/Fusion/test/Fusion.Composition.Tests/SchemaComposerTests.cs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,76 @@ type Product {
4141
entry.Message);
4242
}
4343

44+
[Fact]
45+
public void Compose_OrphanedTypeAfterTagExclusion_DoesNotProduceShareableError()
46+
{
47+
// arrange
48+
// Schema A's Product is only reachable via Mutation. When Mutation is removed by
49+
// tag exclusion, Product becomes orphaned. Without pruning, Product.name would
50+
// collide with Schema B's Product.name and trigger an InvalidFieldSharing error.
51+
var schemaComposer = new SchemaComposer(
52+
[
53+
new SourceSchemaText(
54+
"A",
55+
"""
56+
type Query {
57+
book(id: ID!): Book @lookup
58+
}
59+
60+
type Mutation @tag(name: "internal") {
61+
createProduct(name: String!): Product
62+
}
63+
64+
type Book {
65+
id: ID!
66+
title: String!
67+
}
68+
69+
type Product {
70+
id: ID!
71+
name: String!
72+
}
73+
74+
directive @tag(name: String!) repeatable on OBJECT
75+
"""),
76+
new SourceSchemaText(
77+
"B",
78+
"""
79+
type Query {
80+
productById(id: ID!): Product @lookup
81+
}
82+
83+
type Product {
84+
id: ID!
85+
name: String!
86+
}
87+
""")
88+
],
89+
new SchemaComposerOptions
90+
{
91+
Merger = { AddFusionDefinitions = false },
92+
SourceSchemas =
93+
{
94+
["A"] = new SourceSchemaOptions
95+
{
96+
Preprocessor = new SourceSchemaPreprocessorOptions
97+
{
98+
ExcludeByTag = ["internal"]
99+
}
100+
}
101+
}
102+
},
103+
new CompositionLog());
104+
105+
// act
106+
var result = schemaComposer.Compose();
107+
108+
// assert
109+
Assert.True(result.IsSuccess);
110+
Assert.False(result.Value.Types.ContainsName("Mutation"));
111+
Assert.True(result.Value.Types.ContainsName("Product"));
112+
}
113+
44114
[Fact]
45115
public void Compose_WithExtensions_AppliesExtensions()
46116
{

0 commit comments

Comments
 (0)