Skip to content

Commit

Permalink
fix!: Behavior of CLI when no arguments are passed (#17644)
Browse files Browse the repository at this point in the history
* fix: Behavior of CLI when no arguments are passed

Fixes #14308

* Removed unnecessary check

* Fix edge cases

* Add --pass-on-no-patterns flag

* Rename argument

* Update lib/options.js

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>

* Update docs/src/use/command-line-interface.md

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>

* Update tests/lib/eslint/eslint.js

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>

* Update lib/eslint/flat-eslint.js

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>

* Update tests/lib/cli.js

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>

* lintFiles() should not have default param

* Update docs/src/use/command-line-interface.md

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>

* Fix merge bug

---------

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
  • Loading branch information
nzakas and mdjermanovic committed Dec 21, 2023
1 parent cd1ac20 commit 12be307
Show file tree
Hide file tree
Showing 10 changed files with 275 additions and 36 deletions.
22 changes: 22 additions & 0 deletions docs/src/use/command-line-interface.md
Expand Up @@ -36,6 +36,15 @@ Please note that when passing a glob as a parameter, it is expanded by your shel
npx eslint "lib/**"
```

If you are using a [flat configuration file](./configure/configuration-files-new) (`eslint.config.js`), you can also omit the file arguments and ESLint will use `.`. For instance, these two lines perform the same operation:

```shell
npx eslint .
npx eslint
```

If you are not using a flat configuration file, running ESLint without file arguments results in an error.

**Note:** You can also use alternative package managers such as [Yarn](https://yarnpkg.com/) or [pnpm](https://pnpm.io/) to run ESLint. Please refer to your package manager's documentation for the correct syntax.

## Pass Multiple Values to an Option
Expand Down Expand Up @@ -112,6 +121,7 @@ Miscellaneous:
--no-error-on-unmatched-pattern Prevent errors when pattern is unmatched
--exit-on-fatal-error Exit with exit code 2 in case of fatal error - default: false
--no-warn-ignored Suppress warnings when the file list includes ignored files. *Flat Config Mode Only*
--pass-on-no-patterns Exit with exit code 0 in case no file patterns are passed
--debug Output debugging information
-h, --help Show help
-v, --version Output the version number
Expand Down Expand Up @@ -734,6 +744,18 @@ npx eslint --exit-on-fatal-error file.js
npx eslint --no-warn-ignored --max-warnings 0 ignored-file.js
```

#### `--pass-on-no-patterns`

This option allows ESLint to exit with code 0 when no file or directory patterns are passed. Without this option, ESLint assumes you want to use `.` as the pattern. (When running in legacy eslintrc mode, ESLint will exit with code 1.)

* **Argument Type**: No argument.

##### `--pass-on-no-patterns` example

```shell
npx eslint --pass-on-no-patterns
```

#### `--debug`

This option outputs debugging information to the console. Add this flag to an ESLint command line invocation in order to get extra debugging information while the command runs.
Expand Down
6 changes: 4 additions & 2 deletions lib/cli.js
Expand Up @@ -94,7 +94,8 @@ async function translateOptions({
resolvePluginsRelativeTo,
rule,
rulesdir,
warnIgnored
warnIgnored,
passOnNoPatterns
}, configType) {

let overrideConfig, overrideConfigFile;
Expand Down Expand Up @@ -187,7 +188,8 @@ async function translateOptions({
fixTypes: fixType,
ignore,
overrideConfig,
overrideConfigFile
overrideConfigFile,
passOnNoPatterns
};

if (configType === "flat") {
Expand Down
33 changes: 24 additions & 9 deletions lib/eslint/eslint-helpers.js
Expand Up @@ -105,20 +105,30 @@ class AllFilesIgnoredError extends Error {

/**
* Check if a given value is a non-empty string or not.
* @param {any} x The value to check.
* @returns {boolean} `true` if `x` is a non-empty string.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is a non-empty string.
*/
function isNonEmptyString(x) {
return typeof x === "string" && x.trim() !== "";
function isNonEmptyString(value) {
return typeof value === "string" && value.trim() !== "";
}

/**
* Check if a given value is an array of non-empty strings or not.
* @param {any} x The value to check.
* @returns {boolean} `true` if `x` is an array of non-empty strings.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is an array of non-empty strings.
*/
function isArrayOfNonEmptyString(x) {
return Array.isArray(x) && x.every(isNonEmptyString);
function isArrayOfNonEmptyString(value) {
return Array.isArray(value) && value.length && value.every(isNonEmptyString);
}

/**
* Check if a given value is an empty array or an array of non-empty strings.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is an empty array or an array of non-empty
* strings.
*/
function isEmptyArrayOrArrayOfNonEmptyString(value) {
return Array.isArray(value) && value.every(isNonEmptyString);
}

//-----------------------------------------------------------------------------
Expand Down Expand Up @@ -676,6 +686,7 @@ function processOptions({
overrideConfigFile = null,
plugins = {},
warnIgnored = true,
passOnNoPatterns = false,
...unknownOptions
}) {
const errors = [];
Expand Down Expand Up @@ -759,7 +770,7 @@ function processOptions({
if (typeof ignore !== "boolean") {
errors.push("'ignore' must be a boolean.");
}
if (!isArrayOfNonEmptyString(ignorePatterns) && ignorePatterns !== null) {
if (!isEmptyArrayOrArrayOfNonEmptyString(ignorePatterns) && ignorePatterns !== null) {
errors.push("'ignorePatterns' must be an array of non-empty strings or null.");
}
if (typeof overrideConfig !== "object") {
Expand All @@ -768,6 +779,9 @@ function processOptions({
if (!isNonEmptyString(overrideConfigFile) && overrideConfigFile !== null && overrideConfigFile !== true) {
errors.push("'overrideConfigFile' must be a non-empty string, null, or true.");
}
if (typeof passOnNoPatterns !== "boolean") {
errors.push("'passOnNoPatterns' must be a boolean.");
}
if (typeof plugins !== "object") {
errors.push("'plugins' must be an object or null.");
} else if (plugins !== null && Object.keys(plugins).includes("")) {
Expand Down Expand Up @@ -800,6 +814,7 @@ function processOptions({
globInputPaths,
ignore,
ignorePatterns,
passOnNoPatterns,
warnIgnored
};
}
Expand Down
62 changes: 42 additions & 20 deletions lib/eslint/eslint.js
Expand Up @@ -67,6 +67,8 @@ const { version } = require("../../package.json");
* @property {string} [resolvePluginsRelativeTo] The folder where plugins should be resolved from, defaulting to the CWD.
* @property {string[]} [rulePaths] An array of directories to load custom rules from.
* @property {boolean} [useEslintrc] False disables looking for .eslintrc.* files.
* @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause
* the linting operation to short circuit and not report any failures.
*/

/**
Expand Down Expand Up @@ -97,38 +99,48 @@ const privateMembersMap = new WeakMap();

/**
* Check if a given value is a non-empty string or not.
* @param {any} x The value to check.
* @returns {boolean} `true` if `x` is a non-empty string.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is a non-empty string.
*/
function isNonEmptyString(x) {
return typeof x === "string" && x.trim() !== "";
function isNonEmptyString(value) {
return typeof value === "string" && value.trim() !== "";
}

/**
* Check if a given value is an array of non-empty strings or not.
* @param {any} x The value to check.
* @returns {boolean} `true` if `x` is an array of non-empty strings.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is an array of non-empty strings.
*/
function isArrayOfNonEmptyString(x) {
return Array.isArray(x) && x.every(isNonEmptyString);
function isArrayOfNonEmptyString(value) {
return Array.isArray(value) && value.length && value.every(isNonEmptyString);
}

/**
* Check if a given value is an empty array or an array of non-empty strings.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is an empty array or an array of non-empty
* strings.
*/
function isEmptyArrayOrArrayOfNonEmptyString(value) {
return Array.isArray(value) && value.every(isNonEmptyString);
}

/**
* Check if a given value is a valid fix type or not.
* @param {any} x The value to check.
* @returns {boolean} `true` if `x` is valid fix type.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is valid fix type.
*/
function isFixType(x) {
return x === "directive" || x === "problem" || x === "suggestion" || x === "layout";
function isFixType(value) {
return value === "directive" || value === "problem" || value === "suggestion" || value === "layout";
}

/**
* Check if a given value is an array of fix types or not.
* @param {any} x The value to check.
* @returns {boolean} `true` if `x` is an array of fix types.
* @param {any} value The value to check.
* @returns {boolean} `true` if `value` is an array of fix types.
*/
function isFixTypeArray(x) {
return Array.isArray(x) && x.every(isFixType);
function isFixTypeArray(value) {
return Array.isArray(value) && value.every(isFixType);
}

/**
Expand Down Expand Up @@ -169,6 +181,7 @@ function processOptions({
resolvePluginsRelativeTo = null, // ← should be null by default because if it's a string then it suppresses RFC47 feature.
rulePaths = [],
useEslintrc = true,
passOnNoPatterns = false,
...unknownOptions
}) {
const errors = [];
Expand Down Expand Up @@ -225,7 +238,7 @@ function processOptions({
if (typeof errorOnUnmatchedPattern !== "boolean") {
errors.push("'errorOnUnmatchedPattern' must be a boolean.");
}
if (!isArrayOfNonEmptyString(extensions) && extensions !== null) {
if (!isEmptyArrayOrArrayOfNonEmptyString(extensions) && extensions !== null) {
errors.push("'extensions' must be an array of non-empty strings or null.");
}
if (typeof fix !== "boolean" && typeof fix !== "function") {
Expand Down Expand Up @@ -271,12 +284,15 @@ function processOptions({
) {
errors.push("'resolvePluginsRelativeTo' must be a non-empty string or null.");
}
if (!isArrayOfNonEmptyString(rulePaths)) {
if (!isEmptyArrayOrArrayOfNonEmptyString(rulePaths)) {
errors.push("'rulePaths' must be an array of non-empty strings.");
}
if (typeof useEslintrc !== "boolean") {
errors.push("'useEslintrc' must be a boolean.");
}
if (typeof passOnNoPatterns !== "boolean") {
errors.push("'passOnNoPatterns' must be a boolean.");
}

if (errors.length > 0) {
throw new ESLintInvalidOptionsError(errors);
Expand All @@ -300,7 +316,8 @@ function processOptions({
reportUnusedDisableDirectives,
resolvePluginsRelativeTo,
rulePaths,
useEslintrc
useEslintrc,
passOnNoPatterns
};
}

Expand Down Expand Up @@ -541,10 +558,15 @@ class ESLint {
* @returns {Promise<LintResult[]>} The results of linting the file patterns given.
*/
async lintFiles(patterns) {
const { cliEngine, options } = privateMembersMap.get(this);

if (options.passOnNoPatterns && (patterns === "" || (Array.isArray(patterns) && patterns.length === 0))) {
return [];
}

if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) {
throw new Error("'patterns' must be a non-empty string or an array of non-empty strings");
}
const { cliEngine } = privateMembersMap.get(this);

return processCLIEngineLintReport(
cliEngine,
Expand Down
40 changes: 36 additions & 4 deletions lib/eslint/flat-eslint.js
Expand Up @@ -85,6 +85,8 @@ const LintResultCache = require("../cli-engine/lint-result-cache");
* when a string.
* @property {Record<string,Plugin>} [plugins] An array of plugin implementations.
* @property {boolean} warnIgnored Show warnings when the file list includes ignored files
* @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause
* the linting operation to short circuit and not report any failures.
*/

//------------------------------------------------------------------------------
Expand Down Expand Up @@ -738,16 +740,46 @@ class FlatESLint {
* @returns {Promise<LintResult[]>} The results of linting the file patterns given.
*/
async lintFiles(patterns) {
if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) {
throw new Error("'patterns' must be a non-empty string or an array of non-empty strings");
}

let normalizedPatterns = patterns;
const {
cacheFilePath,
lintResultCache,
linter,
options: eslintOptions
} = privateMembers.get(this);

/*
* Special cases:
* 1. `patterns` is an empty string
* 2. `patterns` is an empty array
*
* In both cases, we use the cwd as the directory to lint.
*/
if (patterns === "" || Array.isArray(patterns) && patterns.length === 0) {

/*
* Special case: If `passOnNoPatterns` is true, then we just exit
* without doing any work.
*/
if (eslintOptions.passOnNoPatterns) {
return [];
}

normalizedPatterns = ["."];
} else {

if (!isNonEmptyString(patterns) && !isArrayOfNonEmptyString(patterns)) {
throw new Error("'patterns' must be a non-empty string or an array of non-empty strings");
}

if (typeof patterns === "string") {
normalizedPatterns = [patterns];
}
}

debug(`Using file patterns: ${normalizedPatterns}`);

const configs = await calculateConfigArray(this, eslintOptions);
const {
allowInlineConfig,
Expand Down Expand Up @@ -779,7 +811,7 @@ class FlatESLint {
}

const filePaths = await findFiles({
patterns: typeof patterns === "string" ? [patterns] : patterns,
patterns: normalizedPatterns,
cwd,
globInputPaths,
configs,
Expand Down
8 changes: 8 additions & 0 deletions lib/options.js
Expand Up @@ -57,6 +57,8 @@ const optionator = require("optionator");
* @property {boolean} quiet Report errors only
* @property {boolean} [version] Output the version number
* @property {boolean} warnIgnored Show warnings when the file list includes ignored files
* @property {boolean} [passOnNoPatterns=false] When set to true, missing patterns cause
* the linting operation to short circuit and not report any failures.
* @property {string[]} _ Positional filenames or patterns
*/

Expand Down Expand Up @@ -370,6 +372,12 @@ module.exports = function(usingFlatConfig) {
description: "Exit with exit code 2 in case of fatal error"
},
warnIgnoredFlag,
{
option: "pass-on-no-patterns",
type: "Boolean",
default: false,
description: "Exit with exit code 0 in case no file patterns are passed"
},
{
option: "debug",
type: "Boolean",
Expand Down
7 changes: 7 additions & 0 deletions tests/lib/cli.js
Expand Up @@ -823,6 +823,13 @@ describe("cli", () => {
assert.strictEqual(exit, useFlatConfig ? 0 : 2);
});

it(`should not lint anything when no files are passed if --pass-on-no-patterns is passed with configType:${configType}`, async () => {
const exit = await cli.execute("--pass-on-no-patterns", null, useFlatConfig);

assert.isFalse(log.info.called);
assert.strictEqual(exit, 0);
});

it(`should suppress the warning if --no-warn-ignored is passed and an ignored file is passed via stdin with configType:${configType}`, async () => {
const options = useFlatConfig
? `--config ${getFixturePath("eslint.config_with_ignores.js")}`
Expand Down

0 comments on commit 12be307

Please sign in to comment.