From 1cbf8ae461ca2e9e4ce8d5a33d13fd2a2d049ae3 Mon Sep 17 00:00:00 2001 From: Damien Cassou Date: Thu, 1 Aug 2024 07:51:01 +0200 Subject: [PATCH] Add support for --fix-to-stdout This option was lost in the recent rewrite. --- README.md | 5 +- lib/forwarder.js | 47 ++++++- lib/forwarder.test.js | 14 ++ lib/help.js | 1 + test/test.integration.js | 269 ++++++++++++++++++++++++++------------- 5 files changed, 241 insertions(+), 95 deletions(-) diff --git a/README.md b/README.md index 8b186a6..b600182 100644 --- a/README.md +++ b/README.md @@ -108,6 +108,7 @@ All arguments are passed to eslint, except for the following commands: status Show daemon status, process id and resolved eslint version --help, -h Show this help --version, -v Show version number of eslint_d and bundled eslint + --fix-to-stdout Print fixed file to stdout (requires --stdin) ``` ## Environment variables @@ -152,7 +153,7 @@ changed: `package.json`, `package-lock.json`, `npm-shrinkwrap.json`, ## Compatibility -- `14.0.0`: eslint 4 - 9, node 18 - 22 (ships with eslint 9) +- `14.0.0`: eslint 4 - 9, node 18 - 22 (ships with eslint 9) (see [^1]) - `13.0.0`: eslint 4 - 8, node 12 - 20 (ships with eslint 8) - `12.0.0`: eslint 4 - 8, node 12 - 16 (ships with eslint 8) - `11.0.0`: eslint 4 - 8, node 12 - 16 (ships with eslint 7) @@ -177,3 +178,5 @@ MIT [syntastic]: https://github.com/scrooloose/syntastic [flycheck]: http://www.flycheck.org/ [SublimeLinter-eslint]: https://github.com/SublimeLinter/SublimeLinter-eslint + +[^1]: The support for `--fix-to-stdout` is only provided with eslint 5 and beyond. diff --git a/lib/forwarder.js b/lib/forwarder.js index e5a1a8d..c9ae1a7 100644 --- a/lib/forwarder.js +++ b/lib/forwarder.js @@ -15,15 +15,35 @@ const EXIT_TOKEN_LENGTH = 7; * @param {Config} config */ export async function forwardToDaemon(resolver, config) { + const eslint_args = process.argv.slice(); const text = process.argv.includes('--stdin') ? await readStdin() : null; const { stdout } = supportsColor; + const fix_to_stdout_index = eslint_args.indexOf('--fix-to-stdout'); + const fix_to_stdout = fix_to_stdout_index !== -1; + + if (fix_to_stdout) { + if (!eslint_args.includes('--stdin')) { + console.error('--fix-to-stdout requires passing --stdin as well'); + // eslint-disable-next-line require-atomic-updates + process.exitCode = 1; + return; + } + eslint_args.splice( + fix_to_stdout_index, + 1, + '--fix-dry-run', + '--format', + 'json' + ); + } + const socket = net.connect(config.port, '127.0.0.1'); const args = [ config.token, stdout ? stdout.level : 0, process.cwd(), - process.argv + eslint_args ]; socket.write(JSON.stringify(args)); if (text) { @@ -39,16 +59,17 @@ export async function forwardToDaemon(resolver, config) { let chunk = ''; while ((chunk = socket.read()) !== null) { content += chunk; - if (content.length > EXIT_TOKEN_LENGTH) { - const message_length = content.length - EXIT_TOKEN_LENGTH; - // Write everything we are sure doesn't contain the termination code: - process.stdout.write(content.substring(0, message_length)); - // Keep only what we haven't written yet: - content = content.substring(message_length); + if (!fix_to_stdout && content.length > EXIT_TOKEN_LENGTH) { + process.stdout.write(flushMessage()); } } }) .on('end', () => { + if (fix_to_stdout) { + const { output } = JSON.parse(flushMessage())[0]; + process.stdout.write(output || text); + } + // The remaining 'content' must be the termination code: const match = content.match(EXIT_TOKEN_REGEXP); if (match) { @@ -69,6 +90,18 @@ export async function forwardToDaemon(resolver, config) { } process.exitCode = 1; }); + + /** + * @returns {string} + */ + function flushMessage() { + const message_length = content.length - EXIT_TOKEN_LENGTH; + // Extract everything we are sure doesn't contain the termination code: + const message = content.substring(0, message_length); + // Keep only what we haven't written yet: + content = content.substring(message_length); + return message; + } } function readStdin() { diff --git a/lib/forwarder.test.js b/lib/forwarder.test.js index c97c998..1eb4135 100644 --- a/lib/forwarder.test.js +++ b/lib/forwarder.test.js @@ -222,5 +222,19 @@ describe('lib/forwarder', () => { assert.equals(process.exitCode, 1); assert.calledOnceWith(fs.unlink, `${resolver.base}/.eslint_d`); }); + + context('--fix-to-stdout', () => { + it('throws if --stdin is absent', async () => { + argv.push('--fix-to-stdout'); + + await forwardToDaemon(resolver, config); + + assert.equals(process.exitCode, 1); + assert.calledOnceWith( + console.error, + '--fix-to-stdout requires passing --stdin as well' + ); + }); + }); }); }); diff --git a/lib/help.js b/lib/help.js index 9ae73f2..2a5a2f2 100644 --- a/lib/help.js +++ b/lib/help.js @@ -7,6 +7,7 @@ All arguments are passed to eslint, except for the following commands: stop Stop the daemon restart Restart the daemon status Show daemon status, process id and resolved eslint version + --fix-to-stdout Print fixed file to stdout (requires --stdin) --help, -h Show this help --version, -v Show version number of eslint_d and bundled eslint diff --git a/test/test.integration.js b/test/test.integration.js index a1a91c6..34bb33d 100644 --- a/test/test.integration.js +++ b/test/test.integration.js @@ -4,6 +4,15 @@ import fs from 'node:fs/promises'; import { createRequire } from 'node:module'; import { assert, refute } from '@sinonjs/referee-sinon'; +const SUPPORTED_ESLINT_VERSIONS = [ + 'v4.0.x', + 'v5.0.x', + 'v6.0.x', + 'v7.0.x', + 'v8.0.x', + 'v9.0.x' +]; + describe('integration tests', () => { const eslint_d = path.resolve('bin/eslint_d.js'); const require = createRequire(import.meta.url); @@ -34,6 +43,20 @@ describe('integration tests', () => { }); } + /** + * @param {string} config + * @returns {() => Promise} + */ + function unlinkHook(config) { + return async () => { + try { + await fs.unlink(config); + } catch { + // ignore + } + }; + } + context('--help', () => { it('prints help for --help', async () => { const { error, stdout, stderr } = await run('--help'); @@ -58,115 +81,187 @@ describe('integration tests', () => { }); }); - ['v4.0.x', 'v5.0.x', 'v6.0.x', 'v7.0.x', 'v8.0.x', 'v9.0.x'].forEach( - (fixture) => { - context(fixture, () => { - const cwd = path.resolve(`test/fixture/${fixture}`); - const { version: eslint_version } = require( - require.resolve('eslint/package.json', { paths: [cwd] }) + SUPPORTED_ESLINT_VERSIONS.forEach((fixture) => { + context(fixture, () => { + const cwd = path.resolve(`test/fixture/${fixture}`); + const { version: eslint_version } = require( + require.resolve('eslint/package.json', { paths: [cwd] }) + ); + const config = `${cwd}/node_modules/eslint/.eslint_d`; + let pid; + + after(unlinkHook(config)); + + it('--version', async () => { + const { error, stdout, stderr } = await run('--version', { cwd }); + + assert.equals( + stdout, + `eslint_d: v${version}, bundled eslint: v${bundled_version}\n` ); - const config = `${cwd}/node_modules/eslint/.eslint_d`; - let pid; - - after(async () => { - try { - await fs.unlink(config); - } catch { - // ignore - } - }); + assert.equals(stderr, ''); + assert.isNull(error); + }); - it('--version', async () => { - const { error, stdout, stderr } = await run('--version', { cwd }); + it('status not running', async () => { + await assert.rejects(fs.stat(config)); - assert.equals( - stdout, - `eslint_d: v${version}, bundled eslint: v${bundled_version}\n` - ); - assert.equals(stderr, ''); - assert.isNull(error); - }); + const { error, stdout, stderr } = await run('status', { cwd }); - it('status not running', async () => { - await assert.rejects(fs.stat(config)); + assert.equals( + stdout, + `eslint_d: Not running - local eslint v${eslint_version}\n` + ); + assert.equals(stderr, ''); + assert.isNull(error); + }); - const { error, stdout, stderr } = await run('status', { cwd }); + it('start', async () => { + const { error, stdout, stderr } = await run('start', { cwd }); - assert.equals( - stdout, - `eslint_d: Not running - local eslint v${eslint_version}\n` - ); - assert.equals(stderr, ''); - assert.isNull(error); - }); + assert.equals(stdout, ''); + assert.equals(stderr, ''); + assert.isNull(error); + await assert.resolves(fs.stat(config)); + }); - it('start', async () => { - const { error, stdout, stderr } = await run('start', { cwd }); + it('status running', async () => { + const raw = await fs.readFile(config, 'utf8'); + [, , pid] = raw.split(' '); - assert.equals(stdout, ''); - assert.equals(stderr, ''); - assert.isNull(error); - await assert.resolves(fs.stat(config)); - }); + const { error, stdout, stderr } = await run('status', { cwd }); + + assert.equals( + stdout, + `eslint_d: Running (${pid}) - local eslint v${eslint_version}\n` + ); + assert.equals(stderr, ''); + assert.isNull(error); + refute.exception(() => process.kill(Number(pid), 0)); + }); - it('status running', async () => { - const raw = await fs.readFile(config, 'utf8'); - [, , pid] = raw.split(' '); + it('pass.js', async () => { + const { error, stdout, stderr } = await run('../pass.js', { cwd }); - const { error, stdout, stderr } = await run('status', { cwd }); + assert.equals(stdout, ''); + assert.equals(stderr, ''); + assert.isNull(error); + }); - assert.equals( - stdout, - `eslint_d: Running (${pid}) - local eslint v${eslint_version}\n` - ); - assert.equals(stderr, ''); - assert.isNull(error); - refute.exception(() => process.kill(Number(pid), 0)); - }); + it('fail.js', async () => { + const { error, stdout, stderr } = await run('../fail.js', { cwd }); - it('pass.js', async () => { - const { error, stdout, stderr } = await run('../pass.js', { cwd }); + assert.match(stdout, '/test/fixture/fail.js'); + assert.match(stdout, 'Strings must use singlequote'); + refute.isNull(error); + assert.equals(error?.['code'], 1); + assert.equals(stderr, ''); + }); - assert.equals(stdout, ''); - assert.equals(stderr, ''); - assert.isNull(error); + it('--stdin', async () => { + const { error, stdout, stderr } = await run('--stdin', { + cwd, + stdin: `/* eslint quotes: ["error", "single"] */ + console.log("hello");` }); - it('fail.js', async () => { - const { error, stdout, stderr } = await run('../fail.js', { cwd }); + assert.match(stdout, ''); + assert.match(stdout, 'Strings must use singlequote'); + assert.equals(stderr, ''); + refute.isNull(error); + assert.equals(error?.['code'], 1); + }); - assert.match(stdout, '/test/fixture/fail.js'); - assert.match(stdout, 'Strings must use singlequote'); - refute.isNull(error); - assert.equals(error?.['code'], 1); - assert.equals(stderr, ''); - }); + it('stop', async () => { + const { error, stdout, stderr } = await run('stop', { cwd }); - it('--stdin', async () => { - const { error, stdout, stderr } = await run('--stdin', { - cwd, - stdin: `/* eslint quotes: ["error", "single"] */ - console.log("hello");` + assert.equals(stdout, ''); + assert.equals(stderr, ''); + assert.isNull(error, stdout); + await assert.rejects(fs.stat(config)); + await new Promise((resolve) => setTimeout(resolve, 50)); + assert.exception(() => process.kill(Number(pid), 0)); + }); + }); + }); + + context('--fix-to-stdout', () => { + SUPPORTED_ESLINT_VERSIONS.filter( + (fixture) => fixture !== 'v4.0.x' // v4 misses --fix-dry-run + ).forEach((fixture) => { + context(fixture, () => { + const cwd = path.resolve(`test/fixture/${fixture}`); + const config = `${cwd}/node_modules/eslint/.eslint_d`; + + after(unlinkHook(config)); + + const run_args = `--fix-to-stdout --stdin --stdin-filename ${cwd}/../foo.js`; + + context('when file only contains fixable problems', () => { + it('prints input if no change is needed', async () => { + const stdin = `console.log('Hello eslint');`; + const { error, stdout, stderr } = await run(run_args, { + cwd, + stdin + }); + + assert.equals(stderr, ''); + assert.equals(stdout, stdin); + assert.isNull(error); }); - assert.match(stdout, ''); - assert.match(stdout, 'Strings must use singlequote'); - assert.equals(stderr, ''); - refute.isNull(error); - assert.equals(error?.['code'], 1); + it('prints fixed output if change is needed', async () => { + const { error, stdout, stderr } = await run(run_args, { + cwd, + stdin: `console.log("Hello eslint");` + }); + + assert.equals(stderr, ''); + assert.equals(stdout, `console.log('Hello eslint');`); + assert.isNull(error); + }); }); - it('stop', async () => { - const { error, stdout, stderr } = await run('stop', { cwd }); + context('when file contains non-fixable problems', () => { + it('prints input if no change is needed', async () => { + const stdin = `/* eslint radix: "error" */ + console.log('Hello' + parseInt('087'))`; + + const { error, stdout, stderr } = await run(run_args, { + cwd, + stdin + }); - assert.equals(stdout, ''); - assert.equals(stderr, ''); - assert.isNull(error, stdout); - await assert.rejects(fs.stat(config)); - await new Promise((resolve) => setTimeout(resolve, 50)); - assert.exception(() => process.kill(Number(pid), 0)); + assert.equals(stderr, ''); + assert.equals( + stdout, + `/* eslint radix: "error" */ + console.log('Hello' + parseInt('087'))` + ); + refute.isNull(error); + assert.equals(error?.['code'], 1); + }); + + it('prints fixed output if change is needed', async () => { + const stdin = `/* eslint radix: "error" */ + console.log("Hello" + parseInt('087'))`; + + const { error, stdout, stderr } = await run(run_args, { + cwd, + stdin + }); + + assert.equals(stderr, ''); + assert.equals( + stdout, + `/* eslint radix: "error" */ + console.log('Hello' + parseInt('087'))` + ); + refute.isNull(error); + assert.equals(error?.['code'], 1); + }); }); }); - } - ); + }); + }); });