source: imaps-frontend/node_modules/eslint/lib/linter/node-event-generator.js@ d565449

main
Last change on this file since d565449 was d565449, checked in by stefan toskovski <stefantoska84@…>, 4 weeks ago

Update repo after prototype presentation

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