import { inspect } from '../../jsutils/inspect.ts';
import type { Maybe } from '../../jsutils/Maybe.ts';
import { GraphQLError } from '../../error/GraphQLError.ts';
import type {
ArgumentNode,
DirectiveNode,
FieldNode,
FragmentArgumentNode,
FragmentDefinitionNode,
FragmentSpreadNode,
SelectionSetNode,
ValueNode,
} from '../../language/ast.ts';
import { Kind } from '../../language/kinds.ts';
import { print } from '../../language/printer.ts';
import type { ASTVisitor } from '../../language/visitor.ts';
import type {
GraphQLField,
GraphQLNamedType,
GraphQLOutputType,
} from '../../type/definition.ts';
import {
getNamedType,
isInterfaceType,
isLeafType,
isListType,
isNonNullType,
isObjectType,
} from '../../type/definition.ts';
import { sortValueNode } from '../../utilities/sortValueNode.ts';
import { typeFromAST } from '../../utilities/typeFromAST.ts';
import type { ValidationContext } from '../ValidationContext.ts';
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 cachedFieldsAndFragmentSpreads: FieldsAndFragmentSpreadsCache =
new Map();
return {
SelectionSet(selectionSet) {
const conflicts = findConflictsWithinSelectionSet(
context,
cachedFieldsAndFragmentSpreads,
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 = Map<string, Array<NodeAndDef>>;
interface FragmentSpread {
key: string;
node: FragmentSpreadNode;
varMap: Map<string, ValueNode> | undefined;
}
type FragmentSpreads = ReadonlyArray<FragmentSpread>;
type FieldsAndFragmentSpreads = readonly [
NodeAndDefCollection,
FragmentSpreads,
];
type FieldsAndFragmentSpreadsCache = Map<
SelectionSetNode,
Map<Map<string, ValueNode> | undefined, FieldsAndFragmentSpreads>
>;
function findConflictsWithinSelectionSet(
context: ValidationContext,
cachedFieldsAndFragmentSpreads: FieldsAndFragmentSpreadsCache,
comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
comparedFragmentPairs: PairSet<string>,
parentType: Maybe<GraphQLNamedType>,
selectionSet: SelectionSetNode,
): Array<Conflict> {
const conflicts: Array<Conflict> = [];
const varMap = getVarMap(context.getFragmentSignature());
const [fieldMap, fragmentSpreads] = getFieldsAndFragmentSpreads(
context,
cachedFieldsAndFragmentSpreads,
parentType,
selectionSet,
varMap,
);
collectConflictsWithin(
context,
conflicts,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
fieldMap,
varMap,
);
if (fragmentSpreads.length !== 0) {
for (let i = 0; i < fragmentSpreads.length; i++) {
collectConflictsBetweenFieldsAndFragment(
context,
conflicts,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
false,
fieldMap,
varMap,
fragmentSpreads[i],
);
for (let j = i + 1; j < fragmentSpreads.length; j++) {
collectConflictsBetweenFragments(
context,
conflicts,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
false,
fragmentSpreads[i],
fragmentSpreads[j],
);
}
}
}
return conflicts;
}
function collectConflictsBetweenFieldsAndFragment(
context: ValidationContext,
conflicts: Array<Conflict>,
cachedFieldsAndFragmentSpreads: FieldsAndFragmentSpreadsCache,
comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
comparedFragmentPairs: PairSet<string>,
areMutuallyExclusive: boolean,
fieldMap: NodeAndDefCollection,
varMap: Map<string, ValueNode> | undefined,
fragmentSpread: FragmentSpread,
): void {
if (
comparedFieldsAndFragmentPairs.has(
fieldMap,
fragmentSpread.key,
areMutuallyExclusive,
)
) {
return;
}
comparedFieldsAndFragmentPairs.add(
fieldMap,
fragmentSpread.key,
areMutuallyExclusive,
);
const fragment = context.getFragment(fragmentSpread.node.name.value);
if (!fragment) {
return;
}
const [fieldMap2, referencedFragmentSpreads] =
getReferencedFieldsAndFragmentSpreads(
context,
cachedFieldsAndFragmentSpreads,
fragment,
fragmentSpread.varMap,
);
if (fieldMap === fieldMap2) {
return;
}
collectConflictsBetween(
context,
conflicts,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fieldMap,
varMap,
fieldMap2,
fragmentSpread.varMap,
);
for (const referencedFragmentSpread of referencedFragmentSpreads) {
collectConflictsBetweenFieldsAndFragment(
context,
conflicts,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fieldMap,
varMap,
referencedFragmentSpread,
);
}
}
function collectConflictsBetweenFragments(
context: ValidationContext,
conflicts: Array<Conflict>,
cachedFieldsAndFragmentSpreads: FieldsAndFragmentSpreadsCache,
comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
comparedFragmentPairs: PairSet<string>,
areMutuallyExclusive: boolean,
fragmentSpread1: FragmentSpread,
fragmentSpread2: FragmentSpread,
): void {
if (fragmentSpread1.key === fragmentSpread2.key) {
return;
}
if (fragmentSpread1.node.name.value === fragmentSpread2.node.name.value) {
if (
comparedFragmentPairs.has(fragmentSpread1.key, fragmentSpread2.key, false)
) {
return;
}
comparedFragmentPairs.add(fragmentSpread1.key, fragmentSpread2.key, false);
context.reportError(
new GraphQLError(
`Spreads "${fragmentSpread1.node.name.value}" conflict because ${getFragmentSpreadDescription(fragmentSpread1.node)} and ${getFragmentSpreadDescription(fragmentSpread2.node)} have different fragment arguments.`,
{ nodes: [fragmentSpread1.node, fragmentSpread2.node] },
),
);
return;
}
if (
comparedFragmentPairs.has(
fragmentSpread1.key,
fragmentSpread2.key,
areMutuallyExclusive,
)
) {
return;
}
comparedFragmentPairs.add(
fragmentSpread1.key,
fragmentSpread2.key,
areMutuallyExclusive,
);
const fragment1 = context.getFragment(fragmentSpread1.node.name.value);
const fragment2 = context.getFragment(fragmentSpread2.node.name.value);
if (!fragment1 || !fragment2) {
return;
}
const [fieldMap1, referencedFragmentSpreads1] =
getReferencedFieldsAndFragmentSpreads(
context,
cachedFieldsAndFragmentSpreads,
fragment1,
fragmentSpread1.varMap,
);
const [fieldMap2, referencedFragmentSpreads2] =
getReferencedFieldsAndFragmentSpreads(
context,
cachedFieldsAndFragmentSpreads,
fragment2,
fragmentSpread2.varMap,
);
collectConflictsBetween(
context,
conflicts,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fieldMap1,
fragmentSpread1.varMap,
fieldMap2,
fragmentSpread2.varMap,
);
for (const referencedFragmentSpread2 of referencedFragmentSpreads2) {
collectConflictsBetweenFragments(
context,
conflicts,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fragmentSpread1,
referencedFragmentSpread2,
);
}
for (const referencedFragmentSpread1 of referencedFragmentSpreads1) {
collectConflictsBetweenFragments(
context,
conflicts,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
referencedFragmentSpread1,
fragmentSpread2,
);
}
}
function findConflictsBetweenSubSelectionSets(
context: ValidationContext,
cachedFieldsAndFragmentSpreads: FieldsAndFragmentSpreadsCache,
comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
comparedFragmentPairs: PairSet<string>,
areMutuallyExclusive: boolean,
parentType1: Maybe<GraphQLNamedType>,
selectionSet1: SelectionSetNode,
varMap1: Map<string, ValueNode> | undefined,
parentType2: Maybe<GraphQLNamedType>,
selectionSet2: SelectionSetNode,
varMap2: Map<string, ValueNode> | undefined,
): Array<Conflict> {
const conflicts: Array<Conflict> = [];
const [fieldMap1, fragmentSpreads1] = getFieldsAndFragmentSpreads(
context,
cachedFieldsAndFragmentSpreads,
parentType1,
selectionSet1,
varMap1,
);
const [fieldMap2, fragmentSpreads2] = getFieldsAndFragmentSpreads(
context,
cachedFieldsAndFragmentSpreads,
parentType2,
selectionSet2,
varMap2,
);
collectConflictsBetween(
context,
conflicts,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fieldMap1,
varMap1,
fieldMap2,
varMap2,
);
for (const fragmentSpread2 of fragmentSpreads2) {
collectConflictsBetweenFieldsAndFragment(
context,
conflicts,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fieldMap1,
varMap1,
fragmentSpread2,
);
}
for (const fragmentSpread1 of fragmentSpreads1) {
collectConflictsBetweenFieldsAndFragment(
context,
conflicts,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fieldMap2,
varMap2,
fragmentSpread1,
);
}
for (const fragmentSpread1 of fragmentSpreads1) {
for (const fragmentSpread2 of fragmentSpreads2) {
collectConflictsBetweenFragments(
context,
conflicts,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
fragmentSpread1,
fragmentSpread2,
);
}
}
return conflicts;
}
function collectConflictsWithin(
context: ValidationContext,
conflicts: Array<Conflict>,
cachedFieldsAndFragmentSpreads: FieldsAndFragmentSpreadsCache,
comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
comparedFragmentPairs: PairSet<string>,
fieldMap: NodeAndDefCollection,
varMap: Map<string, ValueNode> | undefined,
): void {
for (const [responseName, fields] of fieldMap.entries()) {
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,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
false,
responseName,
fields[i],
varMap,
fields[j],
varMap,
);
if (conflict) {
conflicts.push(conflict);
}
}
}
}
}
}
function collectConflictsBetween(
context: ValidationContext,
conflicts: Array<Conflict>,
cachedFieldsAndFragmentSpreads: FieldsAndFragmentSpreadsCache,
comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
comparedFragmentPairs: PairSet<string>,
parentFieldsAreMutuallyExclusive: boolean,
fieldMap1: NodeAndDefCollection,
varMap1: Map<string, ValueNode> | undefined,
fieldMap2: NodeAndDefCollection,
varMap2: Map<string, ValueNode> | undefined,
): void {
for (const [responseName, fields1] of fieldMap1.entries()) {
const fields2 = fieldMap2.get(responseName);
if (fields2 != null) {
for (const field1 of fields1) {
for (const field2 of fields2) {
const conflict = findConflict(
context,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
parentFieldsAreMutuallyExclusive,
responseName,
field1,
varMap1,
field2,
varMap2,
);
if (conflict) {
conflicts.push(conflict);
}
}
}
}
}
}
function findConflict(
context: ValidationContext,
cachedFieldsAndFragmentSpreads: FieldsAndFragmentSpreadsCache,
comparedFieldsAndFragmentPairs: OrderedPairSet<NodeAndDefCollection, string>,
comparedFragmentPairs: PairSet<string>,
parentFieldsAreMutuallyExclusive: boolean,
responseName: string,
field1: NodeAndDef,
varMap1: Map<string, ValueNode> | undefined,
field2: NodeAndDef,
varMap2: Map<string, ValueNode> | undefined,
): 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.arguments, varMap1, node2.arguments, varMap2)) {
return [
[responseName, 'they have differing arguments'],
[node1],
[node2],
];
}
}
const directives1 = node1.directives;
const directives2 = node2.directives;
const overlappingStreamReason = hasNoOverlappingStreams(
directives1,
varMap1,
directives2,
varMap2,
);
if (overlappingStreamReason !== undefined) {
return [[responseName, overlappingStreamReason], [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,
cachedFieldsAndFragmentSpreads,
comparedFieldsAndFragmentPairs,
comparedFragmentPairs,
areMutuallyExclusive,
getNamedType(type1),
selectionSet1,
varMap1,
getNamedType(type2),
selectionSet2,
varMap2,
);
return subfieldConflicts(conflicts, responseName, node1, node2);
}
}
function sameArguments<T extends ArgumentNode | FragmentArgumentNode>(
args1: ReadonlyArray<T> | undefined,
varMap1: Map<string, ValueNode> | undefined,
args2: ReadonlyArray<T> | undefined,
varMap2: Map<string, ValueNode> | undefined,
): boolean {
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,
varMap2 === undefined ? value : replaceFragmentVariables(value, varMap2),
]),
);
return args1.every((arg1) => {
let value1 = arg1.value;
if (varMap1) {
value1 = replaceFragmentVariables(value1, varMap1);
}
const value2 = values2.get(arg1.name.value);
if (value2 === undefined) {
return false;
}
return stringifyValue(value1) === stringifyValue(value2);
});
}
function replaceFragmentVariables(
valueNode: ValueNode,
varMap: ReadonlyMap<string, ValueNode>,
): ValueNode {
switch (valueNode.kind) {
case Kind.VARIABLE:
return varMap.get(valueNode.name.value) ?? valueNode;
case Kind.LIST:
return {
...valueNode,
values: valueNode.values.map((node) =>
replaceFragmentVariables(node, varMap),
),
};
case Kind.OBJECT:
return {
...valueNode,
fields: valueNode.fields.map((field) => ({
...field,
value: replaceFragmentVariables(field.value, varMap),
})),
};
default: {
return valueNode;
}
}
}
function stringifyValue(value: ValueNode): string | null {
return print(sortValueNode(value));
}
function getStreamDirective(
directives: ReadonlyArray<DirectiveNode> | undefined,
): DirectiveNode | undefined {
return directives?.find((directive) => directive.name.value === 'stream');
}
function hasNoOverlappingStreams(
directives1: ReadonlyArray<DirectiveNode> | undefined,
varMap1: Map<string, ValueNode> | undefined,
directives2: ReadonlyArray<DirectiveNode> | undefined,
varMap2: Map<string, ValueNode> | undefined,
): string | undefined {
const stream1 = getStreamDirective(directives1);
const stream2 = getStreamDirective(directives2);
if (!stream1 && !stream2) {
return;
} else if (stream1 && stream2) {
if (sameArguments(stream1.arguments, varMap1, stream2.arguments, varMap2)) {
return 'they have overlapping stream directives. See https://github.com/graphql/defer-stream-wg/discussions/100';
}
return 'they have overlapping stream directives';
}
return 'they have overlapping stream directives';
}
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 getFieldsAndFragmentSpreads(
context: ValidationContext,
cachedFieldsAndFragmentSpreads: FieldsAndFragmentSpreadsCache,
parentType: Maybe<GraphQLNamedType>,
selectionSet: SelectionSetNode,
varMap: Map<string, ValueNode> | undefined,
): FieldsAndFragmentSpreads {
let cache = cachedFieldsAndFragmentSpreads.get(selectionSet);
if (!cache) {
cache = new Map();
cachedFieldsAndFragmentSpreads.set(selectionSet, cache);
}
const cached = cache.get(varMap);
if (cached) {
return cached;
}
const nodeAndDefs: NodeAndDefCollection = new Map();
const fragmentSpreads = new Map<string, FragmentSpread>();
_collectFieldsAndFragmentSpreads(
context,
parentType,
selectionSet,
nodeAndDefs,
fragmentSpreads,
varMap,
);
const result: FieldsAndFragmentSpreads = [
nodeAndDefs,
Array.from(fragmentSpreads.values()),
];
cache.set(varMap, result);
return result;
}
function getReferencedFieldsAndFragmentSpreads(
context: ValidationContext,
cachedFieldsAndFragmentSpreads: FieldsAndFragmentSpreadsCache,
fragment: FragmentDefinitionNode,
varMap: Map<string, ValueNode> | undefined,
) {
const cached = cachedFieldsAndFragmentSpreads
.get(fragment.selectionSet)
?.get(varMap);
if (cached) {
return cached;
}
const fragmentType = typeFromAST(context.getSchema(), fragment.typeCondition);
return getFieldsAndFragmentSpreads(
context,
cachedFieldsAndFragmentSpreads,
fragmentType,
fragment.selectionSet,
varMap,
);
}
function _collectFieldsAndFragmentSpreads(
context: ValidationContext,
parentType: Maybe<GraphQLNamedType>,
selectionSet: SelectionSetNode,
nodeAndDefs: NodeAndDefCollection,
fragmentSpreads: Map<string, FragmentSpread>,
varMap: Map<string, ValueNode> | undefined,
): 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;
let nodeAndDefsList = nodeAndDefs.get(responseName);
if (nodeAndDefsList == null) {
nodeAndDefsList = [];
nodeAndDefs.set(responseName, nodeAndDefsList);
}
nodeAndDefsList.push([parentType, selection, fieldDef]);
break;
}
case Kind.FRAGMENT_SPREAD: {
const fragmentSpread = getFragmentSpread(context, selection, varMap);
fragmentSpreads.set(fragmentSpread.key, fragmentSpread);
break;
}
case Kind.INLINE_FRAGMENT: {
const typeCondition = selection.typeCondition;
const inlineFragmentType = typeCondition
? typeFromAST(context.getSchema(), typeCondition)
: parentType;
_collectFieldsAndFragmentSpreads(
context,
inlineFragmentType,
selection.selectionSet,
nodeAndDefs,
fragmentSpreads,
varMap,
);
break;
}
}
}
}
function getFragmentSpread(
context: ValidationContext,
fragmentSpreadNode: FragmentSpreadNode,
varMap: Map<string, ValueNode> | undefined,
): FragmentSpread {
let key = '';
const fragmentSignature = context.getFragmentSignatureByName()(
fragmentSpreadNode.name.value,
);
const argMap = new Map<string, ValueNode>();
if (fragmentSpreadNode.arguments) {
for (const arg of fragmentSpreadNode.arguments) {
argMap.set(arg.name.value, arg.value);
}
}
if (fragmentSignature?.variableDefinitions) {
key += fragmentSpreadNode.name.value + '(';
for (const varName of fragmentSignature.variableDefinitions.keys()) {
const value = argMap.get(varName);
if (value) {
const scopedValue = varMap
? replaceFragmentVariables(value, varMap)
: value;
key += varName + ': ' + stringifyValue(scopedValue);
}
}
key += ')';
}
return {
key,
node: fragmentSpreadNode,
varMap: getVarMap(fragmentSignature, key),
};
}
function getVarMap(
fragmentSignature: ReturnType<ValidationContext['getFragmentSignature']>,
key = '',
): Map<string, ValueNode> | undefined {
if (!fragmentSignature || fragmentSignature.variableDefinitions.size === 0) {
return;
}
const varMap = new Map<string, ValueNode>();
for (const variableName of fragmentSignature.variableDefinitions.keys()) {
varMap.set(variableName, {
kind: Kind.VARIABLE,
name: { kind: Kind.NAME, value: JSON.stringify([key, variableName]) },
});
}
return varMap;
}
function getFragmentSpreadDescription(node: FragmentSpreadNode): string {
return print({ ...node, directives: [] }).slice(3);
}
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);
}
}
}