diff --git a/docs/user-guide/command-line-interface.md b/docs/user-guide/command-line-interface.md index 18471375d46..3a7e17650c1 100644 --- a/docs/user-guide/command-line-interface.md +++ b/docs/user-guide/command-line-interface.md @@ -69,6 +69,7 @@ Output: Miscellaneous: --init Run config initialization wizard - default: false --fix Automatically fix problems + --fix-dry-run Automatically fix problems without saving the changes to the file system --debug Output debugging information -h, --help Show help -v, --version Output the version number @@ -362,6 +363,20 @@ This option instructs ESLint to try to fix as many issues as possible. The fixes 1. This option throws an error when code is piped to ESLint. 1. This option has no effect on code that uses a processor, unless the processor opts into allowing autofixes. +If you want to fix code from `stdin` or otherwise want to get the fixes without actually writing them to the file, use the [`--fix-dry-run`](#--fix-dry-run) option. + +#### `--fix-dry-run` + +This option has the same effect as `--fix` with one difference: the fixes are not saved to the file system. This makes it possible to fix code from `stdin` (when used with the `--stdin` flag). + +Because the default formatter does not output the fixed code, you'll have to use another one (e.g. `json`) to get the fixes. Here's an example of this pattern: + +``` +getSomeText | eslint --stdin --fix-dry-run --format=json +``` + +This flag can be useful for integrations (e.g. editor plugins) which need to autofix text from the command line without saving it to the filesystem. + #### `--debug` This option outputs debugging information to the console. This information is useful when you're seeing a problem and having a hard time pinpointing it. The ESLint team may ask for this debugging information to help solve bugs. diff --git a/lib/cli.js b/lib/cli.js index 0c8a7d62fb2..962a4be0eff 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -63,7 +63,7 @@ function translateOptions(cliOptions) { cache: cliOptions.cache, cacheFile: cliOptions.cacheFile, cacheLocation: cliOptions.cacheLocation, - fix: cliOptions.fix && (cliOptions.quiet ? quietFixPredicate : true), + fix: (cliOptions.fix || cliOptions.fixDryRun) && (cliOptions.quiet ? quietFixPredicate : true), allowInlineConfig: cliOptions.inlineConfig, reportUnusedDisableDirectives: cliOptions.reportUnusedDisableDirectives }; @@ -171,9 +171,13 @@ const cli = { debug(`Running on ${text ? "text" : "files"}`); - // disable --fix for piped-in code until we know how to do it correctly + if (currentOptions.fix && currentOptions.fixDryRun) { + log.error("The --fix option and the --fix-dry-run option cannot be used together."); + return 1; + } + if (text && currentOptions.fix) { - log.error("The --fix option is not available for piped-in code."); + log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead."); return 1; } diff --git a/lib/options.js b/lib/options.js index 3bc45b3ac75..ee1d3369cec 100644 --- a/lib/options.js +++ b/lib/options.js @@ -190,6 +190,12 @@ module.exports = optionator({ default: false, description: "Automatically fix problems" }, + { + option: "fix-dry-run", + type: "Boolean", + default: false, + description: "Automatically fix problems without saving the changes to the file system" + }, { option: "debug", type: "Boolean", diff --git a/tests/bin/eslint.js b/tests/bin/eslint.js index 39be27fb032..04ae01f3d84 100644 --- a/tests/bin/eslint.js +++ b/tests/bin/eslint.js @@ -71,6 +71,41 @@ describe("bin/eslint.js", () => { return assertExitCode(child, 0); }); + it("has exit code 0 if no linting errors are reported", () => { + const child = runESLint([ + "--stdin", + "--no-eslintrc", + "--rule", + "{'no-extra-semi': 2}", + "--fix-dry-run", + "--format", + "json" + ]); + + const expectedOutput = JSON.stringify([ + { + filePath: "", + messages: [], + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + output: "var foo = bar;\n" + } + ]); + + const exitCodePromise = assertExitCode(child, 0); + const stdoutPromise = getOutput(child).then(output => { + assert.strictEqual(output.stdout.trim(), expectedOutput); + assert.strictEqual(output.stderr, ""); + }); + + child.stdin.write("var foo = bar;;\n"); + child.stdin.end(); + + return Promise.all([exitCodePromise, stdoutPromise]); + }); + it("has exit code 1 if a syntax error is thrown", () => { const child = runESLint(["--stdin", "--no-eslintrc"]); diff --git a/tests/lib/cli.js b/tests/lib/cli.js index c994bac0715..618a7c0fc64 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -755,7 +755,7 @@ describe("cli", () => { results: [] }); sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - fakeCLIEngine.outputFixes = sandbox.stub(); + fakeCLIEngine.outputFixes = sandbox.mock().once(); localCLI = proxyquire("../../lib/cli", { "./cli-engine": fakeCLIEngine, @@ -858,6 +858,163 @@ describe("cli", () => { }); + describe("when passed --fix-dry-run", () => { + const sandbox = sinon.sandbox.create(); + let localCLI; + + afterEach(() => { + sandbox.verifyAndRestore(); + }); + + it("should pass fix:true to CLIEngine when executing on files", () => { + + // create a fake CLIEngine to test with + const fakeCLIEngine = sandbox.mock().withExactArgs(sinon.match({ fix: true })); + + fakeCLIEngine.prototype = leche.fake(CLIEngine.prototype); + sandbox.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({ + errorCount: 0, + warningCount: 0, + results: [] + }); + sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); + fakeCLIEngine.outputFixes = sandbox.mock().never(); + + localCLI = proxyquire("../../lib/cli", { + "./cli-engine": fakeCLIEngine, + "./logging": log + }); + + const exitCode = localCLI.execute("--fix-dry-run ."); + + assert.equal(exitCode, 0); + + }); + + it("should not rewrite files when in fix-dry-run mode", () => { + + const report = { + errorCount: 1, + warningCount: 0, + results: [{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 2, + message: "Fake message" + } + ] + }] + }; + + // create a fake CLIEngine to test with + const fakeCLIEngine = sandbox.mock().withExactArgs(sinon.match({ fix: true })); + + fakeCLIEngine.prototype = leche.fake(CLIEngine.prototype); + sandbox.stub(fakeCLIEngine.prototype, "executeOnFiles").returns(report); + sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); + fakeCLIEngine.outputFixes = sandbox.mock().never(); + + localCLI = proxyquire("../../lib/cli", { + "./cli-engine": fakeCLIEngine, + "./logging": log + }); + + const exitCode = localCLI.execute("--fix-dry-run ."); + + assert.equal(exitCode, 1); + + }); + + it("should provide fix predicate when in fix-dry-run mode and quiet mode", () => { + + const report = { + errorCount: 0, + warningCount: 1, + results: [{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 1, + message: "Fake message" + } + ] + }] + }; + + // create a fake CLIEngine to test with + const fakeCLIEngine = sandbox.mock().withExactArgs(sinon.match({ fix: sinon.match.func })); + + fakeCLIEngine.prototype = leche.fake(CLIEngine.prototype); + sandbox.stub(fakeCLIEngine.prototype, "executeOnFiles").returns(report); + sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); + fakeCLIEngine.getErrorResults = sandbox.stub().returns([]); + fakeCLIEngine.outputFixes = sandbox.mock().never(); + + localCLI = proxyquire("../../lib/cli", { + "./cli-engine": fakeCLIEngine, + "./logging": log + }); + + const exitCode = localCLI.execute("--fix-dry-run --quiet ."); + + assert.equal(exitCode, 0); + + }); + + it("should allow executing on text", () => { + + const report = { + errorCount: 1, + warningCount: 0, + results: [{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 2, + message: "Fake message" + } + ] + }] + }; + + // create a fake CLIEngine to test with + const fakeCLIEngine = sandbox.mock().withExactArgs(sinon.match({ fix: true })); + + fakeCLIEngine.prototype = leche.fake(CLIEngine.prototype); + sandbox.stub(fakeCLIEngine.prototype, "executeOnText").returns(report); + sandbox.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); + fakeCLIEngine.outputFixes = sandbox.mock().never(); + + localCLI = proxyquire("../../lib/cli", { + "./cli-engine": fakeCLIEngine, + "./logging": log + }); + + const exitCode = localCLI.execute("--fix-dry-run .", "foo = bar;"); + + assert.equal(exitCode, 1); + }); + + it("should not call CLIEngine and return 1 when used with --fix", () => { + + // create a fake CLIEngine to test with + const fakeCLIEngine = sandbox.mock().never(); + + localCLI = proxyquire("../../lib/cli", { + "./cli-engine": fakeCLIEngine, + "./logging": log + }); + + const exitCode = localCLI.execute("--fix --fix-dry-run .", "foo = bar;"); + + assert.equal(exitCode, 1); + }); + }); + describe("when passing --print-config", () => { it("should print out the configuration", () => { const filePath = getFixturePath("files");