forked from eslint/eslint
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy patheslint-fuzzer.js
214 lines (183 loc) · 8.57 KB
/
eslint-fuzzer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
/**
* @fileoverview A fuzzer that runs eslint on randomly-generated code samples to detect bugs
* @author Teddy Katz
*/
"use strict";
//------------------------------------------------------------------------------
// Requirements
//------------------------------------------------------------------------------
const assert = require("node:assert");
const eslump = require("eslump");
const espree = require("espree");
const SourceCodeFixer = require("../lib/linter/source-code-fixer");
const ruleConfigs = require("./config-rule").createCoreRuleConfigs(true);
const sampleMinimizer = require("./code-sample-minimizer");
//------------------------------------------------------------------------------
// Helpers
//------------------------------------------------------------------------------
/**
* Gets a random item from an array
* @param {any[]} array The array to sample
* @returns {any} The random item
*/
function sample(array) {
return array[Math.floor(Math.random() * array.length)];
}
//------------------------------------------------------------------------------
// Public API
//------------------------------------------------------------------------------
/**
* Generates random JS code, runs ESLint on it, and returns a list of detected crashes or autofix bugs
* @param {Object} options Config options for fuzzing
* @param {number} options.count The number of fuzz iterations.
* @param {Object} options.linter The linter object to test with.
* @param {function(Object): string} [options.codeGenerator=eslump.generateRandomJS] A function to use to generate random
* code. Accepts an object argument with a `sourceType` key, indicating the source type of the generated code. The object
* might also be passed other keys.
* @param {boolean} [options.checkAutofixes=true] `true` if the fuzzer should check for autofix bugs. The fuzzer runs
* roughly 4 times slower with autofix checking enabled.
* @param {function(number) : void} [options.progressCallback] A function that gets called once for each code sample, with the total number of errors found so far
* @returns {Object[]} A list of problems found. Each problem has the following properties:
* type (string): The type of problem. This is either "crash" (a rule crashes) or "autofix" (an autofix produces a syntax error)
* text (string): The text that ESLint should be run on to reproduce the problem
* config (object): The config object that should be used to reproduce the problem. The fuzzer will try to return a minimal
* config (that only has one rule enabled), but this isn't always possible.
* error (*) The problem that occurred. For crashes, this will be the error stack. For autofix bugs, this will be
* the parsing error object that was thrown when parsing the autofixed code.
*/
function fuzz(options) {
assert.strictEqual(typeof options, "object", "An options object must be provided");
assert.strictEqual(typeof options.count, "number", "The number of iterations (options.count) must be provided");
assert.strictEqual(typeof options.linter, "object", "An linter object (options.linter) must be provided");
const linter = options.linter;
const codeGenerator = options.codeGenerator || (genOptions => eslump.generateRandomJS(Object.assign({ comments: true, whitespace: true }, genOptions)));
const checkAutofixes = options.checkAutofixes !== false;
const progressCallback = options.progressCallback || (() => {});
/**
* Tries to isolate the smallest config that reproduces a problem
* @param {string} text The source text to lint
* @param {Object} config A config object that causes a crash or autofix error
* @param {("crash"|"autofix")} problemType The type of problem that occurred
* @returns {Object} A config object with only one rule enabled that produces the same crash or autofix error, if possible.
* Otherwise, the same as `config`
*/
function isolateBadConfig(text, config, problemType) {
for (const ruleId of Object.keys(config.rules)) {
const reducedConfig = Object.assign({}, config, { rules: { [ruleId]: config.rules[ruleId] } });
let fixResult;
try {
fixResult = linter.verifyAndFix(text, reducedConfig, {});
} catch {
return reducedConfig;
}
if (fixResult.messages.length === 1 && fixResult.messages[0].fatal && problemType === "autofix") {
return reducedConfig;
}
}
return config;
}
/**
* Runs multipass autofix one pass at a time to find the last good source text before a fatal error occurs
* @param {string} originalText Syntactically valid source code that results in a syntax error or crash when autofixing with `config`
* @param {Object} config The config to lint with
* @returns {string} A possibly-modified version of originalText that results in the same syntax error or crash after only one pass
*/
function isolateBadAutofixPass(originalText, config) {
let previousText = originalText;
let currentText = originalText;
do {
let messages;
try {
messages = linter.verify(currentText, config);
} catch {
return currentText;
}
if (messages.length === 1 && messages[0].fatal) {
return previousText;
}
previousText = currentText;
currentText = SourceCodeFixer.applyFixes(currentText, messages).output;
} while (previousText !== currentText);
return currentText;
}
const problems = [];
/**
* Creates a version of espree that always runs with the specified options
* @param {ConfigData} config The config used
* @returns {Parser} a parser
*/
function getParser({ parserOptions }) {
return sourceText => espree.parse(sourceText, {
...parserOptions,
loc: true,
range: true,
raw: true,
tokens: true,
comment: true
});
}
for (let i = 0; i < options.count; progressCallback(problems.length), i++) {
const rules = {};
for (const [id, configs] of Object.entries(ruleConfigs)) {
rules[id] = sample(configs);
}
const sourceType = sample(["script", "module"]);
const text = codeGenerator({ sourceType });
const config = {
rules,
parserOptions: {
sourceType,
ecmaVersion: espree.latestEcmaVersion
}
};
let autofixResult;
try {
if (checkAutofixes) {
autofixResult = linter.verifyAndFix(text, config, {});
} else {
linter.verify(text, config);
}
} catch (err) {
const lastGoodText = checkAutofixes ? isolateBadAutofixPass(text, config) : text;
const smallConfig = isolateBadConfig(lastGoodText, config, "crash");
const smallText = sampleMinimizer({
sourceText: lastGoodText,
parser: { parse: getParser(smallConfig) },
predicate(reducedText) {
try {
linter.verify(reducedText, smallConfig);
return false;
} catch {
return true;
}
}
});
problems.push({ type: "crash", text: smallText, config: smallConfig, error: err.stack });
continue;
}
if (checkAutofixes && autofixResult.fixed && autofixResult.messages.length === 1 && autofixResult.messages[0].fatal) {
const lastGoodText = isolateBadAutofixPass(text, config);
const smallConfig = isolateBadConfig(lastGoodText, config, "autofix");
const smallText = sampleMinimizer({
sourceText: lastGoodText,
parser: { parse: getParser(smallConfig) },
predicate(reducedText) {
try {
const smallFixResult = linter.verifyAndFix(reducedText, smallConfig);
return smallFixResult.fixed && smallFixResult.messages.length === 1 && smallFixResult.messages[0].fatal;
} catch {
return false;
}
}
});
problems.push({
type: "autofix",
text: smallText,
config: smallConfig,
error: autofixResult.messages[0]
});
}
}
return problems;
}
module.exports = fuzz;