.gitignore added
[dotfiles/.git] / .config / coc / extensions / node_modules / coc-prettier / node_modules / eslint / lib / linter / node-event-generator.js
1 /**
2  * @fileoverview The event generator for AST nodes.
3  * @author Toru Nagashima
4  */
5
6 "use strict";
7
8 //------------------------------------------------------------------------------
9 // Requirements
10 //------------------------------------------------------------------------------
11
12 const esquery = require("esquery");
13 const lodash = require("lodash");
14
15 //------------------------------------------------------------------------------
16 // Typedefs
17 //------------------------------------------------------------------------------
18
19 /**
20  * An object describing an AST selector
21  * @typedef {Object} ASTSelector
22  * @property {string} rawSelector The string that was parsed into this selector
23  * @property {boolean} isExit `true` if this should be emitted when exiting the node rather than when entering
24  * @property {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
25  * @property {string[]|null} listenerTypes A list of node types that could possibly cause the selector to match,
26  * or `null` if all node types could cause a match
27  * @property {number} attributeCount The total number of classes, pseudo-classes, and attribute queries in this selector
28  * @property {number} identifierCount The total number of identifier queries in this selector
29  */
30
31 //------------------------------------------------------------------------------
32 // Helpers
33 //------------------------------------------------------------------------------
34
35 /**
36  * Gets the possible types of a selector
37  * @param {Object} parsedSelector An object (from esquery) describing the matching behavior of the selector
38  * @returns {string[]|null} The node types that could possibly trigger this selector, or `null` if all node types could trigger it
39  */
40 function getPossibleTypes(parsedSelector) {
41     switch (parsedSelector.type) {
42         case "identifier":
43             return [parsedSelector.value];
44
45         case "matches": {
46             const typesForComponents = parsedSelector.selectors.map(getPossibleTypes);
47
48             if (typesForComponents.every(Boolean)) {
49                 return lodash.union(...typesForComponents);
50             }
51             return null;
52         }
53
54         case "compound": {
55             const typesForComponents = parsedSelector.selectors.map(getPossibleTypes).filter(typesForComponent => typesForComponent);
56
57             // If all of the components could match any type, then the compound could also match any type.
58             if (!typesForComponents.length) {
59                 return null;
60             }
61
62             /*
63              * If at least one of the components could only match a particular type, the compound could only match
64              * the intersection of those types.
65              */
66             return lodash.intersection(...typesForComponents);
67         }
68
69         case "child":
70         case "descendant":
71         case "sibling":
72         case "adjacent":
73             return getPossibleTypes(parsedSelector.right);
74
75         default:
76             return null;
77
78     }
79 }
80
81 /**
82  * Counts the number of class, pseudo-class, and attribute queries in this selector
83  * @param {Object} parsedSelector An object (from esquery) describing the selector's matching behavior
84  * @returns {number} The number of class, pseudo-class, and attribute queries in this selector
85  */
86 function countClassAttributes(parsedSelector) {
87     switch (parsedSelector.type) {
88         case "child":
89         case "descendant":
90         case "sibling":
91         case "adjacent":
92             return countClassAttributes(parsedSelector.left) + countClassAttributes(parsedSelector.right);
93
94         case "compound":
95         case "not":
96         case "matches":
97             return parsedSelector.selectors.reduce((sum, childSelector) => sum + countClassAttributes(childSelector), 0);
98
99         case "attribute":
100         case "field":
101         case "nth-child":
102         case "nth-last-child":
103             return 1;
104
105         default:
106             return 0;
107     }
108 }
109
110 /**
111  * Counts the number of identifier queries in this selector
112  * @param {Object} parsedSelector An object (from esquery) describing the selector's matching behavior
113  * @returns {number} The number of identifier queries
114  */
115 function countIdentifiers(parsedSelector) {
116     switch (parsedSelector.type) {
117         case "child":
118         case "descendant":
119         case "sibling":
120         case "adjacent":
121             return countIdentifiers(parsedSelector.left) + countIdentifiers(parsedSelector.right);
122
123         case "compound":
124         case "not":
125         case "matches":
126             return parsedSelector.selectors.reduce((sum, childSelector) => sum + countIdentifiers(childSelector), 0);
127
128         case "identifier":
129             return 1;
130
131         default:
132             return 0;
133     }
134 }
135
136 /**
137  * Compares the specificity of two selector objects, with CSS-like rules.
138  * @param {ASTSelector} selectorA An AST selector descriptor
139  * @param {ASTSelector} selectorB Another AST selector descriptor
140  * @returns {number}
141  * a value less than 0 if selectorA is less specific than selectorB
142  * a value greater than 0 if selectorA is more specific than selectorB
143  * a value less than 0 if selectorA and selectorB have the same specificity, and selectorA <= selectorB alphabetically
144  * a value greater than 0 if selectorA and selectorB have the same specificity, and selectorA > selectorB alphabetically
145  */
146 function compareSpecificity(selectorA, selectorB) {
147     return selectorA.attributeCount - selectorB.attributeCount ||
148         selectorA.identifierCount - selectorB.identifierCount ||
149         (selectorA.rawSelector <= selectorB.rawSelector ? -1 : 1);
150 }
151
152 /**
153  * Parses a raw selector string, and throws a useful error if parsing fails.
154  * @param {string} rawSelector A raw AST selector
155  * @returns {Object} An object (from esquery) describing the matching behavior of this selector
156  * @throws {Error} An error if the selector is invalid
157  */
158 function tryParseSelector(rawSelector) {
159     try {
160         return esquery.parse(rawSelector.replace(/:exit$/u, ""));
161     } catch (err) {
162         if (err.location && err.location.start && typeof err.location.start.offset === "number") {
163             throw new SyntaxError(`Syntax error in selector "${rawSelector}" at position ${err.location.start.offset}: ${err.message}`);
164         }
165         throw err;
166     }
167 }
168
169 /**
170  * Parses a raw selector string, and returns the parsed selector along with specificity and type information.
171  * @param {string} rawSelector A raw AST selector
172  * @returns {ASTSelector} A selector descriptor
173  */
174 const parseSelector = lodash.memoize(rawSelector => {
175     const parsedSelector = tryParseSelector(rawSelector);
176
177     return {
178         rawSelector,
179         isExit: rawSelector.endsWith(":exit"),
180         parsedSelector,
181         listenerTypes: getPossibleTypes(parsedSelector),
182         attributeCount: countClassAttributes(parsedSelector),
183         identifierCount: countIdentifiers(parsedSelector)
184     };
185 });
186
187 //------------------------------------------------------------------------------
188 // Public Interface
189 //------------------------------------------------------------------------------
190
191 /**
192  * The event generator for AST nodes.
193  * This implements below interface.
194  *
195  * ```ts
196  * interface EventGenerator {
197  *     emitter: SafeEmitter;
198  *     enterNode(node: ASTNode): void;
199  *     leaveNode(node: ASTNode): void;
200  * }
201  * ```
202  */
203 class NodeEventGenerator {
204
205     // eslint-disable-next-line jsdoc/require-description
206     /**
207      * @param {SafeEmitter} emitter
208      * An SafeEmitter which is the destination of events. This emitter must already
209      * have registered listeners for all of the events that it needs to listen for.
210      * (See lib/linter/safe-emitter.js for more details on `SafeEmitter`.)
211      * @param {ESQueryOptions} esqueryOptions `esquery` options for traversing custom nodes.
212      * @returns {NodeEventGenerator} new instance
213      */
214     constructor(emitter, esqueryOptions) {
215         this.emitter = emitter;
216         this.esqueryOptions = esqueryOptions;
217         this.currentAncestry = [];
218         this.enterSelectorsByNodeType = new Map();
219         this.exitSelectorsByNodeType = new Map();
220         this.anyTypeEnterSelectors = [];
221         this.anyTypeExitSelectors = [];
222
223         emitter.eventNames().forEach(rawSelector => {
224             const selector = parseSelector(rawSelector);
225
226             if (selector.listenerTypes) {
227                 const typeMap = selector.isExit ? this.exitSelectorsByNodeType : this.enterSelectorsByNodeType;
228
229                 selector.listenerTypes.forEach(nodeType => {
230                     if (!typeMap.has(nodeType)) {
231                         typeMap.set(nodeType, []);
232                     }
233                     typeMap.get(nodeType).push(selector);
234                 });
235                 return;
236             }
237             const selectors = selector.isExit ? this.anyTypeExitSelectors : this.anyTypeEnterSelectors;
238
239             selectors.push(selector);
240         });
241
242         this.anyTypeEnterSelectors.sort(compareSpecificity);
243         this.anyTypeExitSelectors.sort(compareSpecificity);
244         this.enterSelectorsByNodeType.forEach(selectorList => selectorList.sort(compareSpecificity));
245         this.exitSelectorsByNodeType.forEach(selectorList => selectorList.sort(compareSpecificity));
246     }
247
248     /**
249      * Checks a selector against a node, and emits it if it matches
250      * @param {ASTNode} node The node to check
251      * @param {ASTSelector} selector An AST selector descriptor
252      * @returns {void}
253      */
254     applySelector(node, selector) {
255         if (esquery.matches(node, selector.parsedSelector, this.currentAncestry, this.esqueryOptions)) {
256             this.emitter.emit(selector.rawSelector, node);
257         }
258     }
259
260     /**
261      * Applies all appropriate selectors to a node, in specificity order
262      * @param {ASTNode} node The node to check
263      * @param {boolean} isExit `false` if the node is currently being entered, `true` if it's currently being exited
264      * @returns {void}
265      */
266     applySelectors(node, isExit) {
267         const selectorsByNodeType = (isExit ? this.exitSelectorsByNodeType : this.enterSelectorsByNodeType).get(node.type) || [];
268         const anyTypeSelectors = isExit ? this.anyTypeExitSelectors : this.anyTypeEnterSelectors;
269
270         /*
271          * selectorsByNodeType and anyTypeSelectors were already sorted by specificity in the constructor.
272          * Iterate through each of them, applying selectors in the right order.
273          */
274         let selectorsByTypeIndex = 0;
275         let anyTypeSelectorsIndex = 0;
276
277         while (selectorsByTypeIndex < selectorsByNodeType.length || anyTypeSelectorsIndex < anyTypeSelectors.length) {
278             if (
279                 selectorsByTypeIndex >= selectorsByNodeType.length ||
280                 anyTypeSelectorsIndex < anyTypeSelectors.length &&
281                 compareSpecificity(anyTypeSelectors[anyTypeSelectorsIndex], selectorsByNodeType[selectorsByTypeIndex]) < 0
282             ) {
283                 this.applySelector(node, anyTypeSelectors[anyTypeSelectorsIndex++]);
284             } else {
285                 this.applySelector(node, selectorsByNodeType[selectorsByTypeIndex++]);
286             }
287         }
288     }
289
290     /**
291      * Emits an event of entering AST node.
292      * @param {ASTNode} node A node which was entered.
293      * @returns {void}
294      */
295     enterNode(node) {
296         if (node.parent) {
297             this.currentAncestry.unshift(node.parent);
298         }
299         this.applySelectors(node, false);
300     }
301
302     /**
303      * Emits an event of leaving AST node.
304      * @param {ASTNode} node A node which was left.
305      * @returns {void}
306      */
307     leaveNode(node) {
308         this.applySelectors(node, true);
309         this.currentAncestry.shift();
310     }
311 }
312
313 module.exports = NodeEventGenerator;