Mirror: The small sibling of the graphql package, slimmed down for client-side libraries.

Add upstream tests for visitor

Changed files
+1372 -21
alias
language
+1337
alias/language/__tests__/visitor.js
···
+
// See: https://github.com/graphql/graphql-js/blob/976d64b/src/language/__tests__/visitor-test.ts
+
+
import { Kind, parse } from 'graphql';
+
import { visit, visitInParallel, BREAK } from '../visitor';
+
+
const kitchenSinkQuery: string = String.raw`
+
query queryName($foo: ComplexType, $site: Site = MOBILE) @onQuery {
+
whoever123is: node(id: [123, 456]) {
+
id
+
... on User @onInlineFragment {
+
field2 {
+
id
+
alias: field1(first: 10, after: $foo) @include(if: $foo) {
+
id
+
...frag @onFragmentSpread
+
}
+
}
+
}
+
... @skip(unless: $foo) {
+
id
+
}
+
... {
+
id
+
}
+
}
+
}
+
mutation likeStory @onMutation {
+
like(story: 123) @onField {
+
story {
+
id @onField
+
}
+
}
+
}
+
subscription StoryLikeSubscription(
+
$input: StoryLikeSubscribeInput @onVariableDefinition
+
)
+
@onSubscription {
+
storyLikeSubscribe(input: $input) {
+
story {
+
likers {
+
count
+
}
+
likeSentence {
+
text
+
}
+
}
+
}
+
}
+
fragment frag on Friend @onFragmentDefinition {
+
foo(
+
size: $size
+
bar: $b
+
obj: {
+
key: "value"
+
block: """
+
block string uses \"""
+
"""
+
}
+
)
+
}
+
{
+
unnamed(truthy: true, falsy: false, nullish: null)
+
query
+
}
+
query {
+
__typename
+
}
+
`;
+
+
function checkVisitorFnArgs(ast, args, isEdited = false) {
+
const [node, key, parent, path, ancestors] = args;
+
+
expect(node).toBeInstanceOf(Object);
+
expect(Object.values(Kind)).toContain(node.kind);
+
+
const isRoot = key === undefined;
+
if (isRoot) {
+
if (!isEdited) {
+
expect(node).toEqual(ast);
+
}
+
expect(parent).toEqual(undefined);
+
expect(path).toEqual([]);
+
expect(ancestors).toEqual([]);
+
return;
+
}
+
+
expect(typeof key).toMatch(/number|string/);
+
+
expect(parent).toHaveProperty([key]);
+
+
expect(path).toBeInstanceOf(Array);
+
expect(path[path.length - 1]).toEqual(key);
+
+
expect(ancestors).toBeInstanceOf(Array);
+
expect(ancestors.length).toEqual(path.length - 1);
+
+
if (!isEdited) {
+
let currentNode = ast;
+
for (let i = 0; i < ancestors.length; ++i) {
+
expect(ancestors[i]).toEqual(currentNode);
+
+
currentNode = currentNode[path[i]];
+
expect(currentNode).not.toEqual(undefined);
+
}
+
+
// expect(parent).toEqual(currentNode);
+
// expect(parent[key]).toEqual(node);
+
}
+
}
+
+
function isNode(node) {
+
return node != null && typeof node.kind === 'string';
+
}
+
+
function getValue(node) {
+
return 'value' in node ? node.value : undefined;
+
}
+
+
describe('Visitor', () => {
+
it('handles empty visitor', () => {
+
const ast = parse('{ a }', { noLocation: true });
+
expect(() => visit(ast, {})).not.toThrow();
+
});
+
+
it('validates path argument', () => {
+
const visited: Array<any> = [];
+
+
const ast = parse('{ a }', { noLocation: true });
+
+
visit(ast, {
+
enter(_node, _key, _parent, path) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', path.slice()]);
+
},
+
leave(_node, _key, _parent, path) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', path.slice()]);
+
},
+
});
+
+
expect(visited).toEqual([
+
['enter', []],
+
['enter', ['definitions', 0]],
+
['enter', ['definitions', 0, 'selectionSet']],
+
['enter', ['definitions', 0, 'selectionSet', 'selections', 0]],
+
['enter', ['definitions', 0, 'selectionSet', 'selections', 0, 'name']],
+
['leave', ['definitions', 0, 'selectionSet', 'selections', 0, 'name']],
+
['leave', ['definitions', 0, 'selectionSet', 'selections', 0]],
+
['leave', ['definitions', 0, 'selectionSet']],
+
['leave', ['definitions', 0]],
+
['leave', []],
+
]);
+
});
+
+
it('validates ancestors argument', () => {
+
const ast = parse('{ a }', { noLocation: true });
+
const visitedNodes: Array<any> = [];
+
+
visit(ast, {
+
enter(node, key, parent, _path, ancestors) {
+
const inArray = typeof key === 'number';
+
if (inArray) {
+
visitedNodes.push(parent);
+
}
+
visitedNodes.push(node);
+
+
const expectedAncestors = visitedNodes.slice(0, -2);
+
expect(ancestors).toEqual(expectedAncestors);
+
},
+
leave(_node, key, _parent, _path, ancestors) {
+
const expectedAncestors = visitedNodes.slice(0, -2);
+
expect(ancestors).toEqual(expectedAncestors);
+
+
const inArray = typeof key === 'number';
+
if (inArray) {
+
visitedNodes.pop();
+
}
+
visitedNodes.pop();
+
},
+
});
+
});
+
+
it('allows editing a node both on enter and on leave', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
+
let selectionSet: SelectionSetNode;
+
+
const editedAST = visit(ast, {
+
OperationDefinition: {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
selectionSet = node.selectionSet;
+
return {
+
...node,
+
selectionSet: {
+
kind: 'SelectionSet',
+
selections: [],
+
},
+
didEnter: true,
+
};
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
return {
+
...node,
+
selectionSet,
+
didLeave: true,
+
};
+
},
+
},
+
});
+
+
expect(editedAST).toEqual({
+
...ast,
+
definitions: [
+
{
+
...ast.definitions[0],
+
didEnter: true,
+
didLeave: true,
+
},
+
],
+
});
+
});
+
+
it('allows editing the root node on enter and on leave', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
+
const { definitions } = ast;
+
+
const editedAST = visit(ast, {
+
Document: {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
return {
+
...node,
+
definitions: [],
+
didEnter: true,
+
};
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
return {
+
...node,
+
definitions,
+
didLeave: true,
+
};
+
},
+
},
+
});
+
+
expect(editedAST).toEqual({
+
...ast,
+
didEnter: true,
+
didLeave: true,
+
});
+
});
+
+
it('allows for editing on enter', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
const editedAST = visit(ast, {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
if (node.kind === 'Field' && node.name.value === 'b') {
+
return null;
+
}
+
},
+
});
+
+
expect(ast).toEqual(parse('{ a, b, c { a, b, c } }', { noLocation: true }));
+
+
expect(editedAST).toEqual(
+
parse('{ a, c { a, c } }', { noLocation: true })
+
);
+
});
+
+
it('allows for editing on leave', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
const editedAST = visit(ast, {
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
if (node.kind === 'Field' && node.name.value === 'b') {
+
return null;
+
}
+
},
+
});
+
+
expect(ast).toEqual(parse('{ a, b, c { a, b, c } }', { noLocation: true }));
+
+
expect(editedAST).toEqual(
+
parse('{ a, c { a, c } }', { noLocation: true })
+
);
+
});
+
+
it('ignores false returned on leave', () => {
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
const returnedAST = visit(ast, {
+
leave() {
+
return false;
+
},
+
});
+
+
expect(returnedAST).toEqual(
+
parse('{ a, b, c { a, b, c } }', { noLocation: true })
+
);
+
});
+
+
it('visits edited node', () => {
+
const addedField = {
+
kind: 'Field',
+
name: {
+
kind: 'Name',
+
value: '__typename',
+
},
+
};
+
+
let didVisitAddedField;
+
+
const ast = parse('{ a { x } }', { noLocation: true });
+
visit(ast, {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
if (node.kind === 'Field' && node.name.value === 'a') {
+
return {
+
kind: 'Field',
+
selectionSet: [addedField, node.selectionSet],
+
};
+
}
+
if (node === addedField) {
+
didVisitAddedField = true;
+
}
+
},
+
});
+
+
expect(didVisitAddedField).toEqual(true);
+
});
+
+
it('allows skipping a sub-tree', () => {
+
const visited: Array<any> = [];
+
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
+
visit(ast, {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
if (node.kind === 'Field' && node.name.value === 'b') {
+
return false;
+
}
+
},
+
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
},
+
});
+
+
expect(visited).toEqual([
+
['enter', 'Document', undefined],
+
['enter', 'OperationDefinition', undefined],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'c'],
+
['leave', 'Name', 'c'],
+
['leave', 'Field', undefined],
+
['leave', 'SelectionSet', undefined],
+
['leave', 'OperationDefinition', undefined],
+
['leave', 'Document', undefined],
+
]);
+
});
+
+
it('allows early exit while visiting', () => {
+
const visited: Array<any> = [];
+
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
+
visit(ast, {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
if (node.kind === 'Name' && node.value === 'x') {
+
return BREAK;
+
}
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
},
+
});
+
+
expect(visited).toEqual([
+
['enter', 'Document', undefined],
+
['enter', 'OperationDefinition', undefined],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'b'],
+
['leave', 'Name', 'b'],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'x'],
+
]);
+
});
+
+
it('allows early exit while leaving', () => {
+
const visited: Array<any> = [];
+
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
+
visit(ast, {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
},
+
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
if (node.kind === 'Name' && node.value === 'x') {
+
return BREAK;
+
}
+
},
+
});
+
+
expect(visited).toEqual([
+
['enter', 'Document', undefined],
+
['enter', 'OperationDefinition', undefined],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'b'],
+
['leave', 'Name', 'b'],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'x'],
+
['leave', 'Name', 'x'],
+
]);
+
});
+
+
it('allows a named functions visitor API', () => {
+
const visited: Array<any> = [];
+
+
const ast = parse('{ a, b { x }, c }', { noLocation: true });
+
visit(ast, {
+
Name(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
},
+
SelectionSet: {
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
},
+
},
+
});
+
+
expect(visited).toEqual([
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Name', 'a'],
+
['enter', 'Name', 'b'],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Name', 'x'],
+
['leave', 'SelectionSet', undefined],
+
['enter', 'Name', 'c'],
+
['leave', 'SelectionSet', undefined],
+
]);
+
});
+
+
it('visits kitchen sink', () => {
+
const ast = parse(kitchenSinkQuery);
+
const visited: Array<any> = [];
+
const argsStack: Array<any> = [];
+
+
visit(ast, {
+
enter(node, key, parent) {
+
visited.push([
+
'enter',
+
node.kind,
+
key,
+
isNode(parent) ? parent.kind : undefined,
+
]);
+
+
checkVisitorFnArgs(ast, arguments);
+
argsStack.push([...arguments]);
+
},
+
+
leave(node, key, parent) {
+
visited.push([
+
'leave',
+
node.kind,
+
key,
+
isNode(parent) ? parent.kind : undefined,
+
]);
+
+
expect(argsStack.pop()).toEqual([...arguments]);
+
},
+
});
+
+
expect(argsStack).toEqual([]);
+
expect(visited).toEqual([
+
['enter', 'Document', undefined, undefined],
+
['enter', 'OperationDefinition', 0, undefined],
+
['enter', 'Name', 'name', 'OperationDefinition'],
+
['leave', 'Name', 'name', 'OperationDefinition'],
+
['enter', 'VariableDefinition', 0, undefined],
+
['enter', 'Variable', 'variable', 'VariableDefinition'],
+
['enter', 'Name', 'name', 'Variable'],
+
['leave', 'Name', 'name', 'Variable'],
+
['leave', 'Variable', 'variable', 'VariableDefinition'],
+
['enter', 'NamedType', 'type', 'VariableDefinition'],
+
['enter', 'Name', 'name', 'NamedType'],
+
['leave', 'Name', 'name', 'NamedType'],
+
['leave', 'NamedType', 'type', 'VariableDefinition'],
+
['leave', 'VariableDefinition', 0, undefined],
+
['enter', 'VariableDefinition', 1, undefined],
+
['enter', 'Variable', 'variable', 'VariableDefinition'],
+
['enter', 'Name', 'name', 'Variable'],
+
['leave', 'Name', 'name', 'Variable'],
+
['leave', 'Variable', 'variable', 'VariableDefinition'],
+
['enter', 'NamedType', 'type', 'VariableDefinition'],
+
['enter', 'Name', 'name', 'NamedType'],
+
['leave', 'Name', 'name', 'NamedType'],
+
['leave', 'NamedType', 'type', 'VariableDefinition'],
+
['enter', 'EnumValue', 'defaultValue', 'VariableDefinition'],
+
['leave', 'EnumValue', 'defaultValue', 'VariableDefinition'],
+
['leave', 'VariableDefinition', 1, undefined],
+
['enter', 'Directive', 0, undefined],
+
['enter', 'Name', 'name', 'Directive'],
+
['leave', 'Name', 'name', 'Directive'],
+
['leave', 'Directive', 0, undefined],
+
['enter', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'alias', 'Field'],
+
['leave', 'Name', 'alias', 'Field'],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['enter', 'Argument', 0, undefined],
+
['enter', 'Name', 'name', 'Argument'],
+
['leave', 'Name', 'name', 'Argument'],
+
['enter', 'ListValue', 'value', 'Argument'],
+
['enter', 'IntValue', 0, undefined],
+
['leave', 'IntValue', 0, undefined],
+
['enter', 'IntValue', 1, undefined],
+
['leave', 'IntValue', 1, undefined],
+
['leave', 'ListValue', 'value', 'Argument'],
+
['leave', 'Argument', 0, undefined],
+
['enter', 'SelectionSet', 'selectionSet', 'Field'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['enter', 'InlineFragment', 1, undefined],
+
['enter', 'NamedType', 'typeCondition', 'InlineFragment'],
+
['enter', 'Name', 'name', 'NamedType'],
+
['leave', 'Name', 'name', 'NamedType'],
+
['leave', 'NamedType', 'typeCondition', 'InlineFragment'],
+
['enter', 'Directive', 0, undefined],
+
['enter', 'Name', 'name', 'Directive'],
+
['leave', 'Name', 'name', 'Directive'],
+
['leave', 'Directive', 0, undefined],
+
['enter', 'SelectionSet', 'selectionSet', 'InlineFragment'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['enter', 'SelectionSet', 'selectionSet', 'Field'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['enter', 'Field', 1, undefined],
+
['enter', 'Name', 'alias', 'Field'],
+
['leave', 'Name', 'alias', 'Field'],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['enter', 'Argument', 0, undefined],
+
['enter', 'Name', 'name', 'Argument'],
+
['leave', 'Name', 'name', 'Argument'],
+
['enter', 'IntValue', 'value', 'Argument'],
+
['leave', 'IntValue', 'value', 'Argument'],
+
['leave', 'Argument', 0, undefined],
+
['enter', 'Argument', 1, undefined],
+
['enter', 'Name', 'name', 'Argument'],
+
['leave', 'Name', 'name', 'Argument'],
+
['enter', 'Variable', 'value', 'Argument'],
+
['enter', 'Name', 'name', 'Variable'],
+
['leave', 'Name', 'name', 'Variable'],
+
['leave', 'Variable', 'value', 'Argument'],
+
['leave', 'Argument', 1, undefined],
+
['enter', 'Directive', 0, undefined],
+
['enter', 'Name', 'name', 'Directive'],
+
['leave', 'Name', 'name', 'Directive'],
+
['enter', 'Argument', 0, undefined],
+
['enter', 'Name', 'name', 'Argument'],
+
['leave', 'Name', 'name', 'Argument'],
+
['enter', 'Variable', 'value', 'Argument'],
+
['enter', 'Name', 'name', 'Variable'],
+
['leave', 'Name', 'name', 'Variable'],
+
['leave', 'Variable', 'value', 'Argument'],
+
['leave', 'Argument', 0, undefined],
+
['leave', 'Directive', 0, undefined],
+
['enter', 'SelectionSet', 'selectionSet', 'Field'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['enter', 'FragmentSpread', 1, undefined],
+
['enter', 'Name', 'name', 'FragmentSpread'],
+
['leave', 'Name', 'name', 'FragmentSpread'],
+
['enter', 'Directive', 0, undefined],
+
['enter', 'Name', 'name', 'Directive'],
+
['leave', 'Name', 'name', 'Directive'],
+
['leave', 'Directive', 0, undefined],
+
['leave', 'FragmentSpread', 1, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'Field'],
+
['leave', 'Field', 1, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'InlineFragment'],
+
['leave', 'InlineFragment', 1, undefined],
+
['enter', 'InlineFragment', 2, undefined],
+
['enter', 'Directive', 0, undefined],
+
['enter', 'Name', 'name', 'Directive'],
+
['leave', 'Name', 'name', 'Directive'],
+
['enter', 'Argument', 0, undefined],
+
['enter', 'Name', 'name', 'Argument'],
+
['leave', 'Name', 'name', 'Argument'],
+
['enter', 'Variable', 'value', 'Argument'],
+
['enter', 'Name', 'name', 'Variable'],
+
['leave', 'Name', 'name', 'Variable'],
+
['leave', 'Variable', 'value', 'Argument'],
+
['leave', 'Argument', 0, undefined],
+
['leave', 'Directive', 0, undefined],
+
['enter', 'SelectionSet', 'selectionSet', 'InlineFragment'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'InlineFragment'],
+
['leave', 'InlineFragment', 2, undefined],
+
['enter', 'InlineFragment', 3, undefined],
+
['enter', 'SelectionSet', 'selectionSet', 'InlineFragment'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'InlineFragment'],
+
['leave', 'InlineFragment', 3, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
+
['leave', 'OperationDefinition', 0, undefined],
+
['enter', 'OperationDefinition', 1, undefined],
+
['enter', 'Name', 'name', 'OperationDefinition'],
+
['leave', 'Name', 'name', 'OperationDefinition'],
+
['enter', 'Directive', 0, undefined],
+
['enter', 'Name', 'name', 'Directive'],
+
['leave', 'Name', 'name', 'Directive'],
+
['leave', 'Directive', 0, undefined],
+
['enter', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['enter', 'Argument', 0, undefined],
+
['enter', 'Name', 'name', 'Argument'],
+
['leave', 'Name', 'name', 'Argument'],
+
['enter', 'IntValue', 'value', 'Argument'],
+
['leave', 'IntValue', 'value', 'Argument'],
+
['leave', 'Argument', 0, undefined],
+
['enter', 'Directive', 0, undefined],
+
['enter', 'Name', 'name', 'Directive'],
+
['leave', 'Name', 'name', 'Directive'],
+
['leave', 'Directive', 0, undefined],
+
['enter', 'SelectionSet', 'selectionSet', 'Field'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['enter', 'SelectionSet', 'selectionSet', 'Field'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['enter', 'Directive', 0, undefined],
+
['enter', 'Name', 'name', 'Directive'],
+
['leave', 'Name', 'name', 'Directive'],
+
['leave', 'Directive', 0, undefined],
+
['leave', 'Field', 0, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
+
['leave', 'OperationDefinition', 1, undefined],
+
['enter', 'OperationDefinition', 2, undefined],
+
['enter', 'Name', 'name', 'OperationDefinition'],
+
['leave', 'Name', 'name', 'OperationDefinition'],
+
['enter', 'VariableDefinition', 0, undefined],
+
['enter', 'Variable', 'variable', 'VariableDefinition'],
+
['enter', 'Name', 'name', 'Variable'],
+
['leave', 'Name', 'name', 'Variable'],
+
['leave', 'Variable', 'variable', 'VariableDefinition'],
+
['enter', 'NamedType', 'type', 'VariableDefinition'],
+
['enter', 'Name', 'name', 'NamedType'],
+
['leave', 'Name', 'name', 'NamedType'],
+
['leave', 'NamedType', 'type', 'VariableDefinition'],
+
['enter', 'Directive', 0, undefined],
+
['enter', 'Name', 'name', 'Directive'],
+
['leave', 'Name', 'name', 'Directive'],
+
['leave', 'Directive', 0, undefined],
+
['leave', 'VariableDefinition', 0, undefined],
+
['enter', 'Directive', 0, undefined],
+
['enter', 'Name', 'name', 'Directive'],
+
['leave', 'Name', 'name', 'Directive'],
+
['leave', 'Directive', 0, undefined],
+
['enter', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['enter', 'Argument', 0, undefined],
+
['enter', 'Name', 'name', 'Argument'],
+
['leave', 'Name', 'name', 'Argument'],
+
['enter', 'Variable', 'value', 'Argument'],
+
['enter', 'Name', 'name', 'Variable'],
+
['leave', 'Name', 'name', 'Variable'],
+
['leave', 'Variable', 'value', 'Argument'],
+
['leave', 'Argument', 0, undefined],
+
['enter', 'SelectionSet', 'selectionSet', 'Field'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['enter', 'SelectionSet', 'selectionSet', 'Field'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['enter', 'SelectionSet', 'selectionSet', 'Field'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['enter', 'Field', 1, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['enter', 'SelectionSet', 'selectionSet', 'Field'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'Field'],
+
['leave', 'Field', 1, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
+
['leave', 'OperationDefinition', 2, undefined],
+
['enter', 'FragmentDefinition', 3, undefined],
+
['enter', 'Name', 'name', 'FragmentDefinition'],
+
['leave', 'Name', 'name', 'FragmentDefinition'],
+
['enter', 'NamedType', 'typeCondition', 'FragmentDefinition'],
+
['enter', 'Name', 'name', 'NamedType'],
+
['leave', 'Name', 'name', 'NamedType'],
+
['leave', 'NamedType', 'typeCondition', 'FragmentDefinition'],
+
['enter', 'Directive', 0, undefined],
+
['enter', 'Name', 'name', 'Directive'],
+
['leave', 'Name', 'name', 'Directive'],
+
['leave', 'Directive', 0, undefined],
+
['enter', 'SelectionSet', 'selectionSet', 'FragmentDefinition'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['enter', 'Argument', 0, undefined],
+
['enter', 'Name', 'name', 'Argument'],
+
['leave', 'Name', 'name', 'Argument'],
+
['enter', 'Variable', 'value', 'Argument'],
+
['enter', 'Name', 'name', 'Variable'],
+
['leave', 'Name', 'name', 'Variable'],
+
['leave', 'Variable', 'value', 'Argument'],
+
['leave', 'Argument', 0, undefined],
+
['enter', 'Argument', 1, undefined],
+
['enter', 'Name', 'name', 'Argument'],
+
['leave', 'Name', 'name', 'Argument'],
+
['enter', 'Variable', 'value', 'Argument'],
+
['enter', 'Name', 'name', 'Variable'],
+
['leave', 'Name', 'name', 'Variable'],
+
['leave', 'Variable', 'value', 'Argument'],
+
['leave', 'Argument', 1, undefined],
+
['enter', 'Argument', 2, undefined],
+
['enter', 'Name', 'name', 'Argument'],
+
['leave', 'Name', 'name', 'Argument'],
+
['enter', 'ObjectValue', 'value', 'Argument'],
+
['enter', 'ObjectField', 0, undefined],
+
['enter', 'Name', 'name', 'ObjectField'],
+
['leave', 'Name', 'name', 'ObjectField'],
+
['enter', 'StringValue', 'value', 'ObjectField'],
+
['leave', 'StringValue', 'value', 'ObjectField'],
+
['leave', 'ObjectField', 0, undefined],
+
['enter', 'ObjectField', 1, undefined],
+
['enter', 'Name', 'name', 'ObjectField'],
+
['leave', 'Name', 'name', 'ObjectField'],
+
['enter', 'StringValue', 'value', 'ObjectField'],
+
['leave', 'StringValue', 'value', 'ObjectField'],
+
['leave', 'ObjectField', 1, undefined],
+
['leave', 'ObjectValue', 'value', 'Argument'],
+
['leave', 'Argument', 2, undefined],
+
['leave', 'Field', 0, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'FragmentDefinition'],
+
['leave', 'FragmentDefinition', 3, undefined],
+
['enter', 'OperationDefinition', 4, undefined],
+
['enter', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['enter', 'Argument', 0, undefined],
+
['enter', 'Name', 'name', 'Argument'],
+
['leave', 'Name', 'name', 'Argument'],
+
['enter', 'BooleanValue', 'value', 'Argument'],
+
['leave', 'BooleanValue', 'value', 'Argument'],
+
['leave', 'Argument', 0, undefined],
+
['enter', 'Argument', 1, undefined],
+
['enter', 'Name', 'name', 'Argument'],
+
['leave', 'Name', 'name', 'Argument'],
+
['enter', 'BooleanValue', 'value', 'Argument'],
+
['leave', 'BooleanValue', 'value', 'Argument'],
+
['leave', 'Argument', 1, undefined],
+
['enter', 'Argument', 2, undefined],
+
['enter', 'Name', 'name', 'Argument'],
+
['leave', 'Name', 'name', 'Argument'],
+
['enter', 'NullValue', 'value', 'Argument'],
+
['leave', 'NullValue', 'value', 'Argument'],
+
['leave', 'Argument', 2, undefined],
+
['leave', 'Field', 0, undefined],
+
['enter', 'Field', 1, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['leave', 'Field', 1, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
+
['leave', 'OperationDefinition', 4, undefined],
+
['enter', 'OperationDefinition', 5, undefined],
+
['enter', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
+
['enter', 'Field', 0, undefined],
+
['enter', 'Name', 'name', 'Field'],
+
['leave', 'Name', 'name', 'Field'],
+
['leave', 'Field', 0, undefined],
+
['leave', 'SelectionSet', 'selectionSet', 'OperationDefinition'],
+
['leave', 'OperationDefinition', 5, undefined],
+
['leave', 'Document', undefined, undefined],
+
]);
+
});
+
+
// TODO: Verify discrepancies
+
describe('visitInParallel', () => {
+
// Note: nearly identical to the above test of the same test but
+
// using visitInParallel.
+
it('allows skipping a sub-tree', () => {
+
const visited: Array<any> = [];
+
+
const ast = parse('{ a, b { x }, c }');
+
visit(
+
ast,
+
visitInParallel([
+
{
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
if (node.kind === 'Field' && node.name.value === 'b') {
+
return false;
+
}
+
},
+
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
},
+
},
+
])
+
);
+
+
expect(visited).toEqual([
+
['enter', 'Document', undefined],
+
['enter', 'OperationDefinition', undefined],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'c'],
+
['leave', 'Name', 'c'],
+
['leave', 'Field', undefined],
+
['leave', 'SelectionSet', undefined],
+
['leave', 'OperationDefinition', undefined],
+
['leave', 'Document', undefined],
+
]);
+
});
+
+
it('allows skipping different sub-trees', () => {
+
const visited: Array<any> = [];
+
+
const ast = parse('{ a { x }, b { y} }');
+
visit(
+
ast,
+
visitInParallel([
+
{
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['no-a', 'enter', node.kind, getValue(node)]);
+
if (node.kind === 'Field' && node.name.value === 'a') {
+
return false;
+
}
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['no-a', 'leave', node.kind, getValue(node)]);
+
},
+
},
+
{
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['no-b', 'enter', node.kind, getValue(node)]);
+
if (node.kind === 'Field' && node.name.value === 'b') {
+
return false;
+
}
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['no-b', 'leave', node.kind, getValue(node)]);
+
},
+
},
+
])
+
);
+
+
expect(visited).toEqual([
+
['no-a', 'enter', 'Document', undefined],
+
['no-b', 'enter', 'Document', undefined],
+
['no-a', 'enter', 'OperationDefinition', undefined],
+
['no-b', 'enter', 'OperationDefinition', undefined],
+
['no-a', 'enter', 'SelectionSet', undefined],
+
['no-b', 'enter', 'SelectionSet', undefined],
+
['no-a', 'enter', 'Field', undefined],
+
['no-b', 'enter', 'Field', undefined],
+
['no-b', 'enter', 'Name', 'a'],
+
['no-b', 'leave', 'Name', 'a'],
+
['no-b', 'enter', 'SelectionSet', undefined],
+
['no-b', 'enter', 'Field', undefined],
+
['no-b', 'enter', 'Name', 'x'],
+
['no-b', 'leave', 'Name', 'x'],
+
['no-b', 'leave', 'Field', undefined],
+
['no-b', 'leave', 'SelectionSet', undefined],
+
['no-b', 'leave', 'Field', undefined],
+
['no-a', 'enter', 'Field', undefined],
+
['no-b', 'enter', 'Field', undefined],
+
['no-a', 'enter', 'Name', 'b'],
+
['no-a', 'leave', 'Name', 'b'],
+
['no-a', 'enter', 'SelectionSet', undefined],
+
['no-a', 'enter', 'Field', undefined],
+
['no-a', 'enter', 'Name', 'y'],
+
['no-a', 'leave', 'Name', 'y'],
+
['no-a', 'leave', 'Field', undefined],
+
['no-a', 'leave', 'SelectionSet', undefined],
+
['no-a', 'leave', 'Field', undefined],
+
['no-a', 'leave', 'SelectionSet', undefined],
+
['no-b', 'leave', 'SelectionSet', undefined],
+
['no-a', 'leave', 'OperationDefinition', undefined],
+
['no-b', 'leave', 'OperationDefinition', undefined],
+
['no-a', 'leave', 'Document', undefined],
+
['no-b', 'leave', 'Document', undefined],
+
]);
+
});
+
+
// Note: nearly identical to the above test of the same test but
+
// using visitInParallel.
+
it('allows early exit while visiting', () => {
+
const visited: Array<any> = [];
+
+
const ast = parse('{ a, b { x }, c }');
+
visit(
+
ast,
+
visitInParallel([
+
{
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
if (node.kind === 'Name' && node.value === 'x') {
+
return BREAK;
+
}
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
},
+
},
+
])
+
);
+
+
expect(visited).toEqual([
+
['enter', 'Document', undefined],
+
['enter', 'OperationDefinition', undefined],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'b'],
+
['leave', 'Name', 'b'],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'x'],
+
]);
+
});
+
+
it('allows early exit from different points', () => {
+
const visited: Array<any> = [];
+
+
const ast = parse('{ a { y }, b { x } }');
+
visit(
+
ast,
+
visitInParallel([
+
{
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['break-a', 'enter', node.kind, getValue(node)]);
+
if (node.kind === 'Name' && node.value === 'a') {
+
return BREAK;
+
}
+
},
+
// istanbul ignore next (Never called and used as a placeholder)
+
leave() {
+
expect.fail('Should not be called');
+
},
+
},
+
{
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['break-b', 'enter', node.kind, getValue(node)]);
+
if (node.kind === 'Name' && node.value === 'b') {
+
return BREAK;
+
}
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['break-b', 'leave', node.kind, getValue(node)]);
+
},
+
},
+
])
+
);
+
+
expect(visited).toEqual([
+
['break-a', 'enter', 'Document', undefined],
+
['break-b', 'enter', 'Document', undefined],
+
['break-a', 'enter', 'OperationDefinition', undefined],
+
['break-b', 'enter', 'OperationDefinition', undefined],
+
['break-a', 'enter', 'SelectionSet', undefined],
+
['break-b', 'enter', 'SelectionSet', undefined],
+
['break-a', 'enter', 'Field', undefined],
+
['break-b', 'enter', 'Field', undefined],
+
['break-a', 'enter', 'Name', 'a'],
+
['break-b', 'enter', 'Name', 'a'],
+
['break-b', 'leave', 'Name', 'a'],
+
['break-b', 'enter', 'SelectionSet', undefined],
+
['break-b', 'enter', 'Field', undefined],
+
['break-b', 'enter', 'Name', 'y'],
+
['break-b', 'leave', 'Name', 'y'],
+
['break-b', 'leave', 'Field', undefined],
+
['break-b', 'leave', 'SelectionSet', undefined],
+
['break-b', 'leave', 'Field', undefined],
+
['break-b', 'enter', 'Field', undefined],
+
['break-b', 'enter', 'Name', 'b'],
+
]);
+
});
+
+
// Note: nearly identical to the above test of the same test but
+
// using visitInParallel.
+
it('allows early exit while leaving', () => {
+
const visited: Array<any> = [];
+
+
const ast = parse('{ a, b { x }, c }');
+
visit(
+
ast,
+
visitInParallel([
+
{
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['leave', node.kind, getValue(node)]);
+
if (node.kind === 'Name' && node.value === 'x') {
+
return BREAK;
+
}
+
},
+
},
+
])
+
);
+
+
expect(visited).toEqual([
+
['enter', 'Document', undefined],
+
['enter', 'OperationDefinition', undefined],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'b'],
+
['leave', 'Name', 'b'],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'x'],
+
['leave', 'Name', 'x'],
+
]);
+
});
+
+
it('allows early exit from leaving different points', () => {
+
const visited: Array<any> = [];
+
+
const ast = parse('{ a { y }, b { x } }');
+
visit(
+
ast,
+
visitInParallel([
+
{
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['break-a', 'enter', node.kind, getValue(node)]);
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['break-a', 'leave', node.kind, getValue(node)]);
+
if (node.kind === 'Field' && node.name.value === 'a') {
+
return BREAK;
+
}
+
},
+
},
+
{
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['break-b', 'enter', node.kind, getValue(node)]);
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['break-b', 'leave', node.kind, getValue(node)]);
+
if (node.kind === 'Field' && node.name.value === 'b') {
+
return BREAK;
+
}
+
},
+
},
+
])
+
);
+
+
expect(visited).toEqual([
+
['break-a', 'enter', 'Document', undefined],
+
['break-b', 'enter', 'Document', undefined],
+
['break-a', 'enter', 'OperationDefinition', undefined],
+
['break-b', 'enter', 'OperationDefinition', undefined],
+
['break-a', 'enter', 'SelectionSet', undefined],
+
['break-b', 'enter', 'SelectionSet', undefined],
+
['break-a', 'enter', 'Field', undefined],
+
['break-b', 'enter', 'Field', undefined],
+
['break-a', 'enter', 'Name', 'a'],
+
['break-b', 'enter', 'Name', 'a'],
+
['break-a', 'leave', 'Name', 'a'],
+
['break-b', 'leave', 'Name', 'a'],
+
['break-a', 'enter', 'SelectionSet', undefined],
+
['break-b', 'enter', 'SelectionSet', undefined],
+
['break-a', 'enter', 'Field', undefined],
+
['break-b', 'enter', 'Field', undefined],
+
['break-a', 'enter', 'Name', 'y'],
+
['break-b', 'enter', 'Name', 'y'],
+
['break-a', 'leave', 'Name', 'y'],
+
['break-b', 'leave', 'Name', 'y'],
+
['break-a', 'leave', 'Field', undefined],
+
['break-b', 'leave', 'Field', undefined],
+
['break-a', 'leave', 'SelectionSet', undefined],
+
['break-b', 'leave', 'SelectionSet', undefined],
+
['break-a', 'leave', 'Field', undefined],
+
['break-b', 'leave', 'Field', undefined],
+
['break-b', 'enter', 'Field', undefined],
+
['break-b', 'enter', 'Name', 'b'],
+
['break-b', 'leave', 'Name', 'b'],
+
['break-b', 'enter', 'SelectionSet', undefined],
+
['break-b', 'enter', 'Field', undefined],
+
['break-b', 'enter', 'Name', 'x'],
+
['break-b', 'leave', 'Name', 'x'],
+
['break-b', 'leave', 'Field', undefined],
+
['break-b', 'leave', 'SelectionSet', undefined],
+
['break-b', 'leave', 'Field', undefined],
+
]);
+
});
+
+
it('allows for editing on enter', () => {
+
const visited: Array<any> = [];
+
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
const editedAST = visit(
+
ast,
+
visitInParallel([
+
{
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
if (node.kind === 'Field' && node.name.value === 'b') {
+
return null;
+
}
+
},
+
},
+
{
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
visited.push(['leave', node.kind, getValue(node)]);
+
},
+
},
+
])
+
);
+
+
expect(ast).toEqual(
+
parse('{ a, b, c { a, b, c } }', { noLocation: true })
+
);
+
+
expect(editedAST).toEqual(
+
parse('{ a, c { a, c } }', { noLocation: true })
+
);
+
+
expect(visited).toEqual([
+
['enter', 'Document', undefined],
+
['enter', 'OperationDefinition', undefined],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'c'],
+
['leave', 'Name', 'c'],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'c'],
+
['leave', 'Name', 'c'],
+
['leave', 'Field', undefined],
+
['leave', 'SelectionSet', undefined],
+
['leave', 'Field', undefined],
+
['leave', 'SelectionSet', undefined],
+
['leave', 'OperationDefinition', undefined],
+
['leave', 'Document', undefined],
+
]);
+
});
+
+
it('allows for editing on leave', () => {
+
const visited: Array<any> = [];
+
+
const ast = parse('{ a, b, c { a, b, c } }', { noLocation: true });
+
const editedAST = visit(
+
ast,
+
visitInParallel([
+
{
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
if (node.kind === 'Field' && node.name.value === 'b') {
+
return null;
+
}
+
},
+
},
+
{
+
enter(node) {
+
checkVisitorFnArgs(ast, arguments);
+
visited.push(['enter', node.kind, getValue(node)]);
+
},
+
leave(node) {
+
checkVisitorFnArgs(ast, arguments, /* isEdited */ true);
+
visited.push(['leave', node.kind, getValue(node)]);
+
},
+
},
+
])
+
);
+
+
expect(ast).toEqual(
+
parse('{ a, b, c { a, b, c } }', { noLocation: true })
+
);
+
+
expect(editedAST).toEqual(
+
parse('{ a, c { a, c } }', { noLocation: true })
+
);
+
+
expect(visited).toEqual([
+
['enter', 'Document', undefined],
+
['enter', 'OperationDefinition', undefined],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'b'],
+
['leave', 'Name', 'b'],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'c'],
+
['leave', 'Name', 'c'],
+
['enter', 'SelectionSet', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'a'],
+
['leave', 'Name', 'a'],
+
['leave', 'Field', undefined],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'b'],
+
['leave', 'Name', 'b'],
+
['enter', 'Field', undefined],
+
['enter', 'Name', 'c'],
+
['leave', 'Name', 'c'],
+
['leave', 'Field', undefined],
+
['leave', 'SelectionSet', undefined],
+
['leave', 'Field', undefined],
+
['leave', 'SelectionSet', undefined],
+
['leave', 'OperationDefinition', undefined],
+
['leave', 'Document', undefined],
+
]);
+
});
+
});
+
});
+34 -21
alias/language/visitor.mjs
···
function traverse(node, key, parent) {
let hasEdited = false;
-
let result;
-
result = callback(node, key, parent, false);
-
if (result === false) {
-
return;
-
} else if (result && typeof result.kind === 'string') {
-
hasEdited = true;
-
node = result;
+
const resultEnter = callback(node, key, parent, false);
+
if (resultEnter === false) {
+
return node;
+
} else if (resultEnter === null) {
+
return null;
+
} else if (resultEnter && typeof resultEnter.kind === 'string') {
+
hasEdited = resultEnter !== node;
+
node = resultEnter;
}
-
ancestors.push(node);
+
if (parent) ancestors.push(parent);
+
+
let result;
const copy = { ...node };
-
for (const nodeKey in node) {
-
let value = node[nodeKey];
path.push(nodeKey);
+
let value = node[nodeKey];
if (Array.isArray(value)) {
-
value = value.slice();
+
const newValue = [];
for (let index = 0; index < value.length; index++) {
if (value[index] != null && typeof value[index].kind === 'string') {
+
ancestors.push(node);
path.push(index);
-
result = traverse(value[index], index, node);
+
result = traverse(value[index], index, value);
path.pop();
-
if (result !== undefined) {
-
value[index] = result;
+
ancestors.pop();
+
if (result === undefined) {
+
newValue.push(value[index]);
+
} else if (result === null) {
hasEdited = true;
+
} else {
+
hasEdited = hasEdited || result !== value[index];
+
newValue.push(result);
}
}
}
+
value = newValue;
} else if (value != null && typeof value.kind === 'string') {
result = traverse(value, nodeKey, node);
if (result !== undefined) {
+
hasEdited = hasEdited || value !== result;
value = result;
-
hasEdited = true;
}
}
···
if (hasEdited) copy[nodeKey] = value;
}
-
ancestors.pop();
-
-
if (hasEdited) node = copy;
-
result = callback(node, key, parent, true);
-
return hasEdited && result === undefined ? node : result;
+
if (parent) ancestors.pop();
+
const resultLeave = callback(node, key, parent, true);
+
if (resultLeave !== undefined) {
+
return resultLeave;
+
} else if (resultEnter !== undefined) {
+
return resultEnter;
+
} else {
+
return hasEdited ? copy : node;
+
}
}
try {
const result = traverse(node);
-
return result !== undefined ? result : node;
+
return result !== undefined && result !== false ? result : node;
} catch (error) {
if (error !== BREAK) throw error;
return node;
+1
package.json
···
"scripts/buildenv"
],
"scripts": {
+
"test": "jest",
"build": "rollup -c scripts/rollup/config.js",
"size-check": "yarn workspace @graphql-web-lite/buildenv build"
},