source-code-traverser.js 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333
  1. /**
  2. * @fileoverview Traverser for SourceCode objects.
  3. * @author Nicholas C. Zakas
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const { parse, matches } = require("./esquery");
  10. const vk = require("eslint-visitor-keys");
  11. //-----------------------------------------------------------------------------
  12. // Typedefs
  13. //-----------------------------------------------------------------------------
  14. /**
  15. * @import { Language, SourceCode } from "@eslint/core";
  16. * @import { ESQueryOptions } from "esquery";
  17. * @import { ESQueryParsedSelector } from "./esquery.js";
  18. * @import { SourceCodeVisitor } from "./source-code-visitor.js";
  19. */
  20. //-----------------------------------------------------------------------------
  21. // Helpers
  22. //-----------------------------------------------------------------------------
  23. const STEP_KIND_VISIT = 1;
  24. const STEP_KIND_CALL = 2;
  25. /**
  26. * Compares two ESQuery selectors by specificity.
  27. * @param {ESQueryParsedSelector} a The first selector to compare.
  28. * @param {ESQueryParsedSelector} b The second selector to compare.
  29. * @returns {number} A negative number if `a` is less specific than `b` or they are equally specific and `a` <= `b` alphabetically, a positive number if `a` is more specific than `b`.
  30. */
  31. function compareSpecificity(a, b) {
  32. return a.compare(b);
  33. }
  34. /**
  35. * Helper to wrap ESQuery operations.
  36. */
  37. class ESQueryHelper {
  38. /**
  39. * Creates a new instance.
  40. * @param {SourceCodeVisitor} visitor The visitor containing the functions to call.
  41. * @param {ESQueryOptions} esqueryOptions `esquery` options for traversing custom nodes.
  42. */
  43. constructor(visitor, esqueryOptions) {
  44. /**
  45. * The visitor to use during traversal.
  46. * @type {SourceCodeVisitor}
  47. */
  48. this.visitor = visitor;
  49. /**
  50. * The options for `esquery` to use during matching.
  51. * @type {ESQueryOptions}
  52. */
  53. this.esqueryOptions = esqueryOptions;
  54. /**
  55. * A map of node type to selectors targeting that node type on the
  56. * enter phase of traversal.
  57. * @type {Map<string, ESQueryParsedSelector[]>}
  58. */
  59. this.enterSelectorsByNodeType = new Map();
  60. /**
  61. * A map of node type to selectors targeting that node type on the
  62. * exit phase of traversal.
  63. * @type {Map<string, ESQueryParsedSelector[]>}
  64. */
  65. this.exitSelectorsByNodeType = new Map();
  66. /**
  67. * An array of selectors that match any node type on the
  68. * enter phase of traversal.
  69. * @type {ESQueryParsedSelector[]}
  70. */
  71. this.anyTypeEnterSelectors = [];
  72. /**
  73. * An array of selectors that match any node type on the
  74. * exit phase of traversal.
  75. * @type {ESQueryParsedSelector[]}
  76. */
  77. this.anyTypeExitSelectors = [];
  78. visitor.forEachName(rawSelector => {
  79. const selector = parse(rawSelector);
  80. /*
  81. * If this selector has identified specific node types,
  82. * add it to the map for these node types for faster lookup.
  83. */
  84. if (selector.nodeTypes) {
  85. const typeMap = selector.isExit
  86. ? this.exitSelectorsByNodeType
  87. : this.enterSelectorsByNodeType;
  88. selector.nodeTypes.forEach(nodeType => {
  89. if (!typeMap.has(nodeType)) {
  90. typeMap.set(nodeType, []);
  91. }
  92. typeMap.get(nodeType).push(selector);
  93. });
  94. return;
  95. }
  96. /*
  97. * Remaining selectors are added to the "any type" selectors
  98. * list for the appropriate phase of traversal. This ensures
  99. * that all selectors will still be applied even if no
  100. * specific node type is matched.
  101. */
  102. const selectors = selector.isExit
  103. ? this.anyTypeExitSelectors
  104. : this.anyTypeEnterSelectors;
  105. selectors.push(selector);
  106. });
  107. // sort all selectors by specificity for prioritizing call order
  108. this.anyTypeEnterSelectors.sort(compareSpecificity);
  109. this.anyTypeExitSelectors.sort(compareSpecificity);
  110. this.enterSelectorsByNodeType.forEach(selectorList =>
  111. selectorList.sort(compareSpecificity),
  112. );
  113. this.exitSelectorsByNodeType.forEach(selectorList =>
  114. selectorList.sort(compareSpecificity),
  115. );
  116. }
  117. /**
  118. * Checks if a node matches a given selector.
  119. * @param {ASTNode} node The node to check
  120. * @param {ASTNode[]} ancestry The ancestry of the node being checked.
  121. * @param {ESQueryParsedSelector} selector An AST selector descriptor
  122. * @returns {boolean} `true` if the selector matches the node, `false` otherwise
  123. */
  124. matches(node, ancestry, selector) {
  125. return matches(node, selector.root, ancestry, this.esqueryOptions);
  126. }
  127. /**
  128. * Calculates all appropriate selectors to a node, in specificity order
  129. * @param {ASTNode} node The node to check
  130. * @param {ASTNode[]} ancestry The ancestry of the node being checked.
  131. * @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
  132. * @returns {string[]} An array of selectors that match the node.
  133. */
  134. calculateSelectors(node, ancestry, isExit) {
  135. const nodeTypeKey = this.esqueryOptions?.nodeTypeKey || "type";
  136. const selectors = [];
  137. /*
  138. * Get the selectors that may match this node. First, check
  139. * to see if the node type has specific selectors,
  140. * then gather the "any type" selectors.
  141. */
  142. const selectorsByNodeType =
  143. (isExit
  144. ? this.exitSelectorsByNodeType
  145. : this.enterSelectorsByNodeType
  146. ).get(node[nodeTypeKey]) || [];
  147. const anyTypeSelectors = isExit
  148. ? this.anyTypeExitSelectors
  149. : this.anyTypeEnterSelectors;
  150. /*
  151. * selectorsByNodeType and anyTypeSelectors were already sorted by specificity in the constructor.
  152. * Iterate through each of them, applying selectors in the right order.
  153. */
  154. let selectorsByNodeTypeIndex = 0;
  155. let anyTypeSelectorsIndex = 0;
  156. while (
  157. selectorsByNodeTypeIndex < selectorsByNodeType.length ||
  158. anyTypeSelectorsIndex < anyTypeSelectors.length
  159. ) {
  160. /*
  161. * If we've already exhausted the selectors for this node type,
  162. * or if the next any type selector is more specific than the
  163. * next selector for this node type, apply the any type selector.
  164. */
  165. const hasMoreNodeTypeSelectors =
  166. selectorsByNodeTypeIndex < selectorsByNodeType.length;
  167. const hasMoreAnyTypeSelectors =
  168. anyTypeSelectorsIndex < anyTypeSelectors.length;
  169. const anyTypeSelector = anyTypeSelectors[anyTypeSelectorsIndex];
  170. const nodeTypeSelector =
  171. selectorsByNodeType[selectorsByNodeTypeIndex];
  172. // Only compare specificity if both selectors exist
  173. const isAnyTypeSelectorLessSpecific =
  174. hasMoreAnyTypeSelectors &&
  175. hasMoreNodeTypeSelectors &&
  176. anyTypeSelector.compare(nodeTypeSelector) < 0;
  177. if (!hasMoreNodeTypeSelectors || isAnyTypeSelectorLessSpecific) {
  178. anyTypeSelectorsIndex++;
  179. if (this.matches(node, ancestry, anyTypeSelector)) {
  180. selectors.push(anyTypeSelector.source);
  181. }
  182. } else {
  183. selectorsByNodeTypeIndex++;
  184. if (this.matches(node, ancestry, nodeTypeSelector)) {
  185. selectors.push(nodeTypeSelector.source);
  186. }
  187. }
  188. }
  189. return selectors;
  190. }
  191. }
  192. //------------------------------------------------------------------------------
  193. // Public Interface
  194. //------------------------------------------------------------------------------
  195. /**
  196. * Traverses source code and ensures that visitor methods are called when
  197. * entering and leaving each node.
  198. */
  199. class SourceCodeTraverser {
  200. /**
  201. * The language of the source code being traversed.
  202. * @type {Language}
  203. */
  204. #language;
  205. /**
  206. * Map of languages to instances of this class.
  207. * @type {WeakMap<Language, SourceCodeTraverser>}
  208. */
  209. static instances = new WeakMap();
  210. /**
  211. * Creates a new instance.
  212. * @param {Language} language The language of the source code being traversed.
  213. */
  214. constructor(language) {
  215. this.#language = language;
  216. }
  217. static getInstance(language) {
  218. if (!this.instances.has(language)) {
  219. this.instances.set(language, new this(language));
  220. }
  221. return this.instances.get(language);
  222. }
  223. /**
  224. * Traverses the given source code synchronously.
  225. * @param {SourceCode} sourceCode The source code to traverse.
  226. * @param {SourceCodeVisitor} visitor The emitter to use for events.
  227. * @param {Object} options Options for traversal.
  228. * @param {ReturnType<SourceCode["traverse"]>} options.steps The steps to take during traversal.
  229. * @returns {void}
  230. * @throws {Error} If an error occurs during traversal.
  231. */
  232. traverseSync(sourceCode, visitor, { steps } = {}) {
  233. const esquery = new ESQueryHelper(visitor, {
  234. visitorKeys: sourceCode.visitorKeys ?? this.#language.visitorKeys,
  235. fallback: vk.getKeys,
  236. matchClass: this.#language.matchesSelectorClass ?? (() => false),
  237. nodeTypeKey: this.#language.nodeTypeKey,
  238. });
  239. const currentAncestry = [];
  240. for (const step of steps ?? sourceCode.traverse()) {
  241. switch (step.kind) {
  242. case STEP_KIND_VISIT: {
  243. try {
  244. if (step.phase === 1) {
  245. esquery
  246. .calculateSelectors(
  247. step.target,
  248. currentAncestry,
  249. false,
  250. )
  251. .forEach(selector => {
  252. visitor.callSync(
  253. selector,
  254. ...(step.args ?? [step.target]),
  255. );
  256. });
  257. currentAncestry.unshift(step.target);
  258. } else {
  259. currentAncestry.shift();
  260. esquery
  261. .calculateSelectors(
  262. step.target,
  263. currentAncestry,
  264. true,
  265. )
  266. .forEach(selector => {
  267. visitor.callSync(
  268. selector,
  269. ...(step.args ?? [step.target]),
  270. );
  271. });
  272. }
  273. } catch (err) {
  274. err.currentNode = step.target;
  275. throw err;
  276. }
  277. break;
  278. }
  279. case STEP_KIND_CALL: {
  280. visitor.callSync(step.target, ...step.args);
  281. break;
  282. }
  283. default:
  284. throw new Error(
  285. `Invalid traversal step found: "${step.kind}".`,
  286. );
  287. }
  288. }
  289. }
  290. }
  291. module.exports = { SourceCodeTraverser };