| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332 |
- /**
- * @fileoverview ESQuery wrapper for ESLint.
- * @author Nicholas C. Zakas
- */
- "use strict";
- //------------------------------------------------------------------------------
- // Requirements
- //------------------------------------------------------------------------------
- const esquery = require("esquery");
- //-----------------------------------------------------------------------------
- // Typedefs
- //-----------------------------------------------------------------------------
- /**
- * @typedef {import("esquery").Selector} ESQuerySelector
- * @typedef {import("esquery").ESQueryOptions} ESQueryOptions
- */
- //------------------------------------------------------------------------------
- // Classes
- //------------------------------------------------------------------------------
- /**
- * The result of parsing and analyzing an ESQuery selector.
- */
- class ESQueryParsedSelector {
- /**
- * The raw selector string that was parsed
- * @type {string}
- */
- source;
- /**
- * Whether this selector is an exit selector
- * @type {boolean}
- */
- isExit;
- /**
- * An object (from esquery) describing the matching behavior of the selector
- * @type {ESQuerySelector}
- */
- root;
- /**
- * The node types that could possibly trigger this selector, or `null` if all node types could trigger it
- * @type {string[]|null}
- */
- nodeTypes;
- /**
- * The number of class, pseudo-class, and attribute queries in this selector
- * @type {number}
- */
- attributeCount;
- /**
- * The number of identifier queries in this selector
- * @type {number}
- */
- identifierCount;
- /**
- * Creates a new parsed selector.
- * @param {string} source The raw selector string that was parsed
- * @param {boolean} isExit Whether this selector is an exit selector
- * @param {ESQuerySelector} root An object (from esquery) describing the matching behavior of the selector
- * @param {string[]|null} nodeTypes The node types that could possibly trigger this selector, or `null` if all node types could trigger it
- * @param {number} attributeCount The number of class, pseudo-class, and attribute queries in this selector
- * @param {number} identifierCount The number of identifier queries in this selector
- */
- constructor(
- source,
- isExit,
- root,
- nodeTypes,
- attributeCount,
- identifierCount,
- ) {
- this.source = source;
- this.isExit = isExit;
- this.root = root;
- this.nodeTypes = nodeTypes;
- this.attributeCount = attributeCount;
- this.identifierCount = identifierCount;
- }
- /**
- * Compares this selector's specificity to another selector for sorting purposes.
- * @param {ESQueryParsedSelector} otherSelector The selector to compare against
- * @returns {number}
- * a value less than 0 if this selector is less specific than otherSelector
- * a value greater than 0 if this selector is more specific than otherSelector
- * a value less than 0 if this selector and otherSelector have the same specificity, and this selector <= otherSelector alphabetically
- * a value greater than 0 if this selector and otherSelector have the same specificity, and this selector > otherSelector alphabetically
- */
- compare(otherSelector) {
- return (
- this.attributeCount - otherSelector.attributeCount ||
- this.identifierCount - otherSelector.identifierCount ||
- (this.source <= otherSelector.source ? -1 : 1)
- );
- }
- }
- //------------------------------------------------------------------------------
- // Helpers
- //------------------------------------------------------------------------------
- const selectorCache = new Map();
- /**
- * Computes the union of one or more arrays
- * @param {...any[]} arrays One or more arrays to union
- * @returns {any[]} The union of the input arrays
- */
- function union(...arrays) {
- return [...new Set(arrays.flat())];
- }
- /**
- * Computes the intersection of one or more arrays
- * @param {...any[]} arrays One or more arrays to intersect
- * @returns {any[]} The intersection of the input arrays
- */
- function intersection(...arrays) {
- if (arrays.length === 0) {
- return [];
- }
- let result = [...new Set(arrays[0])];
- for (const array of arrays.slice(1)) {
- result = result.filter(x => array.includes(x));
- }
- return result;
- }
- /**
- * Analyzes a parsed selector and returns combined data about it
- * @param {ESQuerySelector} parsedSelector An object (from esquery) describing the matching behavior of the selector
- * @returns {{nodeTypes:string[]|null, attributeCount:number, identifierCount:number}} Object containing selector data.
- */
- function analyzeParsedSelector(parsedSelector) {
- let attributeCount = 0;
- let identifierCount = 0;
- /**
- * Analyzes a selector and returns the node types that could possibly trigger it.
- * @param {ESQuerySelector} selector The selector to analyze.
- * @returns {string[]|null} The node types that could possibly trigger this selector, or `null` if all node types could trigger it
- */
- function analyzeSelector(selector) {
- switch (selector.type) {
- case "identifier":
- identifierCount++;
- return [selector.value];
- case "not":
- selector.selectors.map(analyzeSelector);
- return null;
- case "matches": {
- const typesForComponents =
- selector.selectors.map(analyzeSelector);
- if (typesForComponents.every(Boolean)) {
- return union(...typesForComponents);
- }
- return null;
- }
- case "compound": {
- const typesForComponents = selector.selectors
- .map(analyzeSelector)
- .filter(typesForComponent => typesForComponent);
- // If all of the components could match any type, then the compound could also match any type.
- if (!typesForComponents.length) {
- return null;
- }
- /*
- * If at least one of the components could only match a particular type, the compound could only match
- * the intersection of those types.
- */
- return intersection(...typesForComponents);
- }
- case "attribute":
- case "field":
- case "nth-child":
- case "nth-last-child":
- attributeCount++;
- return null;
- case "child":
- case "descendant":
- case "sibling":
- case "adjacent":
- analyzeSelector(selector.left);
- return analyzeSelector(selector.right);
- case "class":
- // TODO: abstract into JSLanguage somehow
- if (selector.name === "function") {
- return [
- "FunctionDeclaration",
- "FunctionExpression",
- "ArrowFunctionExpression",
- ];
- }
- return null;
- default:
- return null;
- }
- }
- const nodeTypes = analyzeSelector(parsedSelector);
- return {
- nodeTypes,
- attributeCount,
- identifierCount,
- };
- }
- /**
- * Tries to parse a simple selector string, such as a single identifier or wildcard.
- * This saves time by avoiding the overhead of esquery parsing for simple cases.
- * @param {string} selector The selector string to parse.
- * @returns {Object|null} An object describing the selector if it is simple, or `null` if it is not.
- */
- function trySimpleParseSelector(selector) {
- if (selector === "*") {
- return {
- type: "wildcard",
- value: "*",
- };
- }
- if (/^[a-z]+$/iu.test(selector)) {
- return {
- type: "identifier",
- value: selector,
- };
- }
- return null;
- }
- /**
- * Parses a raw selector string, and throws a useful error if parsing fails.
- * @param {string} selector The selector string to parse.
- * @returns {Object} An object (from esquery) describing the matching behavior of this selector
- * @throws {Error} An error if the selector is invalid
- */
- function tryParseSelector(selector) {
- try {
- return esquery.parse(selector);
- } catch (err) {
- if (
- err.location &&
- err.location.start &&
- typeof err.location.start.offset === "number"
- ) {
- throw new SyntaxError(
- `Syntax error in selector "${selector}" at position ${err.location.start.offset}: ${err.message}`,
- {
- cause: err,
- },
- );
- }
- throw err;
- }
- }
- /**
- * Parses a raw selector string, and returns the parsed selector along with specificity and type information.
- * @param {string} source A raw AST selector
- * @returns {ESQueryParsedSelector} A selector descriptor
- */
- function parse(source) {
- if (selectorCache.has(source)) {
- return selectorCache.get(source);
- }
- const cleanSource = source.replace(/:exit$/u, "");
- const parsedSelector =
- trySimpleParseSelector(cleanSource) ?? tryParseSelector(cleanSource);
- const { nodeTypes, attributeCount, identifierCount } =
- analyzeParsedSelector(parsedSelector);
- const result = new ESQueryParsedSelector(
- source,
- source.endsWith(":exit"),
- parsedSelector,
- nodeTypes,
- attributeCount,
- identifierCount,
- );
- selectorCache.set(source, result);
- return result;
- }
- /**
- * Checks if a node matches a given selector.
- * @param {Object} node The node to check against the selector.
- * @param {ESQuerySelector} root The root of the selector to match against.
- * @param {Object[]} ancestry The ancestry of the node being checked, which is an array of nodes from the current node to the root.
- * @param {ESQueryOptions} options The options to use for matching.
- * @returns {boolean} `true` if the node matches the selector, `false` otherwise.
- */
- function matches(node, root, ancestry, options) {
- return esquery.matches(node, root, ancestry, options);
- }
- //-----------------------------------------------------------------------------
- // Exports
- //-----------------------------------------------------------------------------
- module.exports = {
- parse,
- matches,
- ESQueryParsedSelector,
- };
|