esquery.js 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. /**
  2. * @fileoverview ESQuery wrapper for ESLint.
  3. * @author Nicholas C. Zakas
  4. */
  5. "use strict";
  6. //------------------------------------------------------------------------------
  7. // Requirements
  8. //------------------------------------------------------------------------------
  9. const esquery = require("esquery");
  10. //-----------------------------------------------------------------------------
  11. // Typedefs
  12. //-----------------------------------------------------------------------------
  13. /**
  14. * @typedef {import("esquery").Selector} ESQuerySelector
  15. * @typedef {import("esquery").ESQueryOptions} ESQueryOptions
  16. */
  17. //------------------------------------------------------------------------------
  18. // Classes
  19. //------------------------------------------------------------------------------
  20. /**
  21. * The result of parsing and analyzing an ESQuery selector.
  22. */
  23. class ESQueryParsedSelector {
  24. /**
  25. * The raw selector string that was parsed
  26. * @type {string}
  27. */
  28. source;
  29. /**
  30. * Whether this selector is an exit selector
  31. * @type {boolean}
  32. */
  33. isExit;
  34. /**
  35. * An object (from esquery) describing the matching behavior of the selector
  36. * @type {ESQuerySelector}
  37. */
  38. root;
  39. /**
  40. * The node types that could possibly trigger this selector, or `null` if all node types could trigger it
  41. * @type {string[]|null}
  42. */
  43. nodeTypes;
  44. /**
  45. * The number of class, pseudo-class, and attribute queries in this selector
  46. * @type {number}
  47. */
  48. attributeCount;
  49. /**
  50. * The number of identifier queries in this selector
  51. * @type {number}
  52. */
  53. identifierCount;
  54. /**
  55. * Creates a new parsed selector.
  56. * @param {string} source The raw selector string that was parsed
  57. * @param {boolean} isExit Whether this selector is an exit selector
  58. * @param {ESQuerySelector} root An object (from esquery) describing the matching behavior of the selector
  59. * @param {string[]|null} nodeTypes The node types that could possibly trigger this selector, or `null` if all node types could trigger it
  60. * @param {number} attributeCount The number of class, pseudo-class, and attribute queries in this selector
  61. * @param {number} identifierCount The number of identifier queries in this selector
  62. */
  63. constructor(
  64. source,
  65. isExit,
  66. root,
  67. nodeTypes,
  68. attributeCount,
  69. identifierCount,
  70. ) {
  71. this.source = source;
  72. this.isExit = isExit;
  73. this.root = root;
  74. this.nodeTypes = nodeTypes;
  75. this.attributeCount = attributeCount;
  76. this.identifierCount = identifierCount;
  77. }
  78. /**
  79. * Compares this selector's specificity to another selector for sorting purposes.
  80. * @param {ESQueryParsedSelector} otherSelector The selector to compare against
  81. * @returns {number}
  82. * a value less than 0 if this selector is less specific than otherSelector
  83. * a value greater than 0 if this selector is more specific than otherSelector
  84. * a value less than 0 if this selector and otherSelector have the same specificity, and this selector <= otherSelector alphabetically
  85. * a value greater than 0 if this selector and otherSelector have the same specificity, and this selector > otherSelector alphabetically
  86. */
  87. compare(otherSelector) {
  88. return (
  89. this.attributeCount - otherSelector.attributeCount ||
  90. this.identifierCount - otherSelector.identifierCount ||
  91. (this.source <= otherSelector.source ? -1 : 1)
  92. );
  93. }
  94. }
  95. //------------------------------------------------------------------------------
  96. // Helpers
  97. //------------------------------------------------------------------------------
  98. const selectorCache = new Map();
  99. /**
  100. * Computes the union of one or more arrays
  101. * @param {...any[]} arrays One or more arrays to union
  102. * @returns {any[]} The union of the input arrays
  103. */
  104. function union(...arrays) {
  105. return [...new Set(arrays.flat())];
  106. }
  107. /**
  108. * Computes the intersection of one or more arrays
  109. * @param {...any[]} arrays One or more arrays to intersect
  110. * @returns {any[]} The intersection of the input arrays
  111. */
  112. function intersection(...arrays) {
  113. if (arrays.length === 0) {
  114. return [];
  115. }
  116. let result = [...new Set(arrays[0])];
  117. for (const array of arrays.slice(1)) {
  118. result = result.filter(x => array.includes(x));
  119. }
  120. return result;
  121. }
  122. /**
  123. * Analyzes a parsed selector and returns combined data about it
  124. * @param {ESQuerySelector} parsedSelector An object (from esquery) describing the matching behavior of the selector
  125. * @returns {{nodeTypes:string[]|null, attributeCount:number, identifierCount:number}} Object containing selector data.
  126. */
  127. function analyzeParsedSelector(parsedSelector) {
  128. let attributeCount = 0;
  129. let identifierCount = 0;
  130. /**
  131. * Analyzes a selector and returns the node types that could possibly trigger it.
  132. * @param {ESQuerySelector} selector The selector to analyze.
  133. * @returns {string[]|null} The node types that could possibly trigger this selector, or `null` if all node types could trigger it
  134. */
  135. function analyzeSelector(selector) {
  136. switch (selector.type) {
  137. case "identifier":
  138. identifierCount++;
  139. return [selector.value];
  140. case "not":
  141. selector.selectors.map(analyzeSelector);
  142. return null;
  143. case "matches": {
  144. const typesForComponents =
  145. selector.selectors.map(analyzeSelector);
  146. if (typesForComponents.every(Boolean)) {
  147. return union(...typesForComponents);
  148. }
  149. return null;
  150. }
  151. case "compound": {
  152. const typesForComponents = selector.selectors
  153. .map(analyzeSelector)
  154. .filter(typesForComponent => typesForComponent);
  155. // If all of the components could match any type, then the compound could also match any type.
  156. if (!typesForComponents.length) {
  157. return null;
  158. }
  159. /*
  160. * If at least one of the components could only match a particular type, the compound could only match
  161. * the intersection of those types.
  162. */
  163. return intersection(...typesForComponents);
  164. }
  165. case "attribute":
  166. case "field":
  167. case "nth-child":
  168. case "nth-last-child":
  169. attributeCount++;
  170. return null;
  171. case "child":
  172. case "descendant":
  173. case "sibling":
  174. case "adjacent":
  175. analyzeSelector(selector.left);
  176. return analyzeSelector(selector.right);
  177. case "class":
  178. // TODO: abstract into JSLanguage somehow
  179. if (selector.name === "function") {
  180. return [
  181. "FunctionDeclaration",
  182. "FunctionExpression",
  183. "ArrowFunctionExpression",
  184. ];
  185. }
  186. return null;
  187. default:
  188. return null;
  189. }
  190. }
  191. const nodeTypes = analyzeSelector(parsedSelector);
  192. return {
  193. nodeTypes,
  194. attributeCount,
  195. identifierCount,
  196. };
  197. }
  198. /**
  199. * Tries to parse a simple selector string, such as a single identifier or wildcard.
  200. * This saves time by avoiding the overhead of esquery parsing for simple cases.
  201. * @param {string} selector The selector string to parse.
  202. * @returns {Object|null} An object describing the selector if it is simple, or `null` if it is not.
  203. */
  204. function trySimpleParseSelector(selector) {
  205. if (selector === "*") {
  206. return {
  207. type: "wildcard",
  208. value: "*",
  209. };
  210. }
  211. if (/^[a-z]+$/iu.test(selector)) {
  212. return {
  213. type: "identifier",
  214. value: selector,
  215. };
  216. }
  217. return null;
  218. }
  219. /**
  220. * Parses a raw selector string, and throws a useful error if parsing fails.
  221. * @param {string} selector The selector string to parse.
  222. * @returns {Object} An object (from esquery) describing the matching behavior of this selector
  223. * @throws {Error} An error if the selector is invalid
  224. */
  225. function tryParseSelector(selector) {
  226. try {
  227. return esquery.parse(selector);
  228. } catch (err) {
  229. if (
  230. err.location &&
  231. err.location.start &&
  232. typeof err.location.start.offset === "number"
  233. ) {
  234. throw new SyntaxError(
  235. `Syntax error in selector "${selector}" at position ${err.location.start.offset}: ${err.message}`,
  236. {
  237. cause: err,
  238. },
  239. );
  240. }
  241. throw err;
  242. }
  243. }
  244. /**
  245. * Parses a raw selector string, and returns the parsed selector along with specificity and type information.
  246. * @param {string} source A raw AST selector
  247. * @returns {ESQueryParsedSelector} A selector descriptor
  248. */
  249. function parse(source) {
  250. if (selectorCache.has(source)) {
  251. return selectorCache.get(source);
  252. }
  253. const cleanSource = source.replace(/:exit$/u, "");
  254. const parsedSelector =
  255. trySimpleParseSelector(cleanSource) ?? tryParseSelector(cleanSource);
  256. const { nodeTypes, attributeCount, identifierCount } =
  257. analyzeParsedSelector(parsedSelector);
  258. const result = new ESQueryParsedSelector(
  259. source,
  260. source.endsWith(":exit"),
  261. parsedSelector,
  262. nodeTypes,
  263. attributeCount,
  264. identifierCount,
  265. );
  266. selectorCache.set(source, result);
  267. return result;
  268. }
  269. /**
  270. * Checks if a node matches a given selector.
  271. * @param {Object} node The node to check against the selector.
  272. * @param {ESQuerySelector} root The root of the selector to match against.
  273. * @param {Object[]} ancestry The ancestry of the node being checked, which is an array of nodes from the current node to the root.
  274. * @param {ESQueryOptions} options The options to use for matching.
  275. * @returns {boolean} `true` if the node matches the selector, `false` otherwise.
  276. */
  277. function matches(node, root, ancestry, options) {
  278. return esquery.matches(node, root, ancestry, options);
  279. }
  280. //-----------------------------------------------------------------------------
  281. // Exports
  282. //-----------------------------------------------------------------------------
  283. module.exports = {
  284. parse,
  285. matches,
  286. ESQueryParsedSelector,
  287. };