diff --git a/bin/eslint.js b/bin/eslint.js index a9f51f1d7d4..75b41314869 100755 --- a/bin/eslint.js +++ b/bin/eslint.js @@ -12,97 +12,135 @@ // to use V8's code cache to speed up instantiation time require("v8-compile-cache"); -//------------------------------------------------------------------------------ -// Helpers -//------------------------------------------------------------------------------ - -const useStdIn = process.argv.includes("--stdin"), - init = process.argv.includes("--init"), - debug = process.argv.includes("--debug"); - // must do this initialization *before* other requires in order to work -if (debug) { +if (process.argv.includes("--debug")) { require("debug").enable("eslint:*,-eslint:code-path"); } //------------------------------------------------------------------------------ -// Requirements +// Helpers //------------------------------------------------------------------------------ -// now we can safely include the other modules that use debug -const path = require("path"), - fs = require("fs"), - cli = require("../lib/cli"); - -//------------------------------------------------------------------------------ -// Execution -//------------------------------------------------------------------------------ +/** + * Read data from stdin til the end. + * + * Note: See + * - https://github.com/nodejs/node/blob/master/doc/api/process.md#processstdin + * - https://github.com/nodejs/node/blob/master/doc/api/process.md#a-note-on-process-io + * - https://lists.gnu.org/archive/html/bug-gnu-emacs/2016-01/msg00419.html + * - https://github.com/nodejs/node/issues/7439 (historical) + * + * On Windows using `fs.readFileSync(STDIN_FILE_DESCRIPTOR, "utf8")` seems + * to read 4096 bytes before blocking and never drains to read further data. + * + * The investigation on the Emacs thread indicates: + * + * > Emacs on MS-Windows uses pipes to communicate with subprocesses; a + * > pipe on Windows has a 4K buffer. So as soon as Emacs writes more than + * > 4096 bytes to the pipe, the pipe becomes full, and Emacs then waits for + * > the subprocess to read its end of the pipe, at which time Emacs will + * > write the rest of the stuff. + * @returns {Promise} The read text. + */ +function readStdin() { + return new Promise((resolve, reject) => { + let content = ""; + let chunk = ""; + + process.stdin + .setEncoding("utf8") + .on("readable", () => { + while ((chunk = process.stdin.read()) !== null) { + content += chunk; + } + }) + .on("end", () => resolve(content)) + .on("error", reject); + }); +} -process.once("uncaughtException", err => { +/** + * Get the error message of a given value. + * @param {any} error The value to get. + * @returns {string} The error message. + */ +function getErrorMessage(error) { - // lazy load + // Lazy loading because those are used only if error happened. + const fs = require("fs"); + const path = require("path"); + const util = require("util"); const lodash = require("lodash"); - if (typeof err.messageTemplate === "string" && err.messageTemplate.length > 0) { - const template = lodash.template(fs.readFileSync(path.resolve(__dirname, `../messages/${err.messageTemplate}.txt`), "utf-8")); - const pkg = require("../package.json"); + // Foolproof -- thirdparty module might throw non-object. + if (typeof error !== "object" || error === null) { + return String(error); + } + + // Use templates if `error.messageTemplate` is present. + if (typeof error.messageTemplate === "string") { + try { + const templateFilePath = path.resolve( + __dirname, + `../messages/${error.messageTemplate}.txt` + ); + + // Use sync API because Node.js should exit at this tick. + const templateText = fs.readFileSync(templateFilePath, "utf-8"); + const template = lodash.template(templateText); + + return template(error.messageData || {}); + } catch { + + // Ignore template error then fallback to use `error.stack`. + } + } - console.error("\nOops! Something went wrong! :("); - console.error(`\nESLint: ${pkg.version}.\n\n${template(err.messageData || {})}`); - } else { - console.error(err.stack); + // Use the stacktrace if it's an error object. + if (typeof error.stack === "string") { + return error.stack; } + // Otherwise, dump the object. + return util.format("%o", error); +} + +/** + * Catch and report unexpected error. + * @param {any} error The thrown error object. + * @returns {void} + */ +function onFatalError(error) { process.exitCode = 2; -}); - -if (useStdIn) { - - /* - * Note: See - * - https://github.com/nodejs/node/blob/master/doc/api/process.md#processstdin - * - https://github.com/nodejs/node/blob/master/doc/api/process.md#a-note-on-process-io - * - https://lists.gnu.org/archive/html/bug-gnu-emacs/2016-01/msg00419.html - * - https://github.com/nodejs/node/issues/7439 (historical) - * - * On Windows using `fs.readFileSync(STDIN_FILE_DESCRIPTOR, "utf8")` seems - * to read 4096 bytes before blocking and never drains to read further data. - * - * The investigation on the Emacs thread indicates: - * - * > Emacs on MS-Windows uses pipes to communicate with subprocesses; a - * > pipe on Windows has a 4K buffer. So as soon as Emacs writes more than - * > 4096 bytes to the pipe, the pipe becomes full, and Emacs then waits for - * > the subprocess to read its end of the pipe, at which time Emacs will - * > write the rest of the stuff. - * - * Using the nodejs code example for reading from stdin. - */ - let contents = "", - chunk = ""; - - process.stdin.setEncoding("utf8"); - process.stdin.on("readable", () => { - - // Use a loop to make sure we read all available data. - while ((chunk = process.stdin.read()) !== null) { - contents += chunk; - } - }); - process.stdin.on("end", () => { - process.exitCode = cli.execute(process.argv, contents, "utf8"); - }); -} else if (init) { - const configInit = require("../lib/init/config-initializer"); - - configInit.initializeConfig().then(() => { - process.exitCode = 0; - }).catch(err => { - process.exitCode = 1; - console.error(err.message); - console.error(err.stack); - }); -} else { - process.exitCode = cli.execute(process.argv); + const { version } = require("../package.json"); + const message = getErrorMessage(error); + + console.error(` +Oops! Something went wrong! :( + +ESLint: ${version} + +${message}`); } + +//------------------------------------------------------------------------------ +// Execution +//------------------------------------------------------------------------------ + +(async function main() { + process.on("uncaughtException", onFatalError); + process.on("unhandledRejection", onFatalError); + + // Call the config initializer if `--init` is present. + if (process.argv.includes("--init")) { + await require("../lib/init/config-initializer").initializeConfig(); + return; + } + + // Otherwise, call the CLI. + process.exitCode = await require("../lib/cli").execute( + process.argv, + process.argv.includes("--stdin") ? await readStdin() : null + ); +}()).catch(onFatalError); diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index ee1a7a44970..e980445447c 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -6,6 +6,20 @@ While ESLint is designed to be run on the command line, it's possible to use ESL ## Table of Contents +* [ESLint] + * [constructor()][eslint-constructor] + * [lintFiles()][eslint-lintFiles] + * [lintText()][eslint-lintText] + * [calculateConfigForFile()][eslint-calculateConfigForFile] + * [isPathIgnored()][eslint-isPathIgnored] + * [loadFormatter()][eslint-loadFormatter] + * [static version][eslint-version] + * [static outputFixes()][eslint-outputFixes] + * [static getErrorResults()][eslint-getErrorResults] + * [LintResult type](lintresult) + * [LintMessage type](lintmessage) + * [EditInfo type](editinfo) + * [Formatter type](formatter) * [SourceCode](#sourcecode) * [splitLines()](#sourcecode-splitlines) * [Linter](#linter) @@ -17,22 +31,361 @@ While ESLint is designed to be run on the command line, it's possible to use ESL * [defineParser()](#linter-defineparser) * [version](#linter-version) * [linter (deprecated)](#linter-1) -* [CLIEngine](#cliengine) - * [executeOnFiles()](#cliengine-executeonfiles) - * [resolveFileGlobPatterns()](#cliengine-resolvefileglobpatterns) - * [getConfigForFile()](#cliengine-getconfigforfile) - * [executeOnText()](#cliengine-executeontext) - * [addPlugin()](#cliengine-addplugin) - * [isPathIgnored()](#cliengine-ispathignored) - * [getFormatter()](#cliengine-getformatter) - * [getErrorResults()](#cliengine-geterrorresults) - * [outputFixes()](#cliengine-outputfixes) - * [getRules()](#cliengine-getrules) - * [version](#cliengine-version) +* [CLIEngine (deprecated)](#cliengine) * [RuleTester](#ruletester) * [Customizing RuleTester](#customizing-ruletester) * [Deprecated APIs](#deprecated-apis) +--- + +## ESLint class + +The `ESLint` class is the primary class to use in Node.js applications. + +This class depends on the Node.js `fs` module and the file system, so you cannot use it in browsers. If you want to lint code on browsers, use the [Linter](#linter) class instead. + +Here's a simple example of using the `ESLint` class: + +```js +const { ESLint } = require("eslint"); + +(async function main() { + // 1. Create an instance. + const eslint = new ESLint(); + + // 2. Lint files. + const results = await eslint.lintFiles(["lib/**/*.js"]); + + // 3. Format the results. + const formatter = await eslint.loadFormatter("stylish"); + const resultText = formatter.format(results); + + // 4. Output it. + console.log(resultText); +})().catch((error) => { + process.exitCode = 1; + console.error(error); +}); +``` + +And here is an example that autofixes lint problems: + +```js +const { ESLint } = require("eslint"); + +(async function main() { + // 1. Create an instance with the `fix` option. + const eslint = new ESLint({ fix: true }); + + // 2. Lint files. This doesn't modify target files. + const results = await eslint.lintFiles(["lib/**/*.js"]); + + // 3. Modify the files with the fixed code. + await ESLint.outputFixes(results); + + // 4. Format the results. + const formatter = await eslint.loadFormatter("stylish"); + const resultText = formatter.format(results); + + // 5. Output it. + console.log(resultText); +})().catch((error) => { + process.exitCode = 1; + console.error(error); +}); +``` + +### ◆ new ESLint(options) + +```js +const eslint = new ESLint(options); +``` + +Create a new `ESLint` instance. + +#### Parameters + +The `ESLint` constructor takes an `options` object. If you omit the `options` object then it uses default values for all options. The `options` object has the following properties. + +##### File Enumeration + +* `options.cwd` (`string`)
+ Default is `process.cwd()`. The working directory. This must be an absolute path. +* `options.errorOnUnmatchedPattern` (`boolean`)
+ Default is `true`. Unless set to `false`, the [`eslint.lintFiles()`][eslint-lintfiles] method will throw an error when no target files are found. +* `options.extensions` (`string[] | null`)
+ Default is `null`. If you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method, ESLint checks the files in those directories that have the given extensions. For example, when passing the `src/` directory and `extensions` is `[".js", ".ts"]`, ESLint will lint `*.js` and `*.ts` files in `src/`. If `extensions` is `null`, ESLint checks `*.js` files and files that match `overrides[].files` patterns in your configuration.
**Note:** This option only applies when you pass directory paths to the [`eslint.lintFiles()`][eslint-lintfiles] method. If you pass glob patterns like `lib/**/*`, ESLint will lint all files matching the glob pattern regardless of extension. +* `options.globInputPaths` (`boolean`)
+ Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't interpret glob patterns. +* `options.ignore` (`boolean`)
+ Default is `true`. If `false` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method doesn't respect `.eslintignore` files or `ignorePatterns` in your configuration. +* `options.ignorePath` (`string | null`)
+ Default is `null`. The path to a file ESLint uses instead of `$CWD/.eslintignore`. If a path is present and the file doesn't exist, this constructor will throw an error. + +##### Linting + +* `options.allowInlineConfig` (`boolean`)
+ Default is `true`. If `false` is present, ESLint suppresses directive comments in source code. If this option is `false`, it overrides the `noInlineConfig` setting in your configurations. +* `options.baseConfig` (`ConfigData | null`)
+ Default is `null`. [Configuration object], extended by all configurations used with this instance. You can use this option to define the default settings that will be used if your configuration files don't configure it. +* `options.overrideConfig` (`ConfigData | null`)
+ Default is `null`. [Configuration object], overrides all configurations used with this instance. You can use this option to define the settings that will be used even if your configuration files configure it. +* `options.overrideConfigFile` (`string | null`)
+ Default is `null`. The path to a configuration file, overrides all configurations used with this instance. The `options.overrideConfig` option is applied after this option is applied. +* `options.plugins` (`Record | null`)
+ Default is `null`. The plugin implementations that ESLint uses for the `plugins` setting of your configuration. This is a map-like object. Those keys are plugin IDs and each value is implementation. +* `options.reportUnusedDisableDirectives` (`"error" | "warn" | "off" | null`)
+ Default is `null`. The severity to report unused eslint-disable directives. If this option is a severity, it overrides the `reportUnusedDisableDirectives` setting in your configurations. +* `options.resolvePluginsRelativeTo` (`string` | `null`)
+ Default is `null`. The path to a directory where plugins should be resolved from. If `null` is present, ESLint loads plugins from the location of the configuration file that contains the plugin setting. If a path is present, ESLint loads all plugins from there. +* `options.rulePaths` (`string[]`)
+ Default is `[]`. An array of paths to directories to load custom rules from. +* `options.useEslintrc` (`boolean`)
+ Default is `true`. If `false` is present, ESLint doesn't load configuration files (`.eslintrc.*` files). Only the configuration of the constructor options is valid. + +##### Autofix + +* `options.fix` (`boolean | (message: LintMessage) => boolean`)
+ Default is `false`. If `true` is present, the [`eslint.lintFiles()`][eslint-lintfiles] and [`eslint.lintText()`][eslint-linttext] methods work in autofix mode. If a predicate function is present, the methods pass each lint message to the function, then use only the lint messages for which the function returned `true`. +* `options.fixTypes` (`("problem" | "suggestion" | "layout")[] | null`)
+ Default is `null`. The types of the rules that the [`eslint.lintFiles()`][eslint-lintfiles] and [`eslint.lintText()`][eslint-linttext] methods use for autofix. + +##### Cache-related + +* `options.cache` (`boolean`)
+ Default is `false`. If `true` is present, the [`eslint.lintFiles()`][eslint-lintfiles] method caches lint results and uses it if each target file is not changed. Please mind that ESLint doesn't clear the cache when you upgrade ESLint plugins. In that case, you have to remove the cache file manually. The [`eslint.lintText()`][eslint-linttext] method doesn't use caches even if you pass the `options.filePath` to the method. +* `options.cacheLocation` (`string`)
+ Default is `.eslintcache`. The [`eslint.lintFiles()`][eslint-lintfiles] method writes caches into this file. + +### ◆ eslint.lintFiles(patterns) + +```js +const results = await eslint.lintFiles(patterns); +``` + +This method lints the files that match the glob patterns and then returns the results. + +#### Parameters + +* `patterns` (`string | string[]`)
+ The lint target files. This can contain any of file paths, directory paths, and glob patterns. + +#### Return Value + +* (`Promise`)
+ The promise that will be fulfilled with an array of [LintResult] objects. + +### ◆ eslint.lintText(code, options) + +```js +const results = await eslint.lintText(code, options); +``` + +This method lints the given source code text and then returns the results. + +By default, this method uses the configuration that applies to files in the current working directory (the `cwd` constructor option). If you want to use a different configuration, pass `options.filePath`, and ESLint will load the same configuration that [`eslint.lintFiles()`][eslint-lintfiles] would use for a file at `options.filePath`. + +If the `options.filePath` value is configured to be ignored, this method returns an empty array. If the `options.warnIgnored` option is set along with the `options.filePath` option, this method returns a [LintResult] object. In that case, the result may contain a warning that indicates the file was ignored. + +#### Parameters + +The second parameter `options` is omittable. + +* `code` (`string`)
+ The source code text to check. +* `options.filePath` (`string`)
+ Optional. The path to the file of the source code text. If omitted, the `result.filePath` becomes the string `""`. +* `options.warnIgnored` (`boolean`)
+ Optional. If `true` is present and the `options.filePath` is a file ESLint should ignore, this method returns a lint result contains a warning message. + +#### Return Value + +* (`Promise`)
+ The promise that will be fulfilled with an array of [LintResult] objects. This is an array (despite there being only one lint result) in order to keep the interfaces between this and the [`eslint.lintFiles()`][eslint-lintfiles] method similar. + +### ◆ eslint.calculateConfigForFile(filePath) + +```js +const config = await eslint.calculateConfigForFile(filePath); +``` + +This method calculates the configuration for a given file, which can be useful for debugging purposes. + +* It resolves and merges `extends` and `overrides` settings into the top level configuration. +* It resolves the `parser` setting to absolute paths. +* It normalizes the `plugins` setting to align short names. (e.g., `eslint-plugin-foo` → `foo`) +* It adds the `processor` setting if a legacy file extension processor is matched. +* It doesn't interpret the `env` setting to the `globals` and `parserOptions` settings, so the result object contains the `env` setting as is. + +#### Parameters + +* `filePath` (`string`)
+ The path to the file whose configuration you would like to calculate. Directory paths are forbidden because ESLint cannot handle the `overrides` setting. + +#### Return Value + +* (`Promise`)
+ The promise that will be fulfilled with a configuration object. + +### ◆ eslint.isPathIgnored(filePath) + +```js +const isPathIgnored = await eslint.isPathIgnored(filePath); +``` + +This method checks if a given file is ignored by your configuration. + +#### Parameters + +* `filePath` (`string`)
+ The path to the file you want to check. + +#### Return Value + +* (`Promise`)
+ The promise that will be fulfilled with whether the file is ignored or not. If the file is ignored, then it will return `true`. + +### ◆ eslint.loadFormatter(nameOrPath) + +```js +const formatter = await eslint.loadFormatter(nameOrPath); +``` + +This method loads a formatter. Formatters convert lint results to a human- or machine-readable string. + +#### Parameters + +* `nameOrPath` (`string | undefined`)
+ The path to the file you want to check. The following values are allowed: + * `undefined`. In this case, loads the `"stylish"` built-in formatter. + * A name of [built-in formatters][builtin-formatters]. + * A name of [third-party formatters][thirdparty-formatters]. For examples: + * `"foo"` will load `eslint-formatter-foo`. + * `"@foo"` will load `@foo/eslint-formatter`. + * `"@foo/bar"` will load `@foo/eslint-formatter-bar`. + * A path to the file that defines a formatter. The path must contain one or more path separators (`/`) in order to distinguish if it's a path or not. For example, start with `./`. + +#### Return Value + +* (`Promise`)
+ The promise that will be fulfilled with a [Formatter] object. + +### ◆ ESLint.version + +```js +const version = ESLint.version; +``` + +The version string of ESLint. E.g. `"7.0.0"`. + +This is a static property. + +### ◆ ESLint.outputFixes(results) + +```js +await ESLint.outputFixes(results); +``` + +This method writes code modified by ESLint's autofix feature into its respective file. If any of the modified files don't exist, this method does nothing. + +This is a static method. + +#### Parameters + +* `results` (`LintResult[]`)
+ The [LintResult] objects to write. + +#### Return Value + +* (`Promise`)
+ The promise that will be fulfilled after all files are written. + +### ◆ ESLint.getErrorResults(results) + +```js +const filteredResults = ESLint.getErrorResults(results); +``` + +This method copies the given results and removes warnings. The returned value contains only errors. + +This is a static method. + +#### Parameters + +* `results` (`LintResult[]`)
+ The [LintResult] objects to filter. + +#### Return Value + +* (`LintResult[]`)
+ The filtered [LintResult] objects. + +### ◆ LintResult type + +The `LintResult` value is the information of the linting result of each file. The [`eslint.lintFiles()`][eslint-lintfiles] and [`eslint.lintText()`][eslint-linttext] methods return it. It has the following properties: + +* `filePath` (`string`)
+ The absolute path to the file of this result. This is the string `""` if the file path is unknown (when you didn't pass the `options.filePath` option to the [`eslint.lintText()`][eslint-linttext] method). +* `messages` (`LintMessage[]`)
+ The array of [LintMessage] objects. +* `fixableErrorCount` (`number`)
+ The number of errors that can be fixed automatically by the `fix` constructor option. +* `fixableWarningCount` (`number`)
+ The number of warnings that can be fixed automatically by the `fix` constructor option. +* `errorCount` (`number`)
+ The number of errors. This includes fixable errors. +* `warningCount` (`number`)
+ The number of warnings. This includes fixable warnings. +* `output` (`string | undefined`)
+ The modified source code text. This property is undefined if any fixable messages didn't exist. +* `source` (`string | undefined`)
+ The original source code text. This property is undefined if any messages didn't exist or the `output` property exists. +* `usedDeprecatedRules` (`{ ruleId: string; replacedBy: string[] }[]`)
+ The information about the deprecated rules that were used to check this file. + +### ◆ LintMessage type + +The `LintMessage` value is the information of each linting error. The `messages` property of the [LintResult] type contains it. It has the following properties: + +* `ruleId` (`string` | `null`)
+ The rule name that generates this lint message. If this message is generated by the ESLint core rather than rules, this is `null`. +* `severity` (`1 | 2`)
+ The severity of this message. `1` means warning and `2` means error. +* `message` (`string`)
+ The error message. +* `line` (`number`)
+ The 1-based line number of the begin point of this message. +* `column` (`number`)
+ The 1-based column number of the begin point of this message. +* `endLine` (`number | undefined`)
+ The 1-based line number of the end point of this message. This property is undefined if this message is not a range. +* `endColumn` (`number | undefined`)
+ The 1-based column number of the end point of this message. This property is undefined if this message is not a range. +* `fix` (`EditInfo | undefined`)
+ The [EditInfo] object of autofix. This property is undefined if this message is not fixable. +* `suggestions` (`{ desc: string; fix: EditInfo }[] | undefined`)
+ The list of suggestions. Each suggestion is the pair of a description and an [EditInfo] object to fix code. API users such as editor integrations can choose one of them to fix the problem of this message. This property is undefined if this message doesn't have any suggestions. + +### ◆ EditInfo type + +The `EditInfo` value is information to edit text. The `fix` and `suggestions` properties of [LintMessage] type contain it. It has following properties: + +* `range` (`[number, number]`)
+ The pair of 0-based indices in source code text to remove. +* `text` (`string`)
+ The text to add. + +This edit information means replacing the range of the `range` property by the `text` property value. It's like `sourceCodeText.slice(0, edit.range[0]) + edit.text + sourceCodeText.slice(edit.range[1])`. Therefore, it's an add if the `range[0]` and `range[1]` property values are the same value, and it's removal if the `text` property value is empty string. + +### ◆ Formatter type + +The `Formatter` value is the object to convert the [LintResult] objects to text. The [eslint.loadFormatter()][eslint-loadformatter] method returns it. It has the following method: + +* `format` (`(results: LintResult[]) => string`)
+ The method to convert the [LintResult] objects to text. + +--- + ## SourceCode The `SourceCode` type represents the parsed source code that ESLint executes on. It's used internally in ESLint and is also available so that already-parsed code can be used. You can create a new instance of `SourceCode` by passing in the text string representing the code and an abstract syntax tree (AST) in [ESTree](https://github.com/estree/estree) format (including location information, range information, comments, and tokens): @@ -78,6 +431,8 @@ const codeLines = SourceCode.splitLines(code); */ ``` +--- + ## Linter The `Linter` object does the actual evaluation of the JavaScript code. It doesn't do any filesystem operations, it simply parses and reports on the code. In particular, the `Linter` object does not process configuration objects or files. @@ -336,8 +691,12 @@ const messages = linter.verify("var foo;", { Note: This API is deprecated as of 4.0.0. +--- + ## CLIEngine +⚠️ The `CLIEngine` class has been deprecated in favor of the `ESLint` class as of v7.0.0. + The primary Node.js API is `CLIEngine`, which is the underlying utility that runs the ESLint command line interface. This object will read the filesystem for configuration and file information but will not output any results. Instead, it allows you direct access to the important information so you can deal with the output yourself. You can get a reference to the `CLIEngine` by doing the following: @@ -819,6 +1178,8 @@ Map { require("eslint").CLIEngine.version; // '4.5.0' ``` +--- + ## RuleTester `eslint.RuleTester` is a utility to write tests for ESLint rules. It is used internally for the bundled rules that come with ESLint, and it can also be used by plugins. @@ -1007,7 +1368,31 @@ ruleTester.run("my-rule", myRule, { }) ``` +--- + ## Deprecated APIs * `cli` - the `cli` object has been deprecated in favor of `CLIEngine`. As of v1.0.0, `cli` is no longer exported and should not be used by external tools. * `linter` - the `linter` object has been deprecated in favor of `Linter` as of v4.0.0. +* `CLIEngine` - the `CLIEngine` class has been deprecated in favor of the `ESLint` class as of v7.0.0. + +--- + +[configuration object]: ../user-guide/configuring.md +[builtin-formatters]: https://eslint.org/docs/user-guide/formatters/ +[thirdparty-formatters]: https://www.npmjs.com/search?q=eslintformatter +[eslint]: #eslint-class +[eslint-constructor]: #-new-eslintoptions +[eslint-lintfiles]: #-eslintlintFilespatterns +[eslint-linttext]: #-eslintlintTextcode-options +[eslint-calculateconfigforfile]: #-eslintcalculateConfigForFilefilePath +[eslint-ispathignored]: #-eslintisPathIgnoredfilePath +[eslint-loadformatter]: #-eslintloadFormatternameOrPath +[eslint-version]: #-eslintversion +[eslint-outputfixes]: #-eslintoutputFixesresults +[eslint-geterrorresults]: #-eslintgetErrorResultsresults +[lintresult]: #-lintresult-type +[lintmessage]: #-lintmessage-type +[editinfo]: #-editinfo-type +[formatter]: #-formatter-type +[linter]: #linter diff --git a/lib/api.js b/lib/api.js index 40a5cc9fa5c..e4b6643b447 100644 --- a/lib/api.js +++ b/lib/api.js @@ -6,6 +6,7 @@ "use strict"; const { CLIEngine } = require("./cli-engine"); +const { ESLint } = require("./eslint"); const { Linter } = require("./linter"); const { RuleTester } = require("./rule-tester"); const { SourceCode } = require("./source-code"); @@ -13,6 +14,7 @@ const { SourceCode } = require("./source-code"); module.exports = { Linter, CLIEngine, + ESLint, RuleTester, SourceCode }; diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index b53f67bd9dc..f54605c4db9 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -279,6 +279,18 @@ class CascadingConfigArrayFactory { ); } + /** + * Set the config data to override all configs. + * Require to call `clearCache()` method after this method is called. + * @param {ConfigData} configData The config data to override all configs. + * @returns {void} + */ + setOverrideConfig(configData) { + const slots = internalSlotsMap.get(this); + + slots.cliConfigData = configData; + } + /** * Clear config cache. * @returns {void} diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index 72d1fa4d5dc..b6aa995beef 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -39,6 +39,7 @@ const validFixTypes = new Set(["problem", "suggestion", "layout"]); // For VSCode IntelliSense /** @typedef {import("../shared/types").ConfigData} ConfigData */ +/** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */ /** @typedef {import("../shared/types").LintMessage} LintMessage */ /** @typedef {import("../shared/types").ParserOptions} ParserOptions */ /** @typedef {import("../shared/types").Plugin} Plugin */ @@ -50,29 +51,29 @@ const validFixTypes = new Set(["problem", "suggestion", "layout"]); /** * The options to configure a CLI engine with. * @typedef {Object} CLIEngineOptions - * @property {boolean} allowInlineConfig Enable or disable inline configuration comments. - * @property {ConfigData} baseConfig Base config object, extended by all configs used with this CLIEngine instance - * @property {boolean} cache Enable result caching. - * @property {string} cacheLocation The cache file to use instead of .eslintcache. - * @property {string} configFile The configuration file to use. - * @property {string} cwd The value to use for the current working directory. - * @property {string[]} envs An array of environments to load. - * @property {string[]|null} extensions An array of file extensions to check. - * @property {boolean|Function} fix Execute in autofix mode. If a function, should return a boolean. - * @property {string[]} fixTypes Array of rule types to apply fixes for. - * @property {string[]} globals An array of global variables to declare. - * @property {boolean} ignore False disables use of .eslintignore. - * @property {string} ignorePath The ignore file to use instead of .eslintignore. - * @property {string|string[]} ignorePattern One or more glob patterns to ignore. - * @property {boolean} useEslintrc False disables looking for .eslintrc - * @property {string} parser The name of the parser to use. - * @property {ParserOptions} parserOptions An object of parserOption settings to use. - * @property {string[]} plugins An array of plugins to load. - * @property {Record} rules An object of rules to use. - * @property {string[]} rulePaths An array of directories to load custom rules from. - * @property {boolean} reportUnusedDisableDirectives `true` adds reports for unused eslint-disable directives - * @property {boolean} globInputPaths Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file. - * @property {string} resolvePluginsRelativeTo The folder where plugins should be resolved from, defaulting to the CWD + * @property {boolean} [allowInlineConfig] Enable or disable inline configuration comments. + * @property {ConfigData} [baseConfig] Base config object, extended by all configs used with this CLIEngine instance + * @property {boolean} [cache] Enable result caching. + * @property {string} [cacheLocation] The cache file to use instead of .eslintcache. + * @property {string} [configFile] The configuration file to use. + * @property {string} [cwd] The value to use for the current working directory. + * @property {string[]} [envs] An array of environments to load. + * @property {string[]|null} [extensions] An array of file extensions to check. + * @property {boolean|Function} [fix] Execute in autofix mode. If a function, should return a boolean. + * @property {string[]} [fixTypes] Array of rule types to apply fixes for. + * @property {string[]} [globals] An array of global variables to declare. + * @property {boolean} [ignore] False disables use of .eslintignore. + * @property {string} [ignorePath] The ignore file to use instead of .eslintignore. + * @property {string|string[]} [ignorePattern] One or more glob patterns to ignore. + * @property {boolean} [useEslintrc] False disables looking for .eslintrc + * @property {string} [parser] The name of the parser to use. + * @property {ParserOptions} [parserOptions] An object of parserOption settings to use. + * @property {string[]} [plugins] An array of plugins to load. + * @property {Record} [rules] An object of rules to use. + * @property {string[]} [rulePaths] An array of directories to load custom rules from. + * @property {boolean} [reportUnusedDisableDirectives] `true` adds reports for unused eslint-disable directives + * @property {boolean} [globInputPaths] Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file. + * @property {string} [resolvePluginsRelativeTo] The folder where plugins should be resolved from, defaulting to the CWD */ /** @@ -88,13 +89,6 @@ const validFixTypes = new Set(["problem", "suggestion", "layout"]); * @property {string} [output] The source code of the file that was linted, with as many fixes applied as possible. */ -/** - * Information of deprecated rules. - * @typedef {Object} DeprecatedRuleInfo - * @property {string} ruleId The rule ID. - * @property {string[]} replacedBy The rule IDs that replace this deprecated rule. - */ - /** * Linting results. * @typedef {Object} LintReport @@ -821,16 +815,22 @@ class CLIEngine { lintResultCache.reconcile(); } - // Collect used deprecated rules. - const usedDeprecatedRules = Array.from( - iterateRuleDeprecationWarnings(lastConfigArrays) - ); - debug(`Linting complete in: ${Date.now() - startTime}ms`); + let usedDeprecatedRules; + return { results, ...calculateStatsPerRun(results), - usedDeprecatedRules + + // Initialize it lazily because CLI and `ESLint` API don't use it. + get usedDeprecatedRules() { + if (!usedDeprecatedRules) { + usedDeprecatedRules = Array.from( + iterateRuleDeprecationWarnings(lastConfigArrays) + ); + } + return usedDeprecatedRules; + } }; } @@ -858,9 +858,9 @@ class CLIEngine { const startTime = Date.now(); const resolvedFilename = filename && path.resolve(cwd, filename); + // Clear the last used config arrays. lastConfigArrays.length = 0; - if (resolvedFilename && this.isPathIgnored(resolvedFilename)) { if (warnIgnored) { results.push(createIgnoreResult(resolvedFilename, cwd)); @@ -892,16 +892,22 @@ class CLIEngine { })); } - // Collect used deprecated rules. - const usedDeprecatedRules = Array.from( - iterateRuleDeprecationWarnings(lastConfigArrays) - ); - debug(`Linting complete in: ${Date.now() - startTime}ms`); + let usedDeprecatedRules; + return { results, ...calculateStatsPerRun(results), - usedDeprecatedRules + + // Initialize it lazily because CLI and `ESLint` API don't use it. + get usedDeprecatedRules() { + if (!usedDeprecatedRules) { + usedDeprecatedRules = Array.from( + iterateRuleDeprecationWarnings(lastConfigArrays) + ); + } + return usedDeprecatedRules; + } }; } @@ -955,11 +961,10 @@ class CLIEngine { } /** - * Returns the formatter representing the given format or null if no formatter - * with the given name can be found. + * Returns the formatter representing the given format or null if the `format` is not a string. * @param {string} [format] The name of the format to load or the path to a * custom formatter. - * @returns {Function} The formatter function or null if not found. + * @returns {(Function|null)} The formatter function or null if the `format` is not a string. */ getFormatter(format) { diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index b1429af6ad9..fa3fdb3bedd 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -817,7 +817,7 @@ class ConfigArrayFactory { if (configData) { return this._normalizeConfigData(configData, { ...ctx, - filePath: plugin.filePath, + filePath: plugin.filePath || ctx.filePath, name: `${ctx.name} » plugin:${plugin.id}/${configName}` }); } @@ -978,7 +978,7 @@ class ConfigArrayFactory { if (plugin) { return new ConfigDependency({ definition: normalizePlugin(plugin), - filePath: ctx.filePath, + filePath: "", // It's unknown where the plugin came from. id, importerName: ctx.name, importerPath: ctx.filePath diff --git a/lib/cli-engine/config-array/config-array.js b/lib/cli-engine/config-array/config-array.js index b3434198b19..42a7362737f 100644 --- a/lib/cli-engine/config-array/config-array.js +++ b/lib/cli-engine/config-array/config-array.js @@ -107,7 +107,7 @@ function getMatchedIndices(elements, filePath) { for (let i = elements.length - 1; i >= 0; --i) { const element = elements[i]; - if (!element.criteria || element.criteria.test(filePath)) { + if (!element.criteria || (filePath && element.criteria.test(filePath))) { indices.push(i); } } diff --git a/lib/cli.js b/lib/cli.js index 815ce68c22f..ce11878008f 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -17,105 +17,176 @@ const fs = require("fs"), path = require("path"), - { CLIEngine } = require("./cli-engine"), - options = require("./options"), + { promisify } = require("util"), + { ESLint } = require("./eslint"), + CLIOptions = require("./options"), log = require("./shared/logging"), RuntimeInfo = require("./shared/runtime-info"); const debug = require("debug")("eslint:cli"); +//------------------------------------------------------------------------------ +// Types +//------------------------------------------------------------------------------ + +/** @typedef {import("./eslint/eslint").ESLintOptions} ESLintOptions */ +/** @typedef {import("./eslint/eslint").LintMessage} LintMessage */ +/** @typedef {import("./eslint/eslint").LintResult} LintResult */ + //------------------------------------------------------------------------------ // Helpers //------------------------------------------------------------------------------ +const mkdir = promisify(fs.mkdir); +const stat = promisify(fs.stat); +const writeFile = promisify(fs.writeFile); + /** * Predicate function for whether or not to apply fixes in quiet mode. * If a message is a warning, do not apply a fix. - * @param {LintResult} lintResult The lint result. + * @param {LintMessage} message The lint result. * @returns {boolean} True if the lint message is an error (and thus should be * autofixed), false otherwise. */ -function quietFixPredicate(lintResult) { - return lintResult.severity === 2; +function quietFixPredicate(message) { + return message.severity === 2; } /** * Translates the CLI options into the options expected by the CLIEngine. * @param {Object} cliOptions The CLI options to translate. - * @returns {CLIEngineOptions} The options object for the CLIEngine. + * @returns {ESLintOptions} The options object for the CLIEngine. * @private */ -function translateOptions(cliOptions) { +function translateOptions({ + cache, + cacheFile, + cacheLocation, + config, + env, + errorOnUnmatchedPattern, + eslintrc, + ext, + fix, + fixDryRun, + fixType, + global, + ignore, + ignorePath, + ignorePattern, + inlineConfig, + parser, + parserOptions, + plugin, + quiet, + reportUnusedDisableDirectives, + resolvePluginsRelativeTo, + rule, + rulesdir +}) { return { - envs: cliOptions.env, - extensions: cliOptions.ext, - rules: cliOptions.rule, - plugins: cliOptions.plugin, - globals: cliOptions.global, - ignore: cliOptions.ignore, - ignorePath: cliOptions.ignorePath, - ignorePattern: cliOptions.ignorePattern, - configFile: cliOptions.config, - rulePaths: cliOptions.rulesdir, - useEslintrc: cliOptions.eslintrc, - parser: cliOptions.parser, - parserOptions: cliOptions.parserOptions, - cache: cliOptions.cache, - cacheFile: cliOptions.cacheFile, - cacheLocation: cliOptions.cacheLocation, - fix: (cliOptions.fix || cliOptions.fixDryRun) && (cliOptions.quiet ? quietFixPredicate : true), - fixTypes: cliOptions.fixType, - allowInlineConfig: cliOptions.inlineConfig, - reportUnusedDisableDirectives: cliOptions.reportUnusedDisableDirectives, - resolvePluginsRelativeTo: cliOptions.resolvePluginsRelativeTo, - errorOnUnmatchedPattern: cliOptions.errorOnUnmatchedPattern + allowInlineConfig: inlineConfig, + cache, + cacheLocation: cacheLocation || cacheFile, + errorOnUnmatchedPattern, + extensions: ext, + fix: (fix || fixDryRun) && (quiet ? quietFixPredicate : true), + fixTypes: fixType, + ignore, + ignorePath, + overrideConfig: { + env: env && env.reduce((obj, name) => { + obj[name] = true; + return obj; + }, {}), + globals: global && global.reduce((obj, name) => { + if (name.endsWith(":true")) { + obj[name.slice(0, -5)] = "writable"; + } else { + obj[name] = "readonly"; + } + return obj; + }, {}), + ignorePatterns: ignorePattern, + parser, + parserOptions, + plugins: plugin, + rules: rule + }, + overrideConfigFile: config, + reportUnusedDisableDirectives: reportUnusedDisableDirectives ? "error" : void 0, + resolvePluginsRelativeTo, + rulePaths: rulesdir, + useEslintrc: eslintrc }; } +/** + * Count error messages. + * @param {LintResult[]} results The lint results. + * @returns {{errorCount:number;warningCount:number}} The number of error messages. + */ +function countErrors(results) { + let errorCount = 0; + let warningCount = 0; + + for (const result of results) { + errorCount += result.errorCount; + warningCount += result.warningCount; + } + + return { errorCount, warningCount }; +} + +/** + * Check if a given file path is a directory or not. + * @param {string} filePath The path to a file to check. + * @returns {Promise} `true` if the given path is a directory. + */ +async function isDirectory(filePath) { + try { + return (await stat(filePath)).isDirectory(); + } catch (error) { + if (error.code === "ENOENT" || error.code === "ENOTDIR") { + return false; + } + throw error; + } +} + /** * Outputs the results of the linting. - * @param {CLIEngine} engine The CLIEngine to use. + * @param {ESLint} engine The ESLint instance to use. * @param {LintResult[]} results The results to print. * @param {string} format The name of the formatter to use or the path to the formatter. * @param {string} outputFile The path for the output file. - * @returns {boolean} True if the printing succeeds, false if not. + * @returns {Promise} True if the printing succeeds, false if not. * @private */ -function printResults(engine, results, format, outputFile) { +async function printResults(engine, results, format, outputFile) { let formatter; - let rulesMeta; try { - formatter = engine.getFormatter(format); + formatter = await engine.loadFormatter(format); } catch (e) { log.error(e.message); return false; } - const output = formatter(results, { - get rulesMeta() { - if (!rulesMeta) { - rulesMeta = {}; - for (const [ruleId, rule] of engine.getRules()) { - rulesMeta[ruleId] = rule.meta; - } - } - return rulesMeta; - } - }); + const output = formatter.format(results); if (output) { if (outputFile) { const filePath = path.resolve(process.cwd(), outputFile); - if (fs.existsSync(filePath) && fs.statSync(filePath).isDirectory()) { + if (await isDirectory(filePath)) { log.error("Cannot write to output file path, it is a directory: %s", outputFile); return false; } try { - fs.mkdirSync(path.dirname(filePath), { recursive: true }); - fs.writeFileSync(filePath, output); + await mkdir(path.dirname(filePath), { recursive: true }); + await writeFile(filePath, output); } catch (ex) { log.error("There was a problem writing the output file:\n%s", ex); return false; @@ -126,7 +197,6 @@ function printResults(engine, results, format, outputFile) { } return true; - } //------------------------------------------------------------------------------ @@ -143,28 +213,33 @@ const cli = { * Executes the CLI based on an array of arguments that is passed in. * @param {string|Array|Object} args The arguments to process. * @param {string} [text] The text to lint (used for TTY). - * @returns {int} The exit code for the operation. + * @returns {Promise} The exit code for the operation. */ - execute(args, text) { + async execute(args, text) { if (Array.isArray(args)) { debug("CLI args: %o", args.slice(2)); } - - let currentOptions; + let options; try { - currentOptions = options.parse(args); + options = CLIOptions.parse(args); } catch (error) { log.error(error.message); return 2; } - const files = currentOptions._; + const files = options._; const useStdin = typeof text === "string"; - if (currentOptions.version) { + if (options.help) { + log.info(CLIOptions.generateHelp()); + return 0; + } + if (options.version) { log.info(RuntimeInfo.version()); - } else if (currentOptions.envInfo) { + return 0; + } + if (options.envInfo) { try { log.info(RuntimeInfo.environment()); return 0; @@ -172,7 +247,9 @@ const cli = { log.error(err.message); return 2; } - } else if (currentOptions.printConfig) { + } + + if (options.printConfig) { if (files.length) { log.error("The --print-config option must be used with exactly one file name."); return 2; @@ -182,58 +259,67 @@ const cli = { return 2; } - const engine = new CLIEngine(translateOptions(currentOptions)); - const fileConfig = engine.getConfigForFile(currentOptions.printConfig); + const engine = new ESLint(translateOptions(options)); + const fileConfig = + await engine.calculateConfigForFile(options.printConfig); log.info(JSON.stringify(fileConfig, null, " ")); return 0; - } else if (currentOptions.help || (!files.length && !useStdin)) { - log.info(options.generateHelp()); - } else { - debug(`Running on ${useStdin ? "text" : "files"}`); - - if (currentOptions.fix && currentOptions.fixDryRun) { - log.error("The --fix option and the --fix-dry-run option cannot be used together."); - return 2; - } + } - if (useStdin && currentOptions.fix) { - log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead."); - return 2; - } + debug(`Running on ${useStdin ? "text" : "files"}`); - if (currentOptions.fixType && !currentOptions.fix && !currentOptions.fixDryRun) { - log.error("The --fix-type option requires either --fix or --fix-dry-run."); - return 2; - } + if (options.fix && options.fixDryRun) { + log.error("The --fix option and the --fix-dry-run option cannot be used together."); + return 2; + } + if (useStdin && options.fix) { + log.error("The --fix option is not available for piped-in code; use --fix-dry-run instead."); + return 2; + } + if (options.fixType && !options.fix && !options.fixDryRun) { + log.error("The --fix-type option requires either --fix or --fix-dry-run."); + return 2; + } - const engine = new CLIEngine(translateOptions(currentOptions)); - const report = useStdin ? engine.executeOnText(text, currentOptions.stdinFilename, true) : engine.executeOnFiles(files); + const engine = new ESLint(translateOptions(options)); + let results; - if (currentOptions.fix) { - debug("Fix mode enabled - applying fixes"); - CLIEngine.outputFixes(report); - } + if (useStdin) { + results = await engine.lintText(text, { + filePath: options.stdinFilename, + warnIgnored: true + }); + } else { + results = await engine.lintFiles(files); + } - if (currentOptions.quiet) { - debug("Quiet mode enabled - filtering out warnings"); - report.results = CLIEngine.getErrorResults(report.results); - } + if (options.fix) { + debug("Fix mode enabled - applying fixes"); + await ESLint.outputFixes(results); + } - if (printResults(engine, report.results, currentOptions.format, currentOptions.outputFile)) { - const tooManyWarnings = currentOptions.maxWarnings >= 0 && report.warningCount > currentOptions.maxWarnings; + if (options.quiet) { + debug("Quiet mode enabled - filtering out warnings"); + results = ESLint.getErrorResults(results); + } - if (!report.errorCount && tooManyWarnings) { - log.error("ESLint found too many warnings (maximum: %s).", currentOptions.maxWarnings); - } + if (await printResults(engine, results, options.format, options.outputFile)) { + const { errorCount, warningCount } = countErrors(results); + const tooManyWarnings = + options.maxWarnings >= 0 && warningCount > options.maxWarnings; - return (report.errorCount || tooManyWarnings) ? 1 : 0; + if (!errorCount && tooManyWarnings) { + log.error( + "ESLint found too many warnings (maximum: %s).", + options.maxWarnings + ); } - return 2; + return (errorCount || tooManyWarnings) ? 1 : 0; } - return 0; + return 2; } }; diff --git a/lib/eslint/eslint.js b/lib/eslint/eslint.js new file mode 100644 index 00000000000..d195aab09f1 --- /dev/null +++ b/lib/eslint/eslint.js @@ -0,0 +1,656 @@ +/** + * @fileoverview Main API Class + * @author Kai Cataldo + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const path = require("path"); +const fs = require("fs"); +const { promisify } = require("util"); +const { CLIEngine, getCLIEngineInternalSlots } = require("../cli-engine/cli-engine"); +const BuiltinRules = require("../rules"); +const { getRuleSeverity } = require("../shared/config-ops"); +const { version } = require("../../package.json"); + +//------------------------------------------------------------------------------ +// Typedefs +//------------------------------------------------------------------------------ + +/** @typedef {import("../cli-engine/cli-engine").LintReport} CLIEngineLintReport */ +/** @typedef {import("../shared/types").DeprecatedRuleInfo} DeprecatedRuleInfo */ +/** @typedef {import("../shared/types").ConfigData} ConfigData */ +/** @typedef {import("../shared/types").LintMessage} LintMessage */ +/** @typedef {import("../shared/types").Plugin} Plugin */ +/** @typedef {import("../shared/types").Rule} Rule */ +/** @typedef {import("./load-formatter").Formatter} Formatter */ + +/** + * The options with which to configure the ESLint instance. + * @typedef {Object} ESLintOptions + * @property {boolean} [allowInlineConfig] Enable or disable inline configuration comments. + * @property {ConfigData} [baseConfig] Base config object, extended by all configs used with this instance + * @property {boolean} [cache] Enable result caching. + * @property {string} [cacheLocation] The cache file to use instead of .eslintcache. + * @property {string} [cwd] The value to use for the current working directory. + * @property {boolean} [errorOnUnmatchedPattern] If `false` then `ESLint#lintFiles()` doesn't throw even if no target files found. Defaults to `true`. + * @property {string[]} [extensions] An array of file extensions to check. + * @property {boolean|Function} [fix] Execute in autofix mode. If a function, should return a boolean. + * @property {string[]} [fixTypes] Array of rule types to apply fixes for. + * @property {boolean} [globInputPaths] Set to false to skip glob resolution of input file paths to lint (default: true). If false, each input file paths is assumed to be a non-glob path to an existing file. + * @property {boolean} [ignore] False disables use of .eslintignore. + * @property {string} [ignorePath] The ignore file to use instead of .eslintignore. + * @property {ConfigData} [overrideConfig] Override config object, overrides all configs used with this instance + * @property {string} [overrideConfigFile] The configuration file to use. + * @property {Record} [plugins] An array of plugin implementations. + * @property {"error" | "warn" | "off"} [reportUnusedDisableDirectives] the severity to report unused eslint-disable directives. + * @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. + */ + +/** + * A rules metadata object. + * @typedef {Object} RulesMeta + * @property {string} id The plugin ID. + * @property {Object} definition The plugin definition. + */ + +/** + * A linting result. + * @typedef {Object} LintResult + * @property {string} filePath The path to the file that was linted. + * @property {LintMessage[]} messages All of the messages for the result. + * @property {number} errorCount Number of errors for the result. + * @property {number} warningCount Number of warnings for the result. + * @property {number} fixableErrorCount Number of fixable errors for the result. + * @property {number} fixableWarningCount Number of fixable warnings for the result. + * @property {string} [source] The source code of the file that was linted. + * @property {string} [output] The source code of the file that was linted, with as many fixes applied as possible. + * @property {DeprecatedRuleInfo[]} usedDeprecatedRules The list of used deprecated rules. + */ + +/** + * Private members for the `ESLint` instance. + * @typedef {Object} ESLintPrivateMembers + * @property {CLIEngine} cliEngine The wrapped CLIEngine instance. + * @property {ESLintOptions} options The options used to instantiate the ESLint instance. + */ + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const writeFile = promisify(fs.writeFile); + +/** + * The map with which to store private class members. + * @type {WeakMap} + */ +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. + */ +function isNonEmptyString(x) { + return typeof x === "string" && x.trim() !== ""; +} + +/** + * Check if a given value is an array of non-empty stringss or not. + * @param {any} x The value to check. + * @returns {boolean} `true` if `x` is an array of non-empty stringss. + */ +function isArrayOfNonEmptyString(x) { + return Array.isArray(x) && x.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. + */ +function isFixType(x) { + return x === "problem" || x === "suggestion" || x === "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. + */ +function isFixTypeArray(x) { + return Array.isArray(x) && x.every(isFixType); +} + +/** + * The error for invalid options. + */ +class ESLintInvalidOptionsError extends Error { + constructor(messages) { + super(`Invalid Options:\n- ${messages.join("\n- ")}`); + this.code = "ESLINT_INVALID_OPTIONS"; + Error.captureStackTrace(this, ESLintInvalidOptionsError); + } +} + +/** + * Validates and normalizes options for the wrapped CLIEngine instance. + * @param {ESLintOptions} options The options to process. + * @returns {ESLintOptions} The normalized options. + */ +function processOptions({ + allowInlineConfig = true, // ← we cannot use `overrideConfig.noInlineConfig` instead because `allowInlineConfig` has side-effect that suppress warnings that show inline configs are ignored. + baseConfig = null, + cache = false, + cacheLocation = ".eslintcache", + cwd = process.cwd(), + errorOnUnmatchedPattern = true, + extensions = null, // ← should be null by default because if it's an array then it suppresses RFC20 feature. + fix = false, + fixTypes = null, // ← should be null by default because if it's an array then it suppresses rules that don't have the `meta.type` property. + globInputPaths = true, + ignore = true, + ignorePath = null, // ← should be null by default because if it's a string then it may throw ENOENT. + overrideConfig = null, + overrideConfigFile = null, + plugins = {}, + reportUnusedDisableDirectives = null, // ← should be null by default because if it's a string then it overrides the 'reportUnusedDisableDirectives' setting in config files. And we cannot use `overrideConfig.reportUnusedDisableDirectives` instead because we cannot configure the `error` severity with that. + resolvePluginsRelativeTo = null, // ← should be null by default because if it's a string then it suppresses RFC47 feature. + rulePaths = [], + useEslintrc = true, + ...unknownOptions +}) { + const errors = []; + const unknownOptionKeys = Object.keys(unknownOptions); + + if (unknownOptionKeys.length >= 1) { + errors.push(`Unknown options: ${unknownOptionKeys.join(", ")}`); + if (unknownOptionKeys.includes("cacheFile")) { + errors.push("'cacheFile' has been removed. Please use the 'cacheLocation' option instead."); + } + if (unknownOptionKeys.includes("configFile")) { + errors.push("'configFile' has been removed. Please use the 'overrideConfigFile' option instead."); + } + if (unknownOptionKeys.includes("envs")) { + errors.push("'envs' has been removed. Please use the 'overrideConfig.env' option instead."); + } + if (unknownOptionKeys.includes("globals")) { + errors.push("'globals' has been removed. Please use the 'overrideConfig.globals' option instead."); + } + if (unknownOptionKeys.includes("ignorePattern")) { + errors.push("'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead."); + } + if (unknownOptionKeys.includes("parser")) { + errors.push("'parser' has been removed. Please use the 'overrideConfig.parser' option instead."); + } + if (unknownOptionKeys.includes("parserOptions")) { + errors.push("'parserOptions' has been removed. Please use the 'overrideConfig.parserOptions' option instead."); + } + if (unknownOptionKeys.includes("rules")) { + errors.push("'rules' has been removed. Please use the 'overrideConfig.rules' option instead."); + } + } + if (typeof allowInlineConfig !== "boolean") { + errors.push("'allowInlineConfig' must be a boolean."); + } + if (typeof baseConfig !== "object") { + errors.push("'baseConfig' must be an object or null."); + } + if (typeof cache !== "boolean") { + errors.push("'cache' must be a boolean."); + } + if (!isNonEmptyString(cacheLocation)) { + errors.push("'cacheLocation' must be a non-empty string."); + } + if (!isNonEmptyString(cwd) || !path.isAbsolute(cwd)) { + errors.push("'cwd' must be an absolute path."); + } + if (typeof errorOnUnmatchedPattern !== "boolean") { + errors.push("'errorOnUnmatchedPattern' must be a boolean."); + } + if (!isArrayOfNonEmptyString(extensions) && extensions !== null) { + errors.push("'extensions' must be an array of non-empty strings or null."); + } + if (typeof fix !== "boolean" && typeof fix !== "function") { + errors.push("'fix' must be a boolean or a function."); + } + if (fixTypes !== null && !isFixTypeArray(fixTypes)) { + errors.push("'fixTypes' must be an array of any of \"problem\", \"suggestion\", and \"layout\"."); + } + if (typeof globInputPaths !== "boolean") { + errors.push("'globInputPaths' must be a boolean."); + } + if (typeof ignore !== "boolean") { + errors.push("'ignore' must be a boolean."); + } + if (!isNonEmptyString(ignorePath) && ignorePath !== null) { + errors.push("'ignorePath' must be a non-empty string or null."); + } + if (typeof overrideConfig !== "object") { + errors.push("'overrideConfig' must be an object or null."); + } + if (!isNonEmptyString(overrideConfigFile) && overrideConfigFile !== null) { + errors.push("'overrideConfigFile' must be a non-empty string or null."); + } + if (typeof plugins !== "object") { + errors.push("'plugins' must be an object or null."); + } else if (plugins !== null && Object.keys(plugins).includes("")) { + errors.push("'plugins' must not include an empty string."); + } + if (Array.isArray(plugins)) { + errors.push("'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead."); + } + if ( + reportUnusedDisableDirectives !== "error" && + reportUnusedDisableDirectives !== "warn" && + reportUnusedDisableDirectives !== "off" && + reportUnusedDisableDirectives !== null + ) { + errors.push("'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null."); + } + if ( + !isNonEmptyString(resolvePluginsRelativeTo) && + resolvePluginsRelativeTo !== null + ) { + errors.push("'resolvePluginsRelativeTo' must be a non-empty string or null."); + } + if (!isArrayOfNonEmptyString(rulePaths)) { + errors.push("'rulePaths' must be an array of non-empty strings."); + } + if (typeof useEslintrc !== "boolean") { + errors.push("'useElintrc' must be a boolean."); + } + + if (errors.length > 0) { + throw new ESLintInvalidOptionsError(errors); + } + + return { + allowInlineConfig, + baseConfig, + cache, + cacheLocation, + configFile: overrideConfigFile, + cwd, + errorOnUnmatchedPattern, + extensions, + fix, + fixTypes, + globInputPaths, + ignore, + ignorePath, + reportUnusedDisableDirectives, + resolvePluginsRelativeTo, + rulePaths, + useEslintrc + }; +} + +/** + * Check if a value has one or more properties and that value is not undefined. + * @param {any} obj The value to check. + * @returns {boolean} `true` if `obj` has one or more properties that that value is not undefined. + */ +function hasDefinedProperty(obj) { + if (typeof obj === "object" && obj !== null) { + for (const key in obj) { + if (typeof obj[key] !== "undefined") { + return true; + } + } + } + return false; +} + +/** + * Create rulesMeta object. + * @param {Map} rules a map of rules from which to generate the object. + * @returns {Object} metadata for all enabled rules. + */ +function createRulesMeta(rules) { + return Array.from(rules).reduce((retVal, [id, rule]) => { + retVal[id] = rule.meta; + return retVal; + }, {}); +} + +/** @type {WeakMap} */ +const usedDeprecatedRulesCache = new WeakMap(); + +/** + * Create used deprecated rule list. + * @param {CLIEngine} cliEngine The CLIEngine instance. + * @param {string} maybeFilePath The absolute path to a lint target file or `""`. + * @returns {DeprecatedRuleInfo[]} The used deprecated rule list. + */ +function getOrFindUsedDeprecatedRules(cliEngine, maybeFilePath) { + const { + configArrayFactory, + options: { cwd } + } = getCLIEngineInternalSlots(cliEngine); + const filePath = path.isAbsolute(maybeFilePath) + ? maybeFilePath + : path.join(cwd, "__placeholder__.js"); + const configArray = configArrayFactory.getConfigArrayForFile(filePath); + const config = configArray.extractConfig(filePath); + + // Most files use the same config, so cache it. + if (!usedDeprecatedRulesCache.has(config)) { + const pluginRules = configArray.pluginRules; + const retv = []; + + for (const [ruleId, ruleConf] of Object.entries(config.rules)) { + if (getRuleSeverity(ruleConf) === 0) { + continue; + } + const rule = pluginRules.get(ruleId) || BuiltinRules.get(ruleId); + const meta = rule && rule.meta; + + if (meta && meta.deprecated) { + retv.push({ ruleId, replacedBy: meta.replacedBy || [] }); + } + } + + usedDeprecatedRulesCache.set(config, Object.freeze(retv)); + } + + return usedDeprecatedRulesCache.get(config); +} + +/** + * Processes the linting results generated by a CLIEngine linting report to + * match the ESLint class's API. + * @param {CLIEngine} cliEngine The CLIEngine instance. + * @param {CLIEngineLintReport} report The CLIEngine linting report to process. + * @returns {LintResult[]} The processed linting results. + */ +function processCLIEngineLintReport(cliEngine, { results }) { + const descriptor = { + configurable: true, + enumerable: true, + get() { + return getOrFindUsedDeprecatedRules(cliEngine, this.filePath); + } + }; + + for (const result of results) { + Object.defineProperty(result, "usedDeprecatedRules", descriptor); + } + + return results; +} + +/** + * An Array.prototype.sort() compatible compare function to order results by their file path. + * @param {LintResult} a The first lint result. + * @param {LintResult} b The second lint result. + * @returns {number} An integer representing the order in which the two results should occur. + */ +function compareResultsByFilePath(a, b) { + if (a.filePath < b.filePath) { + return -1; + } + + if (a.filePath > b.filePath) { + return 1; + } + + return 0; +} + +class ESLint { + + /** + * Creates a new instance of the main ESLint API. + * @param {ESLintOptions} options The options for this instance. + */ + constructor(options = {}) { + const processedOptions = processOptions(options); + const cliEngine = new CLIEngine(processedOptions); + const { + additionalPluginPool, + configArrayFactory, + lastConfigArrays + } = getCLIEngineInternalSlots(cliEngine); + let updated = false; + + /* + * Address `plugins` to add plugin implementations. + * Operate the `additionalPluginPool` internal slot directly to avoid + * using `addPlugin(id, plugin)` method that resets cache everytime. + */ + if (options.plugins) { + for (const [id, plugin] of Object.entries(options.plugins)) { + additionalPluginPool.set(id, plugin); + updated = true; + } + } + + /* + * Address `overrideConfig` to set override config. + * Operate the `configArrayFactory` internal slot directly because this + * functionality doesn't exist as the public API of CLIEngine. + */ + if (hasDefinedProperty(options.overrideConfig)) { + configArrayFactory.setOverrideConfig(options.overrideConfig); + updated = true; + } + + // Update caches. + if (updated) { + configArrayFactory.clearCache(); + lastConfigArrays[0] = configArrayFactory.getConfigArrayForFile(); + } + + // Initialize private properties. + privateMembersMap.set(this, { + cliEngine, + options: processedOptions + }); + } + + /** + * The version text. + * @type {string} + */ + static get version() { + return version; + } + + /** + * Outputs fixes from the given results to files. + * @param {LintResult[]} results The lint results. + * @returns {Promise} Returns a promise that is used to track side effects. + */ + static async outputFixes(results) { + if (!Array.isArray(results)) { + throw new Error("'results' must be an array"); + } + + await Promise.all( + results + .filter(result => { + if (typeof result !== "object" || result === null) { + throw new Error("'results' must include only objects"); + } + return ( + typeof result.output === "string" && + path.isAbsolute(result.filePath) + ); + }) + .map(r => writeFile(r.filePath, r.output)) + ); + } + + /** + * Returns results that only contains errors. + * @param {LintResult[]} results The results to filter. + * @returns {LintResult[]} The filtered results. + */ + static getErrorResults(results) { + return CLIEngine.getErrorResults(results); + } + + /** + * Executes the current configuration on an array of file and directory names. + * @param {string[]} patterns An array of file and directory names. + * @returns {Promise} 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"); + } + const { cliEngine } = privateMembersMap.get(this); + + return processCLIEngineLintReport( + cliEngine, + cliEngine.executeOnFiles(patterns) + ); + } + + /** + * Executes the current configuration on text. + * @param {string} code A string of JavaScript code to lint. + * @param {Object} [options] The options. + * @param {string} [options.filePath] The path to the file of the source code. + * @param {boolean} [options.warnIgnored] When set to true, warn if given filePath is an ignored path. + * @returns {Promise} The results of linting the string of code given. + */ + async lintText(code, options = {}) { + if (typeof code !== "string") { + throw new Error("'code' must be a string"); + } + if (typeof options !== "object") { + throw new Error("'options' must be an object, null, or undefined"); + } + const { + filePath, + warnIgnored = false, + ...unknownOptions + } = options || {}; + + for (const key of Object.keys(unknownOptions)) { + throw new Error(`'options' must not include the unknown option '${key}'`); + } + if (filePath !== void 0 && !isNonEmptyString(filePath)) { + throw new Error("'options.filePath' must be a non-empty string or undefined"); + } + if (typeof warnIgnored !== "boolean") { + throw new Error("'options.warnIgnored' must be a boolean or undefined"); + } + + const { cliEngine } = privateMembersMap.get(this); + + return processCLIEngineLintReport( + cliEngine, + cliEngine.executeOnText(code, filePath, warnIgnored) + ); + } + + /** + * Returns the formatter representing the given formatter name. + * @param {string} [name] The name of the formattter to load. + * The following values are allowed: + * - `undefined` ... Load `stylish` builtin formatter. + * - A builtin formatter name ... Load the builtin formatter. + * - A thirdparty formatter name: + * - `foo` → `eslint-formatter-foo` + * - `@foo` → `@foo/eslint-formatter` + * - `@foo/bar` → `@foo/eslint-formatter-bar` + * - A file path ... Load the file. + * @returns {Promise} A promise resolving to the formatter object. + * This promise will be rejected if the given formatter was not found or not + * a function. + */ + async loadFormatter(name = "stylish") { + if (typeof name !== "string") { + throw new Error("'name' must be a string"); + } + + const { cliEngine } = privateMembersMap.get(this); + const formatter = cliEngine.getFormatter(name); + + if (typeof formatter !== "function") { + throw new Error(`Formatter must be a function, but got a ${typeof formatter}.`); + } + + return { + + /** + * The main formatter method. + * @param {LintResults[]} results The lint results to format. + * @returns {string} The formatted lint results. + */ + format(results) { + let rulesMeta = null; + + results.sort(compareResultsByFilePath); + + return formatter(results, { + get rulesMeta() { + if (!rulesMeta) { + rulesMeta = createRulesMeta(cliEngine.getRules()); + } + + return rulesMeta; + } + }); + } + }; + } + + /** + * Returns a configuration object for the given file based on the CLI options. + * This is the same logic used by the ESLint CLI executable to determine + * configuration for each file it processes. + * @param {string} filePath The path of the file to retrieve a config object for. + * @returns {Promise} A configuration object for the file. + */ + async calculateConfigForFile(filePath) { + if (!isNonEmptyString(filePath)) { + throw new Error("'filePath' must be a non-empty string"); + } + const { cliEngine } = privateMembersMap.get(this); + + return cliEngine.getConfigForFile(filePath); + } + + /** + * Checks if a given path is ignored by ESLint. + * @param {string} filePath The path of the file to check. + * @returns {Promise} Whether or not the given path is ignored. + */ + async isPathIgnored(filePath) { + if (!isNonEmptyString(filePath)) { + throw new Error("'filePath' must be a non-empty string"); + } + const { cliEngine } = privateMembersMap.get(this); + + return cliEngine.isPathIgnored(filePath); + } +} + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + +module.exports = { + ESLint, + + /** + * Get the private class members of a given ESLint instance for tests. + * @param {ESLint} instance The ESLint instance to get. + * @returns {ESLintPrivateMembers} The instance's private class members. + */ + getESLintPrivateMembers(instance) { + return privateMembersMap.get(instance); + } +}; diff --git a/lib/eslint/index.js b/lib/eslint/index.js new file mode 100644 index 00000000000..c9185ee0eba --- /dev/null +++ b/lib/eslint/index.js @@ -0,0 +1,7 @@ +"use strict"; + +const { ESLint } = require("./eslint"); + +module.exports = { + ESLint +}; diff --git a/lib/shared/types.js b/lib/shared/types.js index bf37327fa24..bbd95d1b378 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -141,3 +141,10 @@ module.exports = {}; * @property {Record} [processors] The definition of plugin processors. * @property {Record} [rules] The definition of plugin rules. */ + +/** + * Information of deprecated rules. + * @typedef {Object} DeprecatedRuleInfo + * @property {string} ruleId The rule ID. + * @property {string[]} replacedBy The rule IDs that replace this deprecated rule. + */ diff --git a/package.json b/package.json index cc3b89e8f84..9f6f94582cb 100644 --- a/package.json +++ b/package.json @@ -94,6 +94,7 @@ "core-js": "^3.1.3", "dateformat": "^3.0.3", "ejs": "^3.0.2", + "escape-string-regexp": "^3.0.0", "eslint": "file:.", "eslint-config-eslint": "file:packages/eslint-config-eslint", "eslint-plugin-eslint-plugin": "^2.2.1", diff --git a/tests/lib/cli-engine/_utils.js b/tests/_utils/in-memory-fs.js similarity index 77% rename from tests/lib/cli-engine/_utils.js rename to tests/_utils/in-memory-fs.js index 1d1bcd275b0..5ef4f0453f3 100644 --- a/tests/lib/cli-engine/_utils.js +++ b/tests/_utils/in-memory-fs.js @@ -2,12 +2,13 @@ * @fileoverview Define classes what use the in-memory file system. * * This provides utilities to test `ConfigArrayFactory`, - * `CascadingConfigArrayFactory`, `FileEnumerator`, and `CLIEngine`. + * `CascadingConfigArrayFactory`, `FileEnumerator`, `CLIEngine`, and `ESLint`. * * - `defineConfigArrayFactoryWithInMemoryFileSystem({ cwd, files })` * - `defineCascadingConfigArrayFactoryWithInMemoryFileSystem({ cwd, files })` * - `defineFileEnumeratorWithInMemoryFileSystem({ cwd, files })` * - `defineCLIEngineWithInMemoryFileSystem({ cwd, files })` + * - `defineESLintWithInMemoryFileSystem({ cwd, files })` * * Those functions define correspond classes with the in-memory file system. * Those search config files, parsers, and plugins in the `files` option via the @@ -55,23 +56,25 @@ const path = require("path"); const vm = require("vm"); +const { Volume, createFsFromVolume } = require("memfs"); const Proxyquire = require("proxyquire/lib/proxyquire"); -const { defineInMemoryFs } = require("../_utils"); const CascadingConfigArrayFactoryPath = - require.resolve("../../../lib/cli-engine/cascading-config-array-factory"); + require.resolve("../../lib/cli-engine/cascading-config-array-factory"); const CLIEnginePath = - require.resolve("../../../lib/cli-engine/cli-engine"); + require.resolve("../../lib/cli-engine/cli-engine"); const ConfigArrayFactoryPath = - require.resolve("../../../lib/cli-engine/config-array-factory"); + require.resolve("../../lib/cli-engine/config-array-factory"); const FileEnumeratorPath = - require.resolve("../../../lib/cli-engine/file-enumerator"); + require.resolve("../../lib/cli-engine/file-enumerator"); const LoadRulesPath = - require.resolve("../../../lib/cli-engine/load-rules"); + require.resolve("../../lib/cli-engine/load-rules"); +const ESLintPath = + require.resolve("../../lib/eslint/eslint"); const ESLintAllPath = - require.resolve("../../../conf/eslint-all"); + require.resolve("../../conf/eslint-all"); const ESLintRecommendedPath = - require.resolve("../../../conf/eslint-recommended"); + require.resolve("../../conf/eslint-recommended"); // Ensure the needed files has been loaded and cached. require(CascadingConfigArrayFactoryPath); @@ -79,6 +82,7 @@ require(CLIEnginePath); require(ConfigArrayFactoryPath); require(FileEnumeratorPath); require(LoadRulesPath); +require(ESLintPath); require("js-yaml"); require("espree"); @@ -236,12 +240,57 @@ function fsImportFresh(fs, stubs, absolutePath) { ); } +/** + * Define in-memory file system. + * @param {Object} options The options. + * @param {() => string} [options.cwd] The current working directory. + * @param {Object} [options.files] The initial files definition in the in-memory file system. + * @returns {import("fs")} The stubbed `ConfigArrayFactory` class. + */ +function defineInMemoryFs({ + cwd = process.cwd, + files = {} +} = {}) { + + /** + * The in-memory file system for this mock. + * @type {import("fs")} + */ + const fs = createFsFromVolume(new Volume()); + + fs.mkdirSync(cwd(), { recursive: true }); + + /* + * Write all files to the in-memory file system and compile all JavaScript + * files then set to `stubs`. + */ + (function initFiles(directoryPath, definition) { + for (const [filename, content] of Object.entries(definition)) { + const filePath = path.resolve(directoryPath, filename); + const parentPath = path.dirname(filePath); + + if (typeof content === "object") { + initFiles(filePath, content); + } else if (typeof content === "string") { + if (!fs.existsSync(parentPath)) { + fs.mkdirSync(parentPath, { recursive: true }); + } + fs.writeFileSync(filePath, content); + } else { + throw new Error(`Invalid content: ${typeof content}`); + } + } + }(cwd(), files)); + + return fs; +} + /** * Define stubbed `ConfigArrayFactory` class what uses the in-memory file system. * @param {Object} options The options. * @param {() => string} [options.cwd] The current working directory. * @param {Object} [options.files] The initial files definition in the in-memory file system. - * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../../lib/shared/relative-module-resolver"), ConfigArrayFactory: import("../../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"] }} The stubbed `ConfigArrayFactory` class. + * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../lib/shared/relative-module-resolver"), ConfigArrayFactory: import("../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"] }} The stubbed `ConfigArrayFactory` class. */ function defineConfigArrayFactoryWithInMemoryFileSystem({ cwd = process.cwd, @@ -438,9 +487,55 @@ function defineCLIEngineWithInMemoryFileSystem({ }; } +/** + * Define stubbed `ESLint` class that uses the in-memory file system. + * @param {Object} options The options. + * @param {() => string} [options.cwd] The current working directory. + * @param {Object} [options.files] The initial files definition in the in-memory file system. + * @returns {{ fs: import("fs"), RelativeModuleResolver: import("../../lib/shared/relative-module-resolver"), ConfigArrayFactory: import("../../lib/cli-engine/config-array-factory")["ConfigArrayFactory"], CascadingConfigArrayFactory: import("../../lib/cli-engine/cascading-config-array-factory")["CascadingConfigArrayFactory"], FileEnumerator: import("../../lib/cli-engine/file-enumerator")["FileEnumerator"], ESLint: import("../../lib/eslint/eslint")["ESLint"], getCLIEngineInternalSlots: import("../../lib//eslint/eslint")["getESLintInternalSlots"] }} The stubbed `ESLint` class. + */ +function defineESLintWithInMemoryFileSystem({ + cwd = process.cwd, + files = {} +} = {}) { + const { + fs, + RelativeModuleResolver, + ConfigArrayFactory, + CascadingConfigArrayFactory, + FileEnumerator, + CLIEngine, + getCLIEngineInternalSlots + } = defineCLIEngineWithInMemoryFileSystem({ cwd, files }); + const { ESLint, getESLintPrivateMembers } = proxyquire(ESLintPath, { + "../cli-engine/cli-engine": { CLIEngine, getCLIEngineInternalSlots } + }); + + // Override the default cwd. + return { + fs, + RelativeModuleResolver, + ConfigArrayFactory, + CascadingConfigArrayFactory, + FileEnumerator, + CLIEngine, + getCLIEngineInternalSlots, + ESLint: cwd === process.cwd + ? ESLint + : class extends ESLint { + constructor(options) { + super({ cwd: cwd(), ...options }); + } + }, + getESLintPrivateMembers + }; +} + module.exports = { + defineInMemoryFs, defineConfigArrayFactoryWithInMemoryFileSystem, defineCascadingConfigArrayFactoryWithInMemoryFileSystem, defineFileEnumeratorWithInMemoryFileSystem, - defineCLIEngineWithInMemoryFileSystem + defineCLIEngineWithInMemoryFileSystem, + defineESLintWithInMemoryFileSystem }; diff --git a/tests/_utils/index.js b/tests/_utils/index.js new file mode 100644 index 00000000000..0431e3aced4 --- /dev/null +++ b/tests/_utils/index.js @@ -0,0 +1,39 @@ +"use strict"; + +const { + defineInMemoryFs, + defineConfigArrayFactoryWithInMemoryFileSystem, + defineCascadingConfigArrayFactoryWithInMemoryFileSystem, + defineFileEnumeratorWithInMemoryFileSystem, + defineCLIEngineWithInMemoryFileSystem, + defineESLintWithInMemoryFileSystem +} = require("./in-memory-fs"); + + +/** + * Prevents leading spaces in a multiline template literal from appearing in the resulting string + * @param {string[]} strings The strings in the template literal + * @param {any[]} values The interpolation values in the template literal. + * @returns {string} The template literal, with spaces removed from all lines + */ +function unIndent(strings, ...values) { + const text = strings + .map((s, i) => (i === 0 ? s : values[i - 1] + s)) + .join(""); + const lines = text.replace(/^\n/u, "").replace(/\n\s*$/u, "").split("\n"); + const lineIndents = lines.filter(line => line.trim()).map(line => line.match(/ */u)[0].length); + const minLineIndent = Math.min(...lineIndents); + + return lines.map(line => line.slice(minLineIndent)).join("\n"); +} + + +module.exports = { + unIndent, + defineInMemoryFs, + defineConfigArrayFactoryWithInMemoryFileSystem, + defineCascadingConfigArrayFactoryWithInMemoryFileSystem, + defineFileEnumeratorWithInMemoryFileSystem, + defineCLIEngineWithInMemoryFileSystem, + defineESLintWithInMemoryFileSystem +}; diff --git a/tests/bin/eslint.js b/tests/bin/eslint.js index 4895dff0523..cfc5128dc57 100644 --- a/tests/bin/eslint.js +++ b/tests/bin/eslint.js @@ -92,7 +92,8 @@ describe("bin/eslint.js", () => { warningCount: 0, fixableErrorCount: 0, fixableWarningCount: 0, - output: "var foo = bar;\n" + output: "var foo = bar;\n", + usedDeprecatedRules: [] } ]); @@ -178,8 +179,8 @@ describe("bin/eslint.js", () => { describe("running on files", () => { it("has exit code 0 if no linting errors occur", () => assertExitCode(runESLint(["bin/eslint.js"]), 0)); - it("has exit code 0 if a linting warning is reported", () => assertExitCode(runESLint(["bin/eslint.js", "--env", "es6", "--no-eslintrc", "--rule", "semi: [1, never]"]), 0)); - it("has exit code 1 if a linting error is reported", () => assertExitCode(runESLint(["bin/eslint.js", "--env", "es6", "--no-eslintrc", "--rule", "semi: [2, never]"]), 1)); + it("has exit code 0 if a linting warning is reported", () => assertExitCode(runESLint(["bin/eslint.js", "--env", "es2020", "--no-eslintrc", "--rule", "semi: [1, never]"]), 0)); + it("has exit code 1 if a linting error is reported", () => assertExitCode(runESLint(["bin/eslint.js", "--env", "es2020", "--no-eslintrc", "--rule", "semi: [2, never]"]), 1)); it("has exit code 1 if a syntax error is thrown", () => assertExitCode(runESLint(["README.md"]), 1)); }); diff --git a/tests/fixtures/fix-types/ignore-missing-meta.expected.js b/tests/fixtures/fix-types/ignore-missing-meta.expected.js index b6e3019aa01..dc4cb7223ec 100644 --- a/tests/fixtures/fix-types/ignore-missing-meta.expected.js +++ b/tests/fixtures/fix-types/ignore-missing-meta.expected.js @@ -1,5 +1,5 @@ /* eslint semi: "error" */ -/* eslint no-program: "error" */ +/* eslint test/no-program: "error" */ /* eslint prefer-arrow-callback: "error" */ "use strict"; diff --git a/tests/fixtures/fix-types/ignore-missing-meta.js b/tests/fixtures/fix-types/ignore-missing-meta.js index ee9b9d9d869..4e43de09dc6 100644 --- a/tests/fixtures/fix-types/ignore-missing-meta.js +++ b/tests/fixtures/fix-types/ignore-missing-meta.js @@ -1,5 +1,5 @@ /* eslint semi: "error" */ -/* eslint no-program: "error" */ +/* eslint test/no-program: "error" */ /* eslint prefer-arrow-callback: "error" */ "use strict"; diff --git a/tests/lib/_utils.js b/tests/lib/_utils.js deleted file mode 100644 index 76e973cc667..00000000000 --- a/tests/lib/_utils.js +++ /dev/null @@ -1,76 +0,0 @@ -/** - * @fileoverview utils for rule tests. - * @author 唯然 - */ - -"use strict"; - -const path = require("path"); -const { Volume, createFsFromVolume } = require("memfs"); - -/** - * Prevents leading spaces in a multiline template literal from appearing in the resulting string - * @param {string[]} strings The strings in the template literal - * @param {any[]} values The interpolation values in the template literal. - * @returns {string} The template literal, with spaces removed from all lines - */ -function unIndent(strings, ...values) { - const text = strings - .map((s, i) => (i === 0 ? s : values[i - 1] + s)) - .join(""); - const lines = text.replace(/^\n/u, "").replace(/\n\s*$/u, "").split("\n"); - const lineIndents = lines.filter(line => line.trim()).map(line => line.match(/ */u)[0].length); - const minLineIndent = Math.min(...lineIndents); - - return lines.map(line => line.slice(minLineIndent)).join("\n"); -} - -/** - * Define in-memory file system. - * @param {Object} options The options. - * @param {() => string} [options.cwd] The current working directory. - * @param {Object} [options.files] The initial files definition in the in-memory file system. - * @returns {import("fs")} The stubbed `ConfigArrayFactory` class. - */ -function defineInMemoryFs({ - cwd = process.cwd, - files = {} -} = {}) { - - /** - * The in-memory file system for this mock. - * @type {import("fs")} - */ - const fs = createFsFromVolume(new Volume()); - - fs.mkdirSync(cwd(), { recursive: true }); - - /* - * Write all files to the in-memory file system and compile all JavaScript - * files then set to `stubs`. - */ - (function initFiles(directoryPath, definition) { - for (const [filename, content] of Object.entries(definition)) { - const filePath = path.resolve(directoryPath, filename); - const parentPath = path.dirname(filePath); - - if (typeof content === "object") { - initFiles(filePath, content); - } else if (typeof content === "string") { - if (!fs.existsSync(parentPath)) { - fs.mkdirSync(parentPath, { recursive: true }); - } - fs.writeFileSync(filePath, content); - } else { - throw new Error(`Invalid content: ${typeof content}`); - } - } - }(cwd(), files)); - - return fs; -} - -module.exports = { - defineInMemoryFs, - unIndent -}; diff --git a/tests/lib/cli-engine/cascading-config-array-factory.js b/tests/lib/cli-engine/cascading-config-array-factory.js index f9817717f72..8471ac3b553 100644 --- a/tests/lib/cli-engine/cascading-config-array-factory.js +++ b/tests/lib/cli-engine/cascading-config-array-factory.js @@ -12,7 +12,7 @@ const sh = require("shelljs"); const sinon = require("sinon"); const { ConfigArrayFactory } = require("../../../lib/cli-engine/config-array-factory"); const { ExtractedConfig } = require("../../../lib/cli-engine/config-array/extracted-config"); -const { defineCascadingConfigArrayFactoryWithInMemoryFileSystem } = require("./_utils"); +const { defineCascadingConfigArrayFactoryWithInMemoryFileSystem } = require("../../_utils"); /** @typedef {InstanceType["CascadingConfigArrayFactory"]>} CascadingConfigArrayFactory */ /** @typedef {ReturnType} ConfigArray */ diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index 36d7602bcef..a8394b63634 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -18,8 +18,7 @@ const assert = require("chai").assert, os = require("os"), hash = require("../../../lib/cli-engine/hash"), { CascadingConfigArrayFactory } = require("../../../lib/cli-engine/cascading-config-array-factory"), - { unIndent } = require("../_utils"), - { defineCLIEngineWithInMemoryFileSystem } = require("./_utils"); + { unIndent, defineCLIEngineWithInMemoryFileSystem } = require("../../_utils"); const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); const fCache = require("file-entry-cache"); @@ -822,7 +821,9 @@ describe("CLIEngine", () => { engine = new CLIEngine({ parser: "espree", - envs: ["es6"], + parserOptions: { + ecmaVersion: 2020 + }, useEslintrc: false }); @@ -868,6 +869,29 @@ describe("CLIEngine", () => { assert.strictEqual(report.results[0].messages.length, 0); }); + it("should fall back to defaults when extensions is set to an empty array", () => { + + engine = new CLIEngine({ + cwd: getFixturePath("configurations"), + configFile: getFixturePath("configurations", "quotes-error.json"), + extensions: [] + }); + const report = engine.executeOnFiles([getFixturePath("single-quoted.js")]); + + assert.strictEqual(report.results.length, 1); + assert.strictEqual(report.results[0].messages.length, 1); + assert.strictEqual(report.errorCount, 1); + assert.strictEqual(report.warningCount, 0); + assert.strictEqual(report.fixableErrorCount, 1); + assert.strictEqual(report.fixableWarningCount, 0); + assert.strictEqual(report.results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(report.results[0].messages[0].severity, 2); + assert.strictEqual(report.results[0].errorCount, 1); + assert.strictEqual(report.results[0].warningCount, 0); + assert.strictEqual(report.results[0].fixableErrorCount, 1); + assert.strictEqual(report.results[0].fixableWarningCount, 0); + }); + it("should report zero messages when given a directory with a .js and a .js2 file", () => { engine = new CLIEngine({ diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js index e938277c2f0..3b979c1d38f 100644 --- a/tests/lib/cli-engine/config-array-factory.js +++ b/tests/lib/cli-engine/config-array-factory.js @@ -11,7 +11,7 @@ const { spy } = require("sinon"); const { ConfigArray } = require("../../../lib/cli-engine/config-array"); const { OverrideTester } = require("../../../lib/cli-engine/config-array"); const { createContext } = require("../../../lib/cli-engine/config-array-factory"); -const { defineConfigArrayFactoryWithInMemoryFileSystem } = require("./_utils"); +const { defineConfigArrayFactoryWithInMemoryFileSystem } = require("../../_utils"); const tempDir = path.join(os.tmpdir(), "eslint/config-array-factory"); diff --git a/tests/lib/cli-engine/file-enumerator.js b/tests/lib/cli-engine/file-enumerator.js index c62578c9162..43728862d95 100644 --- a/tests/lib/cli-engine/file-enumerator.js +++ b/tests/lib/cli-engine/file-enumerator.js @@ -11,7 +11,7 @@ const { assert } = require("chai"); const sh = require("shelljs"); const { CascadingConfigArrayFactory } = require("../../../lib/cli-engine/cascading-config-array-factory"); -const { defineFileEnumeratorWithInMemoryFileSystem } = require("./_utils"); +const { defineFileEnumeratorWithInMemoryFileSystem } = require("../../_utils"); describe("FileEnumerator", () => { describe("'iterateFiles(patterns)' method should iterate files and configs.", () => { diff --git a/tests/lib/cli.js b/tests/lib/cli.js index 6af3b5c838e..a1d9a23e491 100644 --- a/tests/lib/cli.js +++ b/tests/lib/cli.js @@ -6,8 +6,8 @@ "use strict"; /* - * NOTE: If you are adding new tests for cli.js, use verifyCLIEngineOpts(). The - * test only needs to verify that CLIEngine receives the correct opts. + * NOTE: If you are adding new tests for cli.js, use verifyESLintOpts(). The + * test only needs to verify that ESLint receives the correct opts. */ //------------------------------------------------------------------------------ @@ -15,7 +15,9 @@ //------------------------------------------------------------------------------ const assert = require("chai").assert, - CLIEngine = require("../../lib/cli-engine/index").CLIEngine, + stdAssert = require("assert"), + { ESLint } = require("../../lib/eslint"), + BuiltinRules = require("../../lib/rules"), path = require("path"), sinon = require("sinon"), fs = require("fs"), @@ -44,30 +46,30 @@ describe("cli", () => { }); /** - * Verify that CLIEngine receives correct opts via cli.execute(). + * Verify that ESLint class receives correct opts via await cli.execute(). * @param {string} cmd CLI command. - * @param {Object} opts Options hash that should match that received by CLIEngine. + * @param {Object} opts Options hash that should match that received by ESLint class. * @returns {void} */ - function verifyCLIEngineOpts(cmd, opts) { + async function verifyESLintOpts(cmd, opts) { - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match(opts)); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match(opts)); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({}); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(sinon.spy()); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: sinon.spy() }); const localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - localCLI.execute(cmd); + await localCLI.execute(cmd); sinon.verifyAndRestore(); } - // verifyCLIEngineOpts + // verifyESLintOpts /** * Returns the path inside of the fixture directory. @@ -96,30 +98,30 @@ describe("cli", () => { }); describe("execute()", () => { - it("should return error when text with incorrect quotes is passed as argument", () => { + it("should return error when text with incorrect quotes is passed as argument", async () => { const configFile = getFixturePath("configurations", "quotes-error.json"); - const result = cli.execute(`-c ${configFile}`, "var foo = 'bar';"); + const result = await cli.execute(`-c ${configFile}`, "var foo = 'bar';"); assert.strictEqual(result, 1); }); - it("should not print debug info when passed the empty string as text", () => { - const result = cli.execute(["--stdin", "--no-eslintrc"], ""); + it("should not print debug info when passed the empty string as text", async () => { + const result = await cli.execute(["--stdin", "--no-eslintrc"], ""); assert.strictEqual(result, 0); assert.isTrue(log.info.notCalled); }); - it("should return no error when --ext .js2 is specified", () => { + it("should return no error when --ext .js2 is specified", async () => { const filePath = getFixturePath("files"); - const result = cli.execute(`--ext .js2 ${filePath}`); + const result = await cli.execute(`--ext .js2 ${filePath}`); assert.strictEqual(result, 0); }); - it("should exit with console error when passed unsupported arguments", () => { + it("should exit with console error when passed unsupported arguments", async () => { const filePath = getFixturePath("files"); - const result = cli.execute(`--blah --another ${filePath}`); + const result = await cli.execute(`--blah --another ${filePath}`); assert.strictEqual(result, 2); }); @@ -127,120 +129,119 @@ describe("cli", () => { }); describe("when given a config file", () => { - it("should load the specified config file", () => { + it("should load the specified config file", async () => { const configPath = getFixturePath(".eslintrc"); const filePath = getFixturePath("passing.js"); - cli.execute(`--config ${configPath} ${filePath}`); + await cli.execute(`--config ${configPath} ${filePath}`); }); }); describe("when there is a local config file", () => { const code = "lib/cli.js"; - it("should load the local config file", () => { + it("should load the local config file", async () => { // Mock CWD process.eslintCwd = getFixturePath("configurations", "single-quotes"); - cli.execute(code); + await cli.execute(code); process.eslintCwd = null; }); }); describe("when given a config with rules with options and severity level set to error", () => { - it("should exit with an error status (1)", () => { + it("should exit with an error status (1)", async () => { const configPath = getFixturePath("configurations", "quotes-error.json"); const filePath = getFixturePath("single-quoted.js"); const code = `--no-ignore --config ${configPath} ${filePath}`; - const exitStatus = cli.execute(code); + const exitStatus = await cli.execute(code); assert.strictEqual(exitStatus, 1); }); }); describe("when given a config file and a directory of files", () => { - it("should load and execute without error", () => { + it("should load and execute without error", async () => { const configPath = getFixturePath("configurations", "semi-error.json"); const filePath = getFixturePath("formatters"); const code = `--config ${configPath} ${filePath}`; - const exitStatus = cli.execute(code); + const exitStatus = await cli.execute(code); assert.strictEqual(exitStatus, 0); }); }); describe("when given a config with environment set to browser", () => { - it("should execute without any errors", () => { + it("should execute without any errors", async () => { const configPath = getFixturePath("configurations", "env-browser.json"); const filePath = getFixturePath("globals-browser.js"); const code = `--config ${configPath} ${filePath}`; - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 0); }); }); describe("when given a config with environment set to Node.js", () => { - it("should execute without any errors", () => { + it("should execute without any errors", async () => { const configPath = getFixturePath("configurations", "env-node.json"); const filePath = getFixturePath("globals-node.js"); const code = `--config ${configPath} ${filePath}`; - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 0); }); }); describe("when given a config with environment set to Nashorn", () => { - it("should execute without any errors", () => { + it("should execute without any errors", async () => { const configPath = getFixturePath("configurations", "env-nashorn.json"); const filePath = getFixturePath("globals-nashorn.js"); const code = `--config ${configPath} ${filePath}`; - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 0); }); }); describe("when given a config with environment set to WebExtensions", () => { - it("should execute without any errors", () => { + it("should execute without any errors", async () => { const configPath = getFixturePath("configurations", "env-webextensions.json"); const filePath = getFixturePath("globals-webextensions.js"); const code = `--config ${configPath} ${filePath}`; - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 0); }); }); describe("when given a valid built-in formatter name", () => { - it("should execute without any errors", () => { + it("should execute without any errors", async () => { const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`-f checkstyle ${filePath}`); + const exit = await cli.execute(`-f checkstyle ${filePath}`); assert.strictEqual(exit, 0); }); }); describe("when given a valid built-in formatter name that uses rules meta.", () => { - it("should execute without any errors", () => { + it("should execute without any errors", async () => { const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`-f json-with-metadata ${filePath} --no-eslintrc`); + const exit = await cli.execute(`-f json-with-metadata ${filePath} --no-eslintrc`); assert.strictEqual(exit, 0); // Check metadata. const { metadata } = JSON.parse(log.info.args[0][0]); - const rules = new CLIEngine({ useEslintrc: false }).getRules(); - const expectedMetadata = Array.from(rules).reduce((obj, [ruleId, rule]) => { + const expectedMetadata = Array.from(BuiltinRules).reduce((obj, [ruleId, rule]) => { obj.rulesMeta[ruleId] = rule.meta; return obj; }, { rulesMeta: {} }); @@ -250,204 +251,204 @@ describe("cli", () => { }); describe("when given an invalid built-in formatter name", () => { - it("should execute with error", () => { + it("should execute with error", async () => { const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`-f fakeformatter ${filePath}`); + const exit = await cli.execute(`-f fakeformatter ${filePath}`); assert.strictEqual(exit, 2); }); }); describe("when given a valid formatter path", () => { - it("should execute without any errors", () => { + it("should execute without any errors", async () => { const formatterPath = getFixturePath("formatters", "simple.js"); const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`-f ${formatterPath} ${filePath}`); + const exit = await cli.execute(`-f ${formatterPath} ${filePath}`); assert.strictEqual(exit, 0); }); }); describe("when given an invalid formatter path", () => { - it("should execute with error", () => { + it("should execute with error", async () => { const formatterPath = getFixturePath("formatters", "file-does-not-exist.js"); const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`-f ${formatterPath} ${filePath}`); + const exit = await cli.execute(`-f ${formatterPath} ${filePath}`); assert.strictEqual(exit, 2); }); }); describe("when executing a file with a lint error", () => { - it("should exit with error", () => { + it("should exit with error", async () => { const filePath = getFixturePath("undef.js"); const code = `--no-ignore --rule no-undef:2 ${filePath}`; - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 1); }); }); describe("when using --fix-type without --fix or --fix-dry-run", () => { - it("should exit with error", () => { + it("should exit with error", async () => { const filePath = getFixturePath("passing.js"); const code = `--fix-type suggestion ${filePath}`; - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 2); }); }); describe("when executing a file with a syntax error", () => { - it("should exit with error", () => { + it("should exit with error", async () => { const filePath = getFixturePath("syntax-error.js"); - const exit = cli.execute(`--no-ignore ${filePath}`); + const exit = await cli.execute(`--no-ignore ${filePath}`); assert.strictEqual(exit, 1); }); }); describe("when calling execute more than once", () => { - it("should not print the results from previous execution", () => { + it("should not print the results from previous execution", async () => { const filePath = getFixturePath("missing-semicolon.js"); const passingPath = getFixturePath("passing.js"); - cli.execute(`--no-ignore --rule semi:2 ${filePath}`); + await cli.execute(`--no-ignore --rule semi:2 ${filePath}`); assert.isTrue(log.info.called, "Log should have been called."); log.info.resetHistory(); - cli.execute(`--no-ignore --rule semi:2 ${passingPath}`); + await cli.execute(`--no-ignore --rule semi:2 ${passingPath}`); assert.isTrue(log.info.notCalled); }); }); describe("when executing with version flag", () => { - it("should print out current version", () => { - assert.strictEqual(cli.execute("-v"), 0); + it("should print out current version", async () => { + assert.strictEqual(await cli.execute("-v"), 0); assert.strictEqual(log.info.callCount, 1); }); }); describe("when executing with env-info flag", () => { - it("should print out environment information", () => { - assert.strictEqual(cli.execute("--env-info"), 0); + it("should print out environment information", async () => { + assert.strictEqual(await cli.execute("--env-info"), 0); assert.strictEqual(log.info.callCount, 1); }); - it("should print error message and return error code", () => { + it("should print error message and return error code", async () => { RuntimeInfo.environment.throws("There was an error!"); - assert.strictEqual(cli.execute("--env-info"), 2); + assert.strictEqual(await cli.execute("--env-info"), 2); assert.strictEqual(log.error.callCount, 1); }); }); describe("when executing without no-error-on-unmatched-pattern flag", () => { - it("should throw an error on unmatched glob pattern", () => { + it("should throw an error on unmatched glob pattern", async () => { const filePath = getFixturePath("unmatched-patterns"); const globPattern = "*.js3"; - assert.throws(() => { - cli.execute(`"${filePath}/${globPattern}"`); - }, `No files matching '${filePath}/${globPattern}' were found.`); + await stdAssert.rejects(async () => { + await cli.execute(`"${filePath}/${globPattern}"`); + }, new Error(`No files matching '${filePath}/${globPattern}' were found.`)); }); - it("should throw an error on unmatched --ext", () => { + it("should throw an error on unmatched --ext", async () => { const filePath = getFixturePath("unmatched-patterns"); const extension = ".js3"; - assert.throws(() => { - cli.execute(`--ext ${extension} ${filePath}`); + await stdAssert.rejects(async () => { + await cli.execute(`--ext ${extension} ${filePath}`); }, `No files matching '${filePath}' were found`); }); }); describe("when executing with no-error-on-unmatched-pattern flag", () => { - it("should not throw an error on unmatched node glob syntax patterns", () => { + it("should not throw an error on unmatched node glob syntax patterns", async () => { const filePath = getFixturePath("unmatched-patterns"); - const exit = cli.execute(`--no-error-on-unmatched-pattern "${filePath}/*.js3"`); + const exit = await cli.execute(`--no-error-on-unmatched-pattern "${filePath}/*.js3"`); assert.strictEqual(exit, 0); }); - it("should not throw an error on unmatched --ext", () => { + it("should not throw an error on unmatched --ext", async () => { const filePath = getFixturePath("unmatched-patterns"); - const exit = cli.execute(`--no-error-on-unmatched-pattern --ext .js3 ${filePath}`); + const exit = await cli.execute(`--no-error-on-unmatched-pattern --ext .js3 ${filePath}`); assert.strictEqual(exit, 0); }); }); describe("when executing with no-error-on-unmatched-pattern flag and multiple patterns", () => { - it("should not throw an error on multiple unmatched node glob syntax patterns", () => { + it("should not throw an error on multiple unmatched node glob syntax patterns", async () => { const filePath = getFixturePath("unmatched-patterns"); - const exit = cli.execute(`--no-error-on-unmatched-pattern ${filePath}/*.js3 ${filePath}/*.js4`); + const exit = await cli.execute(`--no-error-on-unmatched-pattern ${filePath}/*.js3 ${filePath}/*.js4`); assert.strictEqual(exit, 0); }); - it("should still throw an error on when a matched pattern has lint errors", () => { + it("should still throw an error on when a matched pattern has lint errors", async () => { const filePath = getFixturePath("unmatched-patterns"); - const exit = cli.execute(`--no-error-on-unmatched-pattern ${filePath}/*.js3 ${filePath}/*.js`); + const exit = await cli.execute(`--no-error-on-unmatched-pattern ${filePath}/*.js3 ${filePath}/*.js`); assert.strictEqual(exit, 1); }); }); describe("when executing with no-error-on-unmatched-pattern flag and multiple --ext arguments", () => { - it("should not throw an error on multiple unmatched --ext arguments", () => { + it("should not throw an error on multiple unmatched --ext arguments", async () => { const filePath = getFixturePath("unmatched-patterns"); - const exit = cli.execute(`--no-error-on-unmatched-pattern --ext .js3 --ext .js4 ${filePath}`); + const exit = await cli.execute(`--no-error-on-unmatched-pattern --ext .js3 --ext .js4 ${filePath}`); assert.strictEqual(exit, 0); }); - it("should still throw an error on when a matched pattern has lint errors", () => { + it("should still throw an error on when a matched pattern has lint errors", async () => { const filePath = getFixturePath("unmatched-patterns"); - const exit = cli.execute(`--no-error-on-unmatched-pattern --ext .js3 --ext .js ${filePath}`); + const exit = await cli.execute(`--no-error-on-unmatched-pattern --ext .js3 --ext .js ${filePath}`); assert.strictEqual(exit, 1); }); }); describe("when executing with help flag", () => { - it("should print out help", () => { - assert.strictEqual(cli.execute("-h"), 0); + it("should print out help", async () => { + assert.strictEqual(await cli.execute("-h"), 0); assert.strictEqual(log.info.callCount, 1); }); }); describe("when given a directory with eslint excluded files in the directory", () => { - it("should throw an error and not process any files", () => { + it("should throw an error and not process any files", async () => { const ignorePath = getFixturePath(".eslintignore"); const filePath = getFixturePath("cli"); - assert.throws(() => { - cli.execute(`--ignore-path ${ignorePath} ${filePath}`); - }, `All files matched by '${filePath}' are ignored.`); + await stdAssert.rejects(async () => { + await cli.execute(`--ignore-path ${ignorePath} ${filePath}`); + }, new Error(`All files matched by '${filePath}' are ignored.`)); }); }); describe("when given a file in excluded files list", () => { - it("should not process the file", () => { + it("should not process the file", async () => { const ignorePath = getFixturePath(".eslintignore"); const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`--ignore-path ${ignorePath} ${filePath}`); + const exit = await cli.execute(`--ignore-path ${ignorePath} ${filePath}`); // a warning about the ignored file assert.isTrue(log.info.called); assert.strictEqual(exit, 0); }); - it("should process the file when forced", () => { + it("should process the file when forced", async () => { const ignorePath = getFixturePath(".eslintignore"); const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`--ignore-path ${ignorePath} --no-ignore ${filePath}`); + const exit = await cli.execute(`--ignore-path ${ignorePath} --no-ignore ${filePath}`); // no warnings assert.isFalse(log.info.called); @@ -456,10 +457,10 @@ describe("cli", () => { }); describe("when given a pattern to ignore", () => { - it("should not process any files", () => { + it("should not process any files", async () => { const ignoredFile = getFixturePath("cli/syntax-error.js"); const filePath = getFixturePath("cli/passing.js"); - const exit = cli.execute(`--ignore-pattern cli/ ${ignoredFile} ${filePath}`); + const exit = await cli.execute(`--ignore-pattern cli/ ${ignoredFile} ${filePath}`); // warnings about the ignored files assert.isTrue(log.info.called); @@ -468,61 +469,63 @@ describe("cli", () => { }); describe("when given patterns to ignore", () => { - it("should not process any matching files", () => { + it("should not process any matching files", async () => { const ignorePaths = ["a", "b"]; const cmd = ignorePaths.map(ignorePath => `--ignore-pattern ${ignorePath}`).concat(".").join(" "); const opts = { - ignorePattern: ignorePaths + overrideConfig: { + ignorePatterns: ignorePaths + } }; - verifyCLIEngineOpts(cmd, opts); + await verifyESLintOpts(cmd, opts); }); }); describe("when executing a file with a shebang", () => { - it("should execute without error", () => { + it("should execute without error", async () => { const filePath = getFixturePath("shebang.js"); - const exit = cli.execute(`--no-ignore ${filePath}`); + const exit = await cli.execute(`--no-ignore ${filePath}`); assert.strictEqual(exit, 0); }); }); describe("when loading a custom rule", () => { - it("should return an error when rule isn't found", () => { + it("should return an error when rule isn't found", async () => { const rulesPath = getFixturePath("rules", "wrong"); const configPath = getFixturePath("rules", "eslint.json"); const filePath = getFixturePath("rules", "test", "test-custom-rule.js"); const code = `--rulesdir ${rulesPath} --config ${configPath} --no-ignore ${filePath}`; - assert.throws(() => { - const exit = cli.execute(code); + await stdAssert.rejects(async () => { + const exit = await cli.execute(code); assert.strictEqual(exit, 2); }, /Error while loading rule 'custom-rule': Cannot read property/u); }); - it("should return a warning when rule is matched", () => { + it("should return a warning when rule is matched", async () => { const rulesPath = getFixturePath("rules"); const configPath = getFixturePath("rules", "eslint.json"); const filePath = getFixturePath("rules", "test", "test-custom-rule.js"); const code = `--rulesdir ${rulesPath} --config ${configPath} --no-ignore ${filePath}`; - cli.execute(code); + await cli.execute(code); assert.isTrue(log.info.calledOnce); assert.isTrue(log.info.neverCalledWith("")); }); - it("should return warnings from multiple rules in different directories", () => { + it("should return warnings from multiple rules in different directories", async () => { const rulesPath = getFixturePath("rules", "dir1"); const rulesPath2 = getFixturePath("rules", "dir2"); const configPath = getFixturePath("rules", "multi-rulesdirs.json"); const filePath = getFixturePath("rules", "test-multi-rulesdirs.js"); const code = `--rulesdir ${rulesPath} --rulesdir ${rulesPath2} --config ${configPath} --no-ignore ${filePath}`; - const exit = cli.execute(code); + const exit = await cli.execute(code); const call = log.info.getCall(0); @@ -538,9 +541,9 @@ describe("cli", () => { }); describe("when executing with no-eslintrc flag", () => { - it("should ignore a local config file", () => { + it("should ignore a local config file", async () => { const filePath = getFixturePath("eslintrc", "quotes.js"); - const exit = cli.execute(`--no-eslintrc --no-ignore ${filePath}`); + const exit = await cli.execute(`--no-eslintrc --no-ignore ${filePath}`); assert.isTrue(log.info.notCalled); assert.strictEqual(exit, 0); @@ -548,9 +551,9 @@ describe("cli", () => { }); describe("when executing without no-eslintrc flag", () => { - it("should load a local config file", () => { + it("should load a local config file", async () => { const filePath = getFixturePath("eslintrc", "quotes.js"); - const exit = cli.execute(`--no-ignore ${filePath}`); + const exit = await cli.execute(`--no-ignore ${filePath}`); assert.isTrue(log.info.calledOnce); assert.strictEqual(exit, 1); @@ -558,38 +561,38 @@ describe("cli", () => { }); describe("when executing without env flag", () => { - it("should not define environment-specific globals", () => { + it("should not define environment-specific globals", async () => { const files = [ getFixturePath("globals-browser.js"), getFixturePath("globals-node.js") ]; - cli.execute(`--no-eslintrc --config ./conf/eslint-recommended.js --no-ignore ${files.join(" ")}`); + await cli.execute(`--no-eslintrc --config ./conf/eslint-recommended.js --no-ignore ${files.join(" ")}`); assert.strictEqual(log.info.args[0][0].split("\n").length, 10); }); }); describe("when executing with global flag", () => { - it("should default defined variables to read-only", () => { + it("should default defined variables to read-only", async () => { const filePath = getFixturePath("undef.js"); - const exit = cli.execute(`--global baz,bat --no-ignore --rule no-global-assign:2 ${filePath}`); + const exit = await cli.execute(`--global baz,bat --no-ignore --rule no-global-assign:2 ${filePath}`); assert.isTrue(log.info.calledOnce); assert.strictEqual(exit, 1); }); - it("should allow defining writable global variables", () => { + it("should allow defining writable global variables", async () => { const filePath = getFixturePath("undef.js"); - const exit = cli.execute(`--global baz:false,bat:true --no-ignore ${filePath}`); + const exit = await cli.execute(`--global baz:false,bat:true --no-ignore ${filePath}`); assert.isTrue(log.info.notCalled); assert.strictEqual(exit, 0); }); - it("should allow defining variables with multiple flags", () => { + it("should allow defining variables with multiple flags", async () => { const filePath = getFixturePath("undef.js"); - const exit = cli.execute(`--global baz --global bat:true --no-ignore ${filePath}`); + const exit = await cli.execute(`--global baz --global bat:true --no-ignore ${filePath}`); assert.isTrue(log.info.notCalled); assert.strictEqual(exit, 0); @@ -597,10 +600,10 @@ describe("cli", () => { }); describe("when supplied with rule flag and severity level set to error", () => { - it("should exit with an error status (2)", () => { + it("should exit with an error status (2)", async () => { const filePath = getFixturePath("single-quoted.js"); const code = `--no-ignore --rule 'quotes: [2, double]' ${filePath}`; - const exitStatus = cli.execute(code); + const exitStatus = await cli.execute(code); assert.strictEqual(exitStatus, 1); }); @@ -608,11 +611,11 @@ describe("cli", () => { describe("when the quiet option is enabled", () => { - it("should only print error", () => { + it("should only print error", async () => { const filePath = getFixturePath("single-quoted.js"); const cliArgs = `--no-ignore --quiet -f compact --rule 'quotes: [2, double]' --rule 'no-unused-vars: 1' ${filePath}`; - cli.execute(cliArgs); + await cli.execute(cliArgs); sinon.assert.calledOnce(log.info); @@ -622,11 +625,11 @@ describe("cli", () => { assert.notInclude(formattedOutput, "Warning"); }); - it("should print nothing if there are no errors", () => { + it("should print nothing if there are no errors", async () => { const filePath = getFixturePath("single-quoted.js"); const cliArgs = `--quiet -f compact --rule 'quotes: [1, double]' --rule 'no-unused-vars: 1' ${filePath}`; - cli.execute(cliArgs); + await cli.execute(cliArgs); sinon.assert.notCalled(log.info); }); @@ -637,36 +640,36 @@ describe("cli", () => { sh.rm("-rf", "tests/output"); }); - it("should write the file and create dirs if they don't exist", () => { + it("should write the file and create dirs if they don't exist", async () => { const filePath = getFixturePath("single-quoted.js"); const code = `--no-ignore --rule 'quotes: [1, double]' --o tests/output/eslint-output.txt ${filePath}`; - cli.execute(code); + await cli.execute(code); assert.include(fs.readFileSync("tests/output/eslint-output.txt", "utf8"), filePath); assert.isTrue(log.info.notCalled); }); - it("should return an error if the path is a directory", () => { + it("should return an error if the path is a directory", async () => { const filePath = getFixturePath("single-quoted.js"); const code = `--no-ignore --rule 'quotes: [1, double]' --o tests/output ${filePath}`; fs.mkdirSync("tests/output"); - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 2); assert.isTrue(log.info.notCalled); assert.isTrue(log.error.calledOnce); }); - it("should return an error if the path could not be written to", () => { + it("should return an error if the path could not be written to", async () => { const filePath = getFixturePath("single-quoted.js"); const code = `--no-ignore --rule 'quotes: [1, double]' --o tests/output/eslint-output.txt ${filePath}`; fs.writeFileSync("tests/output", "foo"); - const exit = cli.execute(code); + const exit = await cli.execute(code); assert.strictEqual(exit, 2); assert.isTrue(log.info.notCalled); @@ -675,106 +678,108 @@ describe("cli", () => { }); describe("when supplied with a plugin", () => { - it("should pass plugins to CLIEngine", () => { + it("should pass plugins to ESLint", async () => { const examplePluginName = "eslint-plugin-example"; - verifyCLIEngineOpts(`--no-ignore --plugin ${examplePluginName} foo.js`, { - plugins: [examplePluginName] + await verifyESLintOpts(`--no-ignore --plugin ${examplePluginName} foo.js`, { + overrideConfig: { + plugins: [examplePluginName] + } }); }); }); describe("when supplied with a plugin-loading path", () => { - it("should pass the option to CLIEngine", () => { + it("should pass the option to ESLint", async () => { const examplePluginDirPath = "foo/bar"; - verifyCLIEngineOpts(`--resolve-plugins-relative-to ${examplePluginDirPath} foo.js`, { + await verifyESLintOpts(`--resolve-plugins-relative-to ${examplePluginDirPath} foo.js`, { resolvePluginsRelativeTo: examplePluginDirPath }); }); }); describe("when given an parser name", () => { - it("should exit with a fatal error if parser is invalid", () => { + it("should exit with a fatal error if parser is invalid", async () => { const filePath = getFixturePath("passing.js"); - assert.throws(() => cli.execute(`--no-ignore --parser test111 ${filePath}`), "Cannot find module 'test111'"); + await stdAssert.rejects(async () => await cli.execute(`--no-ignore --parser test111 ${filePath}`), "Cannot find module 'test111'"); }); - it("should exit with no error if parser is valid", () => { + it("should exit with no error if parser is valid", async () => { const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`--no-ignore --parser espree ${filePath}`); + const exit = await cli.execute(`--no-ignore --parser espree ${filePath}`); assert.strictEqual(exit, 0); }); }); describe("when given parser options", () => { - it("should exit with error if parser options are invalid", () => { + it("should exit with error if parser options are invalid", async () => { const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`--no-ignore --parser-options test111 ${filePath}`); + const exit = await cli.execute(`--no-ignore --parser-options test111 ${filePath}`); assert.strictEqual(exit, 2); }); - it("should exit with no error if parser is valid", () => { + it("should exit with no error if parser is valid", async () => { const filePath = getFixturePath("passing.js"); - const exit = cli.execute(`--no-ignore --parser-options=ecmaVersion:6 ${filePath}`); + const exit = await cli.execute(`--no-ignore --parser-options=ecmaVersion:6 ${filePath}`); assert.strictEqual(exit, 0); }); - it("should exit with an error on ecmaVersion 7 feature in ecmaVersion 6", () => { + it("should exit with an error on ecmaVersion 7 feature in ecmaVersion 6", async () => { const filePath = getFixturePath("passing-es7.js"); - const exit = cli.execute(`--no-ignore --parser-options=ecmaVersion:6 ${filePath}`); + const exit = await cli.execute(`--no-ignore --parser-options=ecmaVersion:6 ${filePath}`); assert.strictEqual(exit, 1); }); - it("should exit with no error on ecmaVersion 7 feature in ecmaVersion 7", () => { + it("should exit with no error on ecmaVersion 7 feature in ecmaVersion 7", async () => { const filePath = getFixturePath("passing-es7.js"); - const exit = cli.execute(`--no-ignore --parser-options=ecmaVersion:7 ${filePath}`); + const exit = await cli.execute(`--no-ignore --parser-options=ecmaVersion:7 ${filePath}`); assert.strictEqual(exit, 0); }); - it("should exit with no error on ecmaVersion 7 feature with config ecmaVersion 6 and command line ecmaVersion 7", () => { + it("should exit with no error on ecmaVersion 7 feature with config ecmaVersion 6 and command line ecmaVersion 7", async () => { const configPath = getFixturePath("configurations", "es6.json"); const filePath = getFixturePath("passing-es7.js"); - const exit = cli.execute(`--no-ignore --config ${configPath} --parser-options=ecmaVersion:7 ${filePath}`); + const exit = await cli.execute(`--no-ignore --config ${configPath} --parser-options=ecmaVersion:7 ${filePath}`); assert.strictEqual(exit, 0); }); }); describe("when given the max-warnings flag", () => { - it("should not change exit code if warning count under threshold", () => { + it("should not change exit code if warning count under threshold", async () => { const filePath = getFixturePath("max-warnings"); - const exitCode = cli.execute(`--no-ignore --max-warnings 10 ${filePath}`); + const exitCode = await cli.execute(`--no-ignore --max-warnings 10 ${filePath}`); assert.strictEqual(exitCode, 0); }); - it("should exit with exit code 1 if warning count exceeds threshold", () => { + it("should exit with exit code 1 if warning count exceeds threshold", async () => { const filePath = getFixturePath("max-warnings"); - const exitCode = cli.execute(`--no-ignore --max-warnings 5 ${filePath}`); + const exitCode = await cli.execute(`--no-ignore --max-warnings 5 ${filePath}`); assert.strictEqual(exitCode, 1); assert.ok(log.error.calledOnce); assert.include(log.error.getCall(0).args[0], "ESLint found too many warnings"); }); - it("should not change exit code if warning count equals threshold", () => { + it("should not change exit code if warning count equals threshold", async () => { const filePath = getFixturePath("max-warnings"); - const exitCode = cli.execute(`--no-ignore --max-warnings 6 ${filePath}`); + const exitCode = await cli.execute(`--no-ignore --max-warnings 6 ${filePath}`); assert.strictEqual(exitCode, 0); }); - it("should not change exit code if flag is not specified and there are warnings", () => { + it("should not change exit code if flag is not specified and there are warnings", async () => { const filePath = getFixturePath("max-warnings"); - const exitCode = cli.execute(filePath); + const exitCode = await cli.execute(filePath); assert.strictEqual(exitCode, 0); }); @@ -787,58 +792,51 @@ describe("cli", () => { sinon.verifyAndRestore(); }); - it("should pass allowInlineConfig:true to CLIEngine when --no-inline-config is used", () => { + it("should pass allowInlineConfig:false to ESLint when --no-inline-config is used", async () => { - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ allowInlineConfig: false })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ allowInlineConfig: false })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({ + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns([{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 2, + message: "Fake message" + } + ], errorCount: 1, - warningCount: 0, - results: [{ - filePath: "./foo.js", - output: "bar", - messages: [ - { - severity: 2, - message: "Fake message" - } - ] - }] - }); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - fakeCLIEngine.outputFixes = sinon.stub(); + warningCount: 0 + }]); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.stub(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - localCLI.execute("--no-inline-config ."); + await localCLI.execute("--no-inline-config ."); }); - it("should not error and allowInlineConfig should be true by default", () => { + it("should not error and allowInlineConfig should be true by default", async () => { - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ allowInlineConfig: true })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ allowInlineConfig: true })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({ - errorCount: 0, - warningCount: 0, - results: [] - }); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.outputFixes = sinon.stub(); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.stub(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("."); + const exitCode = await localCLI.execute("."); assert.strictEqual(exitCode, 0); @@ -853,119 +851,108 @@ describe("cli", () => { sinon.verifyAndRestore(); }); - it("should pass fix:true to CLIEngine when executing on files", () => { + it("should pass fix:true to ESLint when executing on files", async () => { - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ fix: true })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({ - errorCount: 0, - warningCount: 0, - results: [] - }); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.outputFixes = sinon.mock().once(); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.mock().once(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix ."); + const exitCode = await localCLI.execute("--fix ."); assert.strictEqual(exitCode, 0); }); - it("should rewrite files when in fix mode", () => { + it("should rewrite files when in fix mode", async () => { - const report = { + const report = [{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 2, + message: "Fake message" + } + ], errorCount: 1, - warningCount: 0, - results: [{ - filePath: "./foo.js", - output: "bar", - messages: [ - { - severity: 2, - message: "Fake message" - } - ] - }] - }; + warningCount: 0 + }]; - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ fix: true })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns(report); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.outputFixes = sinon.mock().withExactArgs(report); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns(report); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.mock().withExactArgs(report); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix ."); + const exitCode = await localCLI.execute("--fix ."); assert.strictEqual(exitCode, 1); }); - it("should provide fix predicate and rewrite files when in fix mode and quiet mode", () => { + it("should provide fix predicate and rewrite files when in fix mode and quiet mode", async () => { - const report = { + const report = [{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 1, + message: "Fake message" + } + ], errorCount: 0, - warningCount: 1, - results: [{ - filePath: "./foo.js", - output: "bar", - messages: [ - { - severity: 1, - message: "Fake message" - } - ] - }] - }; + warningCount: 1 + }]; - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ fix: sinon.match.func })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: sinon.match.func })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns(report); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.getErrorResults = sinon.stub().returns([]); - fakeCLIEngine.outputFixes = sinon.mock().withExactArgs(report); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns(report); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.getErrorResults = sinon.stub().returns([]); + fakeESLint.outputFixes = sinon.mock().withExactArgs(report); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix --quiet ."); + const exitCode = await localCLI.execute("--fix --quiet ."); assert.strictEqual(exitCode, 0); }); - it("should not call CLIEngine and return 1 when executing on text", () => { + it("should not call ESLint and return 2 when executing on text", async () => { - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().never(); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix .", "foo = bar;"); + const exitCode = await localCLI.execute("--fix .", "foo = bar;"); assert.strictEqual(exitCode, 2); }); @@ -979,212 +966,193 @@ describe("cli", () => { sinon.verifyAndRestore(); }); - it("should pass fix:true to CLIEngine when executing on files", () => { + it("should pass fix:true to ESLint when executing on files", async () => { - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ fix: true })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({ - errorCount: 0, - warningCount: 0, - results: [] - }); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.outputFixes = sinon.mock().never(); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix-dry-run ."); + const exitCode = await localCLI.execute("--fix-dry-run ."); assert.strictEqual(exitCode, 0); }); - it("should pass fixTypes to CLIEngine when --fix-type is passed", () => { + it("should pass fixTypes to ESLint when --fix-type is passed", async () => { - const expectedCLIEngineOptions = { + const expectedESLintOptions = { fix: true, fixTypes: ["suggestion"] }; - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match(expectedCLIEngineOptions)); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match(expectedESLintOptions)); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns({ - errorCount: 0, - warningCount: 0, - results: [] - }); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.outputFixes = sinon.stub(); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns([]); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.stub(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix-dry-run --fix-type suggestion ."); + const exitCode = await localCLI.execute("--fix-dry-run --fix-type suggestion ."); assert.strictEqual(exitCode, 0); }); - it("should not rewrite files when in fix-dry-run mode", () => { + it("should not rewrite files when in fix-dry-run mode", async () => { - const report = { + const report = [{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 2, + message: "Fake message" + } + ], errorCount: 1, - warningCount: 0, - results: [{ - filePath: "./foo.js", - output: "bar", - messages: [ - { - severity: 2, - message: "Fake message" - } - ] - }] - }; + warningCount: 0 + }]; - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ fix: true })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns(report); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.outputFixes = sinon.mock().never(); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns(report); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix-dry-run ."); + const exitCode = await localCLI.execute("--fix-dry-run ."); assert.strictEqual(exitCode, 1); }); - it("should provide fix predicate when in fix-dry-run mode and quiet mode", () => { + it("should provide fix predicate when in fix-dry-run mode and quiet mode", async () => { - const report = { + const report = [{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 1, + message: "Fake message" + } + ], errorCount: 0, - warningCount: 1, - results: [{ - filePath: "./foo.js", - output: "bar", - messages: [ - { - severity: 1, - message: "Fake message" - } - ] - }] - }; + warningCount: 1 + }]; - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ fix: sinon.match.func })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: sinon.match.func })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnFiles").returns(report); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.getErrorResults = sinon.stub().returns([]); - fakeCLIEngine.outputFixes = sinon.mock().never(); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintFiles").returns(report); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.getErrorResults = sinon.stub().returns([]); + fakeESLint.outputFixes = sinon.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix-dry-run --quiet ."); + const exitCode = await localCLI.execute("--fix-dry-run --quiet ."); assert.strictEqual(exitCode, 0); }); - it("should allow executing on text", () => { + it("should allow executing on text", async () => { - const report = { + const report = [{ + filePath: "./foo.js", + output: "bar", + messages: [ + { + severity: 2, + message: "Fake message" + } + ], errorCount: 1, - warningCount: 0, - results: [{ - filePath: "./foo.js", - output: "bar", - messages: [ - { - severity: 2, - message: "Fake message" - } - ] - }] - }; + warningCount: 0 + }]; - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().withExactArgs(sinon.match({ fix: true })); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().withExactArgs(sinon.match({ fix: true })); - Object.defineProperties(fakeCLIEngine.prototype, Object.getOwnPropertyDescriptors(CLIEngine.prototype)); - sinon.stub(fakeCLIEngine.prototype, "executeOnText").returns(report); - sinon.stub(fakeCLIEngine.prototype, "getFormatter").returns(() => "done"); - sinon.stub(fakeCLIEngine.prototype, "getRules").returns(new Map()); - fakeCLIEngine.outputFixes = sinon.mock().never(); + Object.defineProperties(fakeESLint.prototype, Object.getOwnPropertyDescriptors(ESLint.prototype)); + sinon.stub(fakeESLint.prototype, "lintText").returns(report); + sinon.stub(fakeESLint.prototype, "loadFormatter").returns({ format: () => "done" }); + fakeESLint.outputFixes = sinon.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix-dry-run .", "foo = bar;"); + const exitCode = await localCLI.execute("--fix-dry-run .", "foo = bar;"); assert.strictEqual(exitCode, 1); }); - it("should not call CLIEngine and return 1 when used with --fix", () => { + it("should not call ESLint and return 2 when used with --fix", async () => { - // create a fake CLIEngine to test with - const fakeCLIEngine = sinon.mock().never(); + // create a fake ESLint class to test with + const fakeESLint = sinon.mock().never(); localCLI = proxyquire("../../lib/cli", { - "./cli-engine/index": { CLIEngine: fakeCLIEngine }, + "./eslint": { ESLint: fakeESLint }, "./shared/logging": log }); - const exitCode = localCLI.execute("--fix --fix-dry-run .", "foo = bar;"); + const exitCode = await localCLI.execute("--fix --fix-dry-run .", "foo = bar;"); assert.strictEqual(exitCode, 2); }); }); describe("when passing --print-config", () => { - it("should print out the configuration", () => { + it("should print out the configuration", async () => { const filePath = getFixturePath("xxxx"); - const exitCode = cli.execute(`--print-config ${filePath}`); + const exitCode = await cli.execute(`--print-config ${filePath}`); assert.isTrue(log.info.calledOnce); assert.strictEqual(exitCode, 0); }); - it("should error if any positional file arguments are passed", () => { + it("should error if any positional file arguments are passed", async () => { const filePath1 = getFixturePath("files", "bar.js"); const filePath2 = getFixturePath("files", "foo.js"); - const exitCode = cli.execute(`--print-config ${filePath1} ${filePath2}`); + const exitCode = await cli.execute(`--print-config ${filePath1} ${filePath2}`); assert.isTrue(log.info.notCalled); assert.isTrue(log.error.calledOnce); assert.strictEqual(exitCode, 2); }); - it("should error out when executing on text", () => { - const exitCode = cli.execute("--print-config=myFile.js", "foo = bar;"); + it("should error out when executing on text", async () => { + const exitCode = await cli.execute("--print-config=myFile.js", "foo = bar;"); assert.isTrue(log.info.notCalled); assert.isTrue(log.error.calledOnce); diff --git a/tests/lib/eslint/eslint.js b/tests/lib/eslint/eslint.js new file mode 100644 index 00000000000..fc86a781d0d --- /dev/null +++ b/tests/lib/eslint/eslint.js @@ -0,0 +1,6089 @@ +/** + * @fileoverview Tests for the ESLint class. + * @author Kai Cataldo + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const assert = require("assert"); +const fs = require("fs"); +const os = require("os"); +const path = require("path"); +const escapeStringRegExp = require("escape-string-regexp"); +const fCache = require("file-entry-cache"); +const leche = require("leche"); +const sinon = require("sinon"); +const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); +const shell = require("shelljs"); +const { CascadingConfigArrayFactory } = require("../../../lib/cli-engine/cascading-config-array-factory"); +const hash = require("../../../lib/cli-engine/hash"); +const { unIndent, defineESLintWithInMemoryFileSystem } = require("../../_utils"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +describe("ESLint", () => { + const examplePluginName = "eslint-plugin-example"; + const examplePluginNameWithNamespace = "@eslint/eslint-plugin-example"; + const examplePlugin = { + rules: { + "example-rule": require("../../fixtures/rules/custom-rule"), + "make-syntax-error": require("../../fixtures/rules/make-syntax-error-rule") + } + }; + const examplePreprocessorName = "eslint-plugin-processor"; + const originalDir = process.cwd(); + const fixtureDir = path.resolve(fs.realpathSync(os.tmpdir()), "eslint/fixtures"); + + /** @type {import("../../../lib/eslint")["ESLint"]} */ + let ESLint; + + /** + * Returns the path inside of the fixture directory. + * @param {...string} args file path segments. + * @returns {string} The path inside the fixture directory. + * @private + */ + function getFixturePath(...args) { + const filepath = path.join(fixtureDir, ...args); + + try { + return fs.realpathSync(filepath); + } catch (e) { + return filepath; + } + } + + /** + * Create the ESLint object by mocking some of the plugins + * @param {Object} options options for ESLint + * @returns {ESLint} engine object + * @private + */ + function eslintWithPlugins(options) { + return new ESLint({ + ...options, + plugins: { + [examplePluginName]: examplePlugin, + [examplePluginNameWithNamespace]: examplePlugin, + [examplePreprocessorName]: require("../../fixtures/processors/custom-processor") + } + }); + } + + /** + * Call the last argument. + * @param {any[]} args Arguments + * @returns {void} + */ + function callLastArgument(...args) { + process.nextTick(args[args.length - 1], null); + } + + // copy into clean area so as not to get "infected" by this project's .eslintrc files + before(() => { + shell.mkdir("-p", fixtureDir); + shell.cp("-r", "./tests/fixtures/.", fixtureDir); + }); + + beforeEach(() => { + ({ ESLint } = require("../../../lib/eslint/eslint")); + }); + + after(() => { + shell.rm("-r", fixtureDir); + }); + + describe("ESLint constructor function", () => { + it("the default value of 'options.cwd' should be the current working directory.", async () => { + process.chdir(__dirname); + try { + const engine = new ESLint(); + const results = await engine.lintFiles("eslint.js"); + + assert.strictEqual(path.dirname(results[0].filePath), __dirname); + } finally { + process.chdir(originalDir); + } + }); + + it("should report one fatal message when given a path by --ignore-path that is not a file when ignore is true.", () => { + assert.throws(() => { + // eslint-disable-next-line no-new + new ESLint({ ignorePath: fixtureDir }); + }, new RegExp(escapeStringRegExp(`Cannot read .eslintignore file: ${fixtureDir}\nError: EISDIR: illegal operation on a directory, read`), "u")); + }); + + // https://github.com/eslint/eslint/issues/2380 + it("should not modify baseConfig when format is specified", () => { + const customBaseConfig = { root: true }; + + new ESLint({ baseConfig: customBaseConfig }); // eslint-disable-line no-new + + assert.deepStrictEqual(customBaseConfig, { root: true }); + }); + + it("should throw readable messages if removed options are present", () => { + assert.throws( + () => new ESLint({ + cacheFile: "", + configFile: "", + envs: [], + globals: [], + ignorePattern: [], + parser: "", + parserOptions: {}, + rules: {}, + plugins: [] + }), + new RegExp(escapeStringRegExp([ + "Invalid Options:", + "- Unknown options: cacheFile, configFile, envs, globals, ignorePattern, parser, parserOptions, rules", + "- 'cacheFile' has been removed. Please use the 'cacheLocation' option instead.", + "- 'configFile' has been removed. Please use the 'overrideConfigFile' option instead.", + "- 'envs' has been removed. Please use the 'overrideConfig.env' option instead.", + "- 'globals' has been removed. Please use the 'overrideConfig.globals' option instead.", + "- 'ignorePattern' has been removed. Please use the 'overrideConfig.ignorePatterns' option instead.", + "- 'parser' has been removed. Please use the 'overrideConfig.parser' option instead.", + "- 'parserOptions' has been removed. Please use the 'overrideConfig.parserOptions' option instead.", + "- 'rules' has been removed. Please use the 'overrideConfig.rules' option instead.", + "- 'plugins' doesn't add plugins to configuration to load. Please use the 'overrideConfig.plugins' option instead." + ].join("\n")), "u") + ); + }); + + it("should throw readable messages if wrong type values are given to options", () => { + assert.throws( + () => new ESLint({ + allowInlineConfig: "", + baseConfig: "", + cache: "", + cacheLocation: "", + cwd: "foo", + errorOnUnmatchedPattern: "", + extensions: "", + fix: "", + fixTypes: ["xyz"], + globInputPaths: "", + ignore: "", + ignorePath: "", + overrideConfig: "", + overrideConfigFile: "", + plugins: "", + reportUnusedDisableDirectives: "", + resolvePluginsRelativeTo: "", + rulePaths: "", + useEslintrc: "" + }), + new RegExp(escapeStringRegExp([ + "Invalid Options:", + "- 'allowInlineConfig' must be a boolean.", + "- 'baseConfig' must be an object or null.", + "- 'cache' must be a boolean.", + "- 'cacheLocation' must be a non-empty string.", + "- 'cwd' must be an absolute path.", + "- 'errorOnUnmatchedPattern' must be a boolean.", + "- 'extensions' must be an array of non-empty strings or null.", + "- 'fix' must be a boolean or a function.", + "- 'fixTypes' must be an array of any of \"problem\", \"suggestion\", and \"layout\".", + "- 'globInputPaths' must be a boolean.", + "- 'ignore' must be a boolean.", + "- 'ignorePath' must be a non-empty string or null.", + "- 'overrideConfig' must be an object or null.", + "- 'overrideConfigFile' must be a non-empty string or null.", + "- 'plugins' must be an object or null.", + "- 'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null.", + "- 'resolvePluginsRelativeTo' must be a non-empty string or null.", + "- 'rulePaths' must be an array of non-empty strings.", + "- 'useElintrc' must be a boolean." + ].join("\n")), "u") + ); + }); + + it("should throw readable messages if 'plugins' option contains empty key", () => { + assert.throws( + () => new ESLint({ + plugins: { + "eslint-plugin-foo": {}, + "eslint-plugin-bar": {}, + "": {} + } + }), + new RegExp(escapeStringRegExp([ + "Invalid Options:", + "- 'plugins' must not include an empty string." + ].join("\n")), "u") + ); + }); + }); + + describe("lintText()", () => { + let eslint; + + it("should report the total and per file errors when using local cwd .eslintrc", async () => { + eslint = new ESLint(); + const results = await eslint.lintText("var foo = 'bar';"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 5); + assert.strictEqual(results[0].messages[0].ruleId, "strict"); + assert.strictEqual(results[0].messages[1].ruleId, "no-var"); + assert.strictEqual(results[0].messages[2].ruleId, "no-unused-vars"); + assert.strictEqual(results[0].messages[3].ruleId, "quotes"); + assert.strictEqual(results[0].messages[4].ruleId, "eol-last"); + assert.strictEqual(results[0].fixableErrorCount, 3); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + }); + + it("should report the total and per file warnings when using local cwd .eslintrc", async () => { + eslint = new ESLint({ + overrideConfig: { + rules: { + quotes: 1, + "no-var": 1, + "eol-last": 1, + strict: 1, + "no-unused-vars": 1 + } + } + }); + const results = await eslint.lintText("var foo = 'bar';"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 5); + assert.strictEqual(results[0].messages[0].ruleId, "strict"); + assert.strictEqual(results[0].messages[1].ruleId, "no-var"); + assert.strictEqual(results[0].messages[2].ruleId, "no-unused-vars"); + assert.strictEqual(results[0].messages[3].ruleId, "quotes"); + assert.strictEqual(results[0].messages[4].ruleId, "eol-last"); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 3); + assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + }); + + it("should report one message when using specific config file", async () => { + eslint = new ESLint({ + overrideConfigFile: "fixtures/configurations/quotes-error.json", + useEslintrc: false, + cwd: getFixturePath("..") + }); + const results = await eslint.lintText("var foo = 'bar';"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].output, void 0); + assert.strictEqual(results[0].errorCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 1); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + }); + + it("should report the filename when passed in", async () => { + eslint = new ESLint({ + ignore: false, + cwd: getFixturePath() + }); + const options = { filePath: "test.js" }; + const results = await eslint.lintText("var foo = 'bar';", options); + + assert.strictEqual(results[0].filePath, getFixturePath("test.js")); + }); + + it("should return a warning when given a filename by --stdin-filename in excluded files list if warnIgnored is true", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore"), + cwd: getFixturePath("..") + }); + const options = { filePath: "fixtures/passing.js", warnIgnored: true }; + const results = await eslint.lintText("var bar = foo;", options); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, getFixturePath("passing.js")); + assert.strictEqual(results[0].messages[0].severity, 1); + assert.strictEqual(results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override."); + assert.strictEqual(results[0].messages[0].output, void 0); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + }); + + it("should not return a warning when given a filename by --stdin-filename in excluded files list if warnIgnored is false", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore"), + cwd: getFixturePath("..") + }); + const options = { + filePath: "fixtures/passing.js", + warnIgnored: false + }; + + // intentional parsing error + const results = await eslint.lintText("va r bar = foo;", options); + + // should not report anything because the file is ignored + assert.strictEqual(results.length, 0); + }); + + it("should suppress excluded file warnings by default", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore"), + cwd: getFixturePath("..") + }); + const options = { filePath: "fixtures/passing.js" }; + const results = await eslint.lintText("var bar = foo;", options); + + // should not report anything because there are no errors + assert.strictEqual(results.length, 0); + }); + + it("should return a message when given a filename by --stdin-filename in excluded files list and ignore is off", async () => { + eslint = new ESLint({ + ignorePath: "fixtures/.eslintignore", + cwd: getFixturePath(".."), + ignore: false, + useEslintrc: false, + overrideConfig: { + rules: { + "no-undef": 2 + } + } + }); + const options = { filePath: "fixtures/passing.js" }; + const results = await eslint.lintText("var bar = foo;", options); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, getFixturePath("passing.js")); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].output, void 0); + }); + + it("should return a message and fixed text when in fix mode", async () => { + eslint = new ESLint({ + useEslintrc: false, + fix: true, + overrideConfig: { + rules: { + semi: 2 + } + }, + ignore: false, + cwd: getFixturePath() + }); + const options = { filePath: "passing.js" }; + const results = await eslint.lintText("var bar = foo", options); + + assert.deepStrictEqual(results, [ + { + filePath: getFixturePath("passing.js"), + messages: [], + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + output: "var bar = foo;", + usedDeprecatedRules: [] + } + ]); + }); + + it("correctly autofixes semicolon-conflicting-fixes", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true + }); + const inputPath = getFixturePath("autofix/semicolon-conflicting-fixes.js"); + const outputPath = getFixturePath("autofix/semicolon-conflicting-fixes.expected.js"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + + it("correctly autofixes return-conflicting-fixes", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true + }); + const inputPath = getFixturePath("autofix/return-conflicting-fixes.js"); + const outputPath = getFixturePath("autofix/return-conflicting-fixes.expected.js"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + + describe("Fix Types", () => { + it("should throw an error when an invalid fix type is specified", () => { + assert.throws(() => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["layou"] + }); + }, /'fixTypes' must be an array of any of "problem", "suggestion", and "layout"\./iu); + }); + + it("should not fix any rules when fixTypes is used without fix", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: false, + fixTypes: ["layout"] + }); + const inputPath = getFixturePath("fix-types/fix-only-semi.js"); + const results = await eslint.lintFiles([inputPath]); + + assert.strictEqual(results[0].output, void 0); + }); + + it("should not fix non-style rules when fixTypes has only 'layout'", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["layout"] + }); + const inputPath = getFixturePath("fix-types/fix-only-semi.js"); + const outputPath = getFixturePath("fix-types/fix-only-semi.expected.js"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + + it("should not fix style or problem rules when fixTypes has only 'suggestion'", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["suggestion"] + }); + const inputPath = getFixturePath("fix-types/fix-only-prefer-arrow-callback.js"); + const outputPath = getFixturePath("fix-types/fix-only-prefer-arrow-callback.expected.js"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + + it("should fix both style and problem rules when fixTypes has 'suggestion' and 'layout'", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["suggestion", "layout"] + }); + const inputPath = getFixturePath("fix-types/fix-both-semi-and-prefer-arrow-callback.js"); + const outputPath = getFixturePath("fix-types/fix-both-semi-and-prefer-arrow-callback.expected.js"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + + it("should not throw an error when a rule doesn't have a 'meta' property", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["layout"], + rulePaths: [getFixturePath("rules", "fix-types-test")] + }); + const inputPath = getFixturePath("fix-types/ignore-missing-meta.js"); + const outputPath = getFixturePath("fix-types/ignore-missing-meta.expected.js"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + + it("should not throw an error when a rule is loaded after initialization with lintFiles()", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["layout"], + plugins: { + test: { + rules: { + "no-program": require(getFixturePath("rules", "fix-types-test", "no-program.js")) + } + } + } + }); + const inputPath = getFixturePath("fix-types/ignore-missing-meta.js"); + const outputPath = getFixturePath("fix-types/ignore-missing-meta.expected.js"); + const results = await eslint.lintFiles([inputPath]); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + + it("should not throw an error when a rule is loaded after initialization with lintText()", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + fixTypes: ["layout"], + plugins: { + test: { + rules: { + "no-program": require(getFixturePath("rules", "fix-types-test", "no-program.js")) + } + } + } + }); + const inputPath = getFixturePath("fix-types/ignore-missing-meta.js"); + const outputPath = getFixturePath("fix-types/ignore-missing-meta.expected.js"); + const results = await eslint.lintText(fs.readFileSync(inputPath, { encoding: "utf8" }), { filePath: inputPath }); + const expectedOutput = fs.readFileSync(outputPath, "utf8"); + + assert.strictEqual(results[0].output, expectedOutput); + }); + }); + + it("should return a message and omit fixed text when in fix mode and fixes aren't done", async () => { + eslint = new ESLint({ + useEslintrc: false, + fix: true, + overrideConfig: { + rules: { + "no-undef": 2 + } + }, + ignore: false, + cwd: getFixturePath() + }); + const options = { filePath: "passing.js" }; + const results = await eslint.lintText("var bar = foo", options); + + assert.deepStrictEqual(results, [ + { + filePath: getFixturePath("passing.js"), + messages: [ + { + ruleId: "no-undef", + severity: 2, + messageId: "undef", + message: "'foo' is not defined.", + line: 1, + column: 11, + endLine: 1, + endColumn: 14, + nodeType: "Identifier" + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: "var bar = foo", + usedDeprecatedRules: [] + } + ]); + }); + + it("should not delete code if there is a syntax error after trying to autofix.", async () => { + eslint = eslintWithPlugins({ + useEslintrc: false, + fix: true, + overrideConfig: { + plugins: ["example"], + rules: { + "example/make-syntax-error": "error" + } + }, + ignore: false, + cwd: getFixturePath() + }); + const options = { filePath: "test.js" }; + const results = await eslint.lintText("var bar = foo", options); + + assert.deepStrictEqual(results, [ + { + filePath: getFixturePath("test.js"), + messages: [ + { + ruleId: null, + fatal: true, + severity: 2, + message: "Parsing error: Unexpected token is", + line: 1, + column: 19 + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + output: "var bar = foothis is a syntax error.", + usedDeprecatedRules: [] + } + ]); + }); + + it("should not crash even if there are any syntax error since the first time.", async () => { + eslint = new ESLint({ + useEslintrc: false, + fix: true, + overrideConfig: { + rules: { + "example/make-syntax-error": "error" + } + }, + ignore: false, + cwd: getFixturePath() + }); + const options = { filePath: "test.js" }; + const results = await eslint.lintText("var bar =", options); + + assert.deepStrictEqual(results, [ + { + filePath: getFixturePath("test.js"), + messages: [ + { + ruleId: null, + fatal: true, + severity: 2, + message: "Parsing error: Unexpected token", + line: 1, + column: 10 + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: "var bar =", + usedDeprecatedRules: [] + } + ]); + }); + + it("should return source code of file in `source` property when errors are present", async () => { + eslint = new ESLint({ + useEslintrc: false, + overrideConfig: { + rules: { semi: 2 } + } + }); + const results = await eslint.lintText("var foo = 'bar'"); + + assert.strictEqual(results[0].source, "var foo = 'bar'"); + }); + + it("should return source code of file in `source` property when warnings are present", async () => { + eslint = new ESLint({ + useEslintrc: false, + overrideConfig: { + rules: { semi: 1 } + } + }); + const results = await eslint.lintText("var foo = 'bar'"); + + assert.strictEqual(results[0].source, "var foo = 'bar'"); + }); + + + it("should not return a `source` property when no errors or warnings are present", async () => { + eslint = new ESLint({ + useEslintrc: false, + overrideConfig: { + rules: { semi: 2 } + } + }); + const results = await eslint.lintText("var foo = 'bar';"); + + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[0].source, void 0); + }); + + it("should not return a `source` property when fixes are applied", async () => { + eslint = new ESLint({ + useEslintrc: false, + fix: true, + overrideConfig: { + rules: { + semi: 2, + "no-unused-vars": 2 + } + } + }); + const results = await eslint.lintText("var msg = 'hi' + foo\n"); + + assert.strictEqual(results[0].source, void 0); + assert.strictEqual(results[0].output, "var msg = 'hi' + foo;\n"); + }); + + it("should return a `source` property when a parsing error has occurred", async () => { + eslint = new ESLint({ + useEslintrc: false, + overrideConfig: { + rules: { semi: 2 } + } + }); + const results = await eslint.lintText("var bar = foothis is a syntax error.\n return bar;"); + + assert.deepStrictEqual(results, [ + { + filePath: "", + messages: [ + { + ruleId: null, + fatal: true, + severity: 2, + message: "Parsing error: Unexpected token is", + line: 1, + column: 19 + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: "var bar = foothis is a syntax error.\n return bar;", + usedDeprecatedRules: [] + } + ]); + }); + + // https://github.com/eslint/eslint/issues/5547 + it("should respect default ignore rules, even with --no-ignore", async () => { + eslint = new ESLint({ + cwd: getFixturePath(), + ignore: false + }); + const results = await eslint.lintText("var bar = foo;", { filePath: "node_modules/passing.js", warnIgnored: true }); + const expectedMsg = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override."; + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, getFixturePath("node_modules/passing.js")); + assert.strictEqual(results[0].messages[0].message, expectedMsg); + }); + + describe('plugin shorthand notation ("@scope" for "@scope/eslint-plugin")', () => { + const Module = require("module"); + let originalFindPath = null; + + /* eslint-disable no-underscore-dangle */ + before(() => { + originalFindPath = Module._findPath; + Module._findPath = function(id, ...otherArgs) { + if (id === "@scope/eslint-plugin") { + return path.resolve(__dirname, "../../fixtures/plugin-shorthand/basic/node_modules/@scope/eslint-plugin/index.js"); + } + return originalFindPath.call(this, id, ...otherArgs); + }; + }); + after(() => { + Module._findPath = originalFindPath; + }); + /* eslint-enable no-underscore-dangle */ + + it("should resolve 'plugins:[\"@scope\"]' to 'node_modules/@scope/eslint-plugin'.", async () => { + eslint = new ESLint({ cwd: getFixturePath("plugin-shorthand/basic") }); + const [result] = await eslint.lintText("var x = 0", { filePath: "index.js" }); + + assert.strictEqual(result.filePath, getFixturePath("plugin-shorthand/basic/index.js")); + assert.strictEqual(result.messages[0].ruleId, "@scope/rule"); + assert.strictEqual(result.messages[0].message, "OK"); + }); + + it("should resolve 'extends:[\"plugin:@scope/recommended\"]' to 'node_modules/@scope/eslint-plugin'.", async () => { + eslint = new ESLint({ cwd: getFixturePath("plugin-shorthand/extends") }); + const [result] = await eslint.lintText("var x = 0", { filePath: "index.js" }); + + assert.strictEqual(result.filePath, getFixturePath("plugin-shorthand/extends/index.js")); + assert.strictEqual(result.messages[0].ruleId, "@scope/rule"); + assert.strictEqual(result.messages[0].message, "OK"); + }); + }); + + it("should warn when deprecated rules are found in a config", async () => { + eslint = new ESLint({ + cwd: originalDir, + useEslintrc: false, + overrideConfigFile: "tests/fixtures/cli-engine/deprecated-rule-config/.eslintrc.yml" + }); + const [result] = await eslint.lintText("foo"); + + assert.deepStrictEqual( + result.usedDeprecatedRules, + [{ ruleId: "indent-legacy", replacedBy: ["indent"] }] + ); + }); + + it("should throw if non-string value is given to 'code' parameter", async () => { + eslint = new ESLint(); + await assert.rejects(() => eslint.lintText(100), /'code' must be a string/u); + }); + + it("should throw if non-object value is given to 'options' parameter", async () => { + eslint = new ESLint(); + await assert.rejects(() => eslint.lintText("var a = 0", "foo.js"), /'options' must be an object, null, or undefined/u); + }); + + it("should throw if 'options' argument contains unknown key", async () => { + eslint = new ESLint(); + await assert.rejects(() => eslint.lintText("var a = 0", { filename: "foo.js" }), /'options' must not include the unknown option 'filename'/u); + }); + + it("should throw if non-string value is given to 'options.filePath' option", async () => { + eslint = new ESLint(); + await assert.rejects(() => eslint.lintText("var a = 0", { filePath: "" }), /'options.filePath' must be a non-empty string or undefined/u); + }); + + it("should throw if non-boolean value is given to 'options.warnIgnored' option", async () => { + eslint = new ESLint(); + await assert.rejects(() => eslint.lintText("var a = 0", { warnIgnored: "" }), /'options.warnIgnored' must be a boolean or undefined/u); + }); + }); + + describe("lintFiles()", () => { + + /** @type {InstanceType} */ + let eslint; + + it("should use correct parser when custom parser is specified", async () => { + eslint = new ESLint({ + cwd: originalDir, + ignore: false + }); + const filePath = path.resolve(__dirname, "../../fixtures/configurations/parser/custom.js"); + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "Parsing error: Boom!"); + }); + + it("should report zero messages when given a config file and a valid file", async () => { + eslint = new ESLint({ + cwd: originalDir, + overrideConfigFile: ".eslintrc.js" + }); + const results = await eslint.lintFiles(["lib/**/cli*.js"]); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].messages.length, 0); + }); + + it("should handle multiple patterns with overlapping files", async () => { + eslint = new ESLint({ + cwd: originalDir, + overrideConfigFile: ".eslintrc.js" + }); + const results = await eslint.lintFiles(["lib/**/cli*.js", "lib/cli.?s", "lib/{cli,cli-engine/cli-engine}.js"]); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].messages.length, 0); + }); + + it("should report zero messages when given a config file and a valid file and espree as parser", async () => { + eslint = new ESLint({ + overrideConfig: { + parser: "espree", + parserOptions: { + ecmaVersion: 2020 + } + }, + useEslintrc: false + }); + const results = await eslint.lintFiles(["lib/cli.js"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should report zero messages when given a config file and a valid file and esprima as parser", async () => { + eslint = new ESLint({ + overrideConfig: { + parser: "esprima" + }, + useEslintrc: false, + ignore: false + }); + const results = await eslint.lintFiles(["tests/fixtures/passing.js"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should throw an error when given a config file and a valid file and invalid parser", async () => { + eslint = new ESLint({ + overrideConfig: { + parser: "test11" + }, + useEslintrc: false + }); + + await assert.rejects(async () => await eslint.lintFiles(["lib/cli.js"]), /Cannot find module 'test11'/u); + }); + + it("should report zero messages when given a directory with a .js2 file", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + extensions: [".js2"] + }); + const results = await eslint.lintFiles([getFixturePath("files/foo.js2")]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should fall back to defaults when extensions is set to an empty array", async () => { + eslint = new ESLint({ + cwd: getFixturePath("configurations"), + overrideConfigFile: getFixturePath("configurations", "quotes-error.json"), + extensions: [] + }); + const results = await eslint.lintFiles([getFixturePath("single-quoted.js")]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].errorCount, 1); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 1); + assert.strictEqual(results[0].fixableWarningCount, 0); + }); + + it("should report zero messages when given a directory with a .js and a .js2 file", async () => { + eslint = new ESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: getFixturePath("..") + }); + const results = await eslint.lintFiles(["fixtures/files/"]); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].messages.length, 0); + }); + + it("should report zero messages when given a '**' pattern with a .js and a .js2 file", async () => { + eslint = new ESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: path.join(fixtureDir, "..") + }); + const results = await eslint.lintFiles(["fixtures/files/*"]); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].messages.length, 0); + }); + + it("should resolve globs when 'globInputPaths' option is true", async () => { + eslint = new ESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: getFixturePath("..") + }); + const results = await eslint.lintFiles(["fixtures/files/*"]); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].messages.length, 0); + }); + + it("should not resolve globs when 'globInputPaths' option is false", async () => { + eslint = new ESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: getFixturePath(".."), + globInputPaths: false + }); + + await assert.rejects(async () => { + await eslint.lintFiles(["fixtures/files/*"]); + }, /No files matching 'fixtures\/files\/\*' were found \(glob was disabled\)\./u); + }); + + it("should report on all files passed explicitly, even if ignored by default", async () => { + eslint = new ESLint({ + cwd: getFixturePath("cli-engine") + }); + const results = await eslint.lintFiles(["node_modules/foo.js"]); + const expectedMsg = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override."; + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].messages[0].message, expectedMsg); + }); + + it("should report on globs with explicit inclusion of dotfiles, even though ignored by default", async () => { + eslint = new ESLint({ + cwd: getFixturePath("cli-engine"), + overrideConfig: { + rules: { + quotes: [2, "single"] + } + } + }); + const results = await eslint.lintFiles(["hidden/.hiddenfolder/*.js"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].errorCount, 1); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 1); + assert.strictEqual(results[0].fixableWarningCount, 0); + }); + + it("should not check default ignored files without --no-ignore flag", async () => { + eslint = new ESLint({ + cwd: getFixturePath("cli-engine") + }); + + await assert.rejects(async () => { + await eslint.lintFiles(["node_modules"]); + }, /All files matched by 'node_modules' are ignored\./u); + }); + + // https://github.com/eslint/eslint/issues/5547 + it("should not check node_modules files even with --no-ignore flag", async () => { + eslint = new ESLint({ + cwd: getFixturePath("cli-engine"), + ignore: false + }); + + await assert.rejects(async () => { + await eslint.lintFiles(["node_modules"]); + }, /All files matched by 'node_modules' are ignored\./u); + }); + + it("should not check .hidden files if they are passed explicitly without --no-ignore flag", async () => { + eslint = new ESLint({ + cwd: getFixturePath(".."), + useEslintrc: false, + overrideConfig: { + rules: { + quotes: [2, "single"] + } + } + }); + const results = await eslint.lintFiles(["fixtures/files/.bar.js"]); + const expectedMsg = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!'\") to override."; + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].messages[0].message, expectedMsg); + }); + + // https://github.com/eslint/eslint/issues/12873 + it("should not check files within a .hidden folder if they are passed explicitly without the --no-ignore flag", async () => { + eslint = new ESLint({ + cwd: getFixturePath("cli-engine"), + useEslintrc: false, + overrideConfig: { + rules: { + quotes: [2, "single"] + } + } + }); + const results = await eslint.lintFiles(["hidden/.hiddenfolder/double-quotes.js"]); + const expectedMsg = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!'\") to override."; + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].messages[0].message, expectedMsg); + }); + + it("should check .hidden files if they are passed explicitly with --no-ignore flag", async () => { + eslint = new ESLint({ + cwd: getFixturePath(".."), + ignore: false, + useEslintrc: false, + overrideConfig: { + rules: { + quotes: [2, "single"] + } + } + }); + const results = await eslint.lintFiles(["fixtures/files/.bar.js"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].errorCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 1); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + }); + + it("should check .hidden files if they are unignored with an --ignore-pattern", async () => { + eslint = new ESLint({ + cwd: getFixturePath("cli-engine"), + ignore: true, + useEslintrc: false, + overrideConfig: { + ignorePatterns: "!.hidden*", + rules: { + quotes: [2, "single"] + } + } + }); + const results = await eslint.lintFiles(["hidden/"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].errorCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 1); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + }); + + it("should report zero messages when given a pattern with a .js and a .js2 file", async () => { + eslint = new ESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: path.join(fixtureDir, "..") + }); + const results = await eslint.lintFiles(["fixtures/files/*.?s*"]); + + assert.strictEqual(results.length, 2); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].messages.length, 0); + }); + + it("should return one error message when given a config with rules with options and severity level set to error", async () => { + eslint = new ESLint({ + cwd: getFixturePath("configurations"), + overrideConfigFile: getFixturePath("configurations", "quotes-error.json") + }); + const results = await eslint.lintFiles([getFixturePath("single-quoted.js")]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].errorCount, 1); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 1); + assert.strictEqual(results[0].fixableWarningCount, 0); + }); + + it("should return 3 messages when given a config file and a directory of 3 valid files", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: getFixturePath("configurations", "semi-error.json") + }); + const results = await eslint.lintFiles([getFixturePath("formatters")]); + + assert.strictEqual(results.length, 3); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[1].messages.length, 0); + assert.strictEqual(results[2].messages.length, 0); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[1].errorCount, 0); + assert.strictEqual(results[1].warningCount, 0); + assert.strictEqual(results[1].fixableErrorCount, 0); + assert.strictEqual(results[1].fixableWarningCount, 0); + assert.strictEqual(results[2].errorCount, 0); + assert.strictEqual(results[2].warningCount, 0); + assert.strictEqual(results[2].fixableErrorCount, 0); + assert.strictEqual(results[2].fixableWarningCount, 0); + }); + + it("should process when file is given by not specifying extensions", async () => { + eslint = new ESLint({ + ignore: false, + cwd: path.join(fixtureDir, "..") + }); + const results = await eslint.lintFiles(["fixtures/files/foo.js2"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should return zero messages when given a config with environment set to browser", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: getFixturePath("configurations", "env-browser.json") + }); + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("globals-browser.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should return zero messages when given an option to set environment to browser", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfig: { + env: { browser: true }, + rules: { + "no-alert": 0, + "no-undef": 2 + } + } + }); + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("globals-browser.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should return zero messages when given a config with environment set to Node.js", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: getFixturePath("configurations", "env-node.json") + }); + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("globals-node.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should not return results from previous call when calling more than once", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + ignore: false, + overrideConfig: { + rules: { + semi: 2 + } + } + }); + const failFilePath = fs.realpathSync(getFixturePath("missing-semicolon.js")); + const passFilePath = fs.realpathSync(getFixturePath("passing.js")); + + let results = await eslint.lintFiles([failFilePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, failFilePath); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); + assert.strictEqual(results[0].messages[0].severity, 2); + + results = await eslint.lintFiles([passFilePath]); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, passFilePath); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should throw an error when given a directory with all eslint excluded files in the directory", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore") + }); + + await assert.rejects(async () => { + await eslint.lintFiles([getFixturePath("./cli-engine/")]); + }, new RegExp(escapeStringRegExp(`All files matched by '${getFixturePath("./cli-engine/")}' are ignored.`), "u")); + }); + + it("should throw an error when all given files are ignored", async () => { + await assert.rejects(async () => { + await eslint.lintFiles(["tests/fixtures/cli-engine/"]); + }, /All files matched by 'tests\/fixtures\/cli-engine\/' are ignored\./u); + }); + + it("should throw an error when all given files are ignored even with a `./` prefix", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore") + }); + + await assert.rejects(async () => { + await eslint.lintFiles(["./tests/fixtures/cli-engine/"]); + }, /All files matched by '\.\/tests\/fixtures\/cli-engine\/' are ignored\./u); + }); + + // https://github.com/eslint/eslint/issues/3788 + it("should ignore one-level down node_modules when ignore file has 'node_modules/' in it", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath("cli-engine", "nested_node_modules", ".eslintignore"), + useEslintrc: false, + overrideConfig: { + rules: { + quotes: [2, "double"] + } + }, + cwd: getFixturePath("cli-engine", "nested_node_modules") + }); + const results = await eslint.lintFiles(["."]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + }); + + // https://github.com/eslint/eslint/issues/3812 + it("should ignore all files and throw an error when tests/fixtures/ is in ignore file", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath("cli-engine/.eslintignore2"), + useEslintrc: false, + overrideConfig: { + rules: { + quotes: [2, "double"] + } + } + }); + + await assert.rejects(async () => { + await eslint.lintFiles(["./tests/fixtures/cli-engine/"]); + }, /All files matched by '\.\/tests\/fixtures\/cli-engine\/' are ignored\./u); + }); + + it("should throw an error when all given files are ignored via ignore-pattern", async () => { + eslint = new ESLint({ + overrideConfig: { + ignorePatterns: "tests/fixtures/single-quoted.js" + } + }); + + await assert.rejects(async () => { + await eslint.lintFiles(["tests/fixtures/*-quoted.js"]); + }, /All files matched by 'tests\/fixtures\/\*-quoted\.js' are ignored\./u); + }); + + it("should return a warning when an explicitly given file is ignored", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore"), + cwd: getFixturePath() + }); + const filePath = getFixturePath("passing.js"); + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages[0].severity, 1); + assert.strictEqual(results[0].messages[0].message, "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override."); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 1); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + }); + + it("should return two messages when given a file in excluded files list while ignore is off", async () => { + eslint = new ESLint({ + ignorePath: getFixturePath(".eslintignore"), + ignore: false, + overrideConfig: { + rules: { + "no-undef": 2 + } + } + }); + const filePath = fs.realpathSync(getFixturePath("undef.js")); + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages[0].ruleId, "no-undef"); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[1].ruleId, "no-undef"); + assert.strictEqual(results[0].messages[1].severity, 2); + }); + + it("should return zero messages when executing a file with a shebang", async () => { + eslint = new ESLint({ + ignore: false + }); + const results = await eslint.lintFiles([getFixturePath("shebang.js")]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should give a warning when loading a custom rule that doesn't exist", async () => { + eslint = new ESLint({ + ignore: false, + rulePaths: [getFixturePath("rules", "dir1")], + overrideConfigFile: getFixturePath("rules", "missing-rule.json") + }); + const results = await eslint.lintFiles([getFixturePath("rules", "test", "test-custom-rule.js")]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "missing-rule"); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[0].message, "Definition for rule 'missing-rule' was not found."); + }); + + it("should throw an error when loading a bad custom rule", async () => { + eslint = new ESLint({ + ignore: false, + rulePaths: [getFixturePath("rules", "wrong")], + overrideConfigFile: getFixturePath("rules", "eslint.json") + }); + + + await assert.rejects(async () => { + await eslint.lintFiles([getFixturePath("rules", "test", "test-custom-rule.js")]); + }, /Error while loading rule 'custom-rule'/u); + }); + + it("should return one message when a custom rule matches a file", async () => { + eslint = new ESLint({ + ignore: false, + useEslintrc: false, + rulePaths: [getFixturePath("rules/")], + overrideConfigFile: getFixturePath("rules", "eslint.json") + }); + const filePath = fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js")); + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "custom-rule"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + it("should load custom rule from the provided cwd", async () => { + const cwd = path.resolve(getFixturePath("rules")); + + eslint = new ESLint({ + ignore: false, + cwd, + rulePaths: ["./"], + overrideConfigFile: "eslint.json" + }); + const filePath = fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js")); + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "custom-rule"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + it("should return messages when multiple custom rules match a file", async () => { + eslint = new ESLint({ + ignore: false, + rulePaths: [ + getFixturePath("rules", "dir1"), + getFixturePath("rules", "dir2") + ], + overrideConfigFile: getFixturePath("rules", "multi-rulesdirs.json") + }); + const filePath = fs.realpathSync(getFixturePath("rules", "test-multi-rulesdirs.js")); + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-literals"); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[1].ruleId, "no-strings"); + assert.strictEqual(results[0].messages[1].severity, 2); + }); + + it("should return zero messages when executing without useEslintrc flag", async () => { + eslint = new ESLint({ + ignore: false, + useEslintrc: false + }); + const filePath = fs.realpathSync(getFixturePath("missing-semicolon.js")); + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should return zero messages when executing without useEslintrc flag in Node.js environment", async () => { + eslint = new ESLint({ + ignore: false, + useEslintrc: false, + overrideConfig: { + env: { node: true } + } + }); + const filePath = fs.realpathSync(getFixturePath("process-exit.js")); + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should return zero messages and ignore .eslintrc files when executing with no-eslintrc flag", async () => { + eslint = new ESLint({ + ignore: false, + useEslintrc: false, + overrideConfig: { + env: { node: true } + } + }); + const filePath = fs.realpathSync(getFixturePath("eslintrc", "quotes.js")); + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should return zero messages and ignore package.json files when executing with no-eslintrc flag", async () => { + eslint = new ESLint({ + ignore: false, + useEslintrc: false, + overrideConfig: { + env: { node: true } + } + }); + const filePath = fs.realpathSync(getFixturePath("packagejson", "quotes.js")); + const results = await eslint.lintFiles([filePath]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].filePath, filePath); + assert.strictEqual(results[0].messages.length, 0); + }); + + it("should warn when deprecated rules are configured", async () => { + eslint = new ESLint({ + cwd: originalDir, + overrideConfigFile: ".eslintrc.js", + overrideConfig: { + rules: { + "indent-legacy": 1, + "require-jsdoc": 1, + "valid-jsdoc": 1 + } + } + }); + const results = await eslint.lintFiles(["lib/cli*.js"]); + + assert.deepStrictEqual( + results[0].usedDeprecatedRules, + [ + { ruleId: "indent-legacy", replacedBy: ["indent"] }, + { ruleId: "require-jsdoc", replacedBy: [] }, + { ruleId: "valid-jsdoc", replacedBy: [] } + ] + ); + }); + + it("should not warn when deprecated rules are not configured", async () => { + eslint = new ESLint({ + cwd: originalDir, + overrideConfigFile: ".eslintrc.js", + overrideConfig: { + rules: { indent: 1, "valid-jsdoc": 0, "require-jsdoc": 0 } + } + }); + const results = await eslint.lintFiles(["lib/cli*.js"]); + + assert.deepStrictEqual(results[0].usedDeprecatedRules, []); + }); + + it("should warn when deprecated rules are found in a config", async () => { + eslint = new ESLint({ + cwd: originalDir, + overrideConfigFile: "tests/fixtures/cli-engine/deprecated-rule-config/.eslintrc.yml", + useEslintrc: false + }); + const results = await eslint.lintFiles(["lib/cli*.js"]); + + assert.deepStrictEqual( + results[0].usedDeprecatedRules, + [{ ruleId: "indent-legacy", replacedBy: ["indent"] }] + ); + }); + + describe("Fix Mode", () => { + it("should return fixed text on multiple files when in fix mode", async () => { + + /** + * Converts CRLF to LF in output. + * This is a workaround for git's autocrlf option on Windows. + * @param {Object} result A result object to convert. + * @returns {void} + */ + function convertCRLF(result) { + if (result && result.output) { + result.output = result.output.replace(/\r\n/gu, "\n"); + } + } + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + fix: true, + overrideConfig: { + rules: { + semi: 2, + quotes: [2, "double"], + eqeqeq: 2, + "no-undef": 2, + "space-infix-ops": 2 + } + } + }); + const results = await eslint.lintFiles([path.resolve(fixtureDir, `${fixtureDir}/fixmode`)]); + + results.forEach(convertCRLF); + assert.deepStrictEqual(results, [ + { + filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/multipass.js")), + messages: [], + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + output: "true ? \"yes\" : \"no\";\n", + usedDeprecatedRules: [] + }, + { + filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/ok.js")), + messages: [], + errorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + usedDeprecatedRules: [] + }, + { + filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/quotes-semi-eqeqeq.js")), + messages: [ + { + column: 9, + line: 2, + endColumn: 11, + endLine: 2, + message: "Expected '===' and instead saw '=='.", + messageId: "unexpected", + nodeType: "BinaryExpression", + ruleId: "eqeqeq", + severity: 2 + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + output: "var msg = \"hi\";\nif (msg == \"hi\") {\n\n}\n", + usedDeprecatedRules: [] + }, + { + filePath: fs.realpathSync(path.resolve(fixtureDir, "fixmode/quotes.js")), + messages: [ + { + column: 18, + line: 1, + endColumn: 21, + endLine: 1, + messageId: "undef", + message: "'foo' is not defined.", + nodeType: "Identifier", + ruleId: "no-undef", + severity: 2 + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + output: "var msg = \"hi\" + foo;\n", + usedDeprecatedRules: [] + } + ]); + }); + + it("should run autofix even if files are cached without autofix results", async () => { + const baseOptions = { + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + overrideConfig: { + rules: { + semi: 2, + quotes: [2, "double"], + eqeqeq: 2, + "no-undef": 2, + "space-infix-ops": 2 + } + } + }; + + eslint = new ESLint(Object.assign({}, baseOptions, { cache: true, fix: false })); + + // Do initial lint run and populate the cache file + await eslint.lintFiles([path.resolve(fixtureDir, `${fixtureDir}/fixmode`)]); + + eslint = new ESLint(Object.assign({}, baseOptions, { cache: true, fix: true })); + const results = await eslint.lintFiles([path.resolve(fixtureDir, `${fixtureDir}/fixmode`)]); + + assert(results.some(result => result.output)); + }); + }); + + // These tests have to do with https://github.com/eslint/eslint/issues/963 + + describe("configuration hierarchy", () => { + + // Default configuration - blank + it("should return zero messages when executing with no .eslintrc", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + // No default configuration rules - conf/environments.js (/*eslint-env node*/) + it("should return zero messages when executing with no .eslintrc in the Node.js environment", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes-node.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + // Project configuration - first level .eslintrc + it("should return zero messages when executing with .eslintrc in the Node.js environment", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/process-exit.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + // Project configuration - first level .eslintrc + it("should return zero messages when executing with .eslintrc in the Node.js environment", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/process-exit.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + // Project configuration - first level .eslintrc + it("should return one message when executing with .eslintrc", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 2); + }); + + // Project configuration - second level .eslintrc + it("should return one message when executing with local .eslintrc that overrides parent .eslintrc", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/subbroken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "no-console"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + // Project configuration - third level .eslintrc + it("should return one message when executing with local .eslintrc that overrides parent and grandparent .eslintrc", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/subbroken/subsubbroken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + // Project configuration - first level package.json + it("should return one message when executing with package.json", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/packagejson/subdir/wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + // Project configuration - second level package.json + it("should return zero messages when executing with local package.json that overrides parent package.json", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/packagejson/subdir/subsubdir/wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + // Project configuration - third level package.json + it("should return one message when executing with local package.json that overrides parent and grandparent package.json", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/packagejson/subdir/subsubdir/subsubsubdir/wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 2); + }); + + // Project configuration - .eslintrc overrides package.json in same directory + it("should return one message when executing with .eslintrc that overrides a package.json in the same directory", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/packagejson/wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 2); + }); + + // Command line configuration - --config with first level .eslintrc + it("should return two messages when executing with config file that adds to local .eslintrc", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: `${fixtureDir}/config-hierarchy/broken/add-conf.yaml` + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 2); + assert.strictEqual(results[0].messages[1].ruleId, "semi"); + assert.strictEqual(results[0].messages[1].severity, 1); + }); + + // Command line configuration - --config with first level .eslintrc + it("should return no messages when executing with config file that overrides local .eslintrc", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: `${fixtureDir}/config-hierarchy/broken/override-conf.yaml` + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + // Command line configuration - --config with second level .eslintrc + it("should return two messages when executing with config file that adds to local and parent .eslintrc", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: `${fixtureDir}/config-hierarchy/broken/add-conf.yaml` + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/subbroken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "no-console"); + assert.strictEqual(results[0].messages[0].severity, 1); + assert.strictEqual(results[0].messages[1].ruleId, "semi"); + assert.strictEqual(results[0].messages[1].severity, 1); + }); + + // Command line configuration - --config with second level .eslintrc + it("should return one message when executing with config file that overrides local and parent .eslintrc", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: getFixturePath("config-hierarchy/broken/override-conf.yaml") + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/subbroken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "no-console"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + // Command line configuration - --config with first level .eslintrc + it("should return no messages when executing with config file that overrides local .eslintrc", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: `${fixtureDir}/config-hierarchy/broken/override-conf.yaml` + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + }); + + // Command line configuration - --rule with --config and first level .eslintrc + it("should return one message when executing with command line rule and config file that overrides local .eslintrc", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: getFixturePath("config-hierarchy/broken/override-conf.yaml"), + overrideConfig: { + rules: { + quotes: [1, "double"] + } + } + }); + const results = await eslint.lintFiles([fs.realpathSync(`${fixtureDir}/config-hierarchy/broken/console-wrong-quotes.js`)]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + + // Command line configuration - --rule with --config and first level .eslintrc + it("should return one message when executing with command line rule and config file that overrides local .eslintrc", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: getFixturePath("/config-hierarchy/broken/override-conf.yaml"), + overrideConfig: { + rules: { + quotes: [1, "double"] + } + } + }); + const results = await eslint.lintFiles([getFixturePath("config-hierarchy/broken/console-wrong-quotes.js")]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "quotes"); + assert.strictEqual(results[0].messages[0].severity, 1); + }); + }); + + describe("plugins", () => { + it("should return two messages when executing with config file that specifies a plugin", async () => { + eslint = eslintWithPlugins({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: getFixturePath("configurations", "plugins-with-prefix.json"), + useEslintrc: false + }); + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test/test-custom-rule.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "example/example-rule"); + }); + + it("should return two messages when executing with config file that specifies a plugin with namespace", async () => { + eslint = eslintWithPlugins({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: getFixturePath("configurations", "plugins-with-prefix-and-namespace.json"), + useEslintrc: false + }); + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "@eslint/example/example-rule"); + }); + + it("should return two messages when executing with config file that specifies a plugin without prefix", async () => { + eslint = eslintWithPlugins({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: getFixturePath("configurations", "plugins-without-prefix.json"), + useEslintrc: false + }); + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "example/example-rule"); + }); + + it("should return two messages when executing with config file that specifies a plugin without prefix and with namespace", async () => { + eslint = eslintWithPlugins({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: getFixturePath("configurations", "plugins-without-prefix-with-namespace.json"), + useEslintrc: false + }); + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "@eslint/example/example-rule"); + }); + + it("should return two messages when executing with cli option that specifies a plugin", async () => { + eslint = eslintWithPlugins({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + overrideConfig: { + plugins: ["example"], + rules: { "example/example-rule": 1 } + } + }); + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "example/example-rule"); + }); + + it("should return two messages when executing with cli option that specifies preloaded plugin", async () => { + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + overrideConfig: { + plugins: ["test"], + rules: { "test/example-rule": 1 } + }, + plugins: { + "eslint-plugin-test": { rules: { "example-rule": require("../../fixtures/rules/custom-rule") } } + } + }); + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("rules", "test", "test-custom-rule.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "test/example-rule"); + }); + + it("should load plugins from the `loadPluginsRelativeTo` directory, if specified", async () => { + eslint = new ESLint({ + resolvePluginsRelativeTo: getFixturePath("plugins"), + baseConfig: { + plugins: ["with-rules"], + rules: { "with-rules/rule1": "error" } + }, + useEslintrc: false + }); + const results = await eslint.lintText("foo"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "with-rules/rule1"); + assert.strictEqual(results[0].messages[0].message, "Rule report from plugin"); + }); + }); + + describe("cache", () => { + + /** + * helper method to delete a file without caring about exceptions + * @param {string} filePath The file path + * @returns {void} + */ + function doDelete(filePath) { + try { + fs.unlinkSync(filePath); + } catch (ex) { + + /* + * we don't care if the file didn't exist, since our + * intention was to remove the file + */ + } + } + + /** + * helper method to delete the cache files created during testing + * @returns {void} + */ + function deleteCache() { + doDelete(path.resolve(".eslintcache")); + doDelete(path.resolve(".cache/custom-cache")); + } + + beforeEach(() => { + deleteCache(); + }); + + afterEach(() => { + sinon.restore(); + deleteCache(); + }); + + describe("when the cacheFile is a directory or looks like a directory", () => { + + /** + * helper method to delete the cache files created during testing + * @returns {void} + */ + function deleteCacheDir() { + try { + fs.unlinkSync("./tmp/.cacheFileDir/.cache_hashOfCurrentWorkingDirectory"); + } catch (ex) { + + /* + * we don't care if the file didn't exist, since our + * intention was to remove the file + */ + } + } + beforeEach(() => { + deleteCacheDir(); + }); + + afterEach(() => { + deleteCacheDir(); + }); + + it("should create the cache file inside the provided directory", async () => { + assert(!shell.test("-d", path.resolve("./tmp/.cacheFileDir/.cache_hashOfCurrentWorkingDirectory")), "the cache for eslint does not exist"); + + eslint = new ESLint({ + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + cacheLocation: "./tmp/.cacheFileDir/", + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"], + ignore: false + }); + const file = getFixturePath("cache/src", "test-file.js"); + + await eslint.lintFiles([file]); + + assert(shell.test("-f", path.resolve(`./tmp/.cacheFileDir/.cache_${hash(process.cwd())}`)), "the cache for eslint was created"); + + sinon.restore(); + }); + }); + + it("should create the cache file inside the provided directory using the cacheLocation option", async () => { + assert(!shell.test("-d", path.resolve("./tmp/.cacheFileDir/.cache_hashOfCurrentWorkingDirectory")), "the cache for eslint does not exist"); + + eslint = new ESLint({ + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + cacheLocation: "./tmp/.cacheFileDir/", + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"], + ignore: false + }); + const file = getFixturePath("cache/src", "test-file.js"); + + await eslint.lintFiles([file]); + + assert(shell.test("-f", path.resolve(`./tmp/.cacheFileDir/.cache_${hash(process.cwd())}`)), "the cache for eslint was created"); + + sinon.restore(); + }); + + it("should create the cache file inside cwd when no cacheLocation provided", async () => { + const cwd = path.resolve(getFixturePath("cli-engine")); + + eslint = new ESLint({ + useEslintrc: false, + cache: true, + cwd, + overrideConfig: { + rules: { + "no-console": 0 + } + }, + extensions: ["js"], + ignore: false + }); + const file = getFixturePath("cli-engine", "console.js"); + + await eslint.lintFiles([file]); + + assert(shell.test("-f", path.resolve(cwd, ".eslintcache")), "the cache for eslint was created at provided cwd"); + }); + + it("should invalidate the cache if the configuration changed between executions", async () => { + assert(!shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint does not exist"); + + eslint = new ESLint({ + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"], + ignore: false + }); + + let spy = sinon.spy(fs, "readFileSync"); + + let file = getFixturePath("cache/src", "test-file.js"); + + file = fs.realpathSync(file); + const results = await eslint.lintFiles([file]); + + for (const { errorCount, warningCount } of results) { + assert.strictEqual(errorCount + warningCount, 0, "the file passed without errors or warnings"); + } + assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed"); + assert(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); + + // destroy the spy + sinon.restore(); + + eslint = new ESLint({ + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + overrideConfig: { + rules: { + "no-console": 2, + "no-unused-vars": 2 + } + }, + extensions: ["js"], + ignore: false + }); + + // create a new spy + spy = sinon.spy(fs, "readFileSync"); + + const [cachedResult] = await eslint.lintFiles([file]); + + assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed because the config changed"); + assert.strictEqual(cachedResult.errorCount, 1, "since configuration changed the cache was not used an one error was reported"); + assert(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); + }); + + it("should remember the files from a previous run and do not operate on them if not changed", async () => { + assert(!shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint does not exist"); + + eslint = new ESLint({ + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"], + ignore: false + }); + + let spy = sinon.spy(fs, "readFileSync"); + + let file = getFixturePath("cache/src", "test-file.js"); + + file = fs.realpathSync(file); + + const result = await eslint.lintFiles([file]); + + assert.strictEqual(spy.getCall(0).args[0], file, "the module read the file because is considered changed"); + assert(shell.test("-f", path.resolve(".eslintcache")), "the cache for eslint was created"); + + // destroy the spy + sinon.restore(); + + eslint = new ESLint({ + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"], + ignore: false + }); + + // create a new spy + spy = sinon.spy(fs, "readFileSync"); + + const cachedResult = await eslint.lintFiles([file]); + + assert.deepStrictEqual(result, cachedResult, "the result is the same regardless of using cache or not"); + + // assert the file was not processed because the cache was used + assert(!spy.calledWith(file), "the file was not loaded because it used the cache"); + }); + + it("should remember the files from a previous run and do not operate on then if not changed", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + const eslintOptions = { + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + cacheLocation, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"], + cwd: path.join(fixtureDir, "..") + }; + + assert(!shell.test("-f", cacheLocation), "the cache for eslint does not exist"); + + eslint = new ESLint(eslintOptions); + + let file = getFixturePath("cache/src", "test-file.js"); + + file = fs.realpathSync(file); + + await eslint.lintFiles([file]); + + assert(shell.test("-f", cacheLocation), "the cache for eslint was created"); + + eslintOptions.cache = false; + eslint = new ESLint(eslintOptions); + + await eslint.lintFiles([file]); + + assert(!shell.test("-f", cacheLocation), "the cache for eslint was deleted since last run did not used the cache"); + }); + + it("should store in the cache a file that failed the test", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + + assert(!shell.test("-f", cacheLocation), "the cache for eslint does not exist"); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + cacheLocation, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"] + }); + const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); + const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); + const result = await eslint.lintFiles([badFile, goodFile]); + + assert(shell.test("-f", cacheLocation), "the cache for eslint was created"); + const fileCache = fCache.createFromFile(cacheLocation); + const { cache } = fileCache; + + assert.strictEqual(typeof cache.getKey(goodFile), "object", "the entry for the good file is in the cache"); + assert.strictEqual(typeof cache.getKey(badFile), "object", "the entry for the bad file is in the cache"); + const cachedResult = await eslint.lintFiles([badFile, goodFile]); + + assert.deepStrictEqual(result, cachedResult, "result is the same with or without cache"); + }); + + it("should not contain in the cache a file that was deleted", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + + doDelete(cacheLocation); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + cacheLocation, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"] + }); + const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); + const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); + const toBeDeletedFile = fs.realpathSync(getFixturePath("cache/src", "file-to-delete.js")); + + await eslint.lintFiles([badFile, goodFile, toBeDeletedFile]); + const fileCache = fCache.createFromFile(cacheLocation); + let { cache } = fileCache; + + assert.strictEqual(typeof cache.getKey(toBeDeletedFile), "object", "the entry for the file to be deleted is in the cache"); + + // delete the file from the file system + fs.unlinkSync(toBeDeletedFile); + + /* + * file-entry-cache@2.0.0 will remove from the cache deleted files + * even when they were not part of the array of files to be analyzed + */ + await eslint.lintFiles([badFile, goodFile]); + + cache = JSON.parse(fs.readFileSync(cacheLocation)); + + assert.strictEqual(typeof cache[toBeDeletedFile], "undefined", "the entry for the file to be deleted is not in the cache"); + }); + + it("should contain files that were not visited in the cache provided they still exist", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + + doDelete(cacheLocation); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + + // specifying cache true the cache will be created + cache: true, + cacheLocation, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"] + }); + const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); + const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); + const testFile2 = fs.realpathSync(getFixturePath("cache/src", "test-file2.js")); + + await eslint.lintFiles([badFile, goodFile, testFile2]); + + let fileCache = fCache.createFromFile(cacheLocation); + let { cache } = fileCache; + + assert.strictEqual(typeof cache.getKey(testFile2), "object", "the entry for the test-file2 is in the cache"); + + /* + * we pass a different set of files minus test-file2 + * previous version of file-entry-cache would remove the non visited + * entries. 2.0.0 version will keep them unless they don't exist + */ + await eslint.lintFiles([badFile, goodFile]); + + fileCache = fCache.createFromFile(cacheLocation); + cache = fileCache.cache; + + assert.strictEqual(typeof cache.getKey(testFile2), "object", "the entry for the test-file2 is in the cache"); + }); + + it("should not delete cache when executing on text", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + cacheLocation, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"] + }); + + assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); + + await eslint.lintText("var foo = 'bar';"); + + assert(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + }); + + it("should not delete cache when executing on text with a provided filename", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + cacheLocation, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"] + }); + + assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); + + await eslint.lintText("var bar = foo;", { filePath: "fixtures/passing.js" }); + + assert(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + }); + + it("should not delete cache when executing on files with --cache flag", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + cache: true, + cacheLocation, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"] + }); + const file = getFixturePath("cli-engine", "console.js"); + + assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); + + await eslint.lintFiles([file]); + + assert(shell.test("-f", cacheLocation), "the cache for eslint still exists"); + }); + + it("should delete cache when executing on files without --cache flag", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + + eslint = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + cacheLocation, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"] + }); + const file = getFixturePath("cli-engine", "console.js"); + + assert(shell.test("-f", cacheLocation), "the cache for eslint exists"); + + await eslint.lintFiles([file]); + + assert(!shell.test("-f", cacheLocation), "the cache for eslint has been deleted"); + }); + + describe("cacheFile", () => { + it("should use the specified cache file", async () => { + const customCacheFile = path.resolve(".cache/custom-cache"); + + assert(!shell.test("-f", customCacheFile), "the cache for eslint does not exist"); + + eslint = new ESLint({ + useEslintrc: false, + + // specify a custom cache file + cacheLocation: customCacheFile, + + // specifying cache true the cache will be created + cache: true, + overrideConfig: { + rules: { + "no-console": 0, + "no-unused-vars": 2 + } + }, + extensions: ["js"], + cwd: path.join(fixtureDir, "..") + }); + const badFile = fs.realpathSync(getFixturePath("cache/src", "fail-file.js")); + const goodFile = fs.realpathSync(getFixturePath("cache/src", "test-file.js")); + const result = await eslint.lintFiles([badFile, goodFile]); + + assert(shell.test("-f", customCacheFile), "the cache for eslint was created"); + const fileCache = fCache.createFromFile(customCacheFile); + const { cache } = fileCache; + + assert(typeof cache.getKey(goodFile) === "object", "the entry for the good file is in the cache"); + + assert(typeof cache.getKey(badFile) === "object", "the entry for the bad file is in the cache"); + const cachedResult = await eslint.lintFiles([badFile, goodFile]); + + assert.deepStrictEqual(result, cachedResult, "result is the same with or without cache"); + }); + }); + }); + + describe("processors", () => { + it("should return two messages when executing with config file that specifies a processor", async () => { + eslint = eslintWithPlugins({ + overrideConfigFile: getFixturePath("configurations", "processors.json"), + useEslintrc: false, + extensions: ["js", "txt"], + cwd: path.join(fixtureDir, "..") + }); + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("processors", "test", "test-processor.txt"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + }); + + it("should return two messages when executing with config file that specifies preloaded processor", async () => { + eslint = new ESLint({ + useEslintrc: false, + overrideConfig: { + plugins: ["test-processor"], + rules: { + "no-console": 2, + "no-unused-vars": 2 + } + }, + extensions: ["js", "txt"], + cwd: path.join(fixtureDir, ".."), + plugins: { + "test-processor": { + processors: { + ".txt": { + preprocess(text) { + return [text]; + }, + postprocess(messages) { + return messages[0]; + } + } + } + } + } + }); + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("processors", "test", "test-processor.txt"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + }); + + it("should run processors when calling lintFiles with config file that specifies a processor", async () => { + eslint = eslintWithPlugins({ + overrideConfigFile: getFixturePath("configurations", "processors.json"), + useEslintrc: false, + extensions: ["js", "txt"], + cwd: path.join(fixtureDir, "..") + }); + const results = await eslint.lintFiles([getFixturePath("processors", "test", "test-processor.txt")]); + + assert.strictEqual(results[0].messages[0].message, "'b' is defined but never used."); + assert.strictEqual(results[0].messages[0].ruleId, "post-processed"); + }); + + it("should run processors when calling lintFiles with config file that specifies preloaded processor", async () => { + eslint = new ESLint({ + useEslintrc: false, + overrideConfig: { + plugins: ["test-processor"], + rules: { + "no-console": 2, + "no-unused-vars": 2 + } + }, + extensions: ["js", "txt"], + cwd: path.join(fixtureDir, ".."), + plugins: { + "test-processor": { + processors: { + ".txt": { + preprocess(text) { + return [text.replace("a()", "b()")]; + }, + postprocess(messages) { + messages[0][0].ruleId = "post-processed"; + return messages[0]; + } + } + } + } + } + }); + const results = await eslint.lintFiles([getFixturePath("processors", "test", "test-processor.txt")]); + + assert.strictEqual(results[0].messages[0].message, "'b' is defined but never used."); + assert.strictEqual(results[0].messages[0].ruleId, "post-processed"); + }); + + it("should run processors when calling lintText with config file that specifies a processor", async () => { + eslint = eslintWithPlugins({ + overrideConfigFile: getFixturePath("configurations", "processors.json"), + useEslintrc: false, + extensions: ["js", "txt"], + ignore: false + }); + const results = await eslint.lintText("function a() {console.log(\"Test\");}", { filePath: "tests/fixtures/processors/test/test-processor.txt" }); + + assert.strictEqual(results[0].messages[0].message, "'b' is defined but never used."); + assert.strictEqual(results[0].messages[0].ruleId, "post-processed"); + }); + + it("should run processors when calling lintText with config file that specifies preloaded processor", async () => { + eslint = new ESLint({ + useEslintrc: false, + overrideConfig: { + plugins: ["test-processor"], + rules: { + "no-console": 2, + "no-unused-vars": 2 + } + }, + extensions: ["js", "txt"], + ignore: false, + plugins: { + "test-processor": { + processors: { + ".txt": { + preprocess(text) { + return [text.replace("a()", "b()")]; + }, + postprocess(messages) { + messages[0][0].ruleId = "post-processed"; + return messages[0]; + } + } + } + } + } + }); + const results = await eslint.lintText("function a() {console.log(\"Test\");}", { filePath: "tests/fixtures/processors/test/test-processor.txt" }); + + assert.strictEqual(results[0].messages[0].message, "'b' is defined but never used."); + assert.strictEqual(results[0].messages[0].ruleId, "post-processed"); + }); + + describe("autofixing with processors", () => { + const HTML_PROCESSOR = Object.freeze({ + preprocess(text) { + return [text.replace(/^", { filePath: "foo.html" }); + + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[0].output, ""); + }); + + it("should not run in autofix mode when using a processor that does not support autofixing", async () => { + eslint = new ESLint({ + useEslintrc: false, + overrideConfig: { + plugins: ["test-processor"], + rules: { + semi: 2 + } + }, + extensions: ["js", "txt"], + ignore: false, + fix: true, + plugins: { + "test-processor": { processors: { ".html": HTML_PROCESSOR } } + } + }); + const results = await eslint.lintText("", { filePath: "foo.html" }); + + assert.strictEqual(results[0].messages.length, 1); + assert(!Object.prototype.hasOwnProperty.call(results[0], "output")); + }); + + it("should not run in autofix mode when `fix: true` is not provided, even if the processor supports autofixing", async () => { + eslint = new ESLint({ + useEslintrc: false, + overrideConfig: { + plugins: ["test-processor"], + rules: { + semi: 2 + } + }, + extensions: ["js", "txt"], + ignore: false, + plugins: { + "test-processor": { + processors: { + ".html": Object.assign({ supportsAutofix: true }, HTML_PROCESSOR) + } + } + } + }); + const results = await eslint.lintText("", { filePath: "foo.html" }); + + assert.strictEqual(results[0].messages.length, 1); + assert(!Object.prototype.hasOwnProperty.call(results[0], "output")); + }); + }); + }); + + describe("Patterns which match no file should throw errors.", () => { + beforeEach(() => { + eslint = new ESLint({ + cwd: getFixturePath("cli-engine"), + useEslintrc: false + }); + }); + + it("one file", async () => { + await assert.rejects(async () => { + await eslint.lintFiles(["non-exist.js"]); + }, /No files matching 'non-exist\.js' were found\./u); + }); + + it("should throw if the directory exists and is empty", async () => { + await assert.rejects(async () => { + await eslint.lintFiles(["empty"]); + }, /No files matching 'empty' were found\./u); + }); + + it("one glob pattern", async () => { + await assert.rejects(async () => { + await eslint.lintFiles(["non-exist/**/*.js"]); + }, /No files matching 'non-exist\/\*\*\/\*\.js' were found\./u); + }); + + it("two files", async () => { + await assert.rejects(async () => { + await eslint.lintFiles(["aaa.js", "bbb.js"]); + }, /No files matching 'aaa\.js' were found\./u); + }); + + it("a mix of an existing file and a non-existing file", async () => { + await assert.rejects(async () => { + await eslint.lintFiles(["console.js", "non-exist.js"]); + }, /No files matching 'non-exist\.js' were found\./u); + }); + }); + + describe("overrides", () => { + beforeEach(() => { + eslint = new ESLint({ + cwd: getFixturePath("cli-engine/overrides-with-dot"), + ignore: false + }); + }); + + it("should recognize dotfiles", async () => { + const ret = await eslint.lintFiles([".test-target.js"]); + + assert.strictEqual(ret.length, 1); + assert.strictEqual(ret[0].messages.length, 1); + assert.strictEqual(ret[0].messages[0].ruleId, "no-unused-vars"); + }); + }); + + describe("a config file setting should have higher priority than a shareable config file's settings always; https://github.com/eslint/eslint/issues/11510", () => { + beforeEach(() => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => path.join(os.tmpdir(), "eslint/11510"), + files: { + "no-console-error-in-overrides.json": JSON.stringify({ + overrides: [{ + files: ["*.js"], + rules: { "no-console": "error" } + }] + }), + ".eslintrc.json": JSON.stringify({ + extends: "./no-console-error-in-overrides.json", + rules: { "no-console": "off" } + }), + "a.js": "console.log();" + } + })); + eslint = new ESLint(); + }); + + it("should not report 'no-console' error.", async () => { + const results = await eslint.lintFiles("a.js"); + + assert.strictEqual(results.length, 1); + assert.deepStrictEqual(results[0].messages, []); + }); + }); + + describe("configs of plugin rules should be validated even if 'plugins' key doesn't exist; https://github.com/eslint/eslint/issues/11559", () => { + beforeEach(() => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => path.join(os.tmpdir(), "eslint/11559"), + files: { + "node_modules/eslint-plugin-test/index.js": ` + exports.configs = { + recommended: { plugins: ["test"] } + }; + exports.rules = { + foo: { + meta: { schema: [{ type: "number" }] }, + create() { return {}; } + } + }; + `, + ".eslintrc.json": JSON.stringify({ + + // Import via the recommended config. + extends: "plugin:test/recommended", + + // Has invalid option. + rules: { "test/foo": ["error", "invalid-option"] } + }), + "a.js": "console.log();" + } + })); + eslint = new ESLint(); + }); + + it("should throw fatal error.", async () => { + await assert.rejects(async () => { + await eslint.lintFiles("a.js"); + }, /invalid-option/u); + }); + }); + + describe("'--fix-type' should not crash even if plugin rules exist; https://github.com/eslint/eslint/issues/11586", () => { + beforeEach(() => { + ({ ESLint } = defineESLintWithInMemoryFileSystem({ + cwd: () => path.join(os.tmpdir(), "eslint/11586"), + files: { + "node_modules/eslint-plugin-test/index.js": ` + exports.rules = { + "no-example": { + meta: { type: "problem", fixable: "code" }, + create(context) { + return { + Identifier(node) { + if (node.name === "example") { + context.report({ + node, + message: "fix", + fix: fixer => fixer.replaceText(node, "fixed") + }) + } + } + }; + } + } + }; + `, + ".eslintrc.json": JSON.stringify({ + plugins: ["test"], + rules: { "test/no-example": "error" } + }), + "a.js": "example;" + } + })); + eslint = new ESLint({ fix: true, fixTypes: ["problem"] }); + }); + + it("should not crash.", async () => { + const results = await eslint.lintFiles("a.js"); + + assert.strictEqual(results.length, 1); + assert.deepStrictEqual(results[0].messages, []); + assert.deepStrictEqual(results[0].output, "fixed;"); + }); + }); + + describe("multiple processors", () => { + const root = path.join(os.tmpdir(), "eslint/eslint/multiple-processors"); + const commonFiles = { + "node_modules/pattern-processor/index.js": fs.readFileSync( + require.resolve("../../fixtures/processors/pattern-processor"), + "utf8" + ), + "node_modules/eslint-plugin-markdown/index.js": ` + const { defineProcessor } = require("pattern-processor"); + const processor = defineProcessor(${/```(\w+)\n([\s\S]+?)\n```/gu}); + exports.processors = { + ".md": { ...processor, supportsAutofix: true }, + "non-fixable": processor + }; + `, + "node_modules/eslint-plugin-html/index.js": ` + const { defineProcessor } = require("pattern-processor"); + const processor = defineProcessor(${/ + + \`\`\` + ` + }; + + it("should lint only JavaScript blocks if '--ext' was not given.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" } + }) + } + }).ESLint; + eslint = new ESLint({ cwd: root }); + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); + assert.strictEqual(results[0].messages[0].line, 2); + }); + + it("should fix only JavaScript blocks if '--ext' was not given.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" } + }) + } + }).ESLint; + eslint = new ESLint({ cwd: root, fix: true }); + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[0].output, unIndent` + \`\`\`js + console.log("hello");${/* ← fixed */""} + \`\`\` + \`\`\`html +
Hello
+ + + \`\`\` + `); + }); + + it("should lint HTML blocks as well with multiple processors if '--ext' option was given.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" } + }) + } + }).ESLint; + eslint = new ESLint({ cwd: root, extensions: ["js", "html"] }); + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); // JS block + assert.strictEqual(results[0].messages[0].line, 2); + assert.strictEqual(results[0].messages[1].ruleId, "semi"); // JS block in HTML block + assert.strictEqual(results[0].messages[1].line, 7); + }); + + it("should fix HTML blocks as well with multiple processors if '--ext' option was given.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" } + }) + } + }).ESLint; + eslint = new ESLint({ cwd: root, extensions: ["js", "html"], fix: true }); + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(results[0].output, unIndent` + \`\`\`js + console.log("hello");${/* ← fixed */""} + \`\`\` + \`\`\`html +
Hello
+ + + \`\`\` + `); + }); + + it("should use overriden processor; should report HTML blocks but not fix HTML blocks if the processor for '*.html' didn't support autofix.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" }, + overrides: [ + { + files: "*.html", + processor: "html/non-fixable" // supportsAutofix: false + } + ] + }) + } + }).ESLint; + eslint = new ESLint({ cwd: root, extensions: ["js", "html"], fix: true }); + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); // JS Block in HTML Block + assert.strictEqual(results[0].messages[0].line, 7); + assert.strictEqual(results[0].messages[0].fix, void 0); + assert.strictEqual(results[0].output, unIndent` + \`\`\`js + console.log("hello");${/* ← fixed */""} + \`\`\` + \`\`\`html +
Hello
+ + + \`\`\` + `); + }); + + it("should use the config '**/*.html/*.js' to lint JavaScript blocks in HTML.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" }, + overrides: [ + { + files: "*.html", + + // this rules are not used because ESLint re-resolve configs if a code block had a different file extension. + rules: { + semi: "error", + "no-console": "off" + } + }, + { + files: "**/*.html/*.js", + rules: { + semi: "off", + "no-console": "error" + } + } + ] + }) + } + }).ESLint; + eslint = new ESLint({ cwd: root, extensions: ["js", "html"] }); + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); + assert.strictEqual(results[0].messages[0].line, 2); + assert.strictEqual(results[0].messages[1].ruleId, "no-console"); + assert.strictEqual(results[0].messages[1].line, 7); + }); + + it("should use the same config as one which has 'processor' property in order to lint blocks in HTML if the processor was legacy style.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" }, + overrides: [ + { + files: "*.html", + processor: "html/legacy", // this processor returns strings rather than `{text, filename}` + rules: { + semi: "off", + "no-console": "error" + } + }, + { + files: "**/*.html/*.js", + rules: { + semi: "error", + "no-console": "off" + } + } + ] + }) + } + }).ESLint; + eslint = new ESLint({ cwd: root, extensions: ["js", "html"] }); + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 3); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); + assert.strictEqual(results[0].messages[0].line, 2); + assert.strictEqual(results[0].messages[1].ruleId, "no-console"); + assert.strictEqual(results[0].messages[1].line, 7); + assert.strictEqual(results[0].messages[2].ruleId, "no-console"); + assert.strictEqual(results[0].messages[2].line, 10); + }); + + it("should throw an error if invalid processor was specified.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + processor: "markdown/unknown" + }) + } + }).ESLint; + eslint = new ESLint({ cwd: root }); + + await assert.rejects(async () => { + await eslint.lintFiles(["test.md"]); + }, /ESLint configuration of processor in '\.eslintrc\.json' is invalid: 'markdown\/unknown' was not found\./u); + }); + + it("should lint HTML blocks as well with multiple processors if 'overrides[].files' is present.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" }, + overrides: [ + { + files: "*.html", + processor: "html/.html" + }, + { + files: "*.md", + processor: "markdown/.md" + } + ] + }) + } + }).ESLint; + eslint = new ESLint({ cwd: root }); + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); // JS block + assert.strictEqual(results[0].messages[0].line, 2); + assert.strictEqual(results[0].messages[1].ruleId, "semi"); // JS block in HTML block + assert.strictEqual(results[0].messages[1].line, 7); + }); + }); + + describe("MODULE_NOT_FOUND error handling", () => { + const cwd = getFixturePath("module-not-found"); + + beforeEach(() => { + eslint = new ESLint({ cwd }); + }); + + it("should throw an error with a message template when 'extends' property has a non-existence JavaScript config.", async () => { + try { + await eslint.lintText("test", { filePath: "extends-js/test.js" }); + } catch (err) { + assert.strictEqual(err.messageTemplate, "extend-config-missing"); + assert.deepStrictEqual(err.messageData, { + configName: "nonexistent-config", + importerName: getFixturePath("module-not-found", "extends-js", ".eslintrc.yml") + }); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with a message template when 'extends' property has a non-existence plugin config.", async () => { + try { + await eslint.lintText("test", { filePath: "extends-plugin/test.js" }); + } catch (err) { + assert.strictEqual(err.code, "MODULE_NOT_FOUND"); + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + importerName: `extends-plugin${path.sep}.eslintrc.yml`, + pluginName: "eslint-plugin-nonexistent-plugin", + resolvePluginsRelativeTo: path.join(cwd, "extends-plugin") // the directory of the config file. + }); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with a message template when 'plugins' property has a non-existence plugin.", async () => { + try { + await eslint.lintText("test", { filePath: "plugins/test.js" }); + } catch (err) { + assert.strictEqual(err.code, "MODULE_NOT_FOUND"); + assert.strictEqual(err.messageTemplate, "plugin-missing"); + assert.deepStrictEqual(err.messageData, { + importerName: `plugins${path.sep}.eslintrc.yml`, + pluginName: "eslint-plugin-nonexistent-plugin", + resolvePluginsRelativeTo: path.join(cwd, "plugins") // the directory of the config file. + }); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with no message template when a JavaScript config threw a 'MODULE_NOT_FOUND' error.", async () => { + try { + await eslint.lintText("test", { filePath: "throw-in-config-itself/test.js" }); + } catch (err) { + assert.strictEqual(err.code, "MODULE_NOT_FOUND"); + assert.strictEqual(err.messageTemplate, void 0); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with no message template when 'extends' property has a JavaScript config that throws a 'MODULE_NOT_FOUND' error.", async () => { + try { + await eslint.lintText("test", { filePath: "throw-in-extends-js/test.js" }); + } catch (err) { + assert.strictEqual(err.code, "MODULE_NOT_FOUND"); + assert.strictEqual(err.messageTemplate, void 0); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with no message template when 'extends' property has a plugin config that throws a 'MODULE_NOT_FOUND' error.", async () => { + try { + await eslint.lintText("test", { filePath: "throw-in-extends-plugin/test.js" }); + } catch (err) { + assert.strictEqual(err.code, "MODULE_NOT_FOUND"); + assert.strictEqual(err.messageTemplate, void 0); + return; + } + assert.fail("Expected to throw an error"); + }); + + it("should throw an error with no message template when 'plugins' property has a plugin config that throws a 'MODULE_NOT_FOUND' error.", async () => { + try { + await eslint.lintText("test", { filePath: "throw-in-plugins/test.js" }); + } catch (err) { + assert.strictEqual(err.code, "MODULE_NOT_FOUND"); + assert.strictEqual(err.messageTemplate, void 0); + return; + } + assert.fail("Expected to throw an error"); + }); + }); + + describe("with '--rulesdir' option", () => { + it("should use the configured rules which are defined by '--rulesdir' option.", async () => { + const rootPath = getFixturePath("cli-engine/with-rulesdir"); + const StubbedESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => rootPath, + files: { + "internal-rules/test.js": ` + module.exports = context => ({ + ExpressionStatement(node) { + context.report({ node, message: "ok" }) + } + }) + `, + ".eslintrc.json": JSON.stringify({ + root: true, + rules: { test: "error" } + }), + "test.js": "console.log('hello')" + } + }).ESLint; + + eslint = new StubbedESLint({ + rulePaths: ["internal-rules"] + }); + const results = await eslint.lintFiles(["test.js"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 1); + assert.strictEqual(results[0].messages[0].message, "ok"); + }); + }); + + describe("glob pattern '[ab].js'", () => { + const root = getFixturePath("cli-engine/unmatched-glob"); + + it("should match '[ab].js' if existed.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "a.js": "", + "b.js": "", + "ab.js": "", + "[ab].js": "", + ".eslintrc.yml": "root: true" + } + }).ESLint; + eslint = new ESLint(); + const results = await eslint.lintFiles(["[ab].js"]); + const filenames = results.map(r => path.basename(r.filePath)); + + assert.deepStrictEqual(filenames, ["[ab].js"]); + }); + + it("should match 'a.js' and 'b.js' if '[ab].js' didn't existed.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "a.js": "", + "b.js": "", + "ab.js": "", + ".eslintrc.yml": "root: true" + } + }).ESLint; + eslint = new ESLint(); + const results = await eslint.lintFiles(["[ab].js"]); + const filenames = results.map(r => path.basename(r.filePath)); + + assert.deepStrictEqual(filenames, ["a.js", "b.js"]); + }); + }); + + describe("with 'noInlineConfig' setting", () => { + const root = getFixturePath("cli-engine/noInlineConfig"); + + it("should warn directive comments if 'noInlineConfig' was given.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "test.js": "/* globals foo */", + ".eslintrc.yml": "noInlineConfig: true" + } + }).ESLint; + eslint = new ESLint(); + const results = await eslint.lintFiles(["test.js"]); + const messages = results[0].messages; + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "'/*globals*/' has no effect because you have 'noInlineConfig' setting in your config (.eslintrc.yml)."); + }); + + it("should show the config file what the 'noInlineConfig' came from.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-foo/index.js": "module.exports = {noInlineConfig: true}", + "test.js": "/* globals foo */", + ".eslintrc.yml": "extends: foo" + } + }).ESLint; + eslint = new ESLint(); + const results = await eslint.lintFiles(["test.js"]); + const messages = results[0].messages; + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].message, "'/*globals*/' has no effect because you have 'noInlineConfig' setting in your config (.eslintrc.yml » eslint-config-foo)."); + }); + }); + + describe("with 'reportUnusedDisableDirectives' setting", () => { + const root = getFixturePath("cli-engine/reportUnusedDisableDirectives"); + + it("should warn unused 'eslint-disable' comments if 'reportUnusedDisableDirectives' was given.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "test.js": "/* eslint-disable eqeqeq */", + ".eslintrc.yml": "reportUnusedDisableDirectives: true" + } + }).ESLint; + eslint = new ESLint(); + const results = await eslint.lintFiles(["test.js"]); + const messages = results[0].messages; + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 1); + assert.strictEqual(messages[0].message, "Unused eslint-disable directive (no problems were reported from 'eqeqeq')."); + }); + + describe("the runtime option overrides config files.", () => { + it("should not warn unused 'eslint-disable' comments if 'reportUnusedDisableDirectives=off' was given in runtime.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "test.js": "/* eslint-disable eqeqeq */", + ".eslintrc.yml": "reportUnusedDisableDirectives: true" + } + }).ESLint; + eslint = new ESLint({ reportUnusedDisableDirectives: "off" }); + const results = await eslint.lintFiles(["test.js"]); + const messages = results[0].messages; + + assert.strictEqual(messages.length, 0); + }); + + it("should warn unused 'eslint-disable' comments as error if 'reportUnusedDisableDirectives=error' was given in runtime.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "test.js": "/* eslint-disable eqeqeq */", + ".eslintrc.yml": "reportUnusedDisableDirectives: true" + } + }).ESLint; + eslint = new ESLint({ reportUnusedDisableDirectives: "error" }); + const results = await eslint.lintFiles(["test.js"]); + const messages = results[0].messages; + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].severity, 2); + assert.strictEqual(messages[0].message, "Unused eslint-disable directive (no problems were reported from 'eqeqeq')."); + }); + }); + }); + + describe("with 'overrides[*].extends' setting on deep locations", () => { + const root = getFixturePath("cli-engine/deeply-overrides-i-extends"); + + it("should not throw.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + overrides: [{ files: ["*test*"], extends: "two" }] + })}`, + "node_modules/eslint-config-two/index.js": `module.exports = ${JSON.stringify({ + overrides: [{ files: ["*.js"], extends: "three" }] + })}`, + "node_modules/eslint-config-three/index.js": `module.exports = ${JSON.stringify({ + rules: { "no-console": "error" } + })}`, + "test.js": "console.log('hello')", + ".eslintrc.yml": "extends: one" + } + }).ESLint; + eslint = new ESLint(); + const results = await eslint.lintFiles(["test.js"]); + const messages = results[0].messages; + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-console"); + }); + }); + + describe("don't ignore the entry directory.", () => { + const root = getFixturePath("cli-engine/dont-ignore-entry-dir"); + + it("'lintFiles(\".\")' should not load config files from outside of \".\".", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "../.eslintrc.json": "BROKEN FILE", + ".eslintrc.json": JSON.stringify({ root: true }), + "index.js": "console.log(\"hello\")" + } + }).ESLint; + eslint = new ESLint(); + + // Don't throw "failed to load config file" error. + await eslint.lintFiles("."); + }); + + it("'lintFiles(\".\")' should not ignore '.' even if 'ignorePatterns' contains it.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "../.eslintrc.json": JSON.stringify({ ignorePatterns: ["/dont-ignore-entry-dir"] }), + ".eslintrc.json": JSON.stringify({ root: true }), + "index.js": "console.log(\"hello\")" + } + }).ESLint; + eslint = new ESLint(); + + // Don't throw "file not found" error. + await eslint.lintFiles("."); + }); + + it("'lintFiles(\"subdir\")' should not ignore './subdir' even if 'ignorePatterns' contains it.", async () => { + ESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ ignorePatterns: ["/subdir"] }), + "subdir/.eslintrc.json": JSON.stringify({ root: true }), + "subdir/index.js": "console.log(\"hello\")" + } + }).ESLint; + eslint = new ESLint(); + + // Don't throw "file not found" error. + await eslint.lintFiles("subdir"); + }); + }); + + it("should throw if non-boolean value is given to 'options.warnIgnored' option", async () => { + eslint = new ESLint(); + await assert.rejects(() => eslint.lintFiles(777), /'patterns' must be a non-empty string or an array of non-empty strings/u); + await assert.rejects(() => eslint.lintFiles([null]), /'patterns' must be a non-empty string or an array of non-empty strings/u); + }); + }); + + describe("calculateConfigForFile", () => { + it("should return the info from Config#getConfig when called", async () => { + const options = { + overrideConfigFile: getFixturePath("configurations", "quotes-error.json") + }; + const engine = new ESLint(options); + const filePath = getFixturePath("single-quoted.js"); + const actualConfig = await engine.calculateConfigForFile(filePath); + const expectedConfig = new CascadingConfigArrayFactory({ specificConfigPath: options.overrideConfigFile }) + .getConfigArrayForFile(filePath) + .extractConfig(filePath) + .toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(actualConfig, expectedConfig); + }); + + + it("should return the config when run from within a subdir", async () => { + const options = { + cwd: getFixturePath("config-hierarchy", "root-true", "parent", "root", "subdir") + }; + const engine = new ESLint(options); + const filePath = getFixturePath("config-hierarchy", "root-true", "parent", "root", ".eslintrc"); + const actualConfig = await engine.calculateConfigForFile("./.eslintrc"); + const expectedConfig = new CascadingConfigArrayFactory(options) + .getConfigArrayForFile(filePath) + .extractConfig(filePath) + .toCompatibleObjectAsConfigFileContent(); + + assert.deepStrictEqual(actualConfig, expectedConfig); + }); + + it("should throw an error if a directory path was given.", async () => { + const engine = new ESLint(); + + try { + await engine.calculateConfigForFile("."); + } catch (error) { + assert.strictEqual(error.messageTemplate, "print-config-with-directory-path"); + return; + } + assert.fail("should throw an error"); + }); + + it("should throw if non-string value is given to 'filePath' parameter", async () => { + const eslint = new ESLint(); + + await assert.rejects(() => eslint.calculateConfigForFile(null), /'filePath' must be a non-empty string/u); + }); + }); + + describe("isPathIgnored", () => { + it("should check if the given path is ignored", async () => { + const engine = new ESLint({ + ignorePath: getFixturePath(".eslintignore2"), + cwd: getFixturePath() + }); + + assert(await engine.isPathIgnored("undef.js")); + assert(!await engine.isPathIgnored("passing.js")); + }); + + it("should return false if ignoring is disabled", async () => { + const engine = new ESLint({ + ignore: false, + ignorePath: getFixturePath(".eslintignore2"), + cwd: getFixturePath() + }); + + assert(!await engine.isPathIgnored("undef.js")); + }); + + // https://github.com/eslint/eslint/issues/5547 + it("should return true for default ignores even if ignoring is disabled", async () => { + const engine = new ESLint({ + ignore: false, + cwd: getFixturePath("cli-engine") + }); + + assert(await engine.isPathIgnored("node_modules/foo.js")); + }); + + describe("about the default ignore patterns", () => { + it("should always apply defaultPatterns if ignore option is true", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules/package/file.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/node_modules/package/file.js"))); + }); + + it("should still apply defaultPatterns if ignore option is is false", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ ignore: false, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules/package/file.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/node_modules/package/file.js"))); + }); + + it("should allow subfolders of defaultPatterns to be unignored by ignorePattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ + cwd, + overrideConfig: { + ignorePatterns: "!/node_modules/package" + } + }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules", "package", "file.js"))); + }); + + it("should allow subfolders of defaultPatterns to be unignored by ignorePath", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ cwd, ignorePath: getFixturePath("ignored-paths", ".eslintignoreWithUnignoredDefaults") }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules", "package", "file.js"))); + }); + + it("should ignore dotfiles", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", ".foo"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/.bar"))); + }); + + it("should ignore directories beginning with a dot", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", ".foo/bar"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/.bar/baz"))); + }); + + it("should still ignore dotfiles when ignore option disabled", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ ignore: false, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", ".foo"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/.bar"))); + }); + + it("should still ignore directories beginning with a dot when ignore option disabled", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ ignore: false, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", ".foo/bar"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/.bar/baz"))); + }); + + it("should not ignore absolute paths containing '..'", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ cwd }); + + assert(!await engine.isPathIgnored(`${getFixturePath("ignored-paths", "foo")}/../unignored.js`)); + }); + + it("should ignore /node_modules/ relative to .eslintignore when loaded", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ ignorePath: getFixturePath("ignored-paths", ".eslintignore"), cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules", "existing.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo", "node_modules", "existing.js"))); + }); + + it("should ignore /node_modules/ relative to cwd without an .eslintignore", async () => { + const cwd = getFixturePath("ignored-paths", "no-ignore-file"); + const engine = new ESLint({ cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "no-ignore-file", "node_modules", "existing.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "no-ignore-file", "foo", "node_modules", "existing.js"))); + }); + }); + + describe("with no .eslintignore file", () => { + it("should not travel to parent directories to find .eslintignore when it's missing and cwd is provided", async () => { + const cwd = getFixturePath("ignored-paths", "configurations"); + const engine = new ESLint({ cwd }); + + // a .eslintignore in parent directories includes `*.js`, but don't load it. + assert(!await engine.isPathIgnored("foo.js")); + assert(await engine.isPathIgnored("node_modules/foo.js")); + }); + + it("should return false for files outside of the cwd (with no ignore file provided)", async () => { + + // Default ignore patterns should not inadvertently ignore files in parent directories + const engine = new ESLint({ cwd: getFixturePath("ignored-paths", "no-ignore-file") }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + }); + + describe("with .eslintignore file or package.json file", () => { + it("should load .eslintignore from cwd when explicitly passed", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ cwd }); + + // `${cwd}/.eslintignore` includes `sampleignorepattern`. + assert(await engine.isPathIgnored("sampleignorepattern")); + }); + + it("should use package.json's eslintIgnore files if no specified .eslintignore file", async () => { + const cwd = getFixturePath("ignored-paths", "package-json-ignore"); + const engine = new ESLint({ cwd }); + + assert(await engine.isPathIgnored("hello.js")); + assert(await engine.isPathIgnored("world.js")); + }); + + it("should use correct message template if failed to parse package.json", () => { + const cwd = getFixturePath("ignored-paths", "broken-package-json"); + + assert.throws(() => { + try { + // eslint-disable-next-line no-new + new ESLint({ cwd }); + } catch (error) { + assert.strictEqual(error.messageTemplate, "failed-to-read-json"); + throw error; + } + }); + }); + + it("should not use package.json's eslintIgnore files if specified .eslintignore file", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ cwd }); + + /* + * package.json includes `hello.js` and `world.js`. + * .eslintignore includes `sampleignorepattern`. + */ + assert(!await engine.isPathIgnored("hello.js")); + assert(!await engine.isPathIgnored("world.js")); + assert(await engine.isPathIgnored("sampleignorepattern")); + }); + + it("should error if package.json's eslintIgnore is not an array of file paths", () => { + const cwd = getFixturePath("ignored-paths", "bad-package-json-ignore"); + + assert.throws(() => { + // eslint-disable-next-line no-new + new ESLint({ cwd }); + }, /Package\.json eslintIgnore property requires an array of paths/u); + }); + }); + + describe("with --ignore-pattern option", () => { + it("should accept a string for options.ignorePattern", async () => { + const cwd = getFixturePath("ignored-paths", "ignore-pattern"); + const engine = new ESLint({ + overrideConfig: { + ignorePatterns: "ignore-me.txt" + }, + cwd + }); + + assert(await engine.isPathIgnored("ignore-me.txt")); + }); + + it("should accept an array for options.ignorePattern", async () => { + const engine = new ESLint({ + overrideConfig: { + ignorePatterns: ["a", "b"] + }, + useEslintrc: false + }); + + assert(await engine.isPathIgnored("a")); + assert(await engine.isPathIgnored("b")); + assert(!await engine.isPathIgnored("c")); + }); + + it("should return true for files which match an ignorePattern even if they do not exist on the filesystem", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ + overrideConfig: { + ignorePatterns: "not-a-file" + }, + cwd + }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "not-a-file"))); + }); + + it("should return true for file matching an ignore pattern exactly", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ overrideConfig: { ignorePatterns: "undef.js" }, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + + it("should return false for file matching an invalid ignore pattern with leading './'", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ overrideConfig: { ignorePatterns: "./undef.js" }, cwd }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + + it("should return false for file in subfolder of cwd matching an ignore pattern with leading '/'", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ overrideConfig: { ignorePatterns: "/undef.js" }, cwd }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "subdir", "undef.js"))); + }); + + it("should return true for file matching a child of an ignore pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ overrideConfig: { ignorePatterns: "ignore-pattern" }, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "ignore-pattern", "ignore-me.txt"))); + }); + + it("should return true for file matching a grandchild of an ignore pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ overrideConfig: { ignorePatterns: "ignore-pattern" }, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "ignore-pattern", "subdir", "ignore-me.txt"))); + }); + + it("should return false for file not matching any ignore pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ overrideConfig: { ignorePatterns: "failing.js" }, cwd }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "unignored.js"))); + }); + + it("two globstar '**' ignore pattern should ignore files in nested directories", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ overrideConfig: { ignorePatterns: "**/*.js" }, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar/baz.js"))); + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "foo.j2"))); + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar.j2"))); + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar/baz.j2"))); + }); + }); + + describe("with --ignore-path option", () => { + it("initialization with ignorePath should work when cwd is a parent directory", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "custom-name", "ignore-file"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored("custom-name/foo.js")); + }); + + it("initialization with ignorePath should work when the file is in the cwd", async () => { + const cwd = getFixturePath("ignored-paths", "custom-name"); + const ignorePath = getFixturePath("ignored-paths", "custom-name", "ignore-file"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored("foo.js")); + }); + + it("initialization with ignorePath should work when cwd is a subdirectory", async () => { + const cwd = getFixturePath("ignored-paths", "custom-name", "subdirectory"); + const ignorePath = getFixturePath("ignored-paths", "custom-name", "ignore-file"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored("../custom-name/foo.js")); + }); + + it("initialization with invalid file should throw error", () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "not-a-directory", ".foobaz"); + + assert.throws(() => { + // eslint-disable-next-line no-new + new ESLint({ ignorePath, cwd }); + }, /Cannot read \.eslintignore file/u); + }); + + it("should return false for files outside of ignorePath's directory", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "custom-name", "ignore-file"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + + it("should resolve relative paths from CWD", async () => { + const cwd = getFixturePath("ignored-paths", "subdir"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreForDifferentCwd"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/undef.js"))); + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + + it("should resolve relative paths from CWD when it's in a child directory", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "subdir/.eslintignoreInChildDir"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/undef.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/foo.js"))); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules/bar.js"))); + }); + + it("should resolve relative paths from CWD when it contains negated globs", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "subdir/.eslintignoreInChildDir"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored("subdir/blah.txt")); + assert(await engine.isPathIgnored("blah.txt")); + assert(await engine.isPathIgnored("subdir/bar.txt")); + assert(!await engine.isPathIgnored("bar.txt")); + assert(!await engine.isPathIgnored("subdir/baz.txt")); + assert(!await engine.isPathIgnored("baz.txt")); + }); + + it("should resolve default ignore patterns from the CWD even when the ignorePath is in a subdirectory", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "subdir/.eslintignoreInChildDir"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored("node_modules/blah.js")); + }); + + it("should resolve default ignore patterns from the CWD even when the ignorePath is in a parent directory", async () => { + const cwd = getFixturePath("ignored-paths", "subdir"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreForDifferentCwd"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored("node_modules/blah.js")); + }); + + it("should handle .eslintignore which contains CRLF correctly.", async () => { + const ignoreFileContent = fs.readFileSync(getFixturePath("ignored-paths", "crlf/.eslintignore"), "utf8"); + + assert(ignoreFileContent.includes("\r"), "crlf/.eslintignore should contains CR."); + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "crlf/.eslintignore"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "crlf/hide1/a.js"))); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "crlf/hide2/a.js"))); + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "crlf/hide3/a.js"))); + }); + + it("should not include comments in ignore rules", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreWithComments"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(!await engine.isPathIgnored("# should be ignored")); + assert(await engine.isPathIgnored("this_one_not")); + }); + + it("should ignore a non-negated pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreWithNegation"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "negation", "ignore.js"))); + }); + + it("should not ignore a negated pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreWithNegation"); + const engine = new ESLint({ ignorePath, cwd }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "negation", "unignore.js"))); + }); + }); + + describe("with --ignore-path option and --ignore-pattern option", () => { + it("should return false for ignored file when unignored with ignore pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new ESLint({ + ignorePath: getFixturePath("ignored-paths", ".eslintignore"), + overrideConfig: { + ignorePatterns: "!sampleignorepattern" + }, + cwd + }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "sampleignorepattern"))); + }); + }); + + it("should throw if non-string value is given to 'filePath' parameter", async () => { + const eslint = new ESLint(); + + await assert.rejects(() => eslint.isPathIgnored(null), /'filePath' must be a non-empty string/u); + }); + }); + + describe("loadFormatter()", () => { + it("should return a formatter object when a bundled formatter is requested", async () => { + const engine = new ESLint(); + const formatter = await engine.loadFormatter("compact"); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should return a formatter object when no argument is passed", async () => { + const engine = new ESLint(); + const formatter = await engine.loadFormatter(); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should return a formatter object when a custom formatter is requested", async () => { + const engine = new ESLint(); + const formatter = await engine.loadFormatter(getFixturePath("formatters", "simple.js")); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should return a formatter object when a custom formatter is requested, also if the path has backslashes", async () => { + const engine = new ESLint({ + cwd: path.join(fixtureDir, "..") + }); + const formatter = await engine.loadFormatter(".\\fixtures\\formatters\\simple.js"); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should return a formatter object when a formatter prefixed with eslint-formatter is requested", async () => { + const engine = new ESLint({ + cwd: getFixturePath("cli-engine") + }); + const formatter = await engine.loadFormatter("bar"); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should return a formatter object when a formatter is requested, also when the eslint-formatter prefix is included in the format argument", async () => { + const engine = new ESLint({ + cwd: getFixturePath("cli-engine") + }); + const formatter = await engine.loadFormatter("eslint-formatter-bar"); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should return a formatter object when a formatter is requested within a scoped npm package", async () => { + const engine = new ESLint({ + cwd: getFixturePath("cli-engine") + }); + const formatter = await engine.loadFormatter("@somenamespace/foo"); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should return a formatter object when a formatter is requested within a scoped npm package, also when the eslint-formatter prefix is included in the format argument", async () => { + const engine = new ESLint({ + cwd: getFixturePath("cli-engine") + }); + const formatter = await engine.loadFormatter("@somenamespace/eslint-formatter-foo"); + + assert.strictEqual(typeof formatter, "object"); + assert.strictEqual(typeof formatter.format, "function"); + }); + + it("should throw if a customer formatter doesn't exist", async () => { + const engine = new ESLint(); + const formatterPath = getFixturePath("formatters", "doesntexist.js"); + const fullFormatterPath = path.resolve(formatterPath); + + await assert.rejects(async () => { + await engine.loadFormatter(formatterPath); + }, new RegExp(escapeStringRegExp(`There was a problem loading formatter: ${fullFormatterPath}\nError: Cannot find module '${fullFormatterPath}'`), "u")); + }); + + it("should throw if a built-in formatter doesn't exist", async () => { + const engine = new ESLint(); + const fullFormatterPath = path.resolve(__dirname, "../../../lib/cli-engine/formatters/special"); + + await assert.rejects(async () => { + await engine.loadFormatter("special"); + }, new RegExp(escapeStringRegExp(`There was a problem loading formatter: ${fullFormatterPath}\nError: Cannot find module '${fullFormatterPath}'`), "u")); + }); + + it("should throw if the required formatter exists but has an error", async () => { + const engine = new ESLint(); + const formatterPath = getFixturePath("formatters", "broken.js"); + + await assert.rejects(async () => { + await engine.loadFormatter(formatterPath); + }, new RegExp(escapeStringRegExp(`There was a problem loading formatter: ${formatterPath}\nError: Cannot find module 'this-module-does-not-exist'`), "u")); + }); + + it("should throw if a non-string formatter name is passed", async () => { + const engine = new ESLint(); + + await assert.rejects(async () => { + await engine.loadFormatter(5); + }, /'name' must be a string/u); + }); + }); + + describe("getErrorResults()", () => { + it("should report 5 error messages when looking for errors only", async () => { + process.chdir(originalDir); + const engine = new ESLint(); + const results = await engine.lintText("var foo = 'bar';"); + const errorResults = ESLint.getErrorResults(results); + + assert.strictEqual(errorResults[0].messages.length, 5); + assert.strictEqual(errorResults[0].errorCount, 5); + assert.strictEqual(errorResults[0].fixableErrorCount, 3); + assert.strictEqual(errorResults[0].fixableWarningCount, 0); + assert.strictEqual(errorResults[0].messages[0].ruleId, "strict"); + assert.strictEqual(errorResults[0].messages[0].severity, 2); + assert.strictEqual(errorResults[0].messages[1].ruleId, "no-var"); + assert.strictEqual(errorResults[0].messages[1].severity, 2); + assert.strictEqual(errorResults[0].messages[2].ruleId, "no-unused-vars"); + assert.strictEqual(errorResults[0].messages[2].severity, 2); + assert.strictEqual(errorResults[0].messages[3].ruleId, "quotes"); + assert.strictEqual(errorResults[0].messages[3].severity, 2); + assert.strictEqual(errorResults[0].messages[4].ruleId, "eol-last"); + assert.strictEqual(errorResults[0].messages[4].severity, 2); + }); + + it("should not mutate passed report parameter", async () => { + process.chdir(originalDir); + const engine = new ESLint({ + overrideConfig: { + rules: { quotes: [1, "double"] } + } + }); + const results = await engine.lintText("var foo = 'bar';"); + const reportResultsLength = results[0].messages.length; + + ESLint.getErrorResults(results); + + assert.strictEqual(results[0].messages.length, reportResultsLength); + }); + + it("should report a warningCount of 0 when looking for errors only", async () => { + process.chdir(originalDir); + const engine = new ESLint(); + const results = await engine.lintText("var foo = 'bar';"); + const errorResults = ESLint.getErrorResults(results); + + assert.strictEqual(errorResults[0].warningCount, 0); + assert.strictEqual(errorResults[0].fixableWarningCount, 0); + }); + + it("should return 0 error or warning messages even when the file has warnings", async () => { + const engine = new ESLint({ + ignorePath: path.join(fixtureDir, ".eslintignore"), + cwd: path.join(fixtureDir, "..") + }); + const options = { + filePath: "fixtures/passing.js", + warnIgnored: true + }; + const results = await engine.lintText("var bar = foo;", options); + const errorReport = ESLint.getErrorResults(results); + + assert.strictEqual(errorReport.length, 0); + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 1); + }); + + it("should return source code of file in the `source` property", async () => { + process.chdir(originalDir); + const engine = new ESLint({ + useEslintrc: false, + overrideConfig: { + rules: { quotes: [2, "double"] } + } + }); + const results = await engine.lintText("var foo = 'bar';"); + const errorResults = ESLint.getErrorResults(results); + + assert.strictEqual(errorResults[0].messages.length, 1); + assert.strictEqual(errorResults[0].source, "var foo = 'bar';"); + }); + + it("should contain `output` property after fixes", async () => { + process.chdir(originalDir); + const engine = new ESLint({ + useEslintrc: false, + fix: true, + overrideConfig: { + rules: { + semi: 2, + "no-console": 2 + } + } + }); + const results = await engine.lintText("console.log('foo')"); + const errorResults = ESLint.getErrorResults(results); + + assert.strictEqual(errorResults[0].messages.length, 1); + assert.strictEqual(errorResults[0].output, "console.log('foo');"); + }); + }); + + describe("outputFixes()", () => { + afterEach(() => { + sinon.verifyAndRestore(); + }); + + it("should call fs.writeFile() for each result with output", async () => { + const fakeFS = leche.fake(fs); + const spy = fakeFS.writeFile = sinon.spy(callLastArgument); + const localESLint = proxyquire("../../../lib/eslint/eslint", { + fs: fakeFS + }).ESLint; + const results = [ + { + filePath: path.resolve("foo.js"), + output: "bar" + }, + { + filePath: path.resolve("bar.js"), + output: "baz" + } + ]; + + await localESLint.outputFixes(results); + + assert.strictEqual(spy.callCount, 2); + assert(spy.firstCall.calledWithExactly(path.resolve("foo.js"), "bar", sinon.match.func), "First call was incorrect."); + assert(spy.secondCall.calledWithExactly(path.resolve("bar.js"), "baz", sinon.match.func), "Second call was incorrect."); + }); + + it("should call fs.writeFile() for each result with output and not at all for a result without output", async () => { + const fakeFS = leche.fake(fs); + const spy = fakeFS.writeFile = sinon.spy(callLastArgument); + const localESLint = proxyquire("../../../lib/eslint/eslint", { + fs: fakeFS + }).ESLint; + const results = [ + { + filePath: path.resolve("foo.js"), + output: "bar" + }, + { + filePath: path.resolve("abc.js") + }, + { + filePath: path.resolve("bar.js"), + output: "baz" + } + ]; + + await localESLint.outputFixes(results); + + assert.strictEqual(spy.callCount, 2); + assert(spy.firstCall.calledWithExactly(path.resolve("foo.js"), "bar", sinon.match.func), "First call was incorrect."); + assert(spy.secondCall.calledWithExactly(path.resolve("bar.js"), "baz", sinon.match.func), "Second call was incorrect."); + }); + + it("should throw if non object array is given to 'results' parameter", async () => { + await assert.rejects(() => ESLint.outputFixes(null), /'results' must be an array/u); + await assert.rejects(() => ESLint.outputFixes([null]), /'results' must include only objects/u); + }); + }); + + describe("when evaluating code with comments to change config when allowInlineConfig is disabled", () => { + it("should report a violation for disabling rules", async () => { + const code = [ + "alert('test'); // eslint-disable-line no-alert" + ].join("\n"); + const config = { + ignore: true, + allowInlineConfig: false, + overrideConfig: { + env: { browser: true }, + rules: { + "eol-last": 0, + "no-alert": 1, + "no-trailing-spaces": 0, + strict: 0, + quotes: 0 + } + } + }; + const eslintCLI = new ESLint(config); + const results = await eslintCLI.lintText(code); + const messages = results[0].messages; + + assert.strictEqual(messages.length, 1); + assert.strictEqual(messages[0].ruleId, "no-alert"); + }); + + it("should not report a violation by default", async () => { + const code = [ + "alert('test'); // eslint-disable-line no-alert" + ].join("\n"); + const config = { + ignore: true, + allowInlineConfig: true, + overrideConfig: { + env: { browser: true }, + rules: { + "eol-last": 0, + "no-alert": 1, + "no-trailing-spaces": 0, + strict: 0, + quotes: 0 + } + } + }; + const eslintCLI = new ESLint(config); + const results = await eslintCLI.lintText(code); + const messages = results[0].messages; + + assert.strictEqual(messages.length, 0); + }); + }); + + describe("when evaluating code when reportUnusedDisableDirectives is enabled", () => { + it("should report problems for unused eslint-disable directives", async () => { + const eslint = new ESLint({ useEslintrc: false, reportUnusedDisableDirectives: "error" }); + + assert.deepStrictEqual( + await eslint.lintText("/* eslint-disable */"), + [ + { + filePath: "", + messages: [ + { + ruleId: null, + message: "Unused eslint-disable directive (no problems were reported).", + line: 1, + column: 1, + severity: 2, + nodeType: null + } + ], + errorCount: 1, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: "/* eslint-disable */", + usedDeprecatedRules: [] + } + ] + ); + }); + }); + + describe("when retreiving version number", () => { + it("should return current version number", () => { + const eslintCLI = require("../../../lib/eslint").ESLint; + const version = eslintCLI.version; + + assert.strictEqual(typeof version, "string"); + assert(parseInt(version[0], 10) >= 3); + }); + }); + + describe("mutability", () => { + describe("plugins", () => { + it("Loading plugin in one instance doesn't mutate to another instance", async () => { + const filePath = getFixturePath("single-quoted.js"); + const engine1 = eslintWithPlugins({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + overrideConfig: { + plugins: ["example"], + rules: { "example/example-rule": 1 } + } + }); + const engine2 = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false + }); + const fileConfig1 = await engine1.calculateConfigForFile(filePath); + const fileConfig2 = await engine2.calculateConfigForFile(filePath); + + // plugin + assert.deepStrictEqual(fileConfig1.plugins, ["example"], "Plugin is present for engine 1"); + assert.deepStrictEqual(fileConfig2.plugins, [], "Plugin is not present for engine 2"); + }); + }); + + describe("rules", () => { + it("Loading rules in one instance doesn't mutate to another instance", async () => { + const filePath = getFixturePath("single-quoted.js"); + const engine1 = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false, + overrideConfig: { rules: { "example/example-rule": 1 } } + }); + const engine2 = new ESLint({ + cwd: path.join(fixtureDir, ".."), + useEslintrc: false + }); + const fileConfig1 = await engine1.calculateConfigForFile(filePath); + const fileConfig2 = await engine2.calculateConfigForFile(filePath); + + // plugin + assert.deepStrictEqual(fileConfig1.rules["example/example-rule"], [1], "example is present for engine 1"); + assert.strictEqual(fileConfig2.rules["example/example-rule"], void 0, "example is not present for engine 2"); + }); + }); + }); + + describe("with ignorePatterns config", () => { + const root = getFixturePath("cli-engine/ignore-patterns"); + + /** @type {typeof ESLint} */ + let InMemoryESLint; + + describe("ignorePatterns can add an ignore pattern ('foo.js').", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: "foo.js" + }), + "foo.js": "", + "bar.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), false); + assert.strictEqual(await engine.isPathIgnored("subdir/bar.js"), false); + }); + + it("'lintFiles()' should not verify 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js"), + path.join(root, "subdir/bar.js") + ]); + }); + }); + + describe("ignorePatterns can add ignore patterns ('foo.js', '/bar.js').", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: ["foo.js", "/bar.js"] + }), + "foo.js": "", + "bar.js": "", + "baz.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "", + "subdir/baz.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), true); + }); + + it("'isPathIgnored()' should return 'true' for '/bar.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), true); + assert.strictEqual(await engine.isPathIgnored("subdir/bar.js"), false); + }); + + it("'lintFiles()' should not verify 'foo.js' and '/bar.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "baz.js"), + path.join(root, "subdir/bar.js"), + path.join(root, "subdir/baz.js") + ]); + }); + }); + + describe("ignorePatterns can unignore '/node_modules/foo'.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: "!/node_modules/foo" + }), + "node_modules/foo/index.js": "", + "node_modules/foo/.dot.js": "", + "node_modules/bar/index.js": "", + "foo.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'false' for 'node_modules/foo/index.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("node_modules/foo/index.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for 'node_modules/foo/.dot.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("node_modules/foo/.dot.js"), true); + }); + + it("'isPathIgnored()' should return 'true' for 'node_modules/bar/index.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("node_modules/bar/index.js"), true); + }); + + it("'lintFiles()' should verify 'node_modules/foo/index.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo.js"), + path.join(root, "node_modules/foo/index.js") + ]); + }); + }); + + describe("ignorePatterns can unignore '.eslintrc.js'.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "!.eslintrc.js" + })}`, + "foo.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'false' for '.eslintrc.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored(".eslintrc.js"), false); + }); + + it("'lintFiles()' should verify '.eslintrc.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, ".eslintrc.js"), + path.join(root, "foo.js") + ]); + }); + }); + + describe(".eslintignore can re-ignore files that are unignored by ignorePatterns.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "!.*" + })}`, + ".eslintignore": ".foo*", + ".foo.js": "", + ".bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for re-ignored '.foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored(".foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for unignored '.bar.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored(".bar.js"), false); + }); + + it("'lintFiles()' should not verify re-ignored '.foo.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, ".bar.js"), + path.join(root, ".eslintrc.js") + ]); + }); + }); + + describe(".eslintignore can unignore files that are ignored by ignorePatterns.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "*.js" + })}`, + ".eslintignore": "!foo.js", + "foo.js": "", + "bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'false' for unignored 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for ignored 'bar.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), true); + }); + + it("'lintFiles()' should verify unignored 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo.js") + ]); + }); + }); + + describe("ignorePatterns in the config file in a child directory affects to only in the directory.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: "foo.js" + }), + "subdir/.eslintrc.json": JSON.stringify({ + ignorePatterns: "bar.js" + }), + "foo.js": "", + "bar.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "", + "subdir/subsubdir/foo.js": "", + "subdir/subsubdir/bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), true); + assert.strictEqual(await engine.isPathIgnored("subdir/subsubdir/foo.js"), true); + }); + + it("'isPathIgnored()' should return 'true' for 'bar.js' in 'subdir'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("subdir/bar.js"), true); + assert.strictEqual(await engine.isPathIgnored("subdir/subsubdir/bar.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js' in the outside of 'subdir'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), false); + }); + + it("'lintFiles()' should verify 'bar.js' in the outside of 'subdir'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js") + ]); + }); + }); + + describe("ignorePatterns in the config file in a child directory can unignore the ignored files in the parent directory's config.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: "foo.js" + }), + "subdir/.eslintrc.json": JSON.stringify({ + ignorePatterns: "!foo.js" + }), + "foo.js": "", + "subdir/foo.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js' in the root directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'foo.js' in the child directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), false); + }); + + it("'lintFiles()' should verify 'foo.js' in the child directory.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "subdir/foo.js") + ]); + }); + }); + + describe(".eslintignore can unignore files that are ignored by ignorePatterns in the config file in the child directory.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({}), + "subdir/.eslintrc.json": JSON.stringify({ + ignorePatterns: "*.js" + }), + ".eslintignore": "!foo.js", + "foo.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'false' for unignored 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), false); + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for ignored 'bar.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("subdir/bar.js"), true); + }); + + it("'lintFiles()' should verify unignored 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo.js"), + path.join(root, "subdir/foo.js") + ]); + }); + }); + + describe("if the config in a child directory has 'root:true', ignorePatterns in the config file in the parent directory should not be used.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: "foo.js" + }), + "subdir/.eslintrc.json": JSON.stringify({ + root: true, + ignorePatterns: "bar.js" + }), + "foo.js": "", + "bar.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js' in the root directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js' in the root directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), false); + }); + + it("'isPathIgnored()' should return 'false' for 'foo.js' in the child directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for 'bar.js' in the child directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("subdir/bar.js"), true); + }); + + it("'lintFiles()' should verify 'bar.js' in the root directory and 'foo.js' in the child directory.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js"), + path.join(root, "subdir/foo.js") + ]); + }); + }); + + describe("even if the config in a child directory has 'root:true', .eslintignore should be used.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({}), + "subdir/.eslintrc.json": JSON.stringify({ + root: true, + ignorePatterns: "bar.js" + }), + ".eslintignore": "foo.js", + "foo.js": "", + "bar.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js' in the root directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for 'bar.js' in the child directory.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("subdir/bar.js"), true); + }); + + it("'lintFiles()' should verify 'bar.js' in the root directory.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js") + ]); + }); + }); + + describe("ignorePatterns in the shareable config should be used.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "foo.js" + })}`, + ".eslintrc.json": JSON.stringify({ + extends: "one" + }), + "foo.js": "", + "bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), false); + }); + + it("'lintFiles()' should verify 'bar.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js") + ]); + }); + }); + + describe("ignorePatterns in the shareable config should be relative to the entry config file.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "/foo.js" + })}`, + ".eslintrc.json": JSON.stringify({ + extends: "one" + }), + "foo.js": "", + "subdir/foo.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'subdir/foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("subdir/foo.js"), false); + }); + + it("'lintFiles()' should verify 'subdir/foo.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "subdir/foo.js") + ]); + }); + }); + + describe("ignorePatterns in a config file can unignore the files which are ignored by ignorePatterns in the shareable config.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + ignorePatterns: "*.js" + })}`, + ".eslintrc.json": JSON.stringify({ + extends: "one", + ignorePatterns: "!bar.js" + }), + "foo.js": "", + "bar.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for 'bar.js'.", async () => { + const engine = new InMemoryESLint(); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), false); + }); + + it("'lintFiles()' should verify 'bar.js'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js") + ]); + }); + }); + + describe("ignorePatterns in a config file should not be used if --no-ignore option was given.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + ignorePatterns: "*.js" + }), + "foo.js": "" + } + }).ESLint; + }); + + it("'isPathIgnored()' should return 'false' for 'foo.js'.", async () => { + const engine = new InMemoryESLint({ ignore: false }); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), false); + }); + + it("'lintFiles()' should verify 'foo.js'.", async () => { + const engine = new InMemoryESLint({ ignore: false }); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo.js") + ]); + }); + }); + + describe("ignorePatterns in overrides section is not allowed.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.js": `module.exports = ${JSON.stringify({ + overrides: [ + { + files: "*.js", + ignorePatterns: "foo.js" + } + ] + })}`, + "foo.js": "" + } + }).ESLint; + }); + + it("should throw a configuration error.", async () => { + await assert.rejects(async () => { + const engine = new InMemoryESLint(); + + await engine.lintFiles("*.js"); + }, /Unexpected top-level property "overrides\[0\]\.ignorePatterns"/u); + }); + }); + }); + + describe("'overrides[].files' adds lint targets", () => { + const root = getFixturePath("cli-engine/additional-lint-targets"); + let InMemoryESLint; + + describe("if { files: 'foo/*.txt', excludedFiles: '**/ignore.txt' } is present,", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + overrides: [ + { + files: "foo/*.txt", + excludedFiles: "**/ignore.txt" + } + ] + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "foo/ignore.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "bar/ignore.txt": "", + "test.js": "", + "test.txt": "", + "ignore.txt": "" + } + }).ESLint; + }); + + it("'lintFiles()' with a directory path should contain 'foo/test.txt'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles(".")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "test.js") + ]); + }); + + it("'lintFiles()' with a glob pattern '*.js' should not contain 'foo/test.txt'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/test.js"), + path.join(root, "test.js") + ]); + }); + }); + + describe("if { files: 'foo/**/*.txt' } is present,", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + overrides: [ + { + files: "foo/**/*.txt" + } + ] + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "test.js": "", + "test.txt": "" + } + }).ESLint; + }); + + it("'lintFiles()' with a directory path should contain 'foo/test.txt' and 'foo/nested/test.txt'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles(".")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/nested/test.txt"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "test.js") + ]); + }); + }); + + describe("if { files: 'foo/**/*' } is present,", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + overrides: [ + { + files: "foo/**/*" + } + ] + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "test.js": "", + "test.txt": "" + } + }).ESLint; + }); + + it("'lintFiles()' with a directory path should NOT contain 'foo/test.txt' and 'foo/nested/test.txt'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles(".")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/test.js"), + path.join(root, "test.js") + ]); + }); + }); + + describe("if { files: 'foo/**/*.txt' } is present in a shareable config,", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-foo/index.js": `module.exports = ${JSON.stringify({ + overrides: [ + { + files: "foo/**/*.txt" + } + ] + })}`, + ".eslintrc.json": JSON.stringify({ + extends: "foo" + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "test.js": "", + "test.txt": "" + } + }).ESLint; + }); + + it("'lintFiles()' with a directory path should contain 'foo/test.txt' and 'foo/nested/test.txt'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles(".")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/nested/test.txt"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "test.js") + ]); + }); + }); + + describe("if { files: 'foo/**/*.txt' } is present in a plugin config,", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": `exports.configs = ${JSON.stringify({ + bar: { + overrides: [ + { + files: "foo/**/*.txt" + } + ] + } + })}`, + ".eslintrc.json": JSON.stringify({ + extends: "plugin:foo/bar" + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "test.js": "", + "test.txt": "" + } + }).ESLint; + }); + + it("'lintFiles()' with a directory path should contain 'foo/test.txt' and 'foo/nested/test.txt'.", async () => { + const engine = new InMemoryESLint(); + const filePaths = (await engine.lintFiles(".")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/nested/test.txt"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "test.js") + ]); + }); + }); + }); + + describe("'ignorePatterns', 'overrides[].files', and 'overrides[].excludedFiles' of the configuration that the '--config' option provided should be resolved from CWD.", () => { + const root = getFixturePath("cli-engine/config-and-overrides-files"); + + /** @type {ESLint} */ + let InMemoryESLint; + + describe("if { files: 'foo/*.txt', ... } is present by '--config node_modules/myconf/.eslintrc.json',", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/myconf/.eslintrc.json": JSON.stringify({ + overrides: [ + { + files: "foo/*.js", + rules: { + eqeqeq: "error" + } + } + ] + }), + "node_modules/myconf/foo/test.js": "a == b", + "foo/test.js": "a == b" + } + }).ESLint; + }); + + it("'lintFiles()' with 'foo/test.js' should use the override entry.", async () => { + const engine = new InMemoryESLint({ + overrideConfigFile: "node_modules/myconf/.eslintrc.json", + cwd: root, + ignore: false, + useEslintrc: false + }); + const results = await engine.lintFiles("foo/test.js"); + + // Expected to be an 'eqeqeq' error because the file matches to `$CWD/foo/*.js`. + assert.deepStrictEqual(results, [ + { + errorCount: 1, + filePath: path.join(root, "foo/test.js"), + fixableErrorCount: 0, + fixableWarningCount: 0, + messages: [ + { + column: 3, + endColumn: 5, + endLine: 1, + line: 1, + message: "Expected '===' and instead saw '=='.", + messageId: "unexpected", + nodeType: "BinaryExpression", + ruleId: "eqeqeq", + severity: 2 + } + ], + source: "a == b", + usedDeprecatedRules: [], + warningCount: 0 + } + ]); + }); + + it("'lintFiles()' with 'node_modules/myconf/foo/test.js' should NOT use the override entry.", async () => { + const engine = new InMemoryESLint({ + overrideConfigFile: "node_modules/myconf/.eslintrc.json", + cwd: root, + ignore: false, + useEslintrc: false + }); + const results = await engine.lintFiles("node_modules/myconf/foo/test.js"); + + // Expected to be no errors because the file doesn't match to `$CWD/foo/*.js`. + assert.deepStrictEqual(results, [ + { + errorCount: 0, + filePath: path.join(root, "node_modules/myconf/foo/test.js"), + fixableErrorCount: 0, + fixableWarningCount: 0, + messages: [], + usedDeprecatedRules: [], + warningCount: 0 + } + ]); + }); + }); + + describe("if { files: '*', excludedFiles: 'foo/*.txt', ... } is present by '--config node_modules/myconf/.eslintrc.json',", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/myconf/.eslintrc.json": JSON.stringify({ + overrides: [ + { + files: "*", + excludedFiles: "foo/*.js", + rules: { + eqeqeq: "error" + } + } + ] + }), + "node_modules/myconf/foo/test.js": "a == b", + "foo/test.js": "a == b" + } + }).ESLint; + }); + + it("'lintFiles()' with 'foo/test.js' should NOT use the override entry.", async () => { + const engine = new InMemoryESLint({ + overrideConfigFile: "node_modules/myconf/.eslintrc.json", + cwd: root, + ignore: false, + useEslintrc: false + }); + const results = await engine.lintFiles("foo/test.js"); + + // Expected to be no errors because the file matches to `$CWD/foo/*.js`. + assert.deepStrictEqual(results, [ + { + errorCount: 0, + filePath: path.join(root, "foo/test.js"), + fixableErrorCount: 0, + fixableWarningCount: 0, + messages: [], + usedDeprecatedRules: [], + warningCount: 0 + } + ]); + }); + + it("'lintFiles()' with 'node_modules/myconf/foo/test.js' should use the override entry.", async () => { + const engine = new InMemoryESLint({ + overrideConfigFile: "node_modules/myconf/.eslintrc.json", + cwd: root, + ignore: false, + useEslintrc: false + }); + const results = await engine.lintFiles("node_modules/myconf/foo/test.js"); + + // Expected to be an 'eqeqeq' error because the file doesn't match to `$CWD/foo/*.js`. + assert.deepStrictEqual(results, [ + { + errorCount: 1, + filePath: path.join(root, "node_modules/myconf/foo/test.js"), + fixableErrorCount: 0, + fixableWarningCount: 0, + messages: [ + { + column: 3, + endColumn: 5, + endLine: 1, + line: 1, + message: "Expected '===' and instead saw '=='.", + messageId: "unexpected", + nodeType: "BinaryExpression", + ruleId: "eqeqeq", + severity: 2 + } + ], + source: "a == b", + usedDeprecatedRules: [], + warningCount: 0 + } + ]); + }); + }); + + describe("if { ignorePatterns: 'foo/*.txt', ... } is present by '--config node_modules/myconf/.eslintrc.json',", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/myconf/.eslintrc.json": JSON.stringify({ + ignorePatterns: ["!/node_modules/myconf", "foo/*.js"], + rules: { + eqeqeq: "error" + } + }), + "node_modules/myconf/foo/test.js": "a == b", + "foo/test.js": "a == b" + } + }).ESLint; + }); + + it("'lintFiles()' with '**/*.js' should iterate 'node_modules/myconf/foo/test.js' but not 'foo/test.js'.", async () => { + const engine = new InMemoryESLint({ + overrideConfigFile: "node_modules/myconf/.eslintrc.json", + cwd: root, + useEslintrc: false + }); + const files = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(files, [ + path.join(root, "node_modules/myconf/foo/test.js") + ]); + }); + }); + }); + + describe("plugin conflicts", () => { + let uid = 0; + let root = ""; + + beforeEach(() => { + root = getFixturePath(`eslint/plugin-conflicts-${++uid}`); + }); + + /** @type {typeof ESLint} */ + let InMemoryESLint; + + /** + * Verify thrown errors. + * @param {() => Promise} f The function to run and throw. + * @param {Record} props The properties to verify. + * @returns {Promise} void + */ + async function assertThrows(f, props) { + try { + await f(); + } catch (error) { + for (const [key, value] of Object.entries(props)) { + assert.deepStrictEqual(error[key], value, key); + } + return; + } + + assert.fail("Function should throw an error, but not."); + } + + describe("between a config file and linear extendees.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-one/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + extends: ["two"], + plugins: ["foo"] + })}`, + "node_modules/eslint-config-two/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-two/index.js": `module.exports = ${JSON.stringify({ + plugins: ["foo"] + })}`, + ".eslintrc.json": JSON.stringify({ + extends: ["one"], + plugins: ["foo"] + }), + "test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file.)", async () => { + const engine = new InMemoryESLint({ cwd: root }); + + await engine.lintFiles("test.js"); + }); + }); + + describe("between a config file and same-depth extendees.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-one/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-one/index.js": `module.exports = ${JSON.stringify({ + plugins: ["foo"] + })}`, + "node_modules/eslint-config-two/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/eslint-config-two/index.js": `module.exports = ${JSON.stringify({ + plugins: ["foo"] + })}`, + ".eslintrc.json": JSON.stringify({ + extends: ["one", "two"], + plugins: ["foo"] + }), + "test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file.)", async () => { + const engine = new InMemoryESLint({ cwd: root }); + + await engine.lintFiles("test.js"); + }); + }); + + describe("between two config files in different directories, with single node_modules.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file, but there are two entry config files, but node_modules directory is unique.)", async () => { + const engine = new InMemoryESLint({ cwd: root }); + + await engine.lintFiles("subdir/test.js"); + }); + }); + + describe("between two config files in different directories, with multiple node_modules.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/node_modules/eslint-plugin-foo/index.js": "", + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should throw plugin-conflict error. (Load the plugin from the base directory of the entry config file, but there are two entry config files.)", async () => { + const engine = new InMemoryESLint({ cwd: root }); + + await assertThrows( + () => engine.lintFiles("subdir/test.js"), + { + message: `Plugin "foo" was conflicted between "subdir${path.sep}.eslintrc.json" and ".eslintrc.json".`, + messageTemplate: "plugin-conflict", + messageData: { + pluginId: "foo", + plugins: [ + { + filePath: path.join(root, "subdir/node_modules/eslint-plugin-foo/index.js"), + importerName: `subdir${path.sep}.eslintrc.json` + }, + { + filePath: path.join(root, "node_modules/eslint-plugin-foo/index.js"), + importerName: ".eslintrc.json" + } + ] + } + } + ); + }); + }); + + describe("between '--config' option and a regular config file, with single node_modules.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "node_modules/mine/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file, but there are two entry config files, but node_modules directory is unique.)", async () => { + const engine = new InMemoryESLint({ + cwd: root, + overrideConfigFile: "node_modules/mine/.eslintrc.json" + }); + + await engine.lintFiles("test.js"); + }); + }); + + describe("between '--config' option and a regular config file, with multiple node_modules.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "node_modules/mine/node_modules/eslint-plugin-foo/index.js": "", + "node_modules/mine/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should throw plugin-conflict error. (Load the plugin from the base directory of the entry config file, but there are two entry config files.)", async () => { + const engine = new InMemoryESLint({ + cwd: root, + overrideConfigFile: "node_modules/mine/.eslintrc.json" + }); + + await assertThrows( + () => engine.lintFiles("test.js"), + { + message: "Plugin \"foo\" was conflicted between \"--config\" and \".eslintrc.json\".", + messageTemplate: "plugin-conflict", + messageData: { + pluginId: "foo", + plugins: [ + { + filePath: path.join(root, "node_modules/mine/node_modules/eslint-plugin-foo/index.js"), + importerName: "--config" + }, + { + filePath: path.join(root, "node_modules/eslint-plugin-foo/index.js"), + importerName: ".eslintrc.json" + } + ] + } + } + ); + }); + }); + + describe("between '--plugin' option and a regular config file, with single node_modules.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should NOT throw plugin-conflict error. (Load the plugin from both CWD and the base directory of the entry config file, but node_modules directory is unique.)", async () => { + const engine = new InMemoryESLint({ + cwd: root, + overrideConfig: { plugins: ["foo"] } + }); + + await engine.lintFiles("subdir/test.js"); + }); + }); + + describe("between '--plugin' option and a regular config file, with multiple node_modules.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + "subdir/node_modules/eslint-plugin-foo/index.js": "", + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should throw plugin-conflict error. (Load the plugin from both CWD and the base directory of the entry config file.)", async () => { + const engine = new InMemoryESLint({ + cwd: root, + overrideConfig: { plugins: ["foo"] } + }); + + await assertThrows( + () => engine.lintFiles("subdir/test.js"), + { + message: `Plugin "foo" was conflicted between "CLIOptions" and "subdir${path.sep}.eslintrc.json".`, + messageTemplate: "plugin-conflict", + messageData: { + pluginId: "foo", + plugins: [ + { + filePath: path.join(root, "node_modules/eslint-plugin-foo/index.js"), + importerName: "CLIOptions" + }, + { + filePath: path.join(root, "subdir/node_modules/eslint-plugin-foo/index.js"), + importerName: `subdir${path.sep}.eslintrc.json` + } + ] + } + } + ); + }); + }); + + describe("'--resolve-plugins-relative-to' option overrides the location that ESLint load plugins from.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": "", + ".eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/node_modules/eslint-plugin-foo/index.js": "", + "subdir/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "subdir/test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should NOT throw plugin-conflict error. (Load the plugin from '--resolve-plugins-relative-to'.)", async () => { + const engine = new InMemoryESLint({ + cwd: root, + resolvePluginsRelativeTo: root + }); + + await engine.lintFiles("subdir/test.js"); + }); + }); + + describe("between two config files with different target files.", () => { + beforeEach(() => { + InMemoryESLint = defineESLintWithInMemoryFileSystem({ + cwd: () => root, + files: { + "one/node_modules/eslint-plugin-foo/index.js": "", + "one/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "one/test.js": "", + "two/node_modules/eslint-plugin-foo/index.js": "", + "two/.eslintrc.json": JSON.stringify({ + plugins: ["foo"] + }), + "two/test.js": "" + } + }).ESLint; + }); + + it("'lintFiles()' should NOT throw plugin-conflict error. (Load the plugin from the base directory of the entry config file for each target file. Not related to each other.)", async () => { + const engine = new InMemoryESLint({ cwd: root }); + const results = await engine.lintFiles("*/test.js"); + + assert.strictEqual(results.length, 2); + }); + }); + }); +}); diff --git a/tests/lib/init/npm-utils.js b/tests/lib/init/npm-utils.js index d0326bd1330..8465796a367 100644 --- a/tests/lib/init/npm-utils.js +++ b/tests/lib/init/npm-utils.js @@ -14,7 +14,7 @@ const sinon = require("sinon"), npmUtils = require("../../../lib/init/npm-utils"), log = require("../../../lib/shared/logging"), - { defineInMemoryFs } = require("../_utils"); + { defineInMemoryFs } = require("../../_utils"); const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); diff --git a/tests/lib/rules/implicit-arrow-linebreak.js b/tests/lib/rules/implicit-arrow-linebreak.js index 089424e1c47..c65d4750a7d 100644 --- a/tests/lib/rules/implicit-arrow-linebreak.js +++ b/tests/lib/rules/implicit-arrow-linebreak.js @@ -10,7 +10,7 @@ const rule = require("../../../lib/rules/implicit-arrow-linebreak"); const { RuleTester } = require("../../../lib/rule-tester"); -const { unIndent } = require("../_utils"); +const { unIndent } = require("../../_utils"); const EXPECTED_LINEBREAK = { messageId: "expected" }; const UNEXPECTED_LINEBREAK = { messageId: "unexpected" }; diff --git a/tests/lib/rules/indent.js b/tests/lib/rules/indent.js index 7496da218d1..56589be5169 100644 --- a/tests/lib/rules/indent.js +++ b/tests/lib/rules/indent.js @@ -21,7 +21,7 @@ const path = require("path"); const fixture = fs.readFileSync(path.join(__dirname, "../../fixtures/rules/indent/indent-invalid-fixture-1.js"), "utf8"); const fixedFixture = fs.readFileSync(path.join(__dirname, "../../fixtures/rules/indent/indent-valid-fixture-1.js"), "utf8"); const parser = require("../../fixtures/fixture-parser"); -const { unIndent } = require("../_utils"); +const { unIndent } = require("../../_utils"); /** @@ -5590,9 +5590,9 @@ ruleTester.run("indent", rule, { \${a} \${b} template literal - \`(() => { + \`(() => { foo(); - + tagTwo\`multiline template literal @@ -5609,12 +5609,12 @@ ruleTester.run("indent", rule, { tagOne\`multiline template literal - \${a} \${b}\`({ + \${a} \${b}\`({ foo: 1, bar: tagTwo\`multiline template literal\`(() => { - + baz(); }) }); @@ -5651,7 +5651,7 @@ ruleTester.run("indent", rule, { code: unIndent` foo .bar - .baz\` template + .baz\` template literal \`(() => { baz(); }) @@ -11207,9 +11207,9 @@ ruleTester.run("indent", rule, { tagOne\`multiline \${a} \${b} template literal - \`(() => { + \`(() => { foo(); - + tagTwo\`multiline template literal @@ -11223,9 +11223,9 @@ ruleTester.run("indent", rule, { tagOne\`multiline \${a} \${b} template literal - \`(() => { + \`(() => { foo(); - + tagTwo\`multiline template literal @@ -11250,7 +11250,7 @@ ruleTester.run("indent", rule, { bar: tagTwo\`multiline template literal\`(() => { - + baz(); }) }); @@ -11263,7 +11263,7 @@ ruleTester.run("indent", rule, { bar: tagTwo\`multiline template literal\`(() => { - + baz(); }) }); diff --git a/tests/lib/rules/object-shorthand.js b/tests/lib/rules/object-shorthand.js index b9b81329f91..01a72f4e6f0 100644 --- a/tests/lib/rules/object-shorthand.js +++ b/tests/lib/rules/object-shorthand.js @@ -11,7 +11,7 @@ const rule = require("../../../lib/rules/object-shorthand"), { RuleTester } = require("../../../lib/rule-tester"); -const { unIndent } = require("../_utils"); +const { unIndent } = require("../../_utils"); //------------------------------------------------------------------------------ // Tests diff --git a/tests/lib/shared/runtime-info.js b/tests/lib/shared/runtime-info.js index 79c50a414ac..76ca7c7ca28 100644 --- a/tests/lib/shared/runtime-info.js +++ b/tests/lib/shared/runtime-info.js @@ -12,7 +12,7 @@ const assert = require("chai").assert; const sinon = require("sinon"); const spawn = require("cross-spawn"); -const { unIndent } = require("../_utils"); +const { unIndent } = require("../../_utils"); const RuntimeInfo = require("../../../lib/shared/runtime-info"); const log = require("../../../lib/shared/logging"); const packageJson = require("../../../package.json");