Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Change exit code for CLI flag error #7134

Merged
merged 7 commits into from
Aug 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/grumpy-adults-crash.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"stylelint": major
---

Changed: exit code for CLI flag error
4 changes: 4 additions & 0 deletions docs/migration-guide/to-16.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ Node.js 14 has reached end-of-life. We've removed support for it so that we coul
- 16.13.0
- 18.0.0

## Changed CLI exit code for flag error

We've changed the exit code for CLI flag errors from `2` to `64` so that `2` is only used for lint problems.

## Changed CLI to print problems to stderr instead of stdout

If you use the CLI to fix a source string by using the [`--fix`](../user-guide/cli.md#--fix) and [`--stdin`](../user-guide/cli.md#--stdin) options, the CLI will print the fixed code to stdout and any problems to stderr.
Expand Down
7 changes: 4 additions & 3 deletions docs/user-guide/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,7 @@ stylelint test.css --print-config

The CLI can exit the process with the following exit codes:

- `1` - something unknown went wrong
- `2` - there was at least one rule problem or CLI flag error
- `78` - there was some problem with the configuration file
- `1` - fatal error
- `2` - lint problem
- `64` - invalid CLI usage
- `78` - invalid configuration file
45 changes: 30 additions & 15 deletions lib/__tests__/cli.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,11 @@ import { jest } from '@jest/globals';
import stripAnsi from 'strip-ansi';
import { stripIndent } from 'common-tags';

import {
EXIT_CODE_FATAL_ERROR,
EXIT_CODE_INVALID_USAGE,
EXIT_CODE_LINT_PROBLEM,
} from '../constants.mjs';
import readJSONFile from '../testUtils/readJSONFile.mjs';
import replaceBackslashes from '../testUtils/replaceBackslashes.mjs';

Expand Down Expand Up @@ -259,7 +264,7 @@ describe('CLI', () => {
fixturesPath('empty-block-with-disables.css'),
]);

expect(process.exitCode).toBe(2);
expect(process.exitCode).toBe(EXIT_CODE_LINT_PROBLEM);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(1);
Expand All @@ -275,7 +280,7 @@ describe('CLI', () => {
fixturesPath('empty-block-with-relevant-disable.css'),
]);

expect(process.exitCode).toBe(2);
expect(process.exitCode).toBe(EXIT_CODE_LINT_PROBLEM);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(1);
Expand All @@ -292,7 +297,7 @@ describe('CLI', () => {
fixturesPath('empty-block-with-relevant-disable.css'),
]);

expect(process.exitCode).toBe(2);
expect(process.exitCode).toBe(EXIT_CODE_LINT_PROBLEM);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(1);
Expand All @@ -306,7 +311,7 @@ describe('CLI', () => {

await cli(['--stdin', '--config', fixturesPath('config-no-empty-source.json')]);

expect(process.exitCode).toBe(2);
expect(process.exitCode).toBe(EXIT_CODE_LINT_PROBLEM);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(1);
Expand All @@ -320,7 +325,7 @@ describe('CLI', () => {

await cli(['--stdin']);

expect(process.exitCode).toBe(1);
expect(process.exitCode).toBe(EXIT_CODE_FATAL_ERROR);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(1);
Expand All @@ -334,7 +339,7 @@ describe('CLI', () => {

await cli(['--config', fixturesPath('config-no-empty-source.json')]);

expect(process.exitCode).toBe(2);
expect(process.exitCode).toBe(EXIT_CODE_LINT_PROBLEM);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(1);
Expand All @@ -348,7 +353,7 @@ describe('CLI', () => {
fixturesPath('empty-block-with-disables.css'),
]);

expect(process.exitCode).toBe(1);
expect(process.exitCode).toBe(EXIT_CODE_FATAL_ERROR);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(1);
Expand All @@ -366,7 +371,7 @@ describe('CLI', () => {
fixturesPath('empty-block.css'),
]);

expect(process.exitCode).toBe(2);
expect(process.exitCode).toBe(EXIT_CODE_LINT_PROBLEM);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(2);
Expand Down Expand Up @@ -402,7 +407,7 @@ describe('CLI', () => {
fixturesPath('quiet-deprecation-warnings/style.css'),
]);

expect(process.exitCode).toBe(2);
expect(process.exitCode).toBe(EXIT_CODE_LINT_PROBLEM);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -440,7 +445,7 @@ describe('CLI', () => {
it('output a message when wrong --globby-options provided', async () => {
await cli(['--globby-options=wrong']);

expect(process.exitCode).toBe(2);
expect(process.exitCode).toBe(EXIT_CODE_INVALID_USAGE);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(1);
Expand All @@ -457,7 +462,7 @@ describe('CLI', () => {
fixturesPath('globby-options'),
]);

expect(process.exitCode).toBe(2);
expect(process.exitCode).toBe(EXIT_CODE_LINT_PROBLEM);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(1);
Expand All @@ -472,7 +477,7 @@ describe('CLI', () => {
fixturesPath('invalid-hex.scss'),
]);

expect(process.exitCode).toBe(2);
expect(process.exitCode).toBe(EXIT_CODE_LINT_PROBLEM);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(1);
Expand All @@ -491,7 +496,7 @@ describe('CLI', () => {
fixturesPath('invalid-hex.scss'),
]);

expect(process.exitCode).toBe(2);
expect(process.exitCode).toBe(EXIT_CODE_LINT_PROBLEM);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -520,7 +525,7 @@ describe('CLI', () => {
fixturesPath('empty-block.css'),
]);

expect(process.exitCode).toBe(2);
expect(process.exitCode).toBe(EXIT_CODE_LINT_PROBLEM);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(1);
Expand Down Expand Up @@ -561,7 +566,7 @@ describe('CLI', () => {
fixturesPath('config-block-no-empty-and-color-hex-length-short.json'),
]);

expect(process.exitCode).toBe(2);
expect(process.exitCode).toBe(EXIT_CODE_LINT_PROBLEM);

expect(process.stdout.write).toHaveBeenCalledTimes(1);
expect(process.stdout.write).toHaveBeenCalledWith(stripIndent`
Expand All @@ -571,4 +576,14 @@ describe('CLI', () => {
expect(process.stderr.write).toHaveBeenCalledTimes(1);
expect(process.stderr.write).toHaveBeenCalledWith(expect.stringMatching(/block-no-empty/));
});

it('exits with an error message when an invalid flag is specified', async () => {
await cli(['--foo']);

expect(process.exitCode).toBe(EXIT_CODE_INVALID_USAGE);

expect(process.stdout.write).not.toHaveBeenCalled();
expect(process.stderr.write).toHaveBeenCalledTimes(1);
expect(process.stderr.write).toHaveBeenCalledWith(expect.stringMatching(/--foo/));
});
});
3 changes: 2 additions & 1 deletion lib/__tests__/extends.test.mjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { fileURLToPath } from 'node:url';

import { EXIT_CODE_INVALID_CONFIG } from '../constants.mjs';
import configExtendingWithObject from './fixtures/config-extending-with-object.mjs';
import readJSONFile from '../testUtils/readJSONFile.mjs';
import safeChdir from '../testUtils/safeChdir.mjs';
Expand Down Expand Up @@ -72,7 +73,7 @@ it('extending configuration and no configBasedir', () => {
code: 'a {}',
config: configExtendingOne,
}),
).rejects.toHaveProperty('code', 78);
).rejects.toHaveProperty('code', EXIT_CODE_INVALID_CONFIG);
});

it('extending a config that is overridden', async () => {
Expand Down
28 changes: 17 additions & 11 deletions lib/cli.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@
DEFAULT_CACHE_LOCATION,
DEFAULT_FORMATTER,
DEFAULT_IGNORE_FILENAME,
EXIT_CODE_ERROR,
EXIT_CODE_FATAL,
EXIT_CODE_FATAL_ERROR,
EXIT_CODE_INVALID_USAGE,
EXIT_CODE_LINT_PROBLEM,
EXIT_CODE_SUCCESS,
} from './constants.mjs';

Expand Down Expand Up @@ -161,22 +162,22 @@
--report-needless-disables, --rd

Also report errors for "stylelint-disable" comments that are not blocking
a lint warning. The process will exit with code ${EXIT_CODE_ERROR} if needless disables are found.
a lint warning. The process will exit with code ${EXIT_CODE_LINT_PROBLEM} if needless disables are found.

--report-invalid-scope-disables, --risd

Report "stylelint-disable" comments that used for rules that don't exist
within the configuration object. The process will exit with code ${EXIT_CODE_ERROR} if invalid
within the configuration object. The process will exit with code ${EXIT_CODE_LINT_PROBLEM} if invalid
scope disables are found.

--report-descriptionless-disables, --rdd

Report "stylelint-disable" comments without a description. The process will
exit with code ${EXIT_CODE_ERROR} if descriptionless disables are found.
exit with code ${EXIT_CODE_LINT_PROBLEM} if descriptionless disables are found.

--max-warnings, --mw <number>

The number of warnings above which the process will exit with code ${EXIT_CODE_ERROR}.
The number of warnings above which the process will exit with code ${EXIT_CODE_LINT_PROBLEM}.
Useful when setting "defaultSeverity" to "warning" and expecting the process
to fail on warnings (e.g. CI build).

Expand Down Expand Up @@ -318,7 +319,7 @@

if (invalidOptionsMessage) {
process.stderr.write(invalidOptionsMessage);
process.exitCode = EXIT_CODE_ERROR;
process.exitCode = EXIT_CODE_INVALID_USAGE;

return;
}
Expand Down Expand Up @@ -415,18 +416,18 @@
} catch (error) {
if (typeof error === 'string') {
process.stderr.write(`${error}${EOL}`);
process.exitCode = EXIT_CODE_ERROR;
process.exitCode = EXIT_CODE_INVALID_USAGE;

return;
}

throw error;
}

Check warning on line 425 in lib/cli.mjs

View check run for this annotation

Codecov / codecov/patch

lib/cli.mjs#L424-L425

Added lines #L424 - L425 were not covered by tests
}

if (isString(stdinFilename)) {
options.codeFilename = stdinFilename;
}

Check warning on line 430 in lib/cli.mjs

View check run for this annotation

Codecov / codecov/patch

lib/cli.mjs#L429-L430

Added lines #L429 - L430 were not covered by tests

if (Array.isArray(ignorePath)) {
options.ignorePath = ignorePath;
Expand All @@ -449,12 +450,12 @@
}

if (isString(cacheLocation)) {
options.cacheLocation = cacheLocation;
}

Check warning on line 454 in lib/cli.mjs

View check run for this annotation

Codecov / codecov/patch

lib/cli.mjs#L453-L454

Added lines #L453 - L454 were not covered by tests

if (isString(cacheStrategy)) {
options.cacheStrategy = cacheStrategy;
}

Check warning on line 458 in lib/cli.mjs

View check run for this annotation

Codecov / codecov/patch

lib/cli.mjs#L457-L458

Added lines #L457 - L458 were not covered by tests

if (isBoolean(fix)) {
options.fix = fix;
Expand Down Expand Up @@ -518,11 +519,11 @@
}

if (isString(outputFile)) {
writeOutputFile(output, outputFile).catch(handleError);
}

Check warning on line 523 in lib/cli.mjs

View check run for this annotation

Codecov / codecov/patch

lib/cli.mjs#L522-L523

Added lines #L522 - L523 were not covered by tests

if (errored) {
process.exitCode = EXIT_CODE_ERROR;
process.exitCode = EXIT_CODE_LINT_PROBLEM;
} else if (isNumber(maxWarnings) && maxWarningsExceeded) {
const foundWarnings = maxWarningsExceeded.foundWarnings;

Expand All @@ -531,7 +532,7 @@
`${maxWarnings} allowed${EOL}${EOL}`,
)}`,
);
process.exitCode = EXIT_CODE_ERROR;
process.exitCode = EXIT_CODE_LINT_PROBLEM;
}
})
.catch(handleError);
Expand All @@ -543,14 +544,14 @@
*/
function handleError(err) {
if (!isObject(err)) {
throw err;
}

Check warning on line 548 in lib/cli.mjs

View check run for this annotation

Codecov / codecov/patch

lib/cli.mjs#L547-L548

Added lines #L547 - L548 were not covered by tests

if ('stack' in err && isString(err.stack)) {
process.stderr.write(err.stack + EOL);
}

const exitCode = 'code' in err && isNumber(err.code) ? err.code : EXIT_CODE_FATAL;
const exitCode = 'code' in err && isNumber(err.code) ? err.code : EXIT_CODE_FATAL_ERROR;

process.exitCode = exitCode;
}
Expand All @@ -576,7 +577,7 @@
return Promise.resolve(options);
}

return Promise.reject(errorMessage());

Check warning on line 580 in lib/cli.mjs

View check run for this annotation

Codecov / codecov/patch

lib/cli.mjs#L580

Added line #L580 was not covered by tests
}

/**
Expand Down Expand Up @@ -614,6 +615,11 @@
// @ts-expect-error -- TS2322: Type '{ allowEmptyInput: {...} }' is not assignable to type 'AnyFlags'.
flags,

// NOTE: We must enable `allowUnknownFlags` because meow exits with `2` if `allowUnknownFlags` is disabled.
// Instead, we use our different exit code with `checkInvalidCLIOptions()`.
// See also https://github.com/sindresorhus/meow/blob/v12.0.1/source/validate.js#L75
Mouvedia marked this conversation as resolved.
Show resolved Hide resolved
allowUnknownFlags: true,

// @ts-expect-error -- TS1343: The 'import.meta' meta-property is only allowed when the '--module' option is 'es2020', 'es2022', 'esnext', 'system', 'node16', or 'nodenext'.
importMeta: import.meta,
});
Expand Down
13 changes: 9 additions & 4 deletions lib/constants.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,21 @@ const DEFAULT_IGNORE_FILENAME = '.stylelintignore';

const DEFAULT_FORMATTER = 'string';

// NOTE: Partially based on `sysexits.h`.
const EXIT_CODE_SUCCESS = 0;
const EXIT_CODE_FATAL = 1;
const EXIT_CODE_ERROR = 2;
const EXIT_CODE_FATAL_ERROR = 1;
const EXIT_CODE_LINT_PROBLEM = 2;
const EXIT_CODE_INVALID_USAGE = 64;
const EXIT_CODE_INVALID_CONFIG = 78;
ybiquitous marked this conversation as resolved.
Show resolved Hide resolved

exports.CACHE_STRATEGY_CONTENT = CACHE_STRATEGY_CONTENT;
exports.CACHE_STRATEGY_METADATA = CACHE_STRATEGY_METADATA;
exports.DEFAULT_CACHE_LOCATION = DEFAULT_CACHE_LOCATION;
exports.DEFAULT_CACHE_STRATEGY = DEFAULT_CACHE_STRATEGY;
exports.DEFAULT_FORMATTER = DEFAULT_FORMATTER;
exports.DEFAULT_IGNORE_FILENAME = DEFAULT_IGNORE_FILENAME;
exports.EXIT_CODE_ERROR = EXIT_CODE_ERROR;
exports.EXIT_CODE_FATAL = EXIT_CODE_FATAL;
exports.EXIT_CODE_FATAL_ERROR = EXIT_CODE_FATAL_ERROR;
exports.EXIT_CODE_INVALID_CONFIG = EXIT_CODE_INVALID_CONFIG;
exports.EXIT_CODE_INVALID_USAGE = EXIT_CODE_INVALID_USAGE;
exports.EXIT_CODE_LINT_PROBLEM = EXIT_CODE_LINT_PROBLEM;
exports.EXIT_CODE_SUCCESS = EXIT_CODE_SUCCESS;
7 changes: 5 additions & 2 deletions lib/constants.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export const DEFAULT_IGNORE_FILENAME = '.stylelintignore';

export const DEFAULT_FORMATTER = 'string';

// NOTE: Partially based on `sysexits.h`.
export const EXIT_CODE_SUCCESS = 0;
export const EXIT_CODE_FATAL = 1;
export const EXIT_CODE_ERROR = 2;
export const EXIT_CODE_FATAL_ERROR = 1;
export const EXIT_CODE_LINT_PROBLEM = 2;
export const EXIT_CODE_INVALID_USAGE = 64;
export const EXIT_CODE_INVALID_CONFIG = 78;
4 changes: 3 additions & 1 deletion lib/utils/configurationError.cjs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const constants = require('../constants.cjs');

/** @typedef {Error & { code: number }} ConfigurationError */

/**
Expand All @@ -11,7 +13,7 @@
function configurationError(text) {
const err = /** @type {ConfigurationError} */ (new Error(text));

err.code = 78;
err.code = constants.EXIT_CODE_INVALID_CONFIG;

return err;
}
Expand Down
4 changes: 3 additions & 1 deletion lib/utils/configurationError.mjs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import { EXIT_CODE_INVALID_CONFIG } from '../constants.mjs';

/** @typedef {Error & { code: number }} ConfigurationError */

/**
Expand All @@ -9,7 +11,7 @@
export default function configurationError(text) {
const err = /** @type {ConfigurationError} */ (new Error(text));

err.code = 78;
err.code = EXIT_CODE_INVALID_CONFIG;

return err;
}