Skip to content

Commit d47a255

Browse files
committed
Fix issues with OverlappingFields validator
Add tests for issues reported by @steveluscher, ensure those tests pass. Also fix remaining flow issues with this rule impl.
1 parent 7812eda commit d47a255

File tree

2 files changed

+111
-36
lines changed

2 files changed

+111
-36
lines changed

src/validation/__tests__/OverlappingFieldsCanBeMerged.js

Lines changed: 74 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,11 @@ import {
2121
GraphQLSchema,
2222
GraphQLObjectType,
2323
GraphQLUnionType,
24+
GraphQLList,
2425
GraphQLNonNull,
2526
GraphQLInt,
26-
GraphQLString
27+
GraphQLString,
28+
GraphQLID,
2729
} from '../../type';
2830

2931

@@ -382,11 +384,34 @@ describe('Validate: Overlapping fields can be merged', () => {
382384
types: [ StringBox, IntBox, NonNullStringBox1, NonNullStringBox2 ]
383385
});
384386

387+
var Connection = new GraphQLObjectType({
388+
name: 'Connection',
389+
fields: {
390+
edges: {
391+
type: new GraphQLList(new GraphQLObjectType({
392+
name: 'Edge',
393+
fields: {
394+
node: {
395+
type: new GraphQLObjectType({
396+
name: 'Node',
397+
fields: {
398+
id: { type: GraphQLID },
399+
name: { type: GraphQLString }
400+
}
401+
})
402+
}
403+
}
404+
}))
405+
}
406+
}
407+
});
408+
385409
var schema = new GraphQLSchema({
386410
query: new GraphQLObjectType({
387411
name: 'QueryRoot',
388412
fields: () => ({
389-
boxUnion: { type: BoxUnion }
413+
boxUnion: { type: BoxUnion },
414+
connection: { type: Connection }
390415
})
391416
})
392417
});
@@ -427,6 +452,53 @@ describe('Validate: Overlapping fields can be merged', () => {
427452
`);
428453
});
429454

455+
it('compares deep types including list', () => {
456+
expectFailsRuleWithSchema(schema, OverlappingFieldsCanBeMerged, `
457+
{
458+
connection {
459+
...edgeID
460+
edges {
461+
node {
462+
id: name
463+
}
464+
}
465+
}
466+
}
467+
468+
fragment edgeID on Connection {
469+
edges {
470+
node {
471+
id
472+
}
473+
}
474+
}
475+
`, [
476+
{ message: fieldsConflictMessage(
477+
'edges', [['node', [['id', 'id and name are different fields']]]]
478+
),
479+
locations: [
480+
{ line: 14, column: 11 }, { line: 5, column: 13 },
481+
{ line: 15, column: 13 }, { line: 6, column: 15 },
482+
{ line: 16, column: 15 }, { line: 7, column: 17 },
483+
] }
484+
]);
485+
});
486+
487+
it('ignores unknown types', () => {
488+
expectPassesRuleWithSchema(schema, OverlappingFieldsCanBeMerged, `
489+
{
490+
boxUnion {
491+
...on UnknownType {
492+
scalar
493+
}
494+
...on NonNullStringBox2 {
495+
scalar
496+
}
497+
}
498+
}
499+
`);
500+
});
501+
430502
});
431503

432504
});

src/validation/rules/OverlappingFieldsCanBeMerged.js

Lines changed: 37 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Flow currently disabled for this file.
1+
/*@flow*/
22
/**
33
* Copyright (c) 2015, Facebook, Inc.
44
* All rights reserved.
@@ -18,12 +18,16 @@ import type {
1818
SelectionSet,
1919
Field,
2020
Argument,
21-
InlineFragment,
22-
FragmentSpread,
2321
Directive
2422
} from '../../language/ast';
23+
import {
24+
getNamedType,
25+
GraphQLObjectType,
26+
GraphQLInterfaceType,
27+
} from '../../type/definition';
2528
import type {
2629
GraphQLType,
30+
GraphQLNamedType,
2731
GraphQLFieldDefinition
2832
} from '../../type/definition';
2933
import { typeFromAST } from '../../utilities/typeFromAST';
@@ -83,25 +87,21 @@ export default function OverlappingFieldsCanBeMerged(
8387

8488
var type1 = def1 && def1.type;
8589
var type2 = def2 && def2.type;
86-
if (!sameType(type1, type2)) {
90+
if (type1 && type2 && !sameType(type1, type2)) {
8791
return [
8892
[responseName, `they return differing types ${type1} and ${type2}`],
8993
[ast1, ast2]
9094
];
9195
}
9296

93-
var arguments1 = ast1.arguments || [];
94-
var arguments2 = ast2.arguments || [];
95-
if (!sameArguments(arguments1, arguments2)) {
97+
if (!sameArguments(ast1.arguments || [], ast2.arguments || [])) {
9698
return [
9799
[responseName, 'they have differing arguments'],
98100
[ast1, ast2]
99101
];
100102
}
101103

102-
var directives1 = ast1.directives || [];
103-
var directives2 = ast2.directives || [];
104-
if (!sameDirectives(directives1, directives2)) {
104+
if (!sameDirectives(ast1.directives || [], ast2.directives || [])) {
105105
return [
106106
[responseName, 'they have differing directives'],
107107
[ast1, ast2]
@@ -114,13 +114,13 @@ export default function OverlappingFieldsCanBeMerged(
114114
var visitedFragmentNames = {};
115115
var subfieldMap = collectFieldASTsAndDefs(
116116
context,
117-
type1,
117+
getNamedType(type1),
118118
selectionSet1,
119119
visitedFragmentNames
120120
);
121121
subfieldMap = collectFieldASTsAndDefs(
122122
context,
123-
type2,
123+
getNamedType(type2),
124124
selectionSet2,
125125
visitedFragmentNames,
126126
subfieldMap
@@ -130,7 +130,7 @@ export default function OverlappingFieldsCanBeMerged(
130130
return [
131131
[responseName, conflicts.map(([reason]) => reason)],
132132
conflicts.reduce(
133-
(list: Array<Field>, [, blameNodes]) => list.concat(blameNodes),
133+
(allFields, [, fields]) => allFields.concat(fields),
134134
[ast1, ast2]
135135
)
136136
];
@@ -145,15 +145,15 @@ export default function OverlappingFieldsCanBeMerged(
145145
leave(selectionSet) {
146146
var fieldMap = collectFieldASTsAndDefs(
147147
context,
148-
context.getType(),
148+
context.getParentType(),
149149
selectionSet
150150
);
151151
var conflicts = findConflicts(fieldMap);
152152
if (conflicts.length) {
153-
return conflicts.map(([[responseName, reason], blameNodes]) =>
153+
return conflicts.map(([[responseName, reason], fields]) =>
154154
new GraphQLError(
155155
fieldsConflictMessage(responseName, reason),
156-
blameNodes
156+
fields
157157
)
158158
);
159159
}
@@ -164,7 +164,7 @@ export default function OverlappingFieldsCanBeMerged(
164164

165165
type Conflict = [ConflictReason, Array<Field>];
166166
// Field name and reason, or field name and list of sub-conflicts.
167-
type ConflictReason = [string, string] | [string, Array<ConflictReason>];
167+
type ConflictReason = [string, string | Array<ConflictReason>];
168168

169169
function sameDirectives(
170170
directives1: Array<Directive>,
@@ -181,7 +181,10 @@ function sameDirectives(
181181
if (!directive2) {
182182
return false;
183183
}
184-
return sameArguments(directive1.arguments, directive2.arguments);
184+
return sameArguments(
185+
directive1.arguments || [],
186+
directive2.arguments || []
187+
);
185188
});
186189
}
187190

@@ -208,8 +211,8 @@ function sameValue(value1, value2) {
208211
return (!value1 && !value2) || print(value1) === print(value2);
209212
}
210213

211-
function sameType(type1, type2) {
212-
return (!type1 && !type2) || String(type1) === String(type2);
214+
function sameType(type1: GraphQLType, type2: GraphQLType) {
215+
return String(type1) === String(type2);
213216
}
214217

215218

@@ -223,40 +226,40 @@ function sameType(type1, type2) {
223226
*/
224227
function collectFieldASTsAndDefs(
225228
context: ValidationContext,
226-
parentType: ?GraphQLType,
229+
parentType: ?GraphQLNamedType,
227230
selectionSet: SelectionSet,
228231
visitedFragmentNames?: {[key: string]: boolean},
229-
astAndDefs?: {[key: string]: Array<[Field, GraphQLFieldDefinition]>}
230-
): {[key: string]: Array<[Field, GraphQLFieldDefinition]>} {
232+
astAndDefs?: {[key: string]: Array<[Field, ?GraphQLFieldDefinition]>}
233+
): {[key: string]: Array<[Field, ?GraphQLFieldDefinition]>} {
231234
var _visitedFragmentNames = visitedFragmentNames || {};
232235
var _astAndDefs = astAndDefs || {};
233236
for (var i = 0; i < selectionSet.selections.length; i++) {
234237
var selection = selectionSet.selections[i];
235238
switch (selection.kind) {
236239
case FIELD:
237-
var fieldAST = (selection: Field);
238-
var fieldName = fieldAST.name.value;
239-
var fieldDef = parentType && parentType.getFields &&
240-
parentType.getFields()[fieldName];
241-
var responseName = fieldAST.alias ? fieldAST.alias.value : fieldName;
240+
var fieldName = selection.name.value;
241+
var fieldDef;
242+
if (parentType instanceof GraphQLObjectType ||
243+
parentType instanceof GraphQLInterfaceType) {
244+
fieldDef = parentType.getFields()[fieldName];
245+
}
246+
var responseName = selection.alias ? selection.alias.value : fieldName;
242247
if (!_astAndDefs[responseName]) {
243248
_astAndDefs[responseName] = [];
244249
}
245-
_astAndDefs[responseName].push([fieldAST, fieldDef]);
250+
_astAndDefs[responseName].push([selection, fieldDef]);
246251
break;
247252
case INLINE_FRAGMENT:
248-
var inlineFragment = (selection: InlineFragment);
249253
_astAndDefs = collectFieldASTsAndDefs(
250254
context,
251-
typeFromAST(context.getSchema(), inlineFragment.typeCondition),
252-
inlineFragment.selectionSet,
255+
typeFromAST(context.getSchema(), selection.typeCondition),
256+
selection.selectionSet,
253257
_visitedFragmentNames,
254258
_astAndDefs
255259
);
256260
break;
257261
case FRAGMENT_SPREAD:
258-
var fragmentSpread = (selection: FragmentSpread);
259-
var fragName = fragmentSpread.name.value;
262+
var fragName = selection.name.value;
260263
if (_visitedFragmentNames[fragName]) {
261264
continue;
262265
}

0 commit comments

Comments
 (0)