diff --git a/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts b/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts index f9f4456b5e..9d864902da 100644 --- a/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts +++ b/src/validation/__tests__/OverlappingFieldsCanBeMergedRule-test.ts @@ -1011,18 +1011,64 @@ describe('Validate: Overlapping fields can be merged', () => { it('does not infinite loop on recursive fragment', () => { expectValid(` + { + ...fragA + } + fragment fragA on Human { name, relatives { name, ...fragA } } `); }); it('does not infinite loop on immediately recursive fragment', () => { expectValid(` + { + ...fragA + } + fragment fragA on Human { name, ...fragA } `); }); + it('does not infinite loop on recursive fragment with a field named after fragment', () => { + expectValid(` + { + ...fragA + fragA + } + + fragment fragA on Query { ...fragA } + `); + }); + + it('finds invalid cases even with field named after fragment', () => { + expectErrors(` + { + fragA + ...fragA + } + + fragment fragA on Type { + fragA: b + } + `).toDeepEqual([ + { + message: + 'Fields "fragA" conflict because "fragA" and "b" are different fields. Use different aliases on the fields to fetch both if this was intentional.', + locations: [ + { line: 3, column: 9 }, + { line: 8, column: 9 }, + ], + }, + ]); + }); + it('does not infinite loop on transitively recursive fragment', () => { expectValid(` + { + ...fragA + fragB + } + fragment fragA on Human { name, ...fragB } fragment fragB on Human { name, ...fragC } fragment fragC on Human { name, ...fragA } diff --git a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts index 0c83b87eb0..5796da6f28 100644 --- a/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts +++ b/src/validation/rules/OverlappingFieldsCanBeMergedRule.ts @@ -266,6 +266,22 @@ function collectConflictsBetweenFieldsAndFragment( // (E) Then collect any conflicts between the provided collection of fields // and any fragment names found in the given fragment. for (const referencedFragmentName of referencedFragmentNames) { + // Memoize so two fragments are not compared for conflicts more than once. + if ( + comparedFragmentPairs.has( + referencedFragmentName, + fragmentName, + areMutuallyExclusive, + ) + ) { + continue; + } + comparedFragmentPairs.add( + referencedFragmentName, + fragmentName, + areMutuallyExclusive, + ); + collectConflictsBetweenFieldsAndFragment( context, conflicts,