Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Chore: add a fuzzer to detect bugs in core rules (#8422)
* Chore: add a fuzzer to detect bugs in core rules This commit adds a fuzzer to detect bugs in core rules. The fuzzer can detect two types of problems: crashes (where a rule throws an error given a certain syntax) and autofix errors (where an autofix results in a syntax error). The fuzzer works by running eslint on randomly-generated code with a random config. The code is generated with [eslump](https://github.com/lydell/eslump), and the config is generated with the existing autoconfig logic. The fuzzer can be run with `npm run fuzz`. Eventually, I think we should add the fuzzer to the normal `npm test` build. * Pin eslump version * Update linter logic with new APIs from ESLint 4 * Upgrade eslump to 1.6.0
- Loading branch information
1 parent
45f8cd9
commit 933a9cf
Showing
6 changed files
with
523 additions
and
8 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,7 @@ build/ | |
npm-debug.log | ||
.DS_Store | ||
tmp/ | ||
debug/ | ||
.idea | ||
jsdoc/ | ||
versions.json | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,258 @@ | ||
"use strict"; | ||
|
||
const assert = require("chai").assert; | ||
const eslint = require("../.."); | ||
const espree = require("espree"); | ||
const sinon = require("sinon"); | ||
const configRule = require("../../lib/config/config-rule"); | ||
|
||
describe("eslint-fuzzer", function() { | ||
let fakeRule, fuzz; | ||
|
||
/* | ||
* These tests take awhile because isolating which rule caused an error requires running eslint up to hundreds of | ||
* times, one rule at a time. | ||
*/ | ||
this.timeout(15000); // eslint-disable-line no-invalid-this | ||
|
||
const linter = new eslint.Linter(); | ||
const coreRules = linter.getRules(); | ||
const fixableRuleNames = Array.from(coreRules) | ||
.filter(rulePair => rulePair[1].meta && rulePair[1].meta.fixable) | ||
.map(rulePair => rulePair[0]); | ||
const CRASH_BUG = new TypeError("error thrown from a rule"); | ||
|
||
// A comment to disable all core fixable rules | ||
const disableFixableRulesComment = `// eslint-disable-line ${fixableRuleNames.join(",")}`; | ||
|
||
before(() => { | ||
const realCoreRuleConfigs = configRule.createCoreRuleConfigs(); | ||
|
||
// Make sure the config generator generates a config for "test-fuzzer-rule" | ||
sinon.stub(configRule, "createCoreRuleConfigs").returns(Object.assign(realCoreRuleConfigs, { "test-fuzzer-rule": [2] })); | ||
|
||
// Create a closure around `fakeRule` so that tests can reassign it and have the changes take effect. | ||
linter.defineRule("test-fuzzer-rule", Object.assign(context => fakeRule(context), { meta: { fixable: "code" } })); | ||
|
||
fuzz = require("../../tools/eslint-fuzzer"); | ||
}); | ||
|
||
after(() => { | ||
linter.reset(); | ||
configRule.createCoreRuleConfigs.restore(); | ||
}); | ||
|
||
describe("when running in crash-only mode", () => { | ||
describe("when a rule crashes on the given input", () => { | ||
it("should report the crash with a minimal config", () => { | ||
fakeRule = () => ({ | ||
Program() { | ||
throw CRASH_BUG; | ||
} | ||
}); | ||
|
||
const results = fuzz({ count: 1, codeGenerator: () => "foo", checkAutofixes: false, linter }); | ||
|
||
assert.strictEqual(results.length, 1); | ||
assert.strictEqual(results[0].type, "crash"); | ||
assert.strictEqual(results[0].text, "foo"); | ||
assert.deepEqual(results[0].config.rules, { "test-fuzzer-rule": 2 }); | ||
assert.strictEqual(results[0].error, CRASH_BUG.stack); | ||
}); | ||
}); | ||
|
||
describe("when no rules crash", () => { | ||
it("should return an empty array", () => { | ||
fakeRule = () => ({}); | ||
|
||
assert.deepEqual(fuzz({ count: 1, codeGenerator: () => "foo", checkAutofixes: false, linter }), []); | ||
}); | ||
}); | ||
}); | ||
|
||
describe("when running in crash-and-autofix mode", () => { | ||
const INVALID_SYNTAX = "this is not valid javascript syntax"; | ||
let expectedSyntaxError; | ||
|
||
try { | ||
espree.parse(INVALID_SYNTAX); | ||
} catch (err) { | ||
expectedSyntaxError = err; | ||
} | ||
|
||
describe("when a rule crashes on the given input", () => { | ||
it("should report the crash with a minimal config", () => { | ||
fakeRule = () => ({ | ||
Program() { | ||
throw CRASH_BUG; | ||
} | ||
}); | ||
|
||
const results = fuzz({ count: 1, codeGenerator: () => "foo", checkAutofixes: false, linter }); | ||
|
||
assert.strictEqual(results.length, 1); | ||
assert.strictEqual(results[0].type, "crash"); | ||
assert.strictEqual(results[0].text, "foo"); | ||
assert.deepEqual(results[0].config.rules, { "test-fuzzer-rule": 2 }); | ||
assert.strictEqual(results[0].error, CRASH_BUG.stack); | ||
}); | ||
}); | ||
|
||
describe("when a rule's autofix produces valid syntax", () => { | ||
it("does not report any errors", () => { | ||
|
||
// Replaces programs that start with "foo" with "bar" | ||
fakeRule = context => ({ | ||
Program(node) { | ||
if (context.getSourceCode().text.startsWith("foo")) { | ||
context.report({ | ||
node, | ||
message: "no foos allowed", | ||
fix: fixer => fixer.replaceText(node, `bar ${disableFixableRulesComment}`) | ||
}); | ||
} | ||
} | ||
}); | ||
|
||
const results = fuzz({ | ||
count: 1, | ||
|
||
/* | ||
* To ensure that no other rules produce a different autofix and mess up the test, add a big disable | ||
* comment for all core fixable rules. | ||
*/ | ||
codeGenerator: () => `foo ${disableFixableRulesComment}`, | ||
checkAutofixes: true, | ||
linter | ||
}); | ||
|
||
assert.deepEqual(results, []); | ||
}); | ||
}); | ||
|
||
describe("when a rule's autofix produces invalid syntax on the first pass", () => { | ||
it("reports an autofix error with a minimal config", () => { | ||
|
||
// Replaces programs that start with "foo" with invalid syntax | ||
fakeRule = context => ({ | ||
Program(node) { | ||
const sourceCode = context.getSourceCode(); | ||
|
||
if (sourceCode.text.startsWith("foo")) { | ||
context.report({ | ||
node, | ||
message: "no foos allowed", | ||
fix: fixer => fixer.replaceTextRange([0, sourceCode.text.length], INVALID_SYNTAX) | ||
}); | ||
} | ||
} | ||
}); | ||
|
||
const results = fuzz({ | ||
count: 1, | ||
codeGenerator: () => `foo ${disableFixableRulesComment}`, | ||
checkAutofixes: true, | ||
linter | ||
}); | ||
|
||
assert.strictEqual(results.length, 1); | ||
assert.strictEqual(results[0].type, "autofix"); | ||
assert.strictEqual(results[0].text, `foo ${disableFixableRulesComment}`); | ||
assert.deepEqual(results[0].config.rules, { "test-fuzzer-rule": 2 }); | ||
assert.deepEqual(results[0].error, { | ||
ruleId: null, | ||
fatal: true, | ||
severity: 2, | ||
source: INVALID_SYNTAX, | ||
message: `Parsing error: ${expectedSyntaxError.message}`, | ||
line: expectedSyntaxError.lineNumber, | ||
column: expectedSyntaxError.column | ||
}); | ||
}); | ||
}); | ||
|
||
describe("when a rule's autofix produces invalid syntax on the second pass", () => { | ||
it("reports an autofix error with a minimal config and the text from the second pass", () => { | ||
const intermediateCode = `bar ${disableFixableRulesComment}`; | ||
|
||
// Replaces programs that start with "foo" with invalid syntax | ||
fakeRule = context => ({ | ||
Program(node) { | ||
const sourceCode = context.getSourceCode(); | ||
|
||
if (sourceCode.text.startsWith("foo") || sourceCode.text.startsWith("bar")) { | ||
context.report({ | ||
node, | ||
message: "no foos allowed", | ||
fix(fixer) { | ||
return fixer.replaceTextRange( | ||
[0, sourceCode.text.length], | ||
sourceCode.text === intermediateCode ? INVALID_SYNTAX : intermediateCode | ||
); | ||
} | ||
}); | ||
} | ||
} | ||
}); | ||
|
||
const results = fuzz({ | ||
count: 1, | ||
codeGenerator: () => `foo ${disableFixableRulesComment}`, | ||
checkAutofixes: true, | ||
linter | ||
}); | ||
|
||
assert.strictEqual(results.length, 1); | ||
assert.strictEqual(results[0].type, "autofix"); | ||
assert.strictEqual(results[0].text, intermediateCode); | ||
assert.deepEqual(results[0].config.rules, { "test-fuzzer-rule": 2 }); | ||
assert.deepEqual(results[0].error, { | ||
ruleId: null, | ||
fatal: true, | ||
severity: 2, | ||
source: INVALID_SYNTAX, | ||
message: `Parsing error: ${expectedSyntaxError.message}`, | ||
line: expectedSyntaxError.lineNumber, | ||
column: expectedSyntaxError.column | ||
}); | ||
}); | ||
}); | ||
|
||
describe("when a rule crashes on the second autofix pass", () => { | ||
it("reports a crash error with a minimal config", () => { | ||
|
||
// Replaces programs that start with "foo" with invalid syntax | ||
fakeRule = context => ({ | ||
Program(node) { | ||
const sourceCode = context.getSourceCode(); | ||
|
||
if (sourceCode.text.startsWith("foo")) { | ||
context.report({ | ||
node, | ||
message: "no foos allowed", | ||
fix: fixer => fixer.replaceText(node, "bar") | ||
}); | ||
} else if (sourceCode.text.startsWith("bar")) { | ||
throw CRASH_BUG; | ||
} | ||
} | ||
}); | ||
|
||
const results = fuzz({ | ||
count: 1, | ||
codeGenerator: () => `foo ${disableFixableRulesComment}`, | ||
checkAutofixes: true, | ||
linter | ||
}); | ||
|
||
assert.strictEqual(results.length, 1); | ||
assert.strictEqual(results[0].type, "crash"); | ||
|
||
// TODO: (not-an-aardvark) It might be more useful to output the intermediate code here. | ||
assert.strictEqual(results[0].text, `foo ${disableFixableRulesComment}`); | ||
assert.deepEqual(results[0].config.rules, { "test-fuzzer-rule": 2 }); | ||
assert.strictEqual(results[0].error, CRASH_BUG.stack); | ||
}); | ||
}); | ||
}); | ||
}); |
Oops, something went wrong.