]> git.proxmox.com Git - pve-eslint.git/blob - eslint/tools/code-sample-minimizer.js
import 7.12.1 upstream release
[pve-eslint.git] / eslint / tools / code-sample-minimizer.js
1 "use strict";
2
3 const evk = require("eslint-visitor-keys");
4 const recast = require("recast");
5 const espree = require("espree");
6 const assert = require("assert");
7
8 /**
9 * Determines whether an AST node could be an expression, based on the type
10 * @param {ASTNode} node The node
11 * @returns {boolean} `true` if the node could be an expression
12 */
13 function isMaybeExpression(node) {
14 return node.type.endsWith("Expression") ||
15 node.type === "Identifier" ||
16 node.type === "MetaProperty" ||
17 node.type.endsWith("Literal");
18 }
19
20 /**
21 * Determines whether an AST node is a statement
22 * @param {ASTNode} node The node
23 * @returns {boolean} `true` if the node is a statement
24 */
25 function isStatement(node) {
26 return node.type.endsWith("Statement") || node.type.endsWith("Declaration");
27 }
28
29 /**
30 * Given "bad" source text (e.g. an code sample that causes a rule to crash), tries to return a smaller
31 * piece of source text which is also "bad", to make it easier for a human to figure out where the
32 * problem is.
33 * @param {Object} options Options to process
34 * @param {string} options.sourceText Initial piece of "bad" source text
35 * @param {function(string): boolean} options.predicate A predicate that returns `true` for bad source text and `false` for good source text
36 * @param {Parser} [options.parser] The parser used to parse the source text. Defaults to a modified
37 * version of espree that uses recent parser options.
38 * @param {Object} [options.visitorKeys] The visitor keys of the AST. Defaults to eslint-visitor-keys.
39 * @returns {string} Another piece of "bad" source text, which may or may not be smaller than the original source text.
40 */
41 function reduceBadExampleSize({
42 sourceText,
43 predicate,
44 parser = {
45 parse: (code, options) =>
46 espree.parse(code, {
47 ...options,
48 loc: true,
49 range: true,
50 raw: true,
51 tokens: true,
52 comment: true,
53 eslintVisitorKeys: true,
54 eslintScopeManager: true,
55 ecmaVersion: espree.latestEcmaVersion,
56 sourceType: "script"
57 })
58 },
59 visitorKeys = evk.KEYS
60 }) {
61 let counter = 0;
62
63 /**
64 * Returns a new unique identifier
65 * @returns {string} A name for a new identifier
66 */
67 function generateNewIdentifierName() {
68 return `$${(counter++)}`;
69 }
70
71 /**
72 * Determines whether a source text sample is "bad"
73 * @param {string} updatedSourceText The sample
74 * @returns {boolean} `true` if the sample is "bad"
75 */
76 function reproducesBadCase(updatedSourceText) {
77 try {
78 parser.parse(updatedSourceText);
79 } catch {
80 return false;
81 }
82
83 return predicate(updatedSourceText);
84 }
85
86 assert(reproducesBadCase(sourceText), "Original source text should reproduce issue");
87 const parseResult = recast.parse(sourceText, { parser });
88
89 /**
90 * Recursively removes descendant subtrees of the given AST node and replaces
91 * them with simplified variants to produce a simplified AST which is still considered "bad".
92 * @param {ASTNode} node An AST node to prune. May be mutated by this call, but the
93 * resulting AST will still produce "bad" source code.
94 * @returns {void}
95 */
96 function pruneIrrelevantSubtrees(node) {
97 for (const key of visitorKeys[node.type]) {
98 if (Array.isArray(node[key])) {
99 for (let index = node[key].length - 1; index >= 0; index--) {
100 const [childNode] = node[key].splice(index, 1);
101
102 if (!reproducesBadCase(recast.print(parseResult).code)) {
103 node[key].splice(index, 0, childNode);
104 if (childNode) {
105 pruneIrrelevantSubtrees(childNode);
106 }
107 }
108 }
109 } else if (typeof node[key] === "object" && node[key] !== null) {
110
111 const childNode = node[key];
112
113 if (isMaybeExpression(childNode)) {
114 node[key] = { type: "Identifier", name: generateNewIdentifierName(), range: childNode.range };
115 if (!reproducesBadCase(recast.print(parseResult).code)) {
116 node[key] = childNode;
117 pruneIrrelevantSubtrees(childNode);
118 }
119 } else if (isStatement(childNode)) {
120 node[key] = { type: "EmptyStatement", range: childNode.range };
121 if (!reproducesBadCase(recast.print(parseResult).code)) {
122 node[key] = childNode;
123 pruneIrrelevantSubtrees(childNode);
124 }
125 }
126 }
127 }
128 }
129
130 /**
131 * Recursively tries to extract a descendant node from the AST that is "bad" on its own
132 * @param {ASTNode} node A node which produces "bad" source code
133 * @returns {ASTNode} A descendent of `node` which is also bad
134 */
135 function extractRelevantChild(node) {
136 const childNodes = [].concat(
137 ...visitorKeys[node.type]
138 .map(key => (Array.isArray(node[key]) ? node[key] : [node[key]]))
139 );
140
141 for (const childNode of childNodes) {
142 if (!childNode) {
143 continue;
144 }
145
146 if (isMaybeExpression(childNode)) {
147 if (reproducesBadCase(recast.print(childNode).code)) {
148 return extractRelevantChild(childNode);
149 }
150
151 } else if (isStatement(childNode)) {
152 if (reproducesBadCase(recast.print(childNode).code)) {
153 return extractRelevantChild(childNode);
154 }
155 } else {
156 const childResult = extractRelevantChild(childNode);
157
158 if (reproducesBadCase(recast.print(childResult).code)) {
159 return childResult;
160 }
161 }
162 }
163 return node;
164 }
165
166 /**
167 * Removes and simplifies comments from the source text
168 * @param {string} text A piece of "bad" source text
169 * @returns {string} A piece of "bad" source text with fewer and/or simpler comments.
170 */
171 function removeIrrelevantComments(text) {
172 const ast = parser.parse(text);
173
174 if (ast.comments) {
175 for (const comment of ast.comments) {
176 for (const potentialSimplification of [
177
178 // Try deleting the comment
179 `${text.slice(0, comment.range[0])}${text.slice(comment.range[1])}`,
180
181 // Try replacing the comment with a space
182 `${text.slice(0, comment.range[0])} ${text.slice(comment.range[1])}`,
183
184 // Try deleting the contents of the comment
185 text.slice(0, comment.range[0] + 2) + text.slice(comment.type === "Block" ? comment.range[1] - 2 : comment.range[1])
186 ]) {
187 if (reproducesBadCase(potentialSimplification)) {
188 return removeIrrelevantComments(potentialSimplification);
189 }
190 }
191 }
192 }
193
194 return text;
195 }
196
197 pruneIrrelevantSubtrees(parseResult.program);
198 const relevantChild = recast.print(extractRelevantChild(parseResult.program)).code;
199
200 assert(reproducesBadCase(relevantChild), "Extracted relevant source text should reproduce issue");
201 const result = removeIrrelevantComments(relevantChild);
202
203 assert(reproducesBadCase(result), "Source text with irrelevant comments removed should reproduce issue");
204 return result;
205 }
206
207 module.exports = reduceBadExampleSize;