import { inspect } from '../../jsutils/inspect';
import type { Maybe } from '../../jsutils/Maybe';
import type { ObjMap } from '../../jsutils/ObjMap';
import { GraphQLError } from '../../error/GraphQLError';
import type {
DirectiveNode,
FieldNode,
FragmentDefinitionNode,
SelectionSetNode,
ValueNode,
} from '../../language/ast';
import { Kind } from '../../language/kinds';
import { print } from '../../language/printer';
import type { ASTVisitor } from '../../language/visitor';
import type {
GraphQLField,
GraphQLNamedType,
GraphQLOutputType,
} from '../../type/definition';
import {
getNamedType,
isInterfaceType,
isLeafType,
isListType,
isNonNullType,
isObjectType,
} from '../../type/definition';
import { sortValueNode } from '../../utilities/sortValueNode';
import { typeFromAST } from '../../utilities/typeFromAST';
import type { ValidationContext } from '../ValidationContext';
function reasonMessage(reason: ConflictReasonMessage): string {
if (Array.isArray(reason)) {
return reason
.map(
([responseName, subReason]) =>
`subfields "${responseName}" conflict because ` +
reasonMessage(subReason),
)
.join(' and ');
}
return reason;
}
export function OverlappingFieldsCanBeMergedRule(
context: ValidationContext,
): ASTVisitor {
const comparedFieldsAndFragmentPairs = new OrderedPairSet<
NodeAndDefCollection,
string
>();
const comparedFragmentPairs = new PairSet<string>();
const cachedFieldsAndFragmentNames = new Map();
return {
SelectionSet(selectionSet) {
const conflicts = findConflictsWithinSelectionSet(
context,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
context.getParentType(),
selectionSet,
);
for (const [[responseName, reason], fields1, fields2] of conflicts) {
const reasonMsg = reasonMessage(reason);
context.reportError(
new GraphQLError(
`Fields "${responseName}" conflict because ${reasonMsg}. Use different aliases on the fields to fetch both if this was intentional.`,
{ nodes: fields1.concat(fields2) },
),
);
}
},
};
}
type Conflict = [ConflictReason, Array<FieldNode>, Array<FieldNode>];
type ConflictReason = [string, ConflictReasonMessage];
type ConflictReasonMessage = string | Array<ConflictReason>;
type NodeAndDef = [
Maybe<GraphQLNamedType>,
FieldNode,
Maybe<GraphQLField<unknown, unknown>>,
];
type NodeAndDefCollection = ObjMap<Array<NodeAndDef>>;
type FragmentNames = Array<string>;
type FieldsAndFragmentNames = readonly [NodeAndDefCollection, FragmentNames];
function findConflictsWithinSelectionSet(
context: ValidationContext,
cachedFieldsAndFragmentNames: Map<SelectionSetNode, FieldsAndFragmentNames>,
comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
comparedFragmentPairs: PairSet<string>,
parentType: Maybe<GraphQLNamedType>,
selectionSet: SelectionSetNode,
): Array<Conflict> {
const conflicts: Array<Conflict> = [];
const [fieldMap, fragmentNames] = getFieldsAndFragmentNames(
context,
cachedFieldsAndFragmentNames,
parentType,
selectionSet,
);
collectConflictsWithin(
context,
conflicts,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
fieldMap,
);
if (fragmentNames.length !== 0) {
for (let i = 0; i < fragmentNames.length; i++) {
collectConflictsBetweenFieldsAndFragment(
context,
conflicts,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
false,
fieldMap,
fragmentNames[i],
);
for (let j = i + 1; j < fragmentNames.length; j++) {
collectConflictsBetweenFragments(
context,
conflicts,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
false,
fragmentNames[i],
fragmentNames[j],
);
}
}
}
return conflicts;
}
function collectConflictsBetweenFieldsAndFragment(
context: ValidationContext,
conflicts: Array<Conflict>,
cachedFieldsAndFragmentNames: Map<SelectionSetNode, FieldsAndFragmentNames>,
comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
comparedFragmentPairs: PairSet<string>,
areMutuallyExclusive: boolean,
fieldMap: NodeAndDefCollection,
fragmentName: string,
): void {
if (
comparedFieldsAndFragmentPairs.has(
fieldMap,
fragmentName,
areMutuallyExclusive,
)
) {
return;
}
comparedFieldsAndFragmentPairs.add(
fieldMap,
fragmentName,
areMutuallyExclusive,
);
const fragment = context.getFragment(fragmentName);
if (!fragment) {
return;
}
const [fieldMap2, referencedFragmentNames] =
getReferencedFieldsAndFragmentNames(
context,
cachedFieldsAndFragmentNames,
fragment,
);
if (fieldMap === fieldMap2) {
return;
}
collectConflictsBetween(
context,
conflicts,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fieldMap,
fieldMap2,
);
for (const referencedFragmentName of referencedFragmentNames) {
collectConflictsBetweenFieldsAndFragment(
context,
conflicts,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fieldMap,
referencedFragmentName,
);
}
}
function collectConflictsBetweenFragments(
context: ValidationContext,
conflicts: Array<Conflict>,
cachedFieldsAndFragmentNames: Map<SelectionSetNode, FieldsAndFragmentNames>,
comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
comparedFragmentPairs: PairSet<string>,
areMutuallyExclusive: boolean,
fragmentName1: string,
fragmentName2: string,
): void {
if (fragmentName1 === fragmentName2) {
return;
}
if (
comparedFragmentPairs.has(
fragmentName1,
fragmentName2,
areMutuallyExclusive,
)
) {
return;
}
comparedFragmentPairs.add(fragmentName1, fragmentName2, areMutuallyExclusive);
const fragment1 = context.getFragment(fragmentName1);
const fragment2 = context.getFragment(fragmentName2);
if (!fragment1 || !fragment2) {
return;
}
const [fieldMap1, referencedFragmentNames1] =
getReferencedFieldsAndFragmentNames(
context,
cachedFieldsAndFragmentNames,
fragment1,
);
const [fieldMap2, referencedFragmentNames2] =
getReferencedFieldsAndFragmentNames(
context,
cachedFieldsAndFragmentNames,
fragment2,
);
collectConflictsBetween(
context,
conflicts,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fieldMap1,
fieldMap2,
);
for (const referencedFragmentName2 of referencedFragmentNames2) {
collectConflictsBetweenFragments(
context,
conflicts,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fragmentName1,
referencedFragmentName2,
);
}
for (const referencedFragmentName1 of referencedFragmentNames1) {
collectConflictsBetweenFragments(
context,
conflicts,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
referencedFragmentName1,
fragmentName2,
);
}
}
function findConflictsBetweenSubSelectionSets(
context: ValidationContext,
cachedFieldsAndFragmentNames: Map<SelectionSetNode, FieldsAndFragmentNames>,
comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
comparedFragmentPairs: PairSet<string>,
areMutuallyExclusive: boolean,
parentType1: Maybe<GraphQLNamedType>,
selectionSet1: SelectionSetNode,
parentType2: Maybe<GraphQLNamedType>,
selectionSet2: SelectionSetNode,
): Array<Conflict> {
const conflicts: Array<Conflict> = [];
const [fieldMap1, fragmentNames1] = getFieldsAndFragmentNames(
context,
cachedFieldsAndFragmentNames,
parentType1,
selectionSet1,
);
const [fieldMap2, fragmentNames2] = getFieldsAndFragmentNames(
context,
cachedFieldsAndFragmentNames,
parentType2,
selectionSet2,
);
collectConflictsBetween(
context,
conflicts,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fieldMap1,
fieldMap2,
);
for (const fragmentName2 of fragmentNames2) {
collectConflictsBetweenFieldsAndFragment(
context,
conflicts,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fieldMap1,
fragmentName2,
);
}
for (const fragmentName1 of fragmentNames1) {
collectConflictsBetweenFieldsAndFragment(
context,
conflicts,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fieldMap2,
fragmentName1,
);
}
for (const fragmentName1 of fragmentNames1) {
for (const fragmentName2 of fragmentNames2) {
collectConflictsBetweenFragments(
context,
conflicts,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fragmentName1,
fragmentName2,
);
}
}
return conflicts;
}
function collectConflictsWithin(
context: ValidationContext,
conflicts: Array<Conflict>,
cachedFieldsAndFragmentNames: Map<SelectionSetNode, FieldsAndFragmentNames>,
comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
comparedFragmentPairs: PairSet<string>,
fieldMap: NodeAndDefCollection,
): void {
for (const [responseName, fields] of Object.entries(fieldMap)) {
if (fields.length > 1) {
for (let i = 0; i < fields.length; i++) {
for (let j = i + 1; j < fields.length; j++) {
const conflict = findConflict(
context,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
false,
responseName,
fields[i],
fields[j],
);
if (conflict) {
conflicts.push(conflict);
}
}
}
}
}
}
function collectConflictsBetween(
context: ValidationContext,
conflicts: Array<Conflict>,
cachedFieldsAndFragmentNames: Map<SelectionSetNode, FieldsAndFragmentNames>,
comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
comparedFragmentPairs: PairSet<string>,
parentFieldsAreMutuallyExclusive: boolean,
fieldMap1: NodeAndDefCollection,
fieldMap2: NodeAndDefCollection,
): void {
for (const [responseName, fields1] of Object.entries(fieldMap1)) {
const fields2 = fieldMap2[responseName];
if (fields2) {
for (const field1 of fields1) {
for (const field2 of fields2) {
const conflict = findConflict(
context,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
parentFieldsAreMutuallyExclusive,
responseName,
field1,
field2,
);
if (conflict) {
conflicts.push(conflict);
}
}
}
}
}
}
function findConflict(
context: ValidationContext,
cachedFieldsAndFragmentNames: Map<SelectionSetNode, FieldsAndFragmentNames>,
comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
comparedFragmentPairs: PairSet<string>,
parentFieldsAreMutuallyExclusive: boolean,
responseName: string,
field1: NodeAndDef,
field2: NodeAndDef,
): Maybe<Conflict> {
const [parentType1, node1, def1] = field1;
const [parentType2, node2, def2] = field2;
const areMutuallyExclusive =
parentFieldsAreMutuallyExclusive ||
(parentType1 !== parentType2 &&
isObjectType(parentType1) &&
isObjectType(parentType2));
if (!areMutuallyExclusive) {
const name1 = node1.name.value;
const name2 = node2.name.value;
if (name1 !== name2) {
return [
[responseName, `"${name1}" and "${name2}" are different fields`],
[node1],
[node2],
];
}
if (!sameArguments(node1, node2)) {
return [
[responseName, 'they have differing arguments'],
[node1],
[node2],
];
}
}
const type1 = def1?.type;
const type2 = def2?.type;
if (type1 && type2 && doTypesConflict(type1, type2)) {
return [
[
responseName,
`they return conflicting types "${inspect(type1)}" and "${inspect(
type2,
)}"`,
],
[node1],
[node2],
];
}
const selectionSet1 = node1.selectionSet;
const selectionSet2 = node2.selectionSet;
if (selectionSet1 && selectionSet2) {
const conflicts = findConflictsBetweenSubSelectionSets(
context,
cachedFieldsAndFragmentNames,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
getNamedType(type1),
selectionSet1,
getNamedType(type2),
selectionSet2,
);
return subfieldConflicts(conflicts, responseName, node1, node2);
}
}
function sameArguments(
node1: FieldNode | DirectiveNode,
node2: FieldNode | DirectiveNode,
): boolean {
const args1 = node1.arguments;
const args2 = node2.arguments;
if (args1 === undefined || args1.length === 0) {
return args2 === undefined || args2.length === 0;
}
if (args2 === undefined || args2.length === 0) {
return false;
}
if (args1.length !== args2.length) {
return false;
}
const values2 = new Map(args2.map(({ name, value }) => [name.value, value]));
return args1.every((arg1) => {
const value1 = arg1.value;
const value2 = values2.get(arg1.name.value);
if (value2 === undefined) {
return false;
}
return stringifyValue(value1) === stringifyValue(value2);
});
}
function stringifyValue(value: ValueNode): string | null {
return print(sortValueNode(value));
}
function doTypesConflict(
type1: GraphQLOutputType,
type2: GraphQLOutputType,
): boolean {
if (isListType(type1)) {
return isListType(type2)
? doTypesConflict(type1.ofType, type2.ofType)
: true;
}
if (isListType(type2)) {
return true;
}
if (isNonNullType(type1)) {
return isNonNullType(type2)
? doTypesConflict(type1.ofType, type2.ofType)
: true;
}
if (isNonNullType(type2)) {
return true;
}
if (isLeafType(type1) || isLeafType(type2)) {
return type1 !== type2;
}
return false;
}
function getFieldsAndFragmentNames(
context: ValidationContext,
cachedFieldsAndFragmentNames: Map<SelectionSetNode, FieldsAndFragmentNames>,
parentType: Maybe<GraphQLNamedType>,
selectionSet: SelectionSetNode,
): FieldsAndFragmentNames {
const cached = cachedFieldsAndFragmentNames.get(selectionSet);
if (cached) {
return cached;
}
const nodeAndDefs: NodeAndDefCollection = Object.create(null);
const fragmentNames: ObjMap<boolean> = Object.create(null);
_collectFieldsAndFragmentNames(
context,
parentType,
selectionSet,
nodeAndDefs,
fragmentNames,
);
const result = [nodeAndDefs, Object.keys(fragmentNames)] as const;
cachedFieldsAndFragmentNames.set(selectionSet, result);
return result;
}
function getReferencedFieldsAndFragmentNames(
context: ValidationContext,
cachedFieldsAndFragmentNames: Map<SelectionSetNode, FieldsAndFragmentNames>,
fragment: FragmentDefinitionNode,
) {
const cached = cachedFieldsAndFragmentNames.get(fragment.selectionSet);
if (cached) {
return cached;
}
const fragmentType = typeFromAST(context.getSchema(), fragment.typeCondition);
return getFieldsAndFragmentNames(
context,
cachedFieldsAndFragmentNames,
fragmentType,
fragment.selectionSet,
);
}
function _collectFieldsAndFragmentNames(
context: ValidationContext,
parentType: Maybe<GraphQLNamedType>,
selectionSet: SelectionSetNode,
nodeAndDefs: NodeAndDefCollection,
fragmentNames: ObjMap<boolean>,
): void {
for (const selection of selectionSet.selections) {
switch (selection.kind) {
case Kind.FIELD: {
const fieldName = selection.name.value;
let fieldDef;
if (isObjectType(parentType) || isInterfaceType(parentType)) {
fieldDef = parentType.getFields()[fieldName];
}
const responseName = selection.alias
? selection.alias.value
: fieldName;
if (!nodeAndDefs[responseName]) {
nodeAndDefs[responseName] = [];
}
nodeAndDefs[responseName].push([parentType, selection, fieldDef]);
break;
}
case Kind.FRAGMENT_SPREAD:
fragmentNames[selection.name.value] = true;
break;
case Kind.INLINE_FRAGMENT: {
const typeCondition = selection.typeCondition;
const inlineFragmentType = typeCondition
? typeFromAST(context.getSchema(), typeCondition)
: parentType;
_collectFieldsAndFragmentNames(
context,
inlineFragmentType,
selection.selectionSet,
nodeAndDefs,
fragmentNames,
);
break;
}
}
}
}
function subfieldConflicts(
conflicts: ReadonlyArray<Conflict>,
responseName: string,
node1: FieldNode,
node2: FieldNode,
): Maybe<Conflict> {
if (conflicts.length > 0) {
return [
[responseName, conflicts.map(([reason]) => reason)],
[node1, ...conflicts.map(([, fields1]) => fields1).flat()],
[node2, ...conflicts.map(([, , fields2]) => fields2).flat()],
];
}
}
class OrderedPairSet<T, U> {
_data: Map<T, Map<U, boolean>>;
constructor() {
this._data = new Map();
}
has(a: T, b: U, weaklyPresent: boolean): boolean {
const result = this._data.get(a)?.get(b);
if (result === undefined) {
return false;
}
return weaklyPresent ? true : weaklyPresent === result;
}
add(a: T, b: U, weaklyPresent: boolean): void {
const map = this._data.get(a);
if (map === undefined) {
this._data.set(a, new Map([[b, weaklyPresent]]));
} else {
map.set(b, weaklyPresent);
}
}
}
class PairSet<T> {
_orderedPairSet: OrderedPairSet<T, T>;
constructor() {
this._orderedPairSet = new OrderedPairSet();
}
has(a: T, b: T, weaklyPresent: boolean): boolean {
return a < b
? this._orderedPairSet.has(a, b, weaklyPresent)
: this._orderedPairSet.has(b, a, weaklyPresent);
}
add(a: T, b: T, weaklyPresent: boolean): void {
if (a < b) {
this._orderedPairSet.add(a, b, weaklyPresent);
} else {
this._orderedPairSet.add(b, a, weaklyPresent);
}
}
}