visitor.js.flow 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. // @flow strict
  2. import inspect from '../jsutils/inspect';
  3. import type { ASTNode, ASTKindToNode } from './ast';
  4. import { isNode } from './ast';
  5. /**
  6. * A visitor is provided to visit, it contains the collection of
  7. * relevant functions to be called during the visitor's traversal.
  8. */
  9. export type ASTVisitor = Visitor<ASTKindToNode>;
  10. export type Visitor<KindToNode, Nodes = $Values<KindToNode>> =
  11. | EnterLeave<
  12. | VisitFn<Nodes>
  13. | ShapeMap<KindToNode, <Node>(Node) => VisitFn<Nodes, Node>>,
  14. >
  15. | ShapeMap<
  16. KindToNode,
  17. <Node>(Node) => VisitFn<Nodes, Node> | EnterLeave<VisitFn<Nodes, Node>>,
  18. >;
  19. type EnterLeave<T> = {| +enter?: T, +leave?: T |};
  20. type ShapeMap<O, F> = $Shape<$ObjMap<O, F>>;
  21. /**
  22. * A visitor is comprised of visit functions, which are called on each node
  23. * during the visitor's traversal.
  24. */
  25. export type VisitFn<TAnyNode, TVisitedNode: TAnyNode = TAnyNode> = (
  26. // The current node being visiting.
  27. node: TVisitedNode,
  28. // The index or key to this node from the parent node or Array.
  29. key: string | number | void,
  30. // The parent immediately above this node, which may be an Array.
  31. parent: TAnyNode | $ReadOnlyArray<TAnyNode> | void,
  32. // The key path to get to this node from the root node.
  33. path: $ReadOnlyArray<string | number>,
  34. // All nodes and Arrays visited before reaching parent of this node.
  35. // These correspond to array indices in `path`.
  36. // Note: ancestors includes arrays which contain the parent of visited node.
  37. ancestors: $ReadOnlyArray<TAnyNode | $ReadOnlyArray<TAnyNode>>,
  38. ) => any;
  39. /**
  40. * A KeyMap describes each the traversable properties of each kind of node.
  41. */
  42. export type VisitorKeyMap<KindToNode> = $ObjMap<
  43. KindToNode,
  44. <T>(T) => $ReadOnlyArray<$Keys<T>>,
  45. >;
  46. export const QueryDocumentKeys: VisitorKeyMap<ASTKindToNode> = {
  47. Name: [],
  48. Document: ['definitions'],
  49. OperationDefinition: [
  50. 'name',
  51. 'variableDefinitions',
  52. 'directives',
  53. 'selectionSet',
  54. ],
  55. VariableDefinition: ['variable', 'type', 'defaultValue', 'directives'],
  56. Variable: ['name'],
  57. SelectionSet: ['selections'],
  58. Field: ['alias', 'name', 'arguments', 'directives', 'selectionSet'],
  59. Argument: ['name', 'value'],
  60. FragmentSpread: ['name', 'directives'],
  61. InlineFragment: ['typeCondition', 'directives', 'selectionSet'],
  62. FragmentDefinition: [
  63. 'name',
  64. // Note: fragment variable definitions are experimental and may be changed
  65. // or removed in the future.
  66. 'variableDefinitions',
  67. 'typeCondition',
  68. 'directives',
  69. 'selectionSet',
  70. ],
  71. IntValue: [],
  72. FloatValue: [],
  73. StringValue: [],
  74. BooleanValue: [],
  75. NullValue: [],
  76. EnumValue: [],
  77. ListValue: ['values'],
  78. ObjectValue: ['fields'],
  79. ObjectField: ['name', 'value'],
  80. Directive: ['name', 'arguments'],
  81. NamedType: ['name'],
  82. ListType: ['type'],
  83. NonNullType: ['type'],
  84. SchemaDefinition: ['description', 'directives', 'operationTypes'],
  85. OperationTypeDefinition: ['type'],
  86. ScalarTypeDefinition: ['description', 'name', 'directives'],
  87. ObjectTypeDefinition: [
  88. 'description',
  89. 'name',
  90. 'interfaces',
  91. 'directives',
  92. 'fields',
  93. ],
  94. FieldDefinition: ['description', 'name', 'arguments', 'type', 'directives'],
  95. InputValueDefinition: [
  96. 'description',
  97. 'name',
  98. 'type',
  99. 'defaultValue',
  100. 'directives',
  101. ],
  102. InterfaceTypeDefinition: [
  103. 'description',
  104. 'name',
  105. 'interfaces',
  106. 'directives',
  107. 'fields',
  108. ],
  109. UnionTypeDefinition: ['description', 'name', 'directives', 'types'],
  110. EnumTypeDefinition: ['description', 'name', 'directives', 'values'],
  111. EnumValueDefinition: ['description', 'name', 'directives'],
  112. InputObjectTypeDefinition: ['description', 'name', 'directives', 'fields'],
  113. DirectiveDefinition: ['description', 'name', 'arguments', 'locations'],
  114. SchemaExtension: ['directives', 'operationTypes'],
  115. ScalarTypeExtension: ['name', 'directives'],
  116. ObjectTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
  117. InterfaceTypeExtension: ['name', 'interfaces', 'directives', 'fields'],
  118. UnionTypeExtension: ['name', 'directives', 'types'],
  119. EnumTypeExtension: ['name', 'directives', 'values'],
  120. InputObjectTypeExtension: ['name', 'directives', 'fields'],
  121. };
  122. export const BREAK: { ... } = Object.freeze({});
  123. /**
  124. * visit() will walk through an AST using a depth-first traversal, calling
  125. * the visitor's enter function at each node in the traversal, and calling the
  126. * leave function after visiting that node and all of its child nodes.
  127. *
  128. * By returning different values from the enter and leave functions, the
  129. * behavior of the visitor can be altered, including skipping over a sub-tree of
  130. * the AST (by returning false), editing the AST by returning a value or null
  131. * to remove the value, or to stop the whole traversal by returning BREAK.
  132. *
  133. * When using visit() to edit an AST, the original AST will not be modified, and
  134. * a new version of the AST with the changes applied will be returned from the
  135. * visit function.
  136. *
  137. * const editedAST = visit(ast, {
  138. * enter(node, key, parent, path, ancestors) {
  139. * // @return
  140. * // undefined: no action
  141. * // false: skip visiting this node
  142. * // visitor.BREAK: stop visiting altogether
  143. * // null: delete this node
  144. * // any value: replace this node with the returned value
  145. * },
  146. * leave(node, key, parent, path, ancestors) {
  147. * // @return
  148. * // undefined: no action
  149. * // false: no action
  150. * // visitor.BREAK: stop visiting altogether
  151. * // null: delete this node
  152. * // any value: replace this node with the returned value
  153. * }
  154. * });
  155. *
  156. * Alternatively to providing enter() and leave() functions, a visitor can
  157. * instead provide functions named the same as the kinds of AST nodes, or
  158. * enter/leave visitors at a named key, leading to four permutations of the
  159. * visitor API:
  160. *
  161. * 1) Named visitors triggered when entering a node of a specific kind.
  162. *
  163. * visit(ast, {
  164. * Kind(node) {
  165. * // enter the "Kind" node
  166. * }
  167. * })
  168. *
  169. * 2) Named visitors that trigger upon entering and leaving a node of
  170. * a specific kind.
  171. *
  172. * visit(ast, {
  173. * Kind: {
  174. * enter(node) {
  175. * // enter the "Kind" node
  176. * }
  177. * leave(node) {
  178. * // leave the "Kind" node
  179. * }
  180. * }
  181. * })
  182. *
  183. * 3) Generic visitors that trigger upon entering and leaving any node.
  184. *
  185. * visit(ast, {
  186. * enter(node) {
  187. * // enter any node
  188. * },
  189. * leave(node) {
  190. * // leave any node
  191. * }
  192. * })
  193. *
  194. * 4) Parallel visitors for entering and leaving nodes of a specific kind.
  195. *
  196. * visit(ast, {
  197. * enter: {
  198. * Kind(node) {
  199. * // enter the "Kind" node
  200. * }
  201. * },
  202. * leave: {
  203. * Kind(node) {
  204. * // leave the "Kind" node
  205. * }
  206. * }
  207. * })
  208. */
  209. export function visit(
  210. root: ASTNode,
  211. visitor: Visitor<ASTKindToNode>,
  212. visitorKeys: VisitorKeyMap<ASTKindToNode> = QueryDocumentKeys,
  213. ): any {
  214. /* eslint-disable no-undef-init */
  215. let stack: any = undefined;
  216. let inArray = Array.isArray(root);
  217. let keys: any = [root];
  218. let index = -1;
  219. let edits = [];
  220. let node: any = undefined;
  221. let key: any = undefined;
  222. let parent: any = undefined;
  223. const path: any = [];
  224. const ancestors = [];
  225. let newRoot = root;
  226. /* eslint-enable no-undef-init */
  227. do {
  228. index++;
  229. const isLeaving = index === keys.length;
  230. const isEdited = isLeaving && edits.length !== 0;
  231. if (isLeaving) {
  232. key = ancestors.length === 0 ? undefined : path[path.length - 1];
  233. node = parent;
  234. parent = ancestors.pop();
  235. if (isEdited) {
  236. if (inArray) {
  237. node = node.slice();
  238. } else {
  239. const clone = {};
  240. for (const k of Object.keys(node)) {
  241. clone[k] = node[k];
  242. }
  243. node = clone;
  244. }
  245. let editOffset = 0;
  246. for (let ii = 0; ii < edits.length; ii++) {
  247. let editKey: any = edits[ii][0];
  248. const editValue = edits[ii][1];
  249. if (inArray) {
  250. editKey -= editOffset;
  251. }
  252. if (inArray && editValue === null) {
  253. node.splice(editKey, 1);
  254. editOffset++;
  255. } else {
  256. node[editKey] = editValue;
  257. }
  258. }
  259. }
  260. index = stack.index;
  261. keys = stack.keys;
  262. edits = stack.edits;
  263. inArray = stack.inArray;
  264. stack = stack.prev;
  265. } else {
  266. key = parent ? (inArray ? index : keys[index]) : undefined;
  267. node = parent ? parent[key] : newRoot;
  268. if (node === null || node === undefined) {
  269. continue;
  270. }
  271. if (parent) {
  272. path.push(key);
  273. }
  274. }
  275. let result;
  276. if (!Array.isArray(node)) {
  277. if (!isNode(node)) {
  278. throw new Error(`Invalid AST Node: ${inspect(node)}.`);
  279. }
  280. const visitFn = getVisitFn(visitor, node.kind, isLeaving);
  281. if (visitFn) {
  282. result = visitFn.call(visitor, node, key, parent, path, ancestors);
  283. if (result === BREAK) {
  284. break;
  285. }
  286. if (result === false) {
  287. if (!isLeaving) {
  288. path.pop();
  289. continue;
  290. }
  291. } else if (result !== undefined) {
  292. edits.push([key, result]);
  293. if (!isLeaving) {
  294. if (isNode(result)) {
  295. node = result;
  296. } else {
  297. path.pop();
  298. continue;
  299. }
  300. }
  301. }
  302. }
  303. }
  304. if (result === undefined && isEdited) {
  305. edits.push([key, node]);
  306. }
  307. if (isLeaving) {
  308. path.pop();
  309. } else {
  310. stack = { inArray, index, keys, edits, prev: stack };
  311. inArray = Array.isArray(node);
  312. keys = inArray ? node : visitorKeys[node.kind] ?? [];
  313. index = -1;
  314. edits = [];
  315. if (parent) {
  316. ancestors.push(parent);
  317. }
  318. parent = node;
  319. }
  320. } while (stack !== undefined);
  321. if (edits.length !== 0) {
  322. newRoot = edits[edits.length - 1][1];
  323. }
  324. return newRoot;
  325. }
  326. /**
  327. * Creates a new visitor instance which delegates to many visitors to run in
  328. * parallel. Each visitor will be visited for each node before moving on.
  329. *
  330. * If a prior visitor edits a node, no following visitors will see that node.
  331. */
  332. export function visitInParallel(
  333. visitors: $ReadOnlyArray<Visitor<ASTKindToNode>>,
  334. ): Visitor<ASTKindToNode> {
  335. const skipping = new Array(visitors.length);
  336. return {
  337. enter(node) {
  338. for (let i = 0; i < visitors.length; i++) {
  339. if (skipping[i] == null) {
  340. const fn = getVisitFn(visitors[i], node.kind, /* isLeaving */ false);
  341. if (fn) {
  342. const result = fn.apply(visitors[i], arguments);
  343. if (result === false) {
  344. skipping[i] = node;
  345. } else if (result === BREAK) {
  346. skipping[i] = BREAK;
  347. } else if (result !== undefined) {
  348. return result;
  349. }
  350. }
  351. }
  352. }
  353. },
  354. leave(node) {
  355. for (let i = 0; i < visitors.length; i++) {
  356. if (skipping[i] == null) {
  357. const fn = getVisitFn(visitors[i], node.kind, /* isLeaving */ true);
  358. if (fn) {
  359. const result = fn.apply(visitors[i], arguments);
  360. if (result === BREAK) {
  361. skipping[i] = BREAK;
  362. } else if (result !== undefined && result !== false) {
  363. return result;
  364. }
  365. }
  366. } else if (skipping[i] === node) {
  367. skipping[i] = null;
  368. }
  369. }
  370. },
  371. };
  372. }
  373. /**
  374. * Given a visitor instance, if it is leaving or not, and a node kind, return
  375. * the function the visitor runtime should call.
  376. */
  377. export function getVisitFn(
  378. visitor: Visitor<any>,
  379. kind: string,
  380. isLeaving: boolean,
  381. ): ?VisitFn<any> {
  382. const kindVisitor = visitor[kind];
  383. if (kindVisitor) {
  384. if (!isLeaving && typeof kindVisitor === 'function') {
  385. // { Kind() {} }
  386. return kindVisitor;
  387. }
  388. const kindSpecificVisitor = isLeaving
  389. ? kindVisitor.leave
  390. : kindVisitor.enter;
  391. if (typeof kindSpecificVisitor === 'function') {
  392. // { Kind: { enter() {}, leave() {} } }
  393. return kindSpecificVisitor;
  394. }
  395. } else {
  396. const specificVisitor = isLeaving ? visitor.leave : visitor.enter;
  397. if (specificVisitor) {
  398. if (typeof specificVisitor === 'function') {
  399. // { enter() {}, leave() {} }
  400. return specificVisitor;
  401. }
  402. const specificKindVisitor = specificVisitor[kind];
  403. if (typeof specificKindVisitor === 'function') {
  404. // { enter: { Kind() {} }, leave: { Kind() {} } }
  405. return specificKindVisitor;
  406. }
  407. }
  408. }
  409. }