Comparing version 1.5.1 to 1.6.0
@@ -0,1 +1,8 @@ | ||
### Version 1.6.0 (2017-06-26) | ||
- Added: Node.js 4 support. Thanks to Teddy Katz (@not-an-aardvark)! | ||
- Added: eslump can now explicitly be used as an npm module, exposing the | ||
`generateRandomJS` function. | ||
### Version 1.5.1 (2017-03-26) | ||
@@ -2,0 +9,0 @@ |
101
codegen.js
@@ -1,3 +0,5 @@ | ||
const { FormattedCodeGen, Sep, Paren } = require("shift-codegen"); | ||
const { TokenStream } = require("shift-codegen/dist/token_stream"); | ||
"use strict"; | ||
const ShiftCodegen = require("shift-codegen"); | ||
const TokenStream = require("shift-codegen/dist/token_stream").TokenStream; | ||
const shiftFuzzer = require("shift-fuzzer"); | ||
@@ -22,5 +24,8 @@ const shiftReducer = require("shift-reducer"); | ||
class CustomTokenStream extends TokenStream { | ||
constructor({ comments = false, whitespace = false } = {}) { | ||
constructor(options) { | ||
super(); | ||
this._options = { comments, whitespace }; | ||
this._options = { | ||
comments: options && options.comments, | ||
whitespace: options && options.whitespace | ||
}; | ||
this._probabilities = { | ||
@@ -35,3 +40,3 @@ comments: Math.random(), | ||
put(tokenString, isRegExp) { | ||
const { optionalSemi } = this; | ||
const optionalSemi = this.optionalSemi; | ||
@@ -78,3 +83,4 @@ // Sometimes print semicolons, sometimes newlines. | ||
newTokenString = random.string(randomTimes(), () => | ||
random.item(choices)()); | ||
random.item(choices)() | ||
); | ||
didRandomizeWhitespace = true; | ||
@@ -85,3 +91,4 @@ } | ||
if ( | ||
this._options.comments && Math.random() < this._probabilities.comments | ||
this._options.comments && | ||
Math.random() < this._probabilities.comments | ||
) { | ||
@@ -114,5 +121,5 @@ newTokenString = random.insertComments(newTokenString, { | ||
class CustomFormattedCodeGen extends FormattedCodeGen { | ||
constructor(...args) { | ||
super(...args); | ||
class CustomFormattedCodeGen extends ShiftCodegen.FormattedCodeGen { | ||
constructor() { | ||
super(); | ||
this._probabilities = { | ||
@@ -140,3 +147,3 @@ parentheses: Math.random() | ||
reduceObjectExpression(node, data) { | ||
const { properties } = data; | ||
const properties = data.properties; | ||
const newProperties = properties.map(removeParentheses); | ||
@@ -149,3 +156,3 @@ const newData = Object.assign({}, data, { properties: newProperties }); | ||
reduceExportLocalSpecifier(node, data) { | ||
const { name } = data; | ||
const name = data.name; | ||
const newName = removeParentheses(name); | ||
@@ -158,3 +165,3 @@ const newData = Object.assign({}, data, { name: newName }); | ||
function removeParentheses(node) { | ||
return node instanceof Paren | ||
return node instanceof ShiftCodegen.Paren | ||
? removeParentheses(node.expr.children[1]) | ||
@@ -164,32 +171,36 @@ : node; | ||
const parenthesesAddingHandler = { | ||
get(target, property) { | ||
// Sometimes add extra parentheses. | ||
if ( | ||
PARENTHESES_WRAPPABLE_METHOD.test(property) && | ||
Math.random() < target._probabilities.parentheses | ||
) { | ||
return (...args) => { | ||
const times = randomTimes(); | ||
let node = target[property](...args); | ||
for (let i = 0; i < times; i++) { | ||
node = target.paren( | ||
node, | ||
Sep.EXPRESSION_PAREN_BEFORE, | ||
Sep.EXPRESSION_PAREN_AFTER | ||
); | ||
} | ||
return node; | ||
}; | ||
const overridablePrototypeMethodNames = new Set(); | ||
for ( | ||
let prototype = CustomFormattedCodeGen.prototype; | ||
prototype; | ||
prototype = Object.getPrototypeOf(prototype) | ||
) { | ||
Object.getOwnPropertyNames(prototype) | ||
.filter(methodName => PARENTHESES_WRAPPABLE_METHOD.test(methodName)) | ||
.forEach(methodName => overridablePrototypeMethodNames.add(methodName)); | ||
} | ||
overridablePrototypeMethodNames.forEach(methodName => { | ||
const originalMethod = CustomFormattedCodeGen.prototype[methodName]; | ||
CustomFormattedCodeGen.prototype[methodName] = function() { | ||
let node = originalMethod.apply(this, arguments); | ||
if (Math.random() < this._probabilities.parentheses) { | ||
const times = randomTimes(); | ||
for (let i = 0; i < times; i++) { | ||
node = this.paren( | ||
node, | ||
ShiftCodegen.Sep.EXPRESSION_PAREN_BEFORE, | ||
ShiftCodegen.Sep.EXPRESSION_PAREN_AFTER | ||
); | ||
} | ||
} | ||
return node; | ||
}; | ||
}); | ||
return target[property]; | ||
} | ||
}; | ||
function codeGen(ast, options = {}) { | ||
function codeGen(ast, options) { | ||
const generator = new CustomFormattedCodeGen(); | ||
const proxiedGenerator = new Proxy(generator, parenthesesAddingHandler); | ||
const tokenStream = new CustomTokenStream(options); | ||
const rep = shiftReducer.default(proxiedGenerator, ast); | ||
const tokenStream = new CustomTokenStream(options || {}); | ||
const rep = shiftReducer.default(generator, ast); | ||
rep.emit(tokenStream); | ||
@@ -199,4 +210,4 @@ return tokenStream.result; | ||
function generateRandomJS(options = {}) { | ||
const fuzzer = options.sourceType === "script" | ||
function generateRandomJS(options) { | ||
const fuzzer = options && options.sourceType === "script" | ||
? shiftFuzzer.fuzzScript | ||
@@ -207,3 +218,3 @@ : shiftFuzzer.fuzzModule; | ||
new shiftFuzzer.FuzzerState({ | ||
maxDepth: options.maxDepth | ||
maxDepth: options && options.maxDepth | ||
}) | ||
@@ -213,4 +224,4 @@ ); | ||
return codeGen(randomAST, { | ||
comments: options.comments, | ||
whitespace: options.whitespace | ||
comments: options && options.comments, | ||
whitespace: options && options.whitespace | ||
}); | ||
@@ -217,0 +228,0 @@ } |
354
index.js
"use strict"; | ||
const createCodeFrame = require("babel-code-frame"); | ||
const fs = require("fs"); | ||
const mkdirp = require("mkdirp"); | ||
const optionator = require("optionator"); | ||
const path = require("path"); | ||
const generateRandomJS = require("./codegen"); | ||
const program = optionator({ | ||
prepend: [ | ||
"Usage: eslump [options]", | ||
" or: eslump TEST_MODULE OUTPUT_DIR [options]", | ||
"", | ||
"Options:" | ||
].join("\n"), | ||
append: [ | ||
"When no arguments are provided, random JavaScript is printed to stdout.", | ||
"Otherwise, TEST_MODULE is executed until an error occurs, or you kill the", | ||
"program. When an error occurs, the error is printed to stdout and files", | ||
"are written to OUTPUT_DIR:", | ||
"", | ||
" - random.js contains the random JavaScript that caused the error.", | ||
" - random.backup.js is a backup of random.js.", | ||
" - reproductionData.json contains additional data defined by TEST_MODULE", | ||
" needed to reproduce the error caused by random.js, if any.", | ||
" - Other files, if any, are defined by TEST_MODULE.", | ||
"", | ||
"OUTPUT_DIR is created as with `mkdir -p` if non-existent.", | ||
"", | ||
"The value of TEST_MODULE is passed directly to the `require` function.", | ||
"", | ||
"For information on how to write a TEST_MODULE, see:", | ||
"https://github.com/lydell/eslump#test-files", | ||
"", | ||
"Examples:", | ||
"", | ||
' # See how "prettier" pretty-prints random JavaScript.', | ||
" $ eslump | prettier", | ||
"", | ||
" # Run ./test.js and save the results in output/.", | ||
" $ eslump ./test.js output/", | ||
"", | ||
" # Narrow down the needed JavaScript to produce the error.", | ||
" # output/random.backup.js is handy if you go too far.", | ||
" $ vim output/random.js", | ||
"", | ||
" # Reproduce the narrowed down case.", | ||
" $ eslump ./test.js output/ --reproduce" | ||
].join("\n"), | ||
options: [ | ||
{ | ||
option: "max-depth", | ||
type: "Number", | ||
default: "7", | ||
description: "The maximum depth of the random JavaScript." | ||
}, | ||
{ | ||
option: "source-type", | ||
type: "String", | ||
enum: ["module", "script"], | ||
default: "module", | ||
description: "Parsing mode." | ||
}, | ||
{ | ||
option: "whitespace", | ||
type: "Boolean", | ||
description: "Randomize the whitespace in the random JavaScript." | ||
}, | ||
{ | ||
option: "comments", | ||
type: "Boolean", | ||
description: "Insert random comments into the random JavaScript." | ||
}, | ||
{ | ||
option: "reproduce", | ||
alias: "r", | ||
type: "Boolean", | ||
description: "Reproduce a previous error using files in OUTPUT_DIR." | ||
}, | ||
{ | ||
option: "help", | ||
alias: "h", | ||
type: "Boolean", | ||
description: "Show help", | ||
overrideRequired: true | ||
}, | ||
{ | ||
option: "version", | ||
alias: "v", | ||
type: "Boolean", | ||
description: "Show version", | ||
overrideRequired: true | ||
} | ||
] | ||
}); | ||
const FILES = { | ||
random: "random.js", | ||
randomBackup: "random.backup.js", | ||
reproductionData: "reproductionData.json" | ||
}; | ||
function run(input) { | ||
let options; | ||
try { | ||
options = program.parse(input, { slice: 0 }); | ||
} catch (error) { | ||
return { stderr: error.message, code: 1 }; | ||
} | ||
if (options.help) { | ||
return { stdout: program.generateHelp(), code: 0 }; | ||
} | ||
if (options.version) { | ||
return { stdout: require("./package.json").version, code: 0 }; | ||
} | ||
const numPositional = options._.length; | ||
if (!(numPositional === 0 || numPositional === 2)) { | ||
return { | ||
stderr: `Expected 0 or 2 arguments, but ${numPositional} given.`, | ||
code: 1 | ||
}; | ||
} | ||
if (numPositional === 0 && options.reproduce !== undefined) { | ||
return { | ||
stderr: `The --reproduce flag cannot be used without arguments.`, | ||
code: 1 | ||
}; | ||
} | ||
if (numPositional === 0) { | ||
return { stdout: generateRandomJS(options), code: 0 }; | ||
} | ||
const [testModule, outputDir] = options._; | ||
let testFunction; | ||
try { | ||
testFunction = require(testModule); | ||
} catch (error) { | ||
const message = error && | ||
error.code === "MODULE_NOT_FOUND" && | ||
error.message.includes(testModule) | ||
? error.message | ||
: `Error when loading module '${testModule}':\n${printError(error)}`; | ||
return { stderr: message, code: 1 }; | ||
} | ||
if (typeof testFunction !== "function") { | ||
return { | ||
stderr: `Expected \`require(${JSON.stringify(testModule)})\` to return a function, but got: ${testFunction}`, | ||
code: 1 | ||
}; | ||
} | ||
let reproductionCode; | ||
let reproductionData; | ||
if (options.reproduce) { | ||
const codePath = path.join(outputDir, FILES.random); | ||
const dataPath = path.join(outputDir, FILES.reproductionData); | ||
try { | ||
reproductionCode = fs.readFileSync(codePath, "utf8"); | ||
} catch (error) { | ||
return { | ||
stderr: `Failed to read '${codePath}' for reproduction:\n${error.message}`, | ||
code: 1 | ||
}; | ||
} | ||
let reproductionDataString; | ||
try { | ||
reproductionDataString = fs.readFileSync(dataPath, "utf8"); | ||
} catch (error) { | ||
if (error.code !== "ENOENT") { | ||
return { | ||
stderr: `Failed to read '${dataPath}' for reproduction:\n${error.message}`, | ||
code: 1 | ||
}; | ||
} | ||
} | ||
if (typeof reproductionDataString === "string") { | ||
try { | ||
reproductionData = JSON.parse(reproductionDataString); | ||
} catch (error) { | ||
return { | ||
stderr: `Failed to parse JSON in '${dataPath}':\n${error.message}`, | ||
code: 1 | ||
}; | ||
} | ||
} | ||
} | ||
function* loop() { | ||
let attemptNum = 1; | ||
// eslint-disable-next-line no-constant-condition | ||
while (true) { | ||
const testData = { | ||
code: options.reproduce ? reproductionCode : generateRandomJS(options), | ||
sourceType: options.sourceType, | ||
reproductionData | ||
}; | ||
let result; | ||
try { | ||
result = testFunction(testData); | ||
} catch (error) { | ||
const mainMessage = [ | ||
`Attempt ${attemptNum}: The test function threw an unexpected error:`, | ||
printError(error, testData.code) | ||
].join("\n"); | ||
const extraMessage = writeFiles(outputDir, { | ||
code: testData.code, | ||
reproduce: options.reproduce | ||
}); | ||
yield { | ||
message: extraMessage | ||
? `${mainMessage}\n\n${extraMessage}` | ||
: mainMessage, | ||
code: 1 | ||
}; | ||
break; | ||
} | ||
if (result) { | ||
const mainMessage = [ | ||
`Attempt ${attemptNum}: The test function returned an error:`, | ||
printError(result.error, testData.code) | ||
].join("\n"); | ||
const extraMessage = writeFiles(outputDir, { | ||
code: testData.code, | ||
result, | ||
reproduce: options.reproduce | ||
}); | ||
yield { | ||
message: extraMessage | ||
? `${mainMessage}\n\n${extraMessage}` | ||
: mainMessage, | ||
code: 1 | ||
}; | ||
break; | ||
} else if (options.reproduce) { | ||
yield { | ||
message: "Failed to reproduce the error; the test function succeeded.", | ||
code: 1 | ||
}; | ||
break; | ||
} else { | ||
yield { message: `Attempt ${attemptNum}: Success` }; | ||
} | ||
attemptNum += 1; | ||
} | ||
} | ||
return { loop }; | ||
} | ||
function writeFiles( | ||
outputDir, | ||
{ code = null, result = {}, reproduce = false } = {} | ||
) { | ||
try { | ||
mkdirp.sync(outputDir); | ||
} catch (error) { | ||
return `Failed to \`mkdir -p\` '${outputDir}':\n${error.message}`; | ||
} | ||
const message = []; | ||
function tryWrite(name, content) { | ||
const fullPath = path.join(outputDir, name); | ||
try { | ||
fs.writeFileSync(fullPath, content); | ||
} catch (error) { | ||
message.push(`Failed to write write '${fullPath}':\n${error.message}`); | ||
} | ||
} | ||
let reproductionDataString; | ||
if ("reproductionData" in result && !reproduce) { | ||
try { | ||
reproductionDataString = JSON.stringify(result.reproductionData, null, 2); | ||
} catch (error) { | ||
message.push( | ||
`Failed to run \`JSON.stringify\` on the returned reproductionData:`, | ||
"reproductionData:", | ||
indent(String(result.reproductionData)), | ||
"Error:", | ||
indent(error.message) | ||
); | ||
} | ||
} | ||
if (code !== null && !reproduce) { | ||
tryWrite(FILES.random, code); | ||
tryWrite(FILES.randomBackup, code); | ||
} | ||
if (reproductionDataString && !reproduce) { | ||
tryWrite(FILES.reproductionData, reproductionDataString); | ||
} | ||
if (result.artifacts) { | ||
for (const [name, content] of Object.entries(result.artifacts)) { | ||
tryWrite(name, String(content)); | ||
} | ||
} | ||
return message.length === 0 ? null : message.join("\n\n"); | ||
} | ||
function printError(error, code = "") { | ||
if (code && error) { | ||
const { line, column } = getLocation(error); | ||
if (typeof line === "number") { | ||
const codeFrame = createCodeFrame(code, line, column, { | ||
highlightCode: true | ||
}); | ||
return `${error.stack}\n${codeFrame}`; | ||
} | ||
} | ||
return error && error.stack ? error.stack : String(error); | ||
} | ||
function getLocation(error) { | ||
// Acorn and Babylon has `.loc.line` (1-indexed) and `.loc.column` | ||
// (0-indexed). The Flow example is adjusted to this format. | ||
// Espree and Esprima has `.lineNumber` (1-indexed) and `.column` (1-indexed). | ||
// Shift-parser has `.line` (1-indexed) and `.column` (1-indexed). | ||
const line = error.loc && typeof error.loc.line === "number" | ||
? error.loc.line | ||
: typeof error.lineNumber === "number" | ||
? error.lineNumber | ||
: typeof error.line === "number" ? error.line : undefined; | ||
const column = error.loc && typeof error.loc.column === "number" | ||
? error.loc.column + 1 | ||
: typeof error.column === "number" ? error.column : undefined; | ||
return { line, column }; | ||
} | ||
function indent(string) { | ||
return string.replace(/^/mg, " "); | ||
} | ||
module.exports = { run, generateRandomJS }; | ||
module.exports = { generateRandomJS }; |
{ | ||
"name": "eslump", | ||
"version": "1.5.1", | ||
"version": "1.6.0", | ||
"license": "MIT", | ||
@@ -18,8 +18,8 @@ "author": "Simon Lydell", | ||
], | ||
"preferGlobal": true, | ||
"bin": { | ||
"eslump": "cli.js" | ||
"eslump": "cli-runner.js" | ||
}, | ||
"files": [ | ||
"cli.js", | ||
"cli-program.js", | ||
"cli-runner.js", | ||
"codegen.js", | ||
@@ -40,20 +40,21 @@ "index.js", | ||
"random-item": "^1.0.0", | ||
"shift-codegen": "^5.0.2", | ||
"shift-fuzzer": "^1.0.1", | ||
"shift-codegen": "^5.0.4", | ||
"shift-fuzzer": "^1.0.2", | ||
"shift-reducer": "^4.0.0" | ||
}, | ||
"devDependencies": { | ||
"acorn": "^4.0.11", | ||
"babel-generator": "^6.24.0", | ||
"babylon": "^6.16.1", | ||
"acorn": "^5.0.3", | ||
"babel-generator": "^6.25.0", | ||
"babylon": "^6.17.4", | ||
"escodegen": "^1.8.1", | ||
"eslint": "^3.18.0", | ||
"eslint-plugin-prettier": "^2.0.1", | ||
"espree": "^3.4.0", | ||
"esprima": "^3.1.3", | ||
"flow-parser": "^0.42.0", | ||
"prettier": "^0.22.0", | ||
"shift-parser": "^5.0.4", | ||
"unexpected": "^10.26.3" | ||
"eslint": "^4.1.0", | ||
"eslint-plugin-node": "^5.0.0", | ||
"eslint-plugin-prettier": "^2.1.2", | ||
"espree": "^3.4.3", | ||
"esprima": "^4.0.0", | ||
"flow-parser": "^0.47.0", | ||
"prettier": "^1.4.4", | ||
"shift-parser": "^5.0.7", | ||
"unexpected": "^10.29.0" | ||
} | ||
} |
@@ -0,1 +1,3 @@ | ||
"use strict"; | ||
const randomInt = require("random-int"); | ||
@@ -44,3 +46,3 @@ const randomItem = require("random-item"); | ||
function randomArray(length, randomItem) { | ||
return [...Array(length)].map(() => randomItem()); | ||
return Array.from(Array(length)).map(() => randomItem()); | ||
} | ||
@@ -67,5 +69,5 @@ | ||
function randomMultiLineComment({ allowNewlines = false } = {}) { | ||
function randomMultiLineComment(options) { | ||
const chars = whitespace.concat( | ||
allowNewlines ? lineTerminators : [], | ||
options && options.allowNewlines ? lineTerminators : [], | ||
letters | ||
@@ -86,7 +88,5 @@ ); | ||
function insertComments( | ||
whitespaceString, | ||
{ times = 1, allowNewlines = false } = {} | ||
) { | ||
const choices = allowNewlines | ||
function insertComments(whitespaceString, options) { | ||
const times = options && options.times === undefined ? 1 : options.times; | ||
const choices = options && options.allowNewlines | ||
? insertCommentsChoicesWithNewlines | ||
@@ -93,0 +93,0 @@ : insertCommentsChoices; |
# eslump | ||
CLI tool for fuzz testing JavaScript parsers and suchlike programs. | ||
Fuzz testing JavaScript parsers and suchlike programs. | ||
@@ -13,6 +13,14 @@ > **es :** short for ECMAScript (the JavaScript standard) | ||
eslump is primarily intended to be used as a CLI tool. | ||
`npm install --global eslump` | ||
You can also use parts of it as a Node.js module. | ||
`npm install eslump` | ||
## Usage | ||
### CLI | ||
``` | ||
@@ -66,2 +74,60 @@ Usage: eslump [options] | ||
### Module | ||
### Overview | ||
```js | ||
const {generateRandomJS} = require("eslump"); | ||
const randomJSString = generateRandomJS({ | ||
sourceType: "module", | ||
maxDepth: 7, | ||
comments: false, | ||
whitespace: false, | ||
}); | ||
``` | ||
#### generateRandomJS(options) | ||
Returns a string of random JavaScript code. | ||
If you want, you can pass some options: | ||
Option | Type | Default | Description | ||
-------|------|---------|------------ | ||
sourceType | `"module"` or `"script"` | `"module"` | The type of code to generate. | ||
maxDepth | integer | 7 | How deeply nested AST:s to generate. | ||
comments | boolean | false | Whether or not to generate random comments. | ||
whitespace | boolean | false | Whether or not to generate random whitespace. | ||
## Disclaimer | ||
eslump was created from the need of finding edge cases in [Prettier]. It started | ||
out as a bare-bones little script in a branch on my fork of that repo. As I | ||
wanted more and more features, I extracted it and fleshed it out in its own | ||
repo. Then I realized that it might be useful to others, so I put it on GitHub | ||
and made the CLI installable from npm. | ||
Initially, eslump basically just strung together [shift-fuzzer] and | ||
[shift-codegen]. Then, I realized that no random comments were generated, so I | ||
hacked that in (along with random whitespace) since comments are very difficult | ||
to get right in Prettier. Then, random parentheses and semicolons where | ||
requested, so I hacked that in as well. | ||
eslump has successfully found lots of little edge cases in Prettier, so it | ||
evidently works. But there are no tests. (I’ve just gone meta and fuzz-tested it | ||
using itself basically.) | ||
From the beginning eslump was only ever intended to be a CLI tool, but other | ||
people have started to want to use eslump's code generation as an npm module, so | ||
these days it can also be used as a module. If you know what you're doing. | ||
Here are some features I'd like to see from a proper random JS library: | ||
- No hacks. | ||
- Seeded randomness, so things can be reproduced. | ||
- JSX and Flow support. | ||
- Ability to generate code without any early errors. | ||
- Possibly ways to prevent certain syntax constructs from being generated. | ||
## Examples | ||
@@ -82,3 +148,3 @@ | ||
- [escodegen] | ||
- [prettier] | ||
- [Prettier] | ||
- [shift-codegen] | ||
@@ -181,9 +247,2 @@ | ||
## Ideas for the future | ||
- Fuzzing JSX. | ||
- Fuzzing Flowtype annotations. | ||
- Automatically narrow down JavaScript that causes an error, instead of having | ||
the user do it manually. Looking for a challenge? You’ve just found it. | ||
## License | ||
@@ -201,3 +260,3 @@ | ||
[flow]: https://github.com/facebook/flow | ||
[prettier]: https://github.com/prettier/prettier/ | ||
[Prettier]: https://github.com/prettier/prettier/ | ||
[shift-codegen]: https://github.com/shapesecurity/shift-codegen-js | ||
@@ -204,0 +263,0 @@ [shift-fuzzer]: https://github.com/shapesecurity/shift-fuzzer-js |
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
32994
9
640
261
13
1
Updatedshift-codegen@^5.0.4
Updatedshift-fuzzer@^1.0.2