7878import java .util .Set ;
7979import java .util .function .Predicate ;
8080
81- import static graphql .collect .ImmutableKit .addToList ;
8281import static graphql .collect .ImmutableKit .emptyList ;
8382import static graphql .schema .GraphQLTypeUtil .isEnum ;
8483import static graphql .schema .GraphQLTypeUtil .isInput ;
@@ -310,9 +309,9 @@ public class OperationValidator implements DocumentVisitor {
310309 private @ Nullable Map <String , VariableDefinition > variableDefinitionMap ;
311310
312311 // --- State: OverlappingFieldsCanBeMerged ---
313- private final Set <Set <FieldAndType >> sameResponseShapeChecked = new LinkedHashSet <>();
314- private final Set <Set <FieldAndType >> sameForCommonParentsChecked = new LinkedHashSet <>();
315- private final Set <Set <Field >> conflictsReported = new LinkedHashSet <>();
312+ private final Set <Set <FieldAndType >> sameResponseShapeChecked = new HashSet <>();
313+ private final Set <Set <FieldAndType >> sameForCommonParentsChecked = new HashSet <>();
314+ private final Set <Set <Field >> conflictsReported = new HashSet <>();
316315
317316 // --- State: LoneAnonymousOperation ---
318317 private boolean hasAnonymousOp = false ;
@@ -1169,7 +1168,7 @@ private void validateOverlappingFieldsCanBeMerged(OperationDefinition operationD
11691168
11701169 private void overlappingFieldsImpl (SelectionSet selectionSet , @ Nullable GraphQLOutputType graphQLOutputType ) {
11711170 Map <String , Set <FieldAndType >> fieldMap = new LinkedHashMap <>(selectionSet .getSelections ().size ());
1172- Set <String > visitedFragments = new LinkedHashSet <>();
1171+ Set <String > visitedFragments = new HashSet <>();
11731172 overlappingFields_collectFields (fieldMap , selectionSet , graphQLOutputType , visitedFragments );
11741173 List <Conflict > conflicts = findConflicts (fieldMap );
11751174 for (Conflict conflict : conflicts ) {
@@ -1198,10 +1197,9 @@ private void overlappingFields_collectFieldsForFragmentSpread(Map<String, Set<Fi
11981197 if (fragment == null ) {
11991198 return ;
12001199 }
1201- if (visitedFragments .contains (fragment .getName ())) {
1200+ if (! visitedFragments .add (fragment .getName ())) {
12021201 return ;
12031202 }
1204- visitedFragments .add (fragment .getName ());
12051203 GraphQLType graphQLType = TypeFromAST .getTypeFromAST (validationContext .getSchema (), fragment .getTypeCondition ());
12061204 overlappingFields_collectFields (fieldMap , fragment .getSelectionSet (), graphQLType , visitedFragments );
12071205 }
@@ -1230,56 +1228,78 @@ private void overlappingFields_collectFieldsForField(Map<String, Set<FieldAndTyp
12301228
12311229 private List <Conflict > findConflicts (Map <String , Set <FieldAndType >> fieldMap ) {
12321230 List <Conflict > result = new ArrayList <>();
1233- sameResponseShapeByName (fieldMap , emptyList (), result );
1234- sameForCommonParentsByName (fieldMap , emptyList (), result );
1231+ ArrayList <String > pathStack = new ArrayList <>();
1232+ sameResponseShapeByName (fieldMap , pathStack , result );
1233+ sameForCommonParentsByName (fieldMap , pathStack , result );
12351234 return result ;
12361235 }
12371236
1238- private void sameResponseShapeByName (Map <String , Set <FieldAndType >> fieldMap , ImmutableList <String > currentPath , List <Conflict > conflictsResult ) {
1237+ private void sameResponseShapeByName (Map <String , Set <FieldAndType >> fieldMap , ArrayList <String > pathStack , List <Conflict > conflictsResult ) {
12391238 for (Map .Entry <String , Set <FieldAndType >> entry : fieldMap .entrySet ()) {
1240- if (sameResponseShapeChecked .contains (entry .getValue ())) {
1239+ Set <FieldAndType > fieldAndTypes = entry .getValue ();
1240+ if (sameResponseShapeChecked .contains (fieldAndTypes )) {
12411241 continue ;
12421242 }
1243- ImmutableList <String > newPath = addToList (currentPath , entry .getKey ());
1244- sameResponseShapeChecked .add (entry .getValue ());
1245- Conflict conflict = requireSameOutputTypeShape (newPath , entry .getValue ());
1246- if (conflict != null ) {
1247- conflictsResult .add (conflict );
1248- continue ;
1243+ sameResponseShapeChecked .add (fieldAndTypes );
1244+ if (fieldAndTypes .size () > 1 ) {
1245+ pathStack .add (entry .getKey ());
1246+ Conflict conflict = requireSameOutputTypeShape (pathStack , fieldAndTypes );
1247+ if (conflict != null ) {
1248+ conflictsResult .add (conflict );
1249+ pathStack .remove (pathStack .size () - 1 );
1250+ continue ;
1251+ }
1252+ Map <String , Set <FieldAndType >> subSelections = mergeSubSelections (fieldAndTypes );
1253+ if (!subSelections .isEmpty ()) {
1254+ sameResponseShapeByName (subSelections , pathStack , conflictsResult );
1255+ }
1256+ pathStack .remove (pathStack .size () - 1 );
1257+ } else {
1258+ // Single field can't conflict with itself, but still recurse into sub-selections
1259+ Map <String , Set <FieldAndType >> subSelections = mergeSubSelections (fieldAndTypes );
1260+ if (!subSelections .isEmpty ()) {
1261+ pathStack .add (entry .getKey ());
1262+ sameResponseShapeByName (subSelections , pathStack , conflictsResult );
1263+ pathStack .remove (pathStack .size () - 1 );
1264+ }
12491265 }
1250- Map <String , Set <FieldAndType >> subSelections = mergeSubSelections (entry .getValue ());
1251- sameResponseShapeByName (subSelections , newPath , conflictsResult );
12521266 }
12531267 }
12541268
12551269 private Map <String , Set <FieldAndType >> mergeSubSelections (Set <FieldAndType > sameNameFields ) {
12561270 Map <String , Set <FieldAndType >> fieldMap = new LinkedHashMap <>();
1271+ Set <String > visitedFragments = new HashSet <>();
12571272 for (FieldAndType fieldAndType : sameNameFields ) {
12581273 if (fieldAndType .field .getSelectionSet () != null ) {
1259- Set <String > visitedFragments = new LinkedHashSet <>();
12601274 overlappingFields_collectFields (fieldMap , fieldAndType .field .getSelectionSet (), fieldAndType .graphQLType , visitedFragments );
12611275 }
12621276 }
12631277 return fieldMap ;
12641278 }
12651279
1266- private void sameForCommonParentsByName (Map <String , Set <FieldAndType >> fieldMap , ImmutableList <String > currentPath , List <Conflict > conflictsResult ) {
1280+ private void sameForCommonParentsByName (Map <String , Set <FieldAndType >> fieldMap , ArrayList <String > pathStack , List <Conflict > conflictsResult ) {
12671281 for (Map .Entry <String , Set <FieldAndType >> entry : fieldMap .entrySet ()) {
1268- List <Set <FieldAndType >> groups = groupByCommonParents (entry .getValue ());
1269- ImmutableList <String > newPath = addToList (currentPath , entry .getKey ());
1282+ Set <FieldAndType > fieldAndTypes = entry .getValue ();
1283+ List <Set <FieldAndType >> groups = groupByCommonParents (fieldAndTypes );
1284+ pathStack .add (entry .getKey ());
12701285 for (Set <FieldAndType > group : groups ) {
12711286 if (sameForCommonParentsChecked .contains (group )) {
12721287 continue ;
12731288 }
12741289 sameForCommonParentsChecked .add (group );
1275- Conflict conflict = requireSameNameAndArguments (newPath , group );
1276- if (conflict != null ) {
1277- conflictsResult .add (conflict );
1278- continue ;
1290+ if (group .size () > 1 ) {
1291+ Conflict conflict = requireSameNameAndArguments (pathStack , group );
1292+ if (conflict != null ) {
1293+ conflictsResult .add (conflict );
1294+ continue ;
1295+ }
12791296 }
12801297 Map <String , Set <FieldAndType >> subSelections = mergeSubSelections (group );
1281- sameForCommonParentsByName (subSelections , newPath , conflictsResult );
1298+ if (!subSelections .isEmpty ()) {
1299+ sameForCommonParentsByName (subSelections , pathStack , conflictsResult );
1300+ }
12821301 }
1302+ pathStack .remove (pathStack .size () - 1 );
12831303 }
12841304 }
12851305
@@ -1324,7 +1344,7 @@ private boolean isInterfaceOrUnion(@Nullable GraphQLType type) {
13241344 return type instanceof GraphQLInterfaceType || type instanceof GraphQLUnionType ;
13251345 }
13261346
1327- private @ Nullable Conflict requireSameNameAndArguments (ImmutableList <String > path , Set <FieldAndType > fieldAndTypes ) {
1347+ private @ Nullable Conflict requireSameNameAndArguments (List <String > path , Set <FieldAndType > fieldAndTypes ) {
13281348 if (fieldAndTypes .size () <= 1 ) {
13291349 return null ;
13301350 }
@@ -1351,7 +1371,7 @@ private boolean isInterfaceOrUnion(@Nullable GraphQLType type) {
13511371 return null ;
13521372 }
13531373
1354- private String pathToString (ImmutableList <String > path ) {
1374+ private String pathToString (List <String > path ) {
13551375 return String .join ("/" , path );
13561376 }
13571377
@@ -1380,7 +1400,7 @@ private boolean sameArguments(List<Argument> arguments1, @Nullable List<Argument
13801400 return null ;
13811401 }
13821402
1383- private @ Nullable Conflict requireSameOutputTypeShape (ImmutableList <String > path , Set <FieldAndType > fieldAndTypes ) {
1403+ private @ Nullable Conflict requireSameOutputTypeShape (List <String > path , Set <FieldAndType > fieldAndTypes ) {
13841404 if (fieldAndTypes .size () <= 1 ) {
13851405 return null ;
13861406 }
@@ -1430,7 +1450,7 @@ private boolean sameArguments(List<Argument> arguments1, @Nullable List<Argument
14301450 return null ;
14311451 }
14321452
1433- private Conflict mkNotSameTypeError (ImmutableList <String > path , List <Field > fields , @ Nullable GraphQLType typeA , @ Nullable GraphQLType typeB ) {
1453+ private Conflict mkNotSameTypeError (List <String > path , List <Field > fields , @ Nullable GraphQLType typeA , @ Nullable GraphQLType typeB ) {
14341454 String name1 = typeA != null ? simplePrint (typeA ) : "null" ;
14351455 String name2 = typeB != null ? simplePrint (typeB ) : "null" ;
14361456 String reason = i18n (FieldsConflict , "OverlappingFieldsCanBeMerged.differentReturnTypes" , pathToString (path ), name1 , name2 );
0 commit comments