// See: https://github.com/graphql/graphql-js/blob/976d64b/src/language/__tests__/visitor-test.ts import { Kind, parse, print } from 'graphql'; import { visit, visitInParallel, BREAK } from '../visitor'; const kitchenSinkQuery = 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 = []; 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 = []; 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; 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 = []; 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 = []; 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 = []; 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 = []; 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('handles deep immutable edits correctly when using "enter"', () => { const formatNode = (node) => { if ( node.selectionSet && !node.selectionSet.selections.some( (node) => node.kind === Kind.FIELD && node.name.value === '__typename' && !node.alias ) ) { return { ...node, selectionSet: { ...node.selectionSet, selections: [ ...node.selectionSet.selections, { kind: Kind.FIELD, name: { kind: Kind.NAME, value: '__typename', }, }, ], }, }; } }; const ast = parse('{ players { nodes { id } } }'); const expected = parse( '{ players { nodes { id __typename } __typename } }' ); const visited = visit(ast, { Field: formatNode, InlineFragment: formatNode, }); expect(print(visited)).toEqual(print(expected)); }); it('visits kitchen sink', () => { const ast = parse(kitchenSinkQuery); const visited = []; const argsStack = []; 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 = []; 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 = []; 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 = []; 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 = []; 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 = []; 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 = []; 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 = []; 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 = []; 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], ]); }); }); });