From 7b43ea14a8af5fc3dbac38fa9d5bc71741328c16 Mon Sep 17 00:00:00 2001 From: "Nicholas C. Zakas" Date: Sun, 31 Jul 2022 21:07:04 -0700 Subject: [PATCH] feat: Implement FlatESLint (#16149) * New: FlatESLint class (refs #13481) * More stuff working in isPathIgnored * Fix more isPathIgnored() tests * isPathIgnored is working * loadFormatter works * Most methods working * Linter mostly working with FlatConfigArray * Fix FlatConfigArray * Initial Linter + FlatConfigArray tests passing * FlatESLint lintText almost working * More stuff working * Make lintText() tests work * Start work on lintFiles() * More tests working * Refactor * Refactor findFiles() * Fix error messages when no files found * Fix some ignore file errors * More tests working * More tests passing * Fix multiple processor tests * Finish processors tests * Fix more tests * Fix more tests * Fix some processor autofixing tests * Tests for default ignore patterns passing * More ignore tests fixed * isPathIgnored() tests passing * More tests passing * Fix more tests * More tests passing * Fix globbing tests * Fix more tests * Fix ignores tests * Enable more tests * Constructor tests passing * lintText tests passing * more tests passing * More tests passing * Make fixtypes tests pass * Add fatalErrorCount to ignore results * Fix outputFixes tests * Add FlatESLint to use-at-your-own-risk * Disable cache option * Fix lint errors * Fix Node.js 12 compatibility * Fix more tests * fs/promises -> fs.promises * Catch when no matching config found * Rebase and fix lint errors * Expose FlatRuleTester * eslint-plugin-node -> eslint-plugin-n * Fix unit test errors * Make test pass in Node.js 12 * docs: Document flat config files * Fix lint errors * Fix default ignores Co-authored-by: Brandon Mills --- docs/.eleventy.js | 45 +- docs/package.json | 1 + docs/src/assets/scss/components/alert.scss | 3 + docs/src/assets/scss/foundations.scss | 1 + .../configuring/configuration-files-new.md | 566 ++ eslint.config.js | 227 + lib/config/default-config.js | 23 +- lib/config/flat-config-array.js | 43 +- lib/config/flat-config-helpers.js | 10 +- lib/eslint/eslint-helpers.js | 621 +++ lib/eslint/flat-eslint.js | 1164 +++++ lib/eslint/index.js | 4 +- lib/linter/linter.js | 19 +- lib/rule-tester/flat-rule-tester.js | 79 +- lib/unsupported-api.js | 4 + package.json | 5 +- .../deprecated-rule-config/eslint.config.js | 5 + .../config-hierarchy/broken/add-conf.js | 5 + .../config-hierarchy/broken/override-conf.js | 5 + tests/fixtures/configurations/env-browser.js | 11 + tests/fixtures/configurations/env-node.js | 13 + .../configurations/plugins-with-prefix.js | 5 + tests/fixtures/configurations/processors.js | 12 + tests/fixtures/configurations/quotes-error.js | 5 + tests/fixtures/configurations/semi-error.js | 6 + tests/fixtures/eslint.config.js | 6 + .../.eslintignoreForNegationTest | 1 + tests/fixtures/rules/eslint.js | 10 + tests/fixtures/rules/missing-rule.js | 5 + tests/lib/config/flat-config-array.js | 17 +- tests/lib/config/flat-config-helpers.js | 102 + tests/lib/eslint/eslint.config.js | 11 + tests/lib/eslint/flat-eslint.js | 4647 +++++++++++++++++ tests/lib/linter/linter.js | 29 +- tests/lib/unsupported-api.js | 8 + 35 files changed, 7651 insertions(+), 67 deletions(-) create mode 100644 docs/src/user-guide/configuring/configuration-files-new.md create mode 100644 eslint.config.js create mode 100644 lib/eslint/eslint-helpers.js create mode 100644 lib/eslint/flat-eslint.js create mode 100644 tests/fixtures/cli-engine/deprecated-rule-config/eslint.config.js create mode 100644 tests/fixtures/config-hierarchy/broken/add-conf.js create mode 100644 tests/fixtures/config-hierarchy/broken/override-conf.js create mode 100644 tests/fixtures/configurations/env-browser.js create mode 100644 tests/fixtures/configurations/env-node.js create mode 100644 tests/fixtures/configurations/plugins-with-prefix.js create mode 100644 tests/fixtures/configurations/processors.js create mode 100644 tests/fixtures/configurations/quotes-error.js create mode 100644 tests/fixtures/configurations/semi-error.js create mode 100644 tests/fixtures/eslint.config.js create mode 100644 tests/fixtures/ignored-paths/.eslintignoreForNegationTest create mode 100644 tests/fixtures/rules/eslint.js create mode 100644 tests/fixtures/rules/missing-rule.js create mode 100644 tests/lib/config/flat-config-helpers.js create mode 100644 tests/lib/eslint/eslint.config.js create mode 100644 tests/lib/eslint/flat-eslint.js diff --git a/docs/.eleventy.js b/docs/.eleventy.js index 4cd1c98627e..b3e302646c2 100644 --- a/docs/.eleventy.js +++ b/docs/.eleventy.js @@ -156,7 +156,35 @@ module.exports = function(eleventyConfig) { headingTag: "h2" // Heading tag when showing heading above the wrapper element }); - // add IDs to the headers + /** @typedef {import("markdown-it/lib/token")} MarkdownItToken A MarkdownIt token. */ + + /** + * Generates HTML markup for an inline alert. + * @param {"warning"|"tip"|"important"} type The type of alert to create. + * @param {Array} tokens Array of MarkdownIt tokens to use. + * @param {number} index The index of the current token in the tokens array. + * @returns {string} The markup for the alert. + */ + function generateAlertMarkup(type, tokens, index) { + if (tokens[index].nesting === 1) { + return ` + + `.trim(); + } + const markdownIt = require("markdown-it"); eleventyConfig.setLibrary("md", @@ -166,6 +194,21 @@ module.exports = function(eleventyConfig) { }) .use(markdownItContainer, "correct", {}) .use(markdownItContainer, "incorrect", {}) + .use(markdownItContainer, "warning", { + render(tokens, idx) { + return generateAlertMarkup("warning", tokens, idx); + } + }) + .use(markdownItContainer, "tip", { + render(tokens, idx) { + return generateAlertMarkup("tip", tokens, idx); + } + }) + .use(markdownItContainer, "important", { + render(tokens, idx) { + return generateAlertMarkup("important", tokens, idx); + } + }) .disable("code")); //------------------------------------------------------------------------------ diff --git a/docs/package.json b/docs/package.json index 0d8940233ab..558d41f97a9 100644 --- a/docs/package.json +++ b/docs/package.json @@ -23,6 +23,7 @@ "@11ty/eleventy-navigation": "^0.3.2", "@11ty/eleventy-plugin-rss": "^1.1.1", "@11ty/eleventy-plugin-syntaxhighlight": "^3.1.2", + "@types/markdown-it": "^12.2.3", "algoliasearch": "^4.12.1", "dom-parser": "^0.1.6", "eleventy-plugin-nesting-toc": "^1.3.0", diff --git a/docs/src/assets/scss/components/alert.scss b/docs/src/assets/scss/components/alert.scss index e532ab10d09..ddd5c693d6a 100644 --- a/docs/src/assets/scss/components/alert.scss +++ b/docs/src/assets/scss/components/alert.scss @@ -66,6 +66,9 @@ offset-block-start: 2px; } +.alert__text > p { + margin: 0; +} .alert__type { display: block; diff --git a/docs/src/assets/scss/foundations.scss b/docs/src/assets/scss/foundations.scss index d6b4519e3aa..27849b3c145 100644 --- a/docs/src/assets/scss/foundations.scss +++ b/docs/src/assets/scss/foundations.scss @@ -159,6 +159,7 @@ hr { code, pre { font-family: var(--mono-font); + font-variant-ligatures: none; } code { diff --git a/docs/src/user-guide/configuring/configuration-files-new.md b/docs/src/user-guide/configuring/configuration-files-new.md new file mode 100644 index 00000000000..eddd47d05d9 --- /dev/null +++ b/docs/src/user-guide/configuring/configuration-files-new.md @@ -0,0 +1,566 @@ +--- +title: Configuration Files (New) +layout: doc +edit_link: https://github.com/eslint/eslint/edit/main/docs/src/user-guide/configuring/configuration-files-new.md +eleventyNavigation: + key: configuration files + parent: configuring + title: Configuration Files (New) + order: 1 + +--- + +::: warning +This is an experimental feature that is not enabled by default. You can use the configuration system described on this page by using the `FlatESLint` class, the `FlatRuleTester` class, or by setting `configType: "flat"` in the `Linter` class. +::: + +## Configuration File + +The ESLint configuration file is named `eslint.config.js` and should be placed in the root directory of your project and export an array of configuration objects. Here's an example: + +```js +export default [ + { + rules: { + semi: "error", + "prefer-const": "error" + } + } +] +``` + +Here, the configuration array contains just one configuration object. The configuration object enables two rules: `semi` and `prefer-const`. These rules will be applied to all of the files ESLint processes using this config file. + +## Configuration Objects + +Each configuration object contains all of the information ESLint needs to execute on a set of files. Each configuration object is made up of these properties: + +* `files` - An array of glob patterns indicating the files that the configuration object should apply to. If not specified, the configuration object applies to all files. +* `ignores` - An array of glob patterns indicating the files that the configuration object should not apply to. If not specified, the configuration object applies to all files matched by `files`. +* `languageOptions` - An object containing settings related to how JavaScript is configured for linting. + * `ecmaVersion` - The version of ECMAScript to support. May be any year (i.e., `2022`) or version (i.e., `5`). Set to `"latest"` for the most recent supported version. (default: `"latest"`) + * `sourceType` - The type of JavaScript source code. Possible values are `"script"` for traditional script files, `"module"` for ECMAScript modules (ESM), and `"commonjs"` for CommonJS files. (default: `"module"` for `.js` and `.mjs` files; `"commonjs"` for `.cjs` files) + * `globals` - An object specifying additional objects that should be added to the global scope during linting. + * `parser` - Either an object containing a `parse()` method or a string indicating the name of a parser inside of a plugin (i.e., `"pluginName/parserName"`). (default: `"@/espree"`) + * `parserOptions` - An object specifying additional options that are passed directly to the `parser()` method on the parser. The available options are parser-dependent. +* `linterOptions` - An object containing settings related to the linting process. + * `noInlineConfig` - A Boolean value indicating if inline configuration is allowed. + * `reportUnusedDisableDirectives` - A Boolean value indicating if unused disable directives should be tracked and reported. +* `processor` - Either an object containing `preprocess()` and `postprocess()` methods or a string indicating the name of a processor inside of a plugin (i.e., `"pluginName/processorName"`). +* `plugins` - An object containing a name-value mapping of plugin names to plugin objects. When `files` is specified, these plugins are only available to the matching files. +* `rules` - An object containing the configured rules. When `files` or `ignores` are specified, these rule configurations are only available to the matching files. +* `settings` - An object containing name-value pairs of information that should be available to all rules. + +### Specifying `files` and `ignores` + +::: tip +Patterns specified in `files` and `ignores` use [`minimatch`](https://www.npmjs.com/package/minimatch) syntax and are evaluated relative to the location of the `eslint.config.js` file. +::: + +You can use a combination of `files` and `ignores` to determine which files should apply the configuration object and which should not. By default, ESLint matches `**/*.js`, `**/*.cjs`, and `**/*.mjs`. Because config objects that don't specify `files` or `ignores` apply to all files that have been matched by any other configuration object, by default config objects will apply to any JavaScript files passed to ESLint. For example: + +```js +export default [ + { + rules: { + semi: "error" + } + } +]; +``` + +With this configuration, the `semi` rule is enabled for all files that match the default files in ESLint. So if you pass `example.js` to ESLint, the `semi` rule will be applied. If you pass a non-JavaScript file, like `example.txt`, the `semi` rule will not be applied because there are no other configuration objects that match that filename. (ESLint will output an error message letting you know that the file was ignored due to missing configuration.) + +#### Excluding files with `ignores` + +You can limit which files a configuration object applies to by specifying a combination of `files` and `ignores` patterns. For example, you may want certain rules to apply only to files in your `src` directory, like this: + +```js +export default [ + { + files: ["src/**/*.js"], + rules: { + semi: "error" + } + } +]; +``` + +Here, only the JavaScript files in the `src` directory will have the `semi` rule applied. If you run ESLint on files in another directory, this configuration object will be skipped. By adding `ignores`, you can also remove some of the files in `src` from this configuration object: + +```js +export default [ + { + files: ["src/**/*.js"], + ignores: ["**/*.config.js"], + rules: { + semi: "error" + } + } +]; +``` + +This configuration object matches all JavaScript files in the `src` directory except those that end with `.config.js`. You can also use negation patterns in `ignores` to exclude files from the ignore patterns, such as: + +```js +export default [ + { + files: ["src/**/*.js"], + ignores: ["**/*.config.js", "!**/eslint.config.js"], + rules: { + semi: "error" + } + } +]; +``` + +Here, the configuration object excludes files ending with `.config.js` except for `eslint.config.js`. That file will still have `semi` applied. + +If `ignores` is used without `files` and any other setting, then the configuration object applies to all files except the ones specified in `ignores`, for example: + +```js +export default [ + { + ignores: ["**/*.config.js"], + rules: { + semi: "error" + } + } +]; +``` + +This configuration object applies to all files except those ending with `.config.js`. Effectively, this is like having `files` set to `**/*`. In general, it's a good idea to always include `files` if you are specifying `ignores`. + +#### Globally ignoring files with `ignores` + +If `ignores` is used without any other keys in the configuration object, then the patterns act as additional global ignores, similar to those found in `.eslintignore`. Here's an example: + +```js +export default [ + { + ignores: [".config/*"] + } +]; +``` + +This configuration specifies that all of the files in the `.config` directory should be ignored. This pattern is added after the patterns found in `.eslintignore`. + +#### Cascading configuration objects + +When more than one configuration object matches a given filename, the configuration objects are merged with later objects overriding previous objects when there is a conflict. For example: + +```js +export default [ + { + files: ["**/*.js"], + languageOptions: { + globals: { + MY_CUSTOM_GLOBAL: "readonly" + } + } + }, + { + files: ["tests/**/*.js"], + languageOptions: { + globals: { + it: "readonly", + describe: "readonly" + } + } + } +]; +``` + +Using this configuration, all JavaScript files define a custom global object defined called `MY_CUSTOM_GLOBAL` while those JavaScript files in the `tests` directory have `it` and `describe` defined as global objects in addition to `MY_CUSTOM_GLOBAL`. For any JavaScript file in the tests directory, both configuration objects are applied, so `languageOptions.globals` are merged to create a final result. + +### Configuring linter options + +Options specific to the linting process can be configured using the `linterOptions` object. These effect how linting proceeds and does not affect how the source code of the file is interpreted. + +#### Disabling inline configuration + +Inline configuration is implemented using an `/*eslint*/` comment, such as `/*eslint semi: error*/`. You can disallow inline configuration by setting `noInlineConfig` to `true`. When enabled, all inline configuration is ignored. Here's an example: + +```js +export default [ + { + files: ["**/*.js"], + linterOptions: { + noInlineConfig: true + } + } +]; +``` + +#### Reporting unused disable directives + +Disable directives such as `/*eslint-disable*/` and `/*eslint-disable-next-line*/` are used to disable ESLint rules around certain portions of code. As code changes, it's possible for these directives to no longer be needed because the code has changed in such a way that the rule will no longer be triggered. You can enable reporting of these unused disable directives by setting the `reportUnusedDisableDirectives` option to `true`, as in this example: + +```js +export default [ + { + files: ["**/*.js"], + linterOptions: { + reportUnusedDisableDirectives: true + } + } +]; +``` + +By default, unused disable directives are reported as warnings. You can change this setting using the `--report-unused-disable-directives` command line option. + +### Configuring language options + +Options specific to how ESLint evaluates your JavaScript code can be configured using the `languageOptions` object. + +#### Configuring the JavaScript version + +To configure the version of JavaScript (ECMAScript) that ESLint uses to evaluate your JavaScript, use the `ecmaVersion` property. This property determines which global variables and syntax are valid in your code and can be set to the version number (such as `6`), the year number (such as `2022`), or `"latest"` (for the most recent version that ESLint supports). By default, `ecmaVersion` is set to `"latest"` and it's recommended to keep this default unless you need to ensure that your JavaScript code is evaluated as an older version. For example, some older runtimes might only allow ECMAScript 5, in which case you can configure ESLint like this: + +```js +export default [ + { + files: ["**/*.js"], + languageOptions: { + ecmaVersion: 5 + } + } +]; +``` + +#### Configuring the JavaScript source type + +ESLint can evaluate your code in one of three ways: + +1. ECMAScript module (ESM) - Your code has a module scope and is run in strict mode. +1. CommonJS - Your code has a top-level function scope and runs in nonstrict mode. +1. Script - Your code has a shared global scope and runs in nonstrict mode. + +You can specify which of these modes your code is intended to run in by specifying the `sourceType` property. This property can be set to `"module"`, `"commonjs"`, or `"script"`. By default, `sourceType` is set to `"module"` for `.js` and `.mjs` files and is set to `"commonjs"` for `.cjs` files. Here's an example: + +```js +export default [ + { + files: ["**/*.js"], + languageOptions: { + sourceType: "script" + } + } +]; +``` + +#### Configuring a custom parser and its options + +In many cases, you can use the default parser that ESLint ships with for parsing your JavaScript code. You can optionally override the default parser by using the `parser` property. The `parser` property can be either a string in the format `"pluginName/parserName"` (indicating to retrieve the parser from a plugin) or an object containing either a `parse()` method or a `parseForESLint()` method. For example, you can use the [`@babel/eslint-parser`](https://www.npmjs.com/package/@babel/eslint-parser) package to allow ESLint to parse experimental syntax: + +```js +import babelParser from "@babel/eslint-parser"; + +export default [ + { + files: ["**/*.js", "**/*.mjs"], + languageOptions: { + parser: babelParser + } + } +]; +``` + +This configuration ensures that the Babel parser, rather than the default, will be used to parse all files ending with `.js` and `.mjs`. + +You can also pass options directly to the custom parser by using the `parserOptions` property. This property is an object whose name-value pairs are specific to the parser that you are using. For the Babel parser, you might pass in options like this: + +```js +import babelParser from "@babel/eslint-parser"; + +export default [ + { + files: ["**/*.js", "**/*.mjs"], + languageOptions: { + parser: babelParser, + parserOptions: { + requireConfigFile: false, + babelOptions: { + babelrc: false, + configFile: false, + // your babel options + presets: ["@babel/preset-env"], + } + } + } + } +]; +``` + +#### Configuring global variables + +To configure global variables inside of a configuration object, set the `globals` configuration property to an object containing keys named for each of the global variables you want to use. For each global variable key, set the corresponding value equal to `"writable"` to allow the variable to be overwritten or `"readonly"` to disallow overwriting. For example: + +```js +export default [ + { + files: ["**/*.js"], + languageOptions: { + globals: { + var1: "writable", + var2: "readonly" + } + } + } +]; +``` + +These examples allow `var1` to be overwritten in your code, but disallow it for `var2`. + +Globals can be disabled with the string `"off"`. For example, in an environment where most ES2015 globals are available but `Promise` is unavailable, you might use this config: + +```js +export default [ + { + languageOptions: { + globals: { + Promise: "off" + } + } + } +]; +``` + +For historical reasons, the boolean value `false` and the string value `"readable"` are equivalent to `"readonly"`. Similarly, the boolean value `true` and the string value `"writeable"` are equivalent to `"writable"`. However, the use of older values is deprecated. + +### Using plugins in your configuration + +Plugins are used to share rules, processors, configurations, parsers, and more across ESLint projects. Plugins are specified in a configuration object using the `plugins` key, which is an object where the name of the plugin is the property name and the value is the plugin object itself. Here's an example: + +```js +import jsdoc from "eslint-plugin-jsdoc"; + +export default [ + { + files: ["**/*.js"], + plugins: { + jsdoc: jsdoc + } + rules: { + "jsdoc/require-description": "error", + "jsdoc/check-values": "error" + } + } +]; +``` + +In this configuration, the JSDoc plugin is defined to have the name `jsdoc`. The prefix `jsdoc/` in each rule name indicates that the rule is coming from the plugin with that name rather than from ESLint itself. + +Because the name of the plugin and the plugin object are both `jsdoc`, you can also shorten the configuration to this: + +```js +import jsdoc from "eslint-plugin-jsdoc"; + +export default [ + { + files: ["**/*.js"], + plugins: { + jsdoc: jsdoc + } + rules: { + "jsdoc/require-description": "error", + "jsdoc/check-values": "error" + } + } +]; +``` + +While this is the most common convention, you don't need to use the same name that the plugin prescribes. You can specify any prefix that you'd like, such as: + +```js +import jsdoc from "eslint-plugin-jsdoc"; + +export default [ + { + files: ["**/*.js"], + plugins: { + jsd: jsdoc + } + rules: { + "jsd/require-description": "error", + "jsd/check-values": "error" + } + } +]; +``` + +This configuration object uses `jsd` as the prefix plugin instead of `jsdoc`. + +### Using processors + +Processors allow ESLint to transform text into pieces of code that ESLint can lint. You can specify the processor to use for a given file type by defining a `processor` property that contains either the processor name in the format `"pluginName/processorName"` to reference a processor in a plugin or an object containing both a `preprocess()` and a `postprocess()` method. For example, to extract JavaScript code blocks from a Markdown file, you might add this to your configuration: + +```js +import markdown from "eslint-plugin-markdown"; + +export default [ + { + files: ["**/*.md"], + plugins: { + markdown + }, + processor: "markdown/markdown" + settings: { + sharedData: "Hello" + } + } +]; +``` + +This configuration object specifies that there is a processor called `"markdown"` contained in the plugin named `"markdown"` and will apply the processor to all files ending with `.md`. + +Processors may make named code blocks that function as filenames in configuration objects, such as `0.js` and `1.js`. ESLint handles such a named code block as a child of the original file. You can specify additional configuration objects for named code blocks. For example, the following disables the `strict` rule for the named code blocks which end with `.js` in markdown files. + +```js +import markdown from "eslint-plugin-markdown"; + +export default [ + { + files: ["**/*.md"], + plugins: { + markdown + }, + processor: "markdown/markdown" + settings: { + sharedData: "Hello" + } + }, + + // applies only to code blocks + { + files: ["**/*.md/*.js"], + rules: { + strict: "off" + } + } +]; +``` + +### Configuring rules + +You can configure any number of rules in a configuration object by add a `rules` property containing an object with your rule configurations. The names in this object are the names of the rules and the values are the configurations for each of those rules. Here's an example: + +```js +export default [ + { + rules: { + semi: "error" + } + } +]; +``` + +This configuration object specifies that the [`semi`](/docs/latest/rules/semi) rule should be enabled with a severity of `"error"`. You can also provide options to a rule by specifying an array where the first item is the severity and each item after that is an option for the rule. For example, you can switch the `semi` rule to disallow semicolons by passing `"never"` as an option: + +```js +export default [ + { + rules: { + semi: ["error", "never"] + } + } +]; +``` + +Each rule specifies its own options and can be any valid JSON data type. Please check the documentation for the rule you want to configure for more information about its available options. + +#### Rule severities + +There are three possible severities you can specify for a rule + +* `"error"` (or `2`) - the reported problem should be treated as an error. When using the ESLint CLI, errors cause the CLI to exit with a nonzero code. +* `"warn"` (or `1`) - the reported problem should be treated as a warning. When using the ESLint CLI, warnings are reported but do not change the exit code. If only errors are reported, the exit code will be 0. +* `"off"` (or `0`) - the rule should be turned off completely. + +#### Rule configuration cascade + +When more than one configuration object specifies the same rule, the rule configuration is merged with the later object taking precedence over any previous objects. For example: + +```js +export default [ + { + rules: { + semi: ["error", "never"] + } + }, + { + rules: { + semi: ["warn", "always"] + } + } +]; +``` + +Using this configuration, the final rule configuration for `semi` is `["warn", "always"]` because it appears last in the array. The array indicates that the configuration is for the severity and any options. You can change just the severity by defining only a string or number, as in this example: + +```js +export default [ + { + rules: { + semi: ["error", "never"] + } + }, + { + rules: { + semi: "warn" + } + } +]; +``` + +Here, the second configuration object only overrides the severity, so the final configuration for `semi` is `["warn", "never"]`. + +### Configuring shared settings + +ESLint supports adding shared settings into configuration files. Plugins use `settings` to specify information that should be shared across all of its rules. You can add a `settings` object to a configuration object and it will be supplied to every rule being executed. This may be useful if you are adding custom rules and want them to have access to the same information. Here's an example: + +```js +export default [ + { + settings: { + sharedData: "Hello" + } + } +]; +``` + +### Using predefined configurations + +ESLint has two predefined configurations: + +* `eslint:recommended` - enables the rules that ESLint recommends everyone use to avoid potential errors +* `eslint:all` - enables all of the rules shipped with ESLint + +To include these predefined configurations, you can insert the string values into the returned array and then make any modifications to other properties in subsequent configuration objects: + +```js +export default [ + "eslint:recommended", + { + rules: { + semi: ["warn", "always"] + } + } +]; +``` + +Here, the `eslint:recommended` predefined configuration is applied first and then another configuration object adds the desired configuration for `semi`. + +## Configuration File Resolution + +When ESLint is run on the command line, it first checks the current working directory for `eslint.config.js`, and if not found, will look to the next parent directory for the file. This search continues until either the file is found or the root directory is reached. + +You can prevent this search for `eslint.config.js` by using the `-c` or `--config--file` option on the command line to specify an alternate configuration file, such as: + +```shell +npx eslint -c some-other-file.js **/*.js +``` + +In this case, ESLint will not search for `eslint.config.js` and will instead use `some-other-file.js`. + +Each configuration file exports one or more configuration object. A configuration object diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000000..50f375d3316 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,227 @@ +/** + * @fileoverview ESLint configuration file + * @author Nicholas C. Zakas + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const path = require("path"); +const internalPlugin = require("eslint-plugin-internal-rules"); +const { FlatCompat } = require("@eslint/eslintrc"); +const globals = require("globals"); + +//----------------------------------------------------------------------------- +// Helpers +//----------------------------------------------------------------------------- + +const compat = new FlatCompat({ + baseDirectory: __dirname +}); + +const INTERNAL_FILES = { + CLI_ENGINE_PATTERN: "lib/cli-engine/**/*", + INIT_PATTERN: "lib/init/**/*", + LINTER_PATTERN: "lib/linter/**/*", + RULE_TESTER_PATTERN: "lib/rule-tester/**/*", + RULES_PATTERN: "lib/rules/**/*", + SOURCE_CODE_PATTERN: "lib/source-code/**/*" +}; + +/** + * Resolve an absolute path or glob pattern. + * @param {string} pathOrPattern the path or glob pattern. + * @returns {string} The resolved path or glob pattern. + */ +function resolveAbsolutePath(pathOrPattern) { + return path.resolve(__dirname, pathOrPattern); +} + +/** + * Create an array of `no-restricted-require` entries for ESLint's core files. + * @param {string} [pattern] The glob pattern to create the entries for. + * @returns {Object[]} The array of `no-restricted-require` entries. + */ +function createInternalFilesPatterns(pattern = null) { + return Object.values(INTERNAL_FILES) + .filter(p => p !== pattern) + .map(p => ({ + name: [ + + // Disallow all children modules. + resolveAbsolutePath(p), + + // Allow the main `index.js` module. + `!${resolveAbsolutePath(p.replace(/\*\*\/\*$/u, "index.js"))}` + ] + })); +} + + +//----------------------------------------------------------------------------- +// Config +//----------------------------------------------------------------------------- + +module.exports = [ + ...compat.extends("eslint", "plugin:eslint-plugin/recommended"), + { + plugins: { + "internal-rules": internalPlugin + }, + languageOptions: { + ecmaVersion: "latest" + }, + + /* + * it fixes eslint-plugin-jsdoc's reports: "Invalid JSDoc tag name "template" jsdoc/check-tag-names" + * refs: https://github.com/gajus/eslint-plugin-jsdoc#check-tag-names + */ + settings: { + jsdoc: { + mode: "typescript" + } + }, + rules: { + "eslint-plugin/consistent-output": "error", + "eslint-plugin/no-deprecated-context-methods": "error", + "eslint-plugin/no-only-tests": "error", + "eslint-plugin/prefer-message-ids": "error", + "eslint-plugin/prefer-output-null": "error", + "eslint-plugin/prefer-placeholders": "error", + "eslint-plugin/prefer-replace-text": "error", + "eslint-plugin/report-message-format": ["error", "[^a-z].*\\.$"], + "eslint-plugin/require-meta-docs-description": "error", + "eslint-plugin/require-meta-has-suggestions": "error", + "eslint-plugin/require-meta-schema": "error", + "eslint-plugin/require-meta-type": "error", + "eslint-plugin/test-case-property-ordering": "error", + "eslint-plugin/test-case-shorthand-strings": "error", + "internal-rules/multiline-comment-style": "error" + } + + }, + { + files: ["lib/rules/*", "tools/internal-rules/*"], + ignores: ["index.js"], + rules: { + "eslint-plugin/prefer-object-rule": "error", + "internal-rules/no-invalid-meta": "error" + } + }, + { + files: ["lib/rules/*"], + ignores: ["index.js"], + rules: { + "eslint-plugin/require-meta-docs-url": ["error", { pattern: "https://eslint.org/docs/rules/{{name}}" }] + } + }, + { + files: ["tests/**/*"], + languageOptions: { + globals: { + ...globals.mocha + } + }, + rules: { + "no-restricted-syntax": ["error", { + selector: "CallExpression[callee.object.name='assert'][callee.property.name='doesNotThrow']", + message: "`assert.doesNotThrow()` should be replaced with a comment next to the code." + }] + } + }, + + // Restrict relative path imports + { + files: ["lib/*"], + ignores: ["lib/unsupported-api.js"], + rules: { + "n/no-restricted-require": ["error", [ + ...createInternalFilesPatterns() + ]] + } + }, + { + files: [INTERNAL_FILES.CLI_ENGINE_PATTERN], + rules: { + "n/no-restricted-require": ["error", [ + ...createInternalFilesPatterns(INTERNAL_FILES.CLI_ENGINE_PATTERN), + resolveAbsolutePath("lib/init/index.js") + ]] + } + }, + { + files: [INTERNAL_FILES.INIT_PATTERN], + rules: { + "n/no-restricted-require": ["error", [ + ...createInternalFilesPatterns(INTERNAL_FILES.INIT_PATTERN), + resolveAbsolutePath("lib/rule-tester/index.js") + ]] + } + }, + { + files: [INTERNAL_FILES.LINTER_PATTERN], + rules: { + "n/no-restricted-require": ["error", [ + ...createInternalFilesPatterns(INTERNAL_FILES.LINTER_PATTERN), + "fs", + resolveAbsolutePath("lib/cli-engine/index.js"), + resolveAbsolutePath("lib/init/index.js"), + resolveAbsolutePath("lib/rule-tester/index.js") + ]] + } + }, + { + files: [INTERNAL_FILES.RULES_PATTERN], + rules: { + "n/no-restricted-require": ["error", [ + ...createInternalFilesPatterns(INTERNAL_FILES.RULES_PATTERN), + "fs", + resolveAbsolutePath("lib/cli-engine/index.js"), + resolveAbsolutePath("lib/init/index.js"), + resolveAbsolutePath("lib/linter/index.js"), + resolveAbsolutePath("lib/rule-tester/index.js"), + resolveAbsolutePath("lib/source-code/index.js") + ]] + } + }, + { + files: ["lib/shared/**/*"], + rules: { + "n/no-restricted-require": ["error", [ + ...createInternalFilesPatterns(), + resolveAbsolutePath("lib/cli-engine/index.js"), + resolveAbsolutePath("lib/init/index.js"), + resolveAbsolutePath("lib/linter/index.js"), + resolveAbsolutePath("lib/rule-tester/index.js"), + resolveAbsolutePath("lib/source-code/index.js") + ]] + } + }, + { + files: [INTERNAL_FILES.SOURCE_CODE_PATTERN], + rules: { + "n/no-restricted-require": ["error", [ + ...createInternalFilesPatterns(INTERNAL_FILES.SOURCE_CODE_PATTERN), + "fs", + resolveAbsolutePath("lib/cli-engine/index.js"), + resolveAbsolutePath("lib/init/index.js"), + resolveAbsolutePath("lib/linter/index.js"), + resolveAbsolutePath("lib/rule-tester/index.js"), + resolveAbsolutePath("lib/rules/index.js") + ]] + } + }, + { + files: [INTERNAL_FILES.RULE_TESTER_PATTERN], + rules: { + "n/no-restricted-require": ["error", [ + ...createInternalFilesPatterns(INTERNAL_FILES.RULE_TESTER_PATTERN), + resolveAbsolutePath("lib/cli-engine/index.js"), + resolveAbsolutePath("lib/init/index.js") + ]] + } + } +]; diff --git a/lib/config/default-config.js b/lib/config/default-config.js index a655a6d83ca..c48551a4f2a 100644 --- a/lib/config/default-config.js +++ b/lib/config/default-config.js @@ -15,7 +15,6 @@ const Rules = require("../rules"); // Helpers //----------------------------------------------------------------------------- - exports.defaultConfig = [ { plugins: { @@ -41,21 +40,31 @@ exports.defaultConfig = [ }) } }, - ignores: [ - "**/node_modules/**", - ".git/**" - ], languageOptions: { - ecmaVersion: "latest", sourceType: "module", + ecmaVersion: "latest", parser: "@/espree", parserOptions: {} } }, + + // default ignores are listed here + { + ignores: [ + "**/node_modules/**", + ".git/**" + ] + }, + + // intentionally empty config to ensure these files are globbed by default + { + files: ["**/*.js", "**/*.mjs"] + }, { files: ["**/*.cjs"], languageOptions: { - sourceType: "commonjs" + sourceType: "commonjs", + ecmaVersion: "latest" } } ]; diff --git a/lib/config/flat-config-array.js b/lib/config/flat-config-array.js index fbedf139d8b..becf1e10b09 100644 --- a/lib/config/flat-config-array.js +++ b/lib/config/flat-config-array.js @@ -36,6 +36,8 @@ function splitPluginIdentifier(identifier) { }; } +const originalBaseConfig = Symbol("originalBaseConfig"); + //----------------------------------------------------------------------------- // Exports //----------------------------------------------------------------------------- @@ -48,10 +50,14 @@ class FlatConfigArray extends ConfigArray { /** * Creates a new instance. * @param {*[]} configs An array of configuration information. - * @param {{basePath: string, baseConfig: FlatConfig}} options The options + * @param {{basePath: string, shouldIgnore: boolean, baseConfig: FlatConfig}} options The options * to use for the config array instance. */ - constructor(configs, { basePath, baseConfig = defaultConfig } = {}) { + constructor(configs, { + basePath, + shouldIgnore = true, + baseConfig = defaultConfig + } = {}) { super(configs, { basePath, schema: flatConfigSchema @@ -62,6 +68,22 @@ class FlatConfigArray extends ConfigArray { } else { this.unshift(baseConfig); } + + /** + * The baes config used to build the config array. + * @type {Array} + */ + this[originalBaseConfig] = baseConfig; + Object.defineProperty(this, originalBaseConfig, { writable: false }); + + /** + * Determines if `ignores` fields should be honored. + * If true, then all `ignores` fields are honored. + * if false, then only `ignores` fields in the baseConfig are honored. + * @type {boolean} + */ + this.shouldIgnore = shouldIgnore; + Object.defineProperty(this, "shouldIgnore", { writable: false }); } /* eslint-disable class-methods-use-this -- Desired as instance method */ @@ -87,6 +109,23 @@ class FlatConfigArray extends ConfigArray { return require("../../conf/eslint-all"); } + /* + * If `shouldIgnore` is false, we remove any ignore patterns specified + * in the config so long as it's not a default config and it doesn't + * have a `files` entry. + */ + if ( + !this.shouldIgnore && + !this[originalBaseConfig].includes(config) && + config.ignores && + !config.files + ) { + /* eslint-disable-next-line no-unused-vars -- need to strip off other keys */ + const { ignores, ...otherKeys } = config; + + return otherKeys; + } + return config; } diff --git a/lib/config/flat-config-helpers.js b/lib/config/flat-config-helpers.js index bcc4eb12082..e00c56434cd 100644 --- a/lib/config/flat-config-helpers.js +++ b/lib/config/flat-config-helpers.js @@ -20,7 +20,14 @@ function parseRuleId(ruleId) { // distinguish between core rules and plugin rules if (ruleId.includes("/")) { - pluginName = ruleId.slice(0, ruleId.lastIndexOf("/")); + + // mimic scoped npm packages + if (ruleId.startsWith("@")) { + pluginName = ruleId.slice(0, ruleId.lastIndexOf("/")); + } else { + pluginName = ruleId.slice(0, ruleId.indexOf("/")); + } + ruleName = ruleId.slice(pluginName.length + 1); } else { pluginName = "@"; @@ -47,6 +54,7 @@ function getRuleFromConfig(ruleId, config) { const plugin = config.plugins && config.plugins[pluginName]; let rule = plugin && plugin.rules && plugin.rules[ruleName]; + // normalize function rules into objects if (rule && typeof rule === "function") { rule = { diff --git a/lib/eslint/eslint-helpers.js b/lib/eslint/eslint-helpers.js new file mode 100644 index 00000000000..bf5c32a646d --- /dev/null +++ b/lib/eslint/eslint-helpers.js @@ -0,0 +1,621 @@ +/** + * @fileoverview Helper functions for ESLint class + * @author Nicholas C. Zakas + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const path = require("path"); +const fs = require("fs"); +const fsp = fs.promises; +const isGlob = require("is-glob"); +const globby = require("globby"); +const hash = require("../cli-engine/hash"); + +//----------------------------------------------------------------------------- +// Errors +//----------------------------------------------------------------------------- + +/** + * The error type when no files match a glob. + */ +class NoFilesFoundError extends Error { + + /** + * @param {string} pattern The glob pattern which was not found. + * @param {boolean} globEnabled If `false` then the pattern was a glob pattern, but glob was disabled. + */ + constructor(pattern, globEnabled) { + super(`No files matching '${pattern}' were found${!globEnabled ? " (glob was disabled)" : ""}.`); + this.messageTemplate = "file-not-found"; + this.messageData = { pattern, globDisabled: !globEnabled }; + } +} + +/** + * The error type when there are files matched by a glob, but all of them have been ignored. + */ +class AllFilesIgnoredError extends Error { + + /** + * @param {string} pattern The glob pattern which was not found. + */ + constructor(pattern) { + super(`All files matched by '${pattern}' are ignored.`); + this.messageTemplate = "all-files-ignored"; + this.messageData = { pattern }; + } +} + + +//----------------------------------------------------------------------------- +// General Helpers +//----------------------------------------------------------------------------- + +/** + * 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); +} + +//----------------------------------------------------------------------------- +// File-related Helpers +//----------------------------------------------------------------------------- + +/** + * Normalizes slashes in a file pattern to posix-style. + * @param {string} pattern The pattern to replace slashes in. + * @returns {string} The pattern with slashes normalized. + */ +function normalizeToPosix(pattern) { + return pattern.replace(/\\/gu, "/"); +} + +/** + * Check if a string is a glob pattern or not. + * @param {string} pattern A glob pattern. + * @returns {boolean} `true` if the string is a glob pattern. + */ +function isGlobPattern(pattern) { + return isGlob(path.sep === "\\" ? normalizeToPosix(pattern) : pattern); +} + +/** + * Finds all files matching the options specified. + * @param {Object} args The arguments objects. + * @param {Array} args.patterns An array of glob patterns. + * @param {boolean} args.globInputPaths true to interpret glob patterns, + * false to not interpret glob patterns. + * @param {string} args.cwd The current working directory to find from. + * @param {FlatConfigArray} args.configs The configs for the current run. + * @returns {Promise>} The fully resolved file paths. + * @throws {AllFilesIgnoredError} If there are no results due to an ignore pattern. + * @throws {NoFilesFoundError} If no files matched the given patterns. + */ +async function findFiles({ + patterns, + globInputPaths, + cwd, + configs +}) { + + const results = []; + const globbyPatterns = []; + const missingPatterns = []; + + // check to see if we have explicit files and directories + const filePaths = patterns.map(filePath => path.resolve(cwd, filePath)); + const stats = await Promise.all( + filePaths.map( + filePath => fsp.stat(filePath).catch(() => {}) + ) + ); + + stats.forEach((stat, index) => { + + const filePath = filePaths[index]; + const pattern = patterns[index]; + + if (stat) { + + // files are added directly to the list + if (stat.isFile()) { + results.push({ + filePath, + ignored: configs.isIgnored(filePath) + }); + } + + // directories need extensions attached + if (stat.isDirectory()) { + + // filePatterns are all relative to cwd + const filePatterns = configs.files + .filter(filePattern => { + + // can only do this for strings, not functions + if (typeof filePattern !== "string") { + return false; + } + + // patterns ending with * are not used for file search + if (filePattern.endsWith("*")) { + return false; + } + + // not sure how to handle negated patterns yet + if (filePattern.startsWith("!")) { + return false; + } + + // check if the pattern would be inside the cwd or not + const fullFilePattern = path.join(cwd, filePattern); + const relativeFilePattern = path.relative(configs.basePath, fullFilePattern); + + return !relativeFilePattern.startsWith(".."); + }) + .map(filePattern => { + if (filePattern.startsWith("**")) { + return path.join(pattern, filePattern); + } + + // adjust the path to be relative to the cwd + return path.relative( + cwd, + path.join(configs.basePath, filePattern) + ); + }) + .map(normalizeToPosix); + + if (filePatterns.length) { + globbyPatterns.push(...filePatterns); + } + + } + + return; + } + + // save patterns for later use based on whether globs are enabled + if (globInputPaths && isGlobPattern(filePath)) { + globbyPatterns.push(pattern); + } else { + missingPatterns.push(pattern); + } + }); + + // note: globbyPatterns can be an empty array + const globbyResults = (await globby(globbyPatterns, { + cwd, + absolute: true, + ignore: configs.ignores.filter(matcher => typeof matcher === "string") + })); + + // if there are no results, tell the user why + if (!results.length && !globbyResults.length) { + + // try globby without ignoring anything + /* eslint-disable no-unreachable-loop -- We want to exit early. */ + for (const globbyPattern of globbyPatterns) { + + /* eslint-disable-next-line no-unused-vars -- Want to exit early. */ + for await (const filePath of globby.stream(globbyPattern, { cwd, absolute: true })) { + + // files were found but ignored + throw new AllFilesIgnoredError(globbyPattern); + } + + // no files were found + throw new NoFilesFoundError(globbyPattern, globInputPaths); + } + /* eslint-enable no-unreachable-loop -- Go back to normal. */ + + } + + // there were patterns that didn't match anything, tell the user + if (missingPatterns.length) { + throw new NoFilesFoundError(missingPatterns[0], globInputPaths); + } + + + return [ + ...results, + ...globbyResults.map(filePath => ({ + filePath: path.resolve(filePath), + ignored: false + })) + ]; +} + + +/** + * Checks whether a file exists at the given location + * @param {string} resolvedPath A path from the CWD + * @throws {Error} As thrown by `fs.statSync` or `fs.isFile`. + * @returns {boolean} `true` if a file exists + */ +function fileExists(resolvedPath) { + try { + return fs.statSync(resolvedPath).isFile(); + } catch (error) { + if (error && (error.code === "ENOENT" || error.code === "ENOTDIR")) { + return false; + } + throw error; + } +} + +/** + * Checks whether a directory exists at the given location + * @param {string} resolvedPath A path from the CWD + * @throws {Error} As thrown by `fs.statSync` or `fs.isDirectory`. + * @returns {boolean} `true` if a directory exists + */ +function directoryExists(resolvedPath) { + try { + return fs.statSync(resolvedPath).isDirectory(); + } catch (error) { + if (error && (error.code === "ENOENT" || error.code === "ENOTDIR")) { + return false; + } + throw error; + } +} + +//----------------------------------------------------------------------------- +// Results-related Helpers +//----------------------------------------------------------------------------- + +/** + * Checks if the given message is an error message. + * @param {LintMessage} message The message to check. + * @returns {boolean} Whether or not the message is an error message. + * @private + */ +function isErrorMessage(message) { + return message.severity === 2; +} + +/** + * Returns result with warning by ignore settings + * @param {string} filePath File path of checked code + * @param {string} baseDir Absolute path of base directory + * @returns {LintResult} Result with single warning + * @private + */ +function createIgnoreResult(filePath, baseDir) { + let message; + const isHidden = filePath.split(path.sep) + .find(segment => /^\./u.test(segment)); + const isInNodeModules = baseDir && path.relative(baseDir, filePath).startsWith("node_modules"); + + if (isHidden) { + message = "File ignored by default. Use a negated ignore pattern (like \"--ignore-pattern '!'\") to override."; + } else if (isInNodeModules) { + message = "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override."; + } else { + message = "File ignored because of a matching ignore pattern. Use \"--no-ignore\" to override."; + } + + return { + filePath: path.resolve(filePath), + messages: [ + { + fatal: false, + severity: 1, + message + } + ], + errorCount: 0, + warningCount: 1, + fatalErrorCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0 + }; +} + +//----------------------------------------------------------------------------- +// Options-related Helpers +//----------------------------------------------------------------------------- + + +/** + * 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 === "directive" || 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 {FlatESLintOptions} options The options to process. + * @throws {ESLintInvalidOptionsError} If of any of a variety of type errors. + * @returns {FlatESLintOptions} 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", + cacheStrategy = "metadata", + 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. + ignorePatterns = null, + 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. + ...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."); + } + if (unknownOptionKeys.includes("resolvePluginsRelativeTo")) { + errors.push("'resolvePluginsRelativeTo' has been removed."); + } + if (unknownOptionKeys.includes("globals")) { + errors.push("'globals' has been removed. Please use the 'overrideConfig.languageOptions.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.languageOptions.parser' option instead."); + } + if (unknownOptionKeys.includes("parserOptions")) { + errors.push("'parserOptions' has been removed. Please use the 'overrideConfig.languageOptions.parserOptions' option instead."); + } + if (unknownOptionKeys.includes("rules")) { + errors.push("'rules' has been removed. Please use the 'overrideConfig.rules' option instead."); + } + if (unknownOptionKeys.includes("rulePaths")) { + errors.push("'rulePaths' has been removed. Please define your rules using plugins."); + } + } + 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 (cache) { + errors.push("'cache' option is not yet supported."); + } + if (!isNonEmptyString(cacheLocation)) { + errors.push("'cacheLocation' must be a non-empty string."); + } + if ( + cacheStrategy !== "metadata" && + cacheStrategy !== "content" + ) { + errors.push("'cacheStrategy' must be any of \"metadata\", \"content\"."); + } + 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 \"directive\", \"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 && overrideConfigFile !== true) { + errors.push("'overrideConfigFile' must be a non-empty string, null, or true."); + } + 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 (errors.length > 0) { + throw new ESLintInvalidOptionsError(errors); + } + + return { + allowInlineConfig, + baseConfig, + cache, + cacheLocation, + cacheStrategy, + + // when overrideConfigFile is true that means don't do config file lookup + configFile: overrideConfigFile === true ? false : overrideConfigFile, + overrideConfig, + cwd, + errorOnUnmatchedPattern, + extensions, + fix, + fixTypes, + globInputPaths, + ignore, + ignorePath, + ignorePatterns, + reportUnusedDisableDirectives + }; +} + + +//----------------------------------------------------------------------------- +// Cache-related helpers +//----------------------------------------------------------------------------- + +/** + * return the cacheFile to be used by eslint, based on whether the provided parameter is + * a directory or looks like a directory (ends in `path.sep`), in which case the file + * name will be the `cacheFile/.cache_hashOfCWD` + * + * if cacheFile points to a file or looks like a file then in will just use that file + * @param {string} cacheFile The name of file to be used to store the cache + * @param {string} cwd Current working directory + * @returns {string} the resolved path to the cache file + */ +function getCacheFile(cacheFile, cwd) { + + /* + * make sure the path separators are normalized for the environment/os + * keeping the trailing path separator if present + */ + const normalizedCacheFile = path.normalize(cacheFile); + + const resolvedCacheFile = path.resolve(cwd, normalizedCacheFile); + const looksLikeADirectory = normalizedCacheFile.slice(-1) === path.sep; + + /** + * return the name for the cache file in case the provided parameter is a directory + * @returns {string} the resolved path to the cacheFile + */ + function getCacheFileForDirectory() { + return path.join(resolvedCacheFile, `.cache_${hash(cwd)}`); + } + + let fileStats; + + try { + fileStats = fs.lstatSync(resolvedCacheFile); + } catch { + fileStats = null; + } + + + /* + * in case the file exists we need to verify if the provided path + * is a directory or a file. If it is a directory we want to create a file + * inside that directory + */ + if (fileStats) { + + /* + * is a directory or is a file, but the original file the user provided + * looks like a directory but `path.resolve` removed the `last path.sep` + * so we need to still treat this like a directory + */ + if (fileStats.isDirectory() || looksLikeADirectory) { + return getCacheFileForDirectory(); + } + + // is file so just use that file + return resolvedCacheFile; + } + + /* + * here we known the file or directory doesn't exist, + * so we will try to infer if its a directory if it looks like a directory + * for the current operating system. + */ + + // if the last character passed is a path separator we assume is a directory + if (looksLikeADirectory) { + return getCacheFileForDirectory(); + } + + return resolvedCacheFile; +} + + +//----------------------------------------------------------------------------- +// Exports +//----------------------------------------------------------------------------- + +module.exports = { + isGlobPattern, + directoryExists, + fileExists, + findFiles, + + isNonEmptyString, + isArrayOfNonEmptyString, + + createIgnoreResult, + isErrorMessage, + + processOptions, + + getCacheFile +}; diff --git a/lib/eslint/flat-eslint.js b/lib/eslint/flat-eslint.js new file mode 100644 index 00000000000..1867050e43f --- /dev/null +++ b/lib/eslint/flat-eslint.js @@ -0,0 +1,1164 @@ +/** + * @fileoverview Main class using flat config + * @author Nicholas C. Zakas + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +// Note: Node.js 12 does not support fs/promises. +const fs = require("fs").promises; +const path = require("path"); +const findUp = require("find-up"); +const { version } = require("../../package.json"); +const { Linter } = require("../linter"); +const { getRuleFromConfig } = require("../config/flat-config-helpers"); +const { gitignoreToMinimatch } = require("@humanwhocodes/gitignore-to-minimatch"); +const { + Legacy: { + ConfigOps: { + getRuleSeverity + }, + ModuleResolver, + naming + } +} = require("@eslint/eslintrc"); + +const { + fileExists, + findFiles, + + isNonEmptyString, + isArrayOfNonEmptyString, + + createIgnoreResult, + isErrorMessage, + + processOptions +} = require("./eslint-helpers"); +const { pathToFileURL } = require("url"); +const { FlatConfigArray } = require("../config/flat-config-array"); + +/* + * This is necessary to allow overwriting writeFile for testing purposes. + * We can just use fs/promises once we drop Node.js 12 support. + */ + +//------------------------------------------------------------------------------ +// Typedefs +//------------------------------------------------------------------------------ + +// 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 */ +/** @typedef {import("../shared/types").RuleConf} RuleConf */ +/** @typedef {import("../shared/types").Rule} Rule */ +/** @typedef {ReturnType} ExtractedConfig */ + +/** + * The options with which to configure the ESLint instance. + * @typedef {Object} FlatESLintOptions + * @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 {"metadata" | "content"} [cacheStrategy] The strategy used to detect changed files. + * @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 {string[]} [ignorePatterns] Ignore file patterns to use in addition to .eslintignore. + * @property {ConfigData} [overrideConfig] Override config object, overrides all configs used with this instance + * @property {boolean|string} [overrideConfigFile] Searches for default config file when falsy; + * doesn't do any config file lookup when `true`; considered to be a config filename + * when a string. + * @property {Record} [plugins] An array of plugin implementations. + * @property {"error" | "warn" | "off"} [reportUnusedDisableDirectives] the severity to report unused eslint-disable directives. + */ + +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +const FLAT_CONFIG_FILENAME = "eslint.config.js"; +const debug = require("debug")("eslint:flat-eslint"); +const removedFormatters = new Set(["table", "codeframe"]); +const privateMembers = new WeakMap(); + +/** + * It will calculate the error and warning count for collection of messages per file + * @param {LintMessage[]} messages Collection of messages + * @returns {Object} Contains the stats + * @private + */ +function calculateStatsPerFile(messages) { + return messages.reduce((stat, message) => { + if (message.fatal || message.severity === 2) { + stat.errorCount++; + if (message.fatal) { + stat.fatalErrorCount++; + } + if (message.fix) { + stat.fixableErrorCount++; + } + } else { + stat.warningCount++; + if (message.fix) { + stat.fixableWarningCount++; + } + } + return stat; + }, { + errorCount: 0, + fatalErrorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0 + }); +} + +/** + * It will calculate the error and warning count for collection of results from all files + * @param {LintResult[]} results Collection of messages from all the files + * @returns {Object} Contains the stats + * @private + */ +function calculateStatsPerRun(results) { + return results.reduce((stat, result) => { + stat.errorCount += result.errorCount; + stat.fatalErrorCount += result.fatalErrorCount; + stat.warningCount += result.warningCount; + stat.fixableErrorCount += result.fixableErrorCount; + stat.fixableWarningCount += result.fixableWarningCount; + return stat; + }, { + errorCount: 0, + fatalErrorCount: 0, + warningCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0 + }); +} + +/** + * Loads global ignore patterns from an ignore file (usually .eslintignore). + * @param {string} filePath The filename to load. + * @returns {ignore} A function encapsulating the ignore patterns. + * @throws {Error} If the file cannot be read. + * @private + */ +async function loadIgnoreFilePatterns(filePath) { + debug(`Loading ignore file: ${filePath}`); + + try { + const ignoreFileText = await fs.readFile(filePath, { encoding: "utf8" }); + + return ignoreFileText + .split(/\r?\n/gu) + .filter(line => line.trim() !== "" && !line.startsWith("#")); + + } catch (e) { + debug(`Error reading ignore file: ${filePath}`); + e.message = `Cannot read ignore file: ${filePath}\nError: ${e.message}`; + throw e; + } +} + +/** + * 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} eslint The CLIEngine instance. + * @param {string} maybeFilePath The absolute path to a lint target file or `""`. + * @returns {DeprecatedRuleInfo[]} The used deprecated rule list. + */ +function getOrFindUsedDeprecatedRules(eslint, maybeFilePath) { + const { + configs, + options: { cwd } + } = privateMembers.get(eslint); + const filePath = path.isAbsolute(maybeFilePath) + ? maybeFilePath + : path.join(cwd, "__placeholder__.js"); + const config = configs.getConfig(filePath); + + // Most files use the same config, so cache it. + if (config && !usedDeprecatedRulesCache.has(config)) { + const retv = []; + + if (config.rules) { + for (const [ruleId, ruleConf] of Object.entries(config.rules)) { + if (getRuleSeverity(ruleConf) === 0) { + continue; + } + const rule = getRuleFromConfig(ruleId, config); + const meta = rule && rule.meta; + + if (meta && meta.deprecated) { + retv.push({ ruleId, replacedBy: meta.replacedBy || [] }); + } + } + } + + + usedDeprecatedRulesCache.set(config, Object.freeze(retv)); + } + + return config ? usedDeprecatedRulesCache.get(config) : Object.freeze([]); +} + +/** + * Processes the linting results generated by a CLIEngine linting report to + * match the ESLint class's API. + * @param {CLIEngine} eslint The CLIEngine instance. + * @param {CLIEngineLintReport} report The CLIEngine linting report to process. + * @returns {LintResult[]} The processed linting results. + */ +function processLintReport(eslint, { results }) { + const descriptor = { + configurable: true, + enumerable: true, + get() { + return getOrFindUsedDeprecatedRules(eslint, 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; +} + +/** + * Searches from the current working directory up until finding the + * given flat config filename. + * @param {string} cwd The current working directory to search from. + * @returns {Promise} The filename if found or `null` if not. + */ +function findFlatConfigFile(cwd) { + return findUp( + FLAT_CONFIG_FILENAME, + { cwd } + ); +} + +/** + * Load the config array from the given filename. + * @param {string} filePath The filename to load from. + * @param {Object} options Options to help load the config file. + * @param {string} options.basePath The base path for the config array. + * @param {boolean} options.shouldIgnore Whether to honor ignore patterns. + * @returns {Promise} The config array loaded from the config file. + */ +async function loadFlatConfigFile(filePath, { basePath, shouldIgnore }) { + debug(`Loading config from ${filePath}`); + + const fileURL = pathToFileURL(filePath); + + debug(`Config file URL is ${fileURL}`); + + const module = await import(fileURL); + + return new FlatConfigArray(module.default, { + basePath, + shouldIgnore + }); +} + +/** + * Calculates the config array for this run based on inputs. + * @param {FlatESLint} eslint The instance to create the config array for. + * @param {import("./eslint").ESLintOptions} options The ESLint instance options. + * @returns {FlatConfigArray} The config array for `eslint``. + */ +async function calculateConfigArray(eslint, { + cwd, + overrideConfig, + configFile, + ignore: shouldIgnore, + ignorePath, + ignorePatterns, + extensions +}) { + + // check for cached instance + const slots = privateMembers.get(eslint); + + if (slots.configs) { + return slots.configs; + } + + // determine where to load config file from + let configFilePath; + let basePath = cwd; + + if (typeof configFile === "string") { + debug(`Override config file path is ${configFile}`); + configFilePath = path.resolve(cwd, configFile); + } else if (configFile !== false) { + debug("Searching for eslint.config.js"); + configFilePath = await findFlatConfigFile(cwd); + + if (!configFilePath) { + throw new Error("Could not find config file."); + } + + basePath = path.resolve(path.dirname(configFilePath)); + } + + // load config array + let configs; + + if (configFilePath) { + configs = await loadFlatConfigFile(configFilePath, { + basePath, + shouldIgnore + }); + } else { + configs = new FlatConfigArray([], { basePath, shouldIgnore }); + } + + // add in any configured defaults + configs.push(...slots.defaultConfigs); + + // if there are any extensions, create configs for them for easier matching + if (extensions && extensions.length) { + configs.push({ + files: extensions.map(ext => `**/*${ext}`) + }); + } + + let allIgnorePatterns = []; + let ignoreFilePath; + + // load ignore file if necessary + if (shouldIgnore) { + if (ignorePath) { + ignoreFilePath = path.resolve(cwd, ignorePath); + allIgnorePatterns = await loadIgnoreFilePatterns(ignoreFilePath); + } else { + ignoreFilePath = path.resolve(cwd, ".eslintignore"); + + // no error if .eslintignore doesn't exist` + if (fileExists(ignoreFilePath)) { + allIgnorePatterns = await loadIgnoreFilePatterns(ignoreFilePath); + } + } + } + + // append command line ignore patterns + if (ignorePatterns) { + if (typeof ignorePatterns === "string") { + allIgnorePatterns.push(ignorePatterns); + } else { + allIgnorePatterns.push(...ignorePatterns); + } + } + + /* + * If the config file basePath is different than the cwd, then + * the ignore patterns won't work correctly. Here, we adjust the + * ignore pattern to include the correct relative path. Patterns + * loaded from ignore files are always relative to the cwd, whereas + * the config file basePath can be an ancestor of the cwd. + */ + if (basePath !== cwd && allIgnorePatterns.length) { + + const relativeIgnorePath = path.relative(basePath, cwd); + + allIgnorePatterns = allIgnorePatterns.map(pattern => { + const negated = pattern.startsWith("!"); + const basePattern = negated ? pattern.slice(1) : pattern; + + /* + * Ignore patterns are considered relative to a directory + * when the pattern contains a slash in a position other + * than the last character. If that's the case, we need to + * add the relative ignore path to the current pattern to + * get the correct behavior. Otherwise, no change is needed. + */ + if (!basePattern.includes("/") || basePattern.endsWith("/")) { + return pattern; + } + + return (negated ? "!" : "") + + path.posix.join(relativeIgnorePath, basePattern); + }); + } + + if (allIgnorePatterns.length) { + + /* + * Ignore patterns are added to the end of the config array + * so they can override default ignores. + */ + configs.push({ + ignores: allIgnorePatterns.map(gitignoreToMinimatch) + }); + } + + if (overrideConfig) { + if (Array.isArray(overrideConfig)) { + configs.push(...overrideConfig); + } else { + configs.push(overrideConfig); + } + } + + await configs.normalize(); + + // cache the config array for this instance + slots.configs = configs; + + return configs; +} + +/** + * Processes an source code using ESLint. + * @param {Object} config The config object. + * @param {string} config.text The source code to verify. + * @param {string} config.cwd The path to the current working directory. + * @param {string|undefined} config.filePath The path to the file of `text`. If this is undefined, it uses ``. + * @param {FlatConfigArray} config.configs The config. + * @param {boolean} config.fix If `true` then it does fix. + * @param {boolean} config.allowInlineConfig If `true` then it uses directive comments. + * @param {boolean} config.reportUnusedDisableDirectives If `true` then it reports unused `eslint-disable` comments. + * @param {Linter} config.linter The linter instance to verify. + * @returns {LintResult} The result of linting. + * @private + */ +function verifyText({ + text, + cwd, + filePath: providedFilePath, + configs, + fix, + allowInlineConfig, + reportUnusedDisableDirectives, + linter +}) { + const filePath = providedFilePath || ""; + + debug(`Lint ${filePath}`); + + /* + * Verify. + * `config.extractConfig(filePath)` requires an absolute path, but `linter` + * doesn't know CWD, so it gives `linter` an absolute path always. + */ + const filePathToVerify = filePath === "" ? path.join(cwd, "__placeholder__.js") : filePath; + const { fixed, messages, output } = linter.verifyAndFix( + text, + configs, + { + allowInlineConfig, + filename: filePathToVerify, + fix, + reportUnusedDisableDirectives, + + /** + * Check if the linter should adopt a given code block or not. + * @param {string} blockFilename The virtual filename of a code block. + * @returns {boolean} `true` if the linter should adopt the code block. + */ + filterCodeBlock(blockFilename) { + return configs.isExplicitMatch(blockFilename); + } + } + ); + + // Tweak and return. + const result = { + filePath: filePath === "" ? filePath : path.resolve(filePath), + messages, + ...calculateStatsPerFile(messages) + }; + + if (fixed) { + result.output = output; + } + + if ( + result.errorCount + result.warningCount > 0 && + typeof result.output === "undefined" + ) { + result.source = text; + } + + return result; +} + +/** + * Checks whether a message's rule type should be fixed. + * @param {LintMessage} message The message to check. + * @param {FlatConfig} config The config for the file that generated the message. + * @param {string[]} fixTypes An array of fix types to check. + * @returns {boolean} Whether the message should be fixed. + */ +function shouldMessageBeFixed(message, config, fixTypes) { + if (!message.ruleId) { + return fixTypes.has("directive"); + } + + const rule = message.ruleId && getRuleFromConfig(message.ruleId, config); + + return Boolean(rule && rule.meta && fixTypes.has(rule.meta.type)); +} + +/** + * Collect used deprecated rules. + * @param {Array} configs The configs to evaluate. + * @returns {IterableIterator} Used deprecated rules. + */ +function *iterateRuleDeprecationWarnings(configs) { + const processedRuleIds = new Set(); + + for (const config of configs) { + for (const [ruleId, ruleConfig] of Object.entries(config.rules)) { + + // Skip if it was processed. + if (processedRuleIds.has(ruleId)) { + continue; + } + processedRuleIds.add(ruleId); + + // Skip if it's not used. + if (!getRuleSeverity(ruleConfig)) { + continue; + } + const rule = getRuleFromConfig(ruleId, config); + + // Skip if it's not deprecated. + if (!(rule && rule.meta && rule.meta.deprecated)) { + continue; + } + + // This rule was used and deprecated. + yield { + ruleId, + replacedBy: rule.meta.replacedBy || [] + }; + } + } +} + +//----------------------------------------------------------------------------- +// Main API +//----------------------------------------------------------------------------- + +/** + * Primary Node.js API for ESLint. + */ +class FlatESLint { + + /** + * Creates a new instance of the main ESLint API. + * @param {FlatESLintOptions} options The options for this instance. + */ + constructor(options = {}) { + + const defaultConfigs = []; + const processedOptions = processOptions(options); + const linter = new Linter({ + cwd: processedOptions.cwd, + configType: "flat" + }); + + privateMembers.set(this, { + options: processedOptions, + linter, + defaultConfigs, + defaultIgnores: () => false, + configs: null + }); + + /** + * If additional plugins are passed in, add that to the default + * configs for this instance. + */ + if (options.plugins) { + + const plugins = {}; + + for (const [pluginName, plugin] of Object.entries(options.plugins)) { + plugins[naming.getShorthandName(pluginName, "eslint-plugin")] = plugin; + } + + defaultConfigs.push({ + plugins + }); + } + + } + + /** + * 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 => fs.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) { + const filtered = []; + + results.forEach(result => { + const filteredMessages = result.messages.filter(isErrorMessage); + + if (filteredMessages.length > 0) { + filtered.push({ + ...result, + messages: filteredMessages, + errorCount: filteredMessages.length, + warningCount: 0, + fixableErrorCount: result.fixableErrorCount, + fixableWarningCount: 0 + }); + } + }); + + return filtered; + } + + /** + * Returns meta objects for each rule represented in the lint results. + * @param {LintResult[]} results The results to fetch rules meta for. + * @returns {Object} A mapping of ruleIds to rule meta objects. + * @throws {TypeError} When the results object wasn't created from this ESLint instance. + * @throws {TypeError} When a plugin or rule is missing. + */ + getRulesMetaForResults(results) { + + const resultRules = new Map(); + + // short-circuit simple case + if (results.length === 0) { + return resultRules; + } + + const { configs } = privateMembers.get(this); + + /* + * We can only accurately return rules meta information for linting results if the + * results were created by this instance. Otherwise, the necessary rules data is + * not available. So if the config array doesn't already exist, just throw an error + * to let the user know we can't do anything here. + */ + if (!configs) { + throw new TypeError("Results object was not created from this ESLint instance."); + } + + for (const result of results) { + + /* + * Normalize filename for . + */ + const filePath = result.filePath === "" + ? "__placeholder__.js" : result.filePath; + + /* + * All of the plugin and rule information is contained within the + * calculated config for the given file. + */ + const config = configs.getConfig(filePath); + + for (const { ruleId } of result.messages) { + const rule = getRuleFromConfig(ruleId, config); + + // ensure the rule exists exists + if (!rule) { + throw new TypeError(`Could not find the rule "${ruleId}".`); + } + + resultRules.set(ruleId, rule); + } + } + + return createRulesMeta(resultRules); + } + + /** + * Executes the current configuration on an array of file and directory names. + * @param {string|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 { + cacheFilePath, + lintResultCache, + linter, + options: eslintOptions + } = privateMembers.get(this); + const configs = await calculateConfigArray(this, eslintOptions); + const { + allowInlineConfig, + cache, + cwd, + fix, + fixTypes, + reportUnusedDisableDirectives, + extensions, + globInputPaths + } = eslintOptions; + const startTime = Date.now(); + const usedConfigs = []; + const fixTypesSet = fixTypes ? new Set(fixTypes) : null; + + // Delete cache file; should this be done here? + if (!cache && cacheFilePath) { + try { + await fs.unlink(cacheFilePath); + } catch (error) { + const errorCode = error && error.code; + + // Ignore errors when no such file exists or file system is read only (and cache file does not exist) + if (errorCode !== "ENOENT" && !(errorCode === "EROFS" && !(await fs.exists(cacheFilePath)))) { + throw error; + } + } + } + + const filePaths = await findFiles({ + patterns: typeof patterns === "string" ? [patterns] : patterns, + cwd, + extensions, + globInputPaths, + configs + }); + + debug(`${filePaths.length} files found in: ${Date.now() - startTime}ms`); + + /* + * Because we need to process multiple files, including reading from disk, + * it is most efficient to start by reading each file via promises so that + * they can be done in parallel. Then, we can lint the returned text. This + * ensures we are waiting the minimum amount of time in between lints. + */ + const results = await Promise.all( + + filePaths.map(({ filePath, ignored }) => { + + /* + * If a filename was entered that matches an ignore + * pattern, then notify the user. + */ + if (ignored) { + return createIgnoreResult(filePath, cwd); + } + + const config = configs.getConfig(filePath); + + /* + * Sometimes a file found through a glob pattern will + * be ignored. In this case, `config` will be undefined + * and we just silently ignore the file. + */ + if (!config) { + return void 0; + } + + /* + * Store used configs for: + * - this method uses to collect used deprecated rules. + * - `--fix-type` option uses to get the loaded rule's meta data. + */ + if (!usedConfigs.includes(config)) { + usedConfigs.push(config); + } + + // Skip if there is cached result. + if (lintResultCache) { + const cachedResult = + lintResultCache.getCachedLintResults(filePath, config); + + if (cachedResult) { + const hadMessages = + cachedResult.messages && + cachedResult.messages.length > 0; + + if (hadMessages && fix) { + debug(`Reprocessing cached file to allow autofix: ${filePath}`); + } else { + debug(`Skipping file since it hasn't changed: ${filePath}`); + return cachedResult; + } + } + } + + + // set up fixer for fixtypes if necessary + let fixer = fix; + + if (fix && fixTypesSet) { + + // save original value of options.fix in case it's a function + const originalFix = (typeof fix === "function") + ? fix : () => true; + + fixer = message => shouldMessageBeFixed(message, config, fixTypesSet) && originalFix(message); + } + + return fs.readFile(filePath, "utf8") + .then(text => { + + // do the linting + const result = verifyText({ + text, + filePath, + configs, + cwd, + fix: fixer, + allowInlineConfig, + reportUnusedDisableDirectives, + linter + }); + + /* + * Store the lint result in the LintResultCache. + * NOTE: The LintResultCache will remove the file source and any + * other properties that are difficult to serialize, and will + * hydrate those properties back in on future lint runs. + */ + if (lintResultCache) { + lintResultCache.setCachedLintResults(filePath, config, result); + } + + return result; + }); + + }) + ); + + // Persist the cache to disk. + if (lintResultCache) { + lintResultCache.reconcile(); + } + + let usedDeprecatedRules; + const finalResults = results.filter(result => !!result); + + return processLintReport(this, { + results: finalResults, + ...calculateStatsPerRun(finalResults), + + // Initialize it lazily because CLI and `ESLint` API don't use it. + get usedDeprecatedRules() { + if (!usedDeprecatedRules) { + usedDeprecatedRules = Array.from( + iterateRuleDeprecationWarnings(usedConfigs) + ); + } + return usedDeprecatedRules; + } + }); + } + + /** + * 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 = {}) { + + // Parameter validation + + 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"); + } + + // Options validation + + const { + filePath, + warnIgnored = false, + ...unknownOptions + } = options || {}; + + const unknownOptionKeys = Object.keys(unknownOptions); + + if (unknownOptionKeys.length > 0) { + throw new Error(`'options' must not include the unknown option(s): ${unknownOptionKeys.join(", ")}`); + } + + 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"); + } + + // Now we can get down to linting + + const { + linter, + options: eslintOptions + } = privateMembers.get(this); + const configs = await calculateConfigArray(this, eslintOptions); + const { + allowInlineConfig, + cwd, + fix, + reportUnusedDisableDirectives + } = eslintOptions; + const results = []; + const startTime = Date.now(); + const resolvedFilename = path.resolve(cwd, filePath || "__placeholder__.js"); + let config; + + // Clear the last used config arrays. + if (resolvedFilename && await this.isPathIgnored(resolvedFilename)) { + if (warnIgnored) { + results.push(createIgnoreResult(resolvedFilename, cwd)); + } + } else { + + // TODO: Needed? + config = configs.getConfig(resolvedFilename); + + // Do lint. + results.push(verifyText({ + text: code, + filePath: resolvedFilename.endsWith("__placeholder__.js") ? "" : resolvedFilename, + configs, + cwd, + fix, + allowInlineConfig, + reportUnusedDisableDirectives, + linter + })); + } + + debug(`Linting complete in: ${Date.now() - startTime}ms`); + let usedDeprecatedRules; + + return processLintReport(this, { + results, + ...calculateStatsPerRun(results), + + // Initialize it lazily because CLI and `ESLint` API don't use it. + get usedDeprecatedRules() { + if (!usedDeprecatedRules) { + usedDeprecatedRules = Array.from( + iterateRuleDeprecationWarnings(config) + ); + } + return usedDeprecatedRules; + } + }); + + } + + /** + * Returns the formatter representing the given formatter name. + * @param {string} [name] The name of the formatter 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"); + } + + // replace \ with / for Windows compatibility + const normalizedFormatName = name.replace(/\\/gu, "/"); + const namespace = naming.getNamespaceFromTerm(normalizedFormatName); + + // grab our options + const { cwd } = privateMembers.get(this).options; + + + let formatterPath; + + // if there's a slash, then it's a file (TODO: this check seems dubious for scoped npm packages) + if (!namespace && normalizedFormatName.includes("/")) { + formatterPath = path.resolve(cwd, normalizedFormatName); + } else { + try { + const npmFormat = naming.normalizePackageName(normalizedFormatName, "eslint-formatter"); + + // TODO: This is pretty dirty...would be nice to clean up at some point. + formatterPath = ModuleResolver.resolve(npmFormat, path.join(cwd, "__placeholder__.js")); + } catch { + formatterPath = path.resolve(__dirname, "../", "cli-engine", "formatters", `${normalizedFormatName}.js`); + } + } + + let formatter; + + try { + formatter = (await import(pathToFileURL(formatterPath))).default; + } catch (ex) { + + // check for formatters that have been removed + if (removedFormatters.has(name)) { + ex.message = `The ${name} formatter is no longer part of core ESLint. Install it manually with \`npm install -D eslint-formatter-${name}\``; + } else { + ex.message = `There was a problem loading formatter: ${formatterPath}\nError: ${ex.message}`; + } + + throw ex; + } + + + if (typeof formatter !== "function") { + throw new TypeError(`Formatter must be a function, but got a ${typeof formatter}.`); + } + + const eslint = this; + + 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 = eslint.getRulesMetaForResults(results); + } + + 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 + * or `undefined` if there is no configuration data for the object. + */ + async calculateConfigForFile(filePath) { + if (!isNonEmptyString(filePath)) { + throw new Error("'filePath' must be a non-empty string"); + } + const options = privateMembers.get(this).options; + const absolutePath = path.resolve(options.cwd, filePath); + const configs = await calculateConfigArray(this, options); + + return configs.getConfig(absolutePath); + } + + /** + * 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) { + const config = await this.calculateConfigForFile(filePath); + + return config === void 0; + } +} + +//------------------------------------------------------------------------------ +// Public Interface +//------------------------------------------------------------------------------ + +module.exports = { + FlatESLint +}; diff --git a/lib/eslint/index.js b/lib/eslint/index.js index c9185ee0eba..017b768ecd0 100644 --- a/lib/eslint/index.js +++ b/lib/eslint/index.js @@ -1,7 +1,9 @@ "use strict"; const { ESLint } = require("./eslint"); +const { FlatESLint } = require("./flat-eslint"); module.exports = { - ESLint + ESLint, + FlatESLint }; diff --git a/lib/linter/linter.js b/lib/linter/linter.js index 29d78da3969..a29ce923792 100644 --- a/lib/linter/linter.js +++ b/lib/linter/linter.js @@ -1608,6 +1608,11 @@ class Linter { ...languageOptions.globals }; + // double check that there is a parser to avoid mysterious error messages + if (!languageOptions.parser) { + throw new TypeError(`No parser specified for ${options.filename}`); + } + // Espree expects this information to be passed in if (isEspree(languageOptions.parser)) { const parserOptions = languageOptions.parserOptions; @@ -1770,12 +1775,24 @@ class Linter { debug("With flat config: %s", options.filename); // we need a filename to match configs against - const filename = options.filename || ""; + const filename = options.filename || "__placeholder__.js"; // Store the config array in order to get plugin envs and rules later. internalSlotsMap.get(this).lastConfigArray = configArray; const config = configArray.getConfig(filename); + if (!config) { + return [ + { + ruleId: null, + severity: 1, + message: `No matching configuration found for ${filename}.`, + line: 0, + column: 0 + } + ]; + } + // Verify. if (config.processor) { debug("Apply the processor: %o", config.processor); diff --git a/lib/rule-tester/flat-rule-tester.js b/lib/rule-tester/flat-rule-tester.js index b829484149a..19bd8e354f6 100644 --- a/lib/rule-tester/flat-rule-tester.js +++ b/lib/rule-tester/flat-rule-tester.js @@ -480,51 +480,54 @@ class FlatRuleTester { ].concat(scenarioErrors).join("\n")); } - const baseConfig = { - plugins: { - - // copy root plugin over - "@": { - - /* - * Parsers are wrapped to detect more errors, so this needs - * to be a new object for each call to run(), otherwise the - * parsers will be wrapped multiple times. - */ - parsers: { - ...defaultConfig[0].plugins["@"].parsers - }, + const baseConfig = [ + { + plugins: { - /* - * The rules key on the default plugin is a proxy to lazy-load - * just the rules that are needed. So, don't create a new object - * here, just use the default one to keep that performance - * enhancement. - */ - rules: defaultConfig[0].plugins["@"].rules - }, - "rule-to-test": { - rules: { - [ruleName]: Object.assign({}, rule, { + // copy root plugin over + "@": { + + /* + * Parsers are wrapped to detect more errors, so this needs + * to be a new object for each call to run(), otherwise the + * parsers will be wrapped multiple times. + */ + parsers: { + ...defaultConfig[0].plugins["@"].parsers + }, + + /* + * The rules key on the default plugin is a proxy to lazy-load + * just the rules that are needed. So, don't create a new object + * here, just use the default one to keep that performance + * enhancement. + */ + rules: defaultConfig[0].plugins["@"].rules + }, + "rule-to-test": { + rules: { + [ruleName]: Object.assign({}, rule, { - // Create a wrapper rule that freezes the `context` properties. - create(context) { - freezeDeeply(context.options); - freezeDeeply(context.settings); - freezeDeeply(context.parserOptions); + // Create a wrapper rule that freezes the `context` properties. + create(context) { + freezeDeeply(context.options); + freezeDeeply(context.settings); + freezeDeeply(context.parserOptions); - // freezeDeeply(context.languageOptions); + // freezeDeeply(context.languageOptions); - return (typeof rule === "function" ? rule : rule.create)(context); - } - }) + return (typeof rule === "function" ? rule : rule.create)(context); + } + }) + } } + }, + languageOptions: { + ...defaultConfig[0].languageOptions } }, - languageOptions: { - ...defaultConfig[0].languageOptions - } - }; + ...defaultConfig.slice(1) + ]; /** * Run the rule for the given item diff --git a/lib/unsupported-api.js b/lib/unsupported-api.js index 110b35a47a4..c1daf54d6ae 100644 --- a/lib/unsupported-api.js +++ b/lib/unsupported-api.js @@ -12,6 +12,8 @@ //----------------------------------------------------------------------------- const { FileEnumerator } = require("./cli-engine/file-enumerator"); +const { FlatESLint } = require("./eslint/flat-eslint"); +const FlatRuleTester = require("./rule-tester/flat-rule-tester"); //----------------------------------------------------------------------------- // Exports @@ -19,5 +21,7 @@ const { FileEnumerator } = require("./cli-engine/file-enumerator"); module.exports = { builtinRules: require("./rules"), + FlatESLint, + FlatRuleTester, FileEnumerator }; diff --git a/package.json b/package.json index 7ce56b9608b..08548c5e16c 100644 --- a/package.json +++ b/package.json @@ -55,7 +55,8 @@ "bugs": "https://github.com/eslint/eslint/issues/", "dependencies": { "@eslint/eslintrc": "^1.3.0", - "@humanwhocodes/config-array": "^0.9.2", + "@humanwhocodes/config-array": "^0.10.4", + "@humanwhocodes/gitignore-to-minimatch": "^1.0.2", "ajv": "^6.10.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.2", @@ -70,9 +71,11 @@ "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", "functional-red-black-tree": "^1.0.1", "glob-parent": "^6.0.1", "globals": "^13.15.0", + "globby": "^11.1.0", "grapheme-splitter": "^1.0.4", "ignore": "^5.2.0", "import-fresh": "^3.0.0", diff --git a/tests/fixtures/cli-engine/deprecated-rule-config/eslint.config.js b/tests/fixtures/cli-engine/deprecated-rule-config/eslint.config.js new file mode 100644 index 00000000000..eb9b7f0ee06 --- /dev/null +++ b/tests/fixtures/cli-engine/deprecated-rule-config/eslint.config.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + "indent-legacy": "error" + } +}; diff --git a/tests/fixtures/config-hierarchy/broken/add-conf.js b/tests/fixtures/config-hierarchy/broken/add-conf.js new file mode 100644 index 00000000000..75c32923a32 --- /dev/null +++ b/tests/fixtures/config-hierarchy/broken/add-conf.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + semi: [1, "never"] + } +}; diff --git a/tests/fixtures/config-hierarchy/broken/override-conf.js b/tests/fixtures/config-hierarchy/broken/override-conf.js new file mode 100644 index 00000000000..6287d08ff28 --- /dev/null +++ b/tests/fixtures/config-hierarchy/broken/override-conf.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + quotes: 0; + } +}; diff --git a/tests/fixtures/configurations/env-browser.js b/tests/fixtures/configurations/env-browser.js new file mode 100644 index 00000000000..a99c5707d8a --- /dev/null +++ b/tests/fixtures/configurations/env-browser.js @@ -0,0 +1,11 @@ +module.exports = { + languageOptions: { + globals: { + window: false + } + }, + rules: { + "no-alert": 0, + "no-undef": 2 + } +}; diff --git a/tests/fixtures/configurations/env-node.js b/tests/fixtures/configurations/env-node.js new file mode 100644 index 00000000000..27d5fe77de2 --- /dev/null +++ b/tests/fixtures/configurations/env-node.js @@ -0,0 +1,13 @@ +module.exports = { + languageOptions: { + globals: { + __dirname: false, + console: false + }, + sourceType: "commonjs" + }, + rules: { + "no-console": 0, + "no-undef": 2 + } +}; diff --git a/tests/fixtures/configurations/plugins-with-prefix.js b/tests/fixtures/configurations/plugins-with-prefix.js new file mode 100644 index 00000000000..42411144b4f --- /dev/null +++ b/tests/fixtures/configurations/plugins-with-prefix.js @@ -0,0 +1,5 @@ +module.exports = { + "rules": { + "example/example-rule": 1 + } +}; diff --git a/tests/fixtures/configurations/processors.js b/tests/fixtures/configurations/processors.js new file mode 100644 index 00000000000..e8bcce32060 --- /dev/null +++ b/tests/fixtures/configurations/processors.js @@ -0,0 +1,12 @@ +module.exports = { + "plugins": [ + "processor", + "example" + ], + + "rules": { + "no-console": 2, + "no-unused-vars": 2, + "example/example-rule": 1 + } +} diff --git a/tests/fixtures/configurations/quotes-error.js b/tests/fixtures/configurations/quotes-error.js new file mode 100644 index 00000000000..bbf45f0e817 --- /dev/null +++ b/tests/fixtures/configurations/quotes-error.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + quotes: [2, "double"] + } +}; diff --git a/tests/fixtures/configurations/semi-error.js b/tests/fixtures/configurations/semi-error.js new file mode 100644 index 00000000000..2dcc0b196b7 --- /dev/null +++ b/tests/fixtures/configurations/semi-error.js @@ -0,0 +1,6 @@ +module.exports = { + rules: { + semi: 1, + strict: 0 + } +}; diff --git a/tests/fixtures/eslint.config.js b/tests/fixtures/eslint.config.js new file mode 100644 index 00000000000..2d9e0848d41 --- /dev/null +++ b/tests/fixtures/eslint.config.js @@ -0,0 +1,6 @@ + +module.exports = { + rules: { + strict: 0 + } +}; diff --git a/tests/fixtures/ignored-paths/.eslintignoreForNegationTest b/tests/fixtures/ignored-paths/.eslintignoreForNegationTest new file mode 100644 index 00000000000..d1cc10c7d73 --- /dev/null +++ b/tests/fixtures/ignored-paths/.eslintignoreForNegationTest @@ -0,0 +1 @@ +undef.js diff --git a/tests/fixtures/rules/eslint.js b/tests/fixtures/rules/eslint.js new file mode 100644 index 00000000000..aff3e58062c --- /dev/null +++ b/tests/fixtures/rules/eslint.js @@ -0,0 +1,10 @@ +module.exports = { + languageOptions: { + "globals": { + "test": true + } + }, + "rules": { + "custom-rule": 1 + } +}; diff --git a/tests/fixtures/rules/missing-rule.js b/tests/fixtures/rules/missing-rule.js new file mode 100644 index 00000000000..39a8c00f17e --- /dev/null +++ b/tests/fixtures/rules/missing-rule.js @@ -0,0 +1,5 @@ +module.exports = { + rules: { + "missing-rule": 1 + } +}; diff --git a/tests/lib/config/flat-config-array.js b/tests/lib/config/flat-config-array.js index 8dc45b6e236..bf154ccffe7 100644 --- a/tests/lib/config/flat-config-array.js +++ b/tests/lib/config/flat-config-array.js @@ -19,6 +19,7 @@ const recommendedConfig = require("../../../conf/eslint-recommended"); //----------------------------------------------------------------------------- const baseConfig = { + files: ["**/*.js"], plugins: { "@": { rules: { @@ -77,7 +78,6 @@ const baseConfig = { */ function createFlatConfigArray(configs) { return new FlatConfigArray(configs, { - basePath: __dirname, baseConfig: [baseConfig] }); } @@ -153,7 +153,6 @@ describe("FlatConfigArray", () => { }; const configs = new FlatConfigArray([], { - basePath: __dirname, baseConfig: base }); @@ -163,6 +162,7 @@ describe("FlatConfigArray", () => { it("should not reuse languageOptions.parserOptions across configs", () => { const base = [{ + files: ["**/*.js"], languageOptions: { parserOptions: { foo: true @@ -171,7 +171,6 @@ describe("FlatConfigArray", () => { }]; const configs = new FlatConfigArray([], { - basePath: __dirname, baseConfig: base }); @@ -185,7 +184,7 @@ describe("FlatConfigArray", () => { describe("Special configs", () => { it("eslint:recommended is replaced with an actual config", async () => { - const configs = new FlatConfigArray(["eslint:recommended"], { basePath: __dirname }); + const configs = new FlatConfigArray(["eslint:recommended"]); await configs.normalize(); const config = configs.getConfig("foo.js"); @@ -194,7 +193,7 @@ describe("FlatConfigArray", () => { }); it("eslint:all is replaced with an actual config", async () => { - const configs = new FlatConfigArray(["eslint:all"], { basePath: __dirname }); + const configs = new FlatConfigArray(["eslint:all"]); await configs.normalize(); const config = configs.getConfig("foo.js"); @@ -1459,7 +1458,7 @@ describe("FlatConfigArray", () => { }, { plugins: { - "foo/baz/boom": { + "@foo/baz/boom": { rules: { bang: {} } @@ -1468,13 +1467,13 @@ describe("FlatConfigArray", () => { rules: { foo: ["error"], bar: 0, - "foo/baz/boom/bang": "error" + "@foo/baz/boom/bang": "error" } } ], { plugins: { ...baseConfig.plugins, - "foo/baz/boom": { + "@foo/baz/boom": { rules: { bang: {} } @@ -1483,7 +1482,7 @@ describe("FlatConfigArray", () => { rules: { foo: [2, "always"], bar: [0], - "foo/baz/boom/bang": [2] + "@foo/baz/boom/bang": [2] } })); diff --git a/tests/lib/config/flat-config-helpers.js b/tests/lib/config/flat-config-helpers.js new file mode 100644 index 00000000000..004fb82b13c --- /dev/null +++ b/tests/lib/config/flat-config-helpers.js @@ -0,0 +1,102 @@ +/** + * @fileoverview Tests for FlatConfigArray + * @author Nicholas C. Zakas + */ + +"use strict"; + +//----------------------------------------------------------------------------- +// Requirements +//----------------------------------------------------------------------------- + +const { + parseRuleId, + getRuleFromConfig +} = require("../../../lib/config/flat-config-helpers"); +const assert = require("chai").assert; + +//----------------------------------------------------------------------------- +// Tests +//----------------------------------------------------------------------------- + +describe("Config Helpers", () => { + + + describe("parseRuleId()", () => { + + it("should return plugin name and rule name for core rule", () => { + const result = parseRuleId("foo"); + + assert.deepStrictEqual(result, { + pluginName: "@", + ruleName: "foo" + }); + }); + + it("should return plugin name and rule name with a/b format", () => { + const result = parseRuleId("test/foo"); + + assert.deepStrictEqual(result, { + pluginName: "test", + ruleName: "foo" + }); + }); + + it("should return plugin name and rule name with a/b/c format", () => { + const result = parseRuleId("node/no-unsupported-features/es-builtins"); + + assert.deepStrictEqual(result, { + pluginName: "node", + ruleName: "no-unsupported-features/es-builtins" + }); + }); + + it("should return plugin name and rule name with @a/b/c format", () => { + const result = parseRuleId("@test/foo/bar"); + + assert.deepStrictEqual(result, { + pluginName: "@test/foo", + ruleName: "bar" + }); + }); + }); + + describe("getRuleFromConfig", () => { + it("should retrieve rule from plugin in config", () => { + const rule = {}; + const config = { + plugins: { + test: { + rules: { + one: rule + } + } + } + }; + + const result = getRuleFromConfig("test/one", config); + + assert.strictEqual(result, rule); + + }); + + it("should retrieve rule from core in config", () => { + const rule = {}; + const config = { + plugins: { + "@": { + rules: { + semi: rule + } + } + } + }; + + const result = getRuleFromConfig("semi", config); + + assert.strictEqual(result, rule); + + }); + }); + +}); diff --git a/tests/lib/eslint/eslint.config.js b/tests/lib/eslint/eslint.config.js new file mode 100644 index 00000000000..6b389dc670e --- /dev/null +++ b/tests/lib/eslint/eslint.config.js @@ -0,0 +1,11 @@ +"use strict"; + +module.exports = { + rules: { + quotes: 2, + "no-var": 2, + "eol-last": 2, + strict: 2, + "no-unused-vars": 2 + } +}; diff --git a/tests/lib/eslint/flat-eslint.js b/tests/lib/eslint/flat-eslint.js new file mode 100644 index 00000000000..62f1afd6b8d --- /dev/null +++ b/tests/lib/eslint/flat-eslint.js @@ -0,0 +1,4647 @@ +/** + * @fileoverview Tests for the ESLint class. + * @author Kai Cataldo + * @author Toru Nagashima + */ + +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const assert = require("assert"); +const fs = require("fs"); +const fsp = fs.promises; +const os = require("os"); +const path = require("path"); +const escapeStringRegExp = require("escape-string-regexp"); +const fCache = require("file-entry-cache"); +const sinon = require("sinon"); +const proxyquire = require("proxyquire").noCallThru().noPreserveCache(); +const shell = require("shelljs"); +const hash = require("../../../lib/cli-engine/hash"); +const { unIndent, createCustomTeardown } = require("../../_utils"); +const coreRules = require("../../../lib/rules"); + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +describe("FlatESLint", () => { + 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/flat-eslint").FlatESLint} */ + let FlatESLint; + + /** + * 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 { + 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 FlatESLint({ + ...options, + plugins: { + [examplePluginName]: examplePlugin, + [examplePluginNameWithNamespace]: examplePlugin, + [examplePreprocessorName]: require("../../fixtures/processors/custom-processor") + } + }); + } + + // copy into clean area so as not to get "infected" by this project's .eslintrc files + before(function() { + + /* + * GitHub Actions Windows and macOS runners occasionally exhibit + * extremely slow filesystem operations, during which copying fixtures + * exceeds the default test timeout, so raise it just for this hook. + * Mocha uses `this` to set timeouts on an individual hook level. + */ + this.timeout(60 * 1000); // eslint-disable-line no-invalid-this -- Mocha API + shell.mkdir("-p", fixtureDir); + shell.cp("-r", "./tests/fixtures/.", fixtureDir); + }); + + beforeEach(() => { + ({ FlatESLint } = require("../../../lib/eslint/flat-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 FlatESLint(); + const results = await engine.lintFiles("eslint.js"); + + assert.strictEqual(path.dirname(results[0].filePath), __dirname); + } finally { + process.chdir(originalDir); + } + }); + + // https://github.com/eslint/eslint/issues/2380 + it("should not modify baseConfig when format is specified", () => { + const customBaseConfig = { root: true }; + + new FlatESLint({ baseConfig: customBaseConfig }); // eslint-disable-line no-new -- Check for argument side effects + + assert.deepStrictEqual(customBaseConfig, { root: true }); + }); + + it("should throw readable messages if removed options are present", () => { + assert.throws( + () => new FlatESLint({ + cacheFile: "", + configFile: "", + envs: [], + globals: [], + ignorePattern: [], + parser: "", + parserOptions: {}, + rules: {}, + plugins: [] + }), + new RegExp(escapeStringRegExp([ + "Invalid Options:", + "- Unknown options: cacheFile, configFile, envs, globals, ignorePattern, parser, parserOptions, rules" + ].join("\n")), "u") + ); + }); + + it("should throw readable messages if wrong type values are given to options", () => { + assert.throws( + () => new FlatESLint({ + allowInlineConfig: "", + baseConfig: "", + cache: "", + cacheLocation: "", + cwd: "foo", + errorOnUnmatchedPattern: "", + extensions: "", + fix: "", + fixTypes: ["xyz"], + globInputPaths: "", + ignore: "", + ignorePath: "", + overrideConfig: "", + overrideConfigFile: "", + plugins: "", + reportUnusedDisableDirectives: "" + }), + 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 \"directive\", \"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, null, or true.", + "- 'plugins' must be an object or null.", + "- 'reportUnusedDisableDirectives' must be any of \"error\", \"warn\", \"off\", and null." + ].join("\n")), "u") + ); + }); + + it("should throw readable messages if 'plugins' option contains empty key", () => { + assert.throws( + () => new FlatESLint({ + 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 eslint.config.js", async () => { + eslint = new FlatESLint({ + cwd: __dirname + }); + + const results = await eslint.lintText("var foo = 'bar';"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 4); + assert.strictEqual(results[0].messages[0].ruleId, "no-var"); + assert.strictEqual(results[0].messages[1].ruleId, "no-unused-vars"); + assert.strictEqual(results[0].messages[2].ruleId, "quotes"); + assert.strictEqual(results[0].messages[3].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 FlatESLint({ + overrideConfig: { + rules: { + quotes: 1, + "no-var": 1, + "eol-last": 1, + "no-unused-vars": 1 + } + }, + overrideConfigFile: true + }); + const results = await eslint.lintText("var foo = 'bar';"); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 4); + assert.strictEqual(results[0].messages[0].ruleId, "no-var"); + assert.strictEqual(results[0].messages[1].ruleId, "no-unused-vars"); + assert.strictEqual(results[0].messages[2].ruleId, "quotes"); + assert.strictEqual(results[0].messages[3].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 FlatESLint({ + overrideConfigFile: "fixtures/configurations/quotes-error.js", + 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].fatalErrorCount, 0); + assert.strictEqual(results[0].usedDeprecatedRules.length, 0); + }); + + it("should report the filename when passed in", async () => { + eslint = new FlatESLint({ + 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 FlatESLint({ + ignorePath: getFixturePath(".eslintignore"), + cwd: getFixturePath(".."), + overrideConfigFile: "fixtures/eslint.config.js" + }); + + 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].fatalErrorCount, 0); + 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 FlatESLint({ + ignorePath: getFixturePath(".eslintignore"), + cwd: getFixturePath(".."), + overrideConfigFile: "fixtures/eslint.config.js" + }); + 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 FlatESLint({ + ignorePath: getFixturePath(".eslintignore"), + cwd: getFixturePath(".."), + overrideConfigFile: "fixtures/eslint.config.js" + }); + 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 FlatESLint({ + ignorePath: "fixtures/.eslintignore", + cwd: getFixturePath(".."), + ignore: false, + overrideConfigFile: true, + 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 FlatESLint({ + overrideConfigFile: true, + 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, + fatalErrorCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + output: "var bar = foo;", + usedDeprecatedRules: [] + } + ]); + }); + + it("should return a message and omit fixed text when in fix mode and fixes aren't done", async () => { + eslint = new FlatESLint({ + overrideConfigFile: true, + 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, + fatalErrorCount: 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({ + overrideConfigFile: true, + 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 = 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, + fatalErrorCount: 1, + 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 = eslintWithPlugins({ + overrideConfigFile: true, + 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, + fatalErrorCount: 1, + 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 FlatESLint({ + overrideConfigFile: true, + 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 FlatESLint({ + overrideConfigFile: true, + 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 FlatESLint({ + overrideConfigFile: true, + 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 FlatESLint({ + overrideConfigFile: true, + 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 FlatESLint({ + overrideConfigFile: true, + 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, + fatalErrorCount: 1, + 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 (ignoring node_modules), even with --no-ignore", async () => { + eslint = new FlatESLint({ + 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); + }); + + it("should warn when deprecated rules are found in a config", async () => { + eslint = new FlatESLint({ + cwd: originalDir, + overrideConfigFile: "tests/fixtures/cli-engine/deprecated-rule-config/eslint.config.js" + }); + 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 FlatESLint(); + 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 FlatESLint(); + 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 FlatESLint(); + await assert.rejects(() => eslint.lintText("var a = 0", { filename: "foo.js" }), /'options' must not include the unknown option\(s\): filename/u); + }); + + it("should throw if non-string value is given to 'options.filePath' option", async () => { + eslint = new FlatESLint(); + 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 FlatESLint(); + 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 () => { + const filePath = path.resolve(__dirname, "../../fixtures/configurations/parser/custom.js"); + + eslint = new FlatESLint({ + cwd: originalDir, + ignore: false, + overrideConfigFile: true, + overrideConfig: { + languageOptions: { + parser: require(filePath) + } + } + }); + + 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 FlatESLint({ + cwd: originalDir, + overrideConfigFile: "eslint.config.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 FlatESLint({ + cwd: originalDir, + overrideConfigFile: "eslint.config.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 FlatESLint({ + overrideConfig: { + languageOptions: { + parser: require("espree"), + parserOptions: { + ecmaVersion: 2021 + } + } + }, + overrideConfigFile: true + }); + 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 FlatESLint({ + overrideConfig: { + languageOptions: { + parser: require("esprima") + } + }, + overrideConfigFile: true, + 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 FlatESLint({ + overrideConfig: { + languageOptions: { + parser: "test11" + } + }, + overrideConfigFile: true + }); + + await assert.rejects(async () => await eslint.lintFiles(["lib/cli.js"]), /Expected string in the form "pluginName\/objectName" but found "test11"/u); + }); + + it("should report zero messages when given a directory with a .js2 file", async () => { + eslint = new FlatESLint({ + cwd: path.join(fixtureDir, ".."), + extensions: [".js2"], + overrideConfigFile: getFixturePath("eslint.config.js") + }); + 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 FlatESLint({ + cwd: getFixturePath(), + overrideConfigFile: true, + ignore: false, + overrideConfig: { + rules: { + quotes: ["error", "double"] + } + }, + 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].fatalErrorCount, 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 FlatESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: getFixturePath(".."), + overrideConfigFile: getFixturePath("eslint.config.js") + }); + 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 FlatESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: getFixturePath("eslint.config.js") + + }); + 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 FlatESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: getFixturePath(".."), + overrideConfigFile: getFixturePath("eslint.config.js") + + }); + 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 FlatESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: getFixturePath(".."), + overrideConfigFile: true, + globInputPaths: false + }); + + await assert.rejects(async () => { + await eslint.lintFiles(["fixtures/files/*"]); + }, /No files matching 'fixtures\/files\/\*' were found \(glob was disabled\)\./u); + }); + + describe("Ignoring Files", () => { + + it("should report on all files passed explicitly, even if ignored by default", async () => { + eslint = new FlatESLint({ + 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].fatalErrorCount, 0); + 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", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath("cli-engine"), + overrideConfigFile: true, + 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].fatalErrorCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 1); + assert.strictEqual(results[0].fixableWarningCount, 0); + }); + + it("should ignore node_modules files when using ignore file", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath("cli-engine"), + overrideConfigFile: true + }); + + await assert.rejects(async () => { + await eslint.lintFiles(["node_modules"]); + }, /All files matched by 'node_modules\/\*\*\/\*.js' are ignored\./u); + }); + + // https://github.com/eslint/eslint/issues/5547 + it("should ignore node_modules files even with ignore: false", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath("cli-engine"), + ignore: false + }); + + await assert.rejects(async () => { + await eslint.lintFiles(["node_modules"]); + }, /All files matched by 'node_modules\/\*\*\/\*\.js' are ignored\./u); + }); + + it("should throw an error when given a directory with all eslint excluded files in the directory", async () => { + eslint = new FlatESLint({ + ignorePath: getFixturePath(".eslintignore") + }); + + await assert.rejects(async () => { + await eslint.lintFiles([getFixturePath("./cli-engine/")]); + }, /All files matched by '.*?cli-engine[\\/]\*\*[\\/]\*\.js' are ignored/u); + }); + + it("should throw an error when all given files are ignored", async () => { + eslint = new FlatESLint({ + ignorePath: getFixturePath(".eslintignore") + }); + + await assert.rejects(async () => { + await eslint.lintFiles(["tests/fixtures/cli-engine/"]); + }, /All files matched by 'tests\/fixtures\/cli-engine\/\*\*\/\*\.js' are ignored\./u); + }); + + it("should throw an error when all given files are ignored even with a `./` prefix", async () => { + eslint = new FlatESLint({ + ignorePath: getFixturePath(".eslintignore") + }); + + await assert.rejects(async () => { + await eslint.lintFiles(["./tests/fixtures/cli-engine/"]); + }, /All files matched by 'tests\/fixtures\/cli-engine\/\*\*\/\*\.js' 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 FlatESLint({ + ignorePath: getFixturePath("cli-engine", "nested_node_modules", ".eslintignore"), + overrideConfigFile: true, + 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].fatalErrorCount, 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 fixtures/ is in ignore file", async () => { + eslint = new FlatESLint({ + ignorePath: getFixturePath("cli-engine/.eslintignore2"), + overrideConfigFile: true, + overrideConfig: { + rules: { + quotes: [2, "double"] + } + } + }); + + await assert.rejects(async () => { + await eslint.lintFiles(["./tests/fixtures/cli-engine/"]); + }, /All files matched by 'tests\/fixtures\/cli-engine\/\*\*\/\*\.js' are ignored\./u); + }); + + it("should throw an error when all given files are ignored via ignore-pattern", async () => { + eslint = new FlatESLint({ + 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 FlatESLint({ + 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].fatalErrorCount, 0); + 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 FlatESLint({ + cwd: getFixturePath(), + ignorePath: getFixturePath(".eslintignore"), + ignore: false, + overrideConfigFile: true, + 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 report zero messages when given a pattern with a .js and a .js2 file", async () => { + eslint = new FlatESLint({ + extensions: [".js", ".js2"], + ignore: false, + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true + }); + 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 FlatESLint({ + cwd: getFixturePath(), + overrideConfigFile: true, + overrideConfig: { + rules: { + quotes: ["error", "double"] + } + }, + ignore: false + }); + 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].fatalErrorCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 1); + assert.strictEqual(results[0].fixableWarningCount, 0); + }); + + it("should return 5 results when given a config and a directory of 5 valid files", async () => { + eslint = new FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + overrideConfig: { + rules: { + semi: 1, + strict: 0 + } + } + }); + + const formattersDir = getFixturePath("formatters"); + const results = await eslint.lintFiles([formattersDir]); + + assert.strictEqual(results.length, 5); + assert.strictEqual(path.relative(formattersDir, results[0].filePath), "async.js"); + assert.strictEqual(results[0].errorCount, 0); + assert.strictEqual(results[0].warningCount, 0); + assert.strictEqual(results[0].fatalErrorCount, 0); + assert.strictEqual(results[0].fixableErrorCount, 0); + assert.strictEqual(results[0].fixableWarningCount, 0); + assert.strictEqual(results[0].messages.length, 0); + assert.strictEqual(path.relative(formattersDir, results[1].filePath), "broken.js"); + assert.strictEqual(results[1].errorCount, 0); + assert.strictEqual(results[1].warningCount, 0); + assert.strictEqual(results[1].fatalErrorCount, 0); + assert.strictEqual(results[1].fixableErrorCount, 0); + assert.strictEqual(results[1].fixableWarningCount, 0); + assert.strictEqual(results[1].messages.length, 0); + assert.strictEqual(path.relative(formattersDir, results[2].filePath), "cwd.js"); + assert.strictEqual(results[2].errorCount, 0); + assert.strictEqual(results[2].warningCount, 0); + assert.strictEqual(results[2].fatalErrorCount, 0); + assert.strictEqual(results[2].fixableErrorCount, 0); + assert.strictEqual(results[2].fixableWarningCount, 0); + assert.strictEqual(results[2].messages.length, 0); + assert.strictEqual(path.relative(formattersDir, results[3].filePath), "simple.js"); + assert.strictEqual(results[3].errorCount, 0); + assert.strictEqual(results[3].warningCount, 0); + assert.strictEqual(results[3].fatalErrorCount, 0); + assert.strictEqual(results[3].fixableErrorCount, 0); + assert.strictEqual(results[3].fixableWarningCount, 0); + assert.strictEqual(results[3].messages.length, 0); + assert.strictEqual(path.relative(formattersDir, results[4].filePath), path.join("test", "simple.js")); + assert.strictEqual(results[4].errorCount, 0); + assert.strictEqual(results[4].warningCount, 0); + assert.strictEqual(results[4].fatalErrorCount, 0); + assert.strictEqual(results[4].fixableErrorCount, 0); + assert.strictEqual(results[4].fixableWarningCount, 0); + assert.strictEqual(results[4].messages.length, 0); + }); + + it("should return zero messages when given a config with browser globals", async () => { + eslint = new FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: getFixturePath("configurations", "env-browser.js") + }); + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("globals-browser.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0, "Should have no messages."); + }); + + it("should return zero messages when given an option to add browser globals", async () => { + eslint = new FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + overrideConfig: { + languageOptions: { + globals: { + window: false + } + }, + 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 sourceType set to commonjs and Node.js globals", async () => { + eslint = new FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: getFixturePath("configurations", "env-node.js") + }); + const results = await eslint.lintFiles([fs.realpathSync(getFixturePath("globals-node.js"))]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0, "Should have no messages."); + }); + + it("should not return results from previous call when calling more than once", async () => { + eslint = new FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: getFixturePath("eslint.config.js"), + 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 return zero messages when executing a file with a shebang", async () => { + eslint = new FlatESLint({ + ignore: false, + cwd: getFixturePath(), + overrideConfigFile: getFixturePath("eslint.config.js") + }); + const results = await eslint.lintFiles([getFixturePath("shebang.js")]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 0, "Should have lint messages."); + }); + + it("should return zero messages when executing without a config file", async () => { + eslint = new FlatESLint({ + cwd: getFixturePath(), + ignore: false, + overrideConfigFile: true + }); + 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); + }); + + // working + describe("Deprecated Rules", () => { + + it("should warn when deprecated rules are configured", async () => { + eslint = new FlatESLint({ + cwd: originalDir, + 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 FlatESLint({ + cwd: originalDir, + 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 FlatESLint({ + cwd: originalDir, + overrideConfigFile: "tests/fixtures/cli-engine/deprecated-rule-config/eslint.config.js" + }); + const results = await eslint.lintFiles(["lib/cli*.js"]); + + assert.deepStrictEqual( + results[0].usedDeprecatedRules, + [{ ruleId: "indent-legacy", replacedBy: ["indent"] }] + ); + }); + }); + + // working + describe("Fix Mode", () => { + + it("correctly autofixes semicolon-conflicting-fixes", async () => { + eslint = new FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + 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 FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + 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); + }); + + 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 FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + 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, + fatalErrorCount: 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, + fatalErrorCount: 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, + fatalErrorCount: 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, + fatalErrorCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + output: "var msg = \"hi\" + foo;\n", + usedDeprecatedRules: [] + } + ]); + }); + + // Cannot be run properly until cache is implemented + xit("should run autofix even if files are cached without autofix results", async () => { + const baseOptions = { + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + overrideConfig: { + rules: { + semi: 2, + quotes: [2, "double"], + eqeqeq: 2, + "no-undef": 2, + "space-infix-ops": 2 + } + } + }; + + eslint = new FlatESLint(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 FlatESLint(Object.assign({}, baseOptions, { cache: true, fix: true })); + const results = await eslint.lintFiles([path.resolve(fixtureDir, `${fixtureDir}/fixmode`)]); + + assert(results.some(result => result.output)); + }); + }); + + describe("plugins", () => { + it("should return two messages when executing with config file that specifies a plugin", async () => { + eslint = eslintWithPlugins({ + cwd: path.resolve(fixtureDir, ".."), + overrideConfigFile: getFixturePath("configurations", "plugins-with-prefix.js") + }); + 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, "Expected two messages."); + assert.strictEqual(results[0].messages[0].ruleId, "example/example-rule"); + }); + + it("should return two messages when executing with cli option that specifies a plugin", async () => { + eslint = eslintWithPlugins({ + cwd: path.resolve(fixtureDir, ".."), + overrideConfigFile: true, + overrideConfig: { + 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 FlatESLint({ + cwd: path.resolve(fixtureDir, ".."), + overrideConfigFile: true, + overrideConfig: { + 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"); + }); + }); + + xdescribe("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 { + + /* + * 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 { + + /* + * 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 FlatESLint({ + overrideConfigFile: true, + + // 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 FlatESLint({ + overrideConfigFile: true, + + // 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 FlatESLint({ + overrideConfigFile: true, + 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 FlatESLint({ + overrideConfigFile: true, + + // 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 FlatESLint({ + overrideConfigFile: true, + + // 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 FlatESLint({ + overrideConfigFile: true, + + // 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 FlatESLint({ + overrideConfigFile: true, + + // 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 = { + overrideConfigFile: true, + + // 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 FlatESLint(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 FlatESLint(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 FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + + // 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 FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + + // 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 FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + + // 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 FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + 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 FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + 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 FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + 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 FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: 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 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 FlatESLint({ + overrideConfigFile: true, + + // 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("cacheStrategy", () => { + it("should detect changes using a file's modification time when set to 'metadata'", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + + doDelete(cacheLocation); + + eslint = new FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + + // specifying cache true the cache will be created + cache: true, + cacheLocation, + cacheStrategy: "metadata", + 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")); + + await eslint.lintFiles([badFile, goodFile]); + let fileCache = fCache.createFromFile(cacheLocation); + const entries = fileCache.normalizeEntries([badFile, goodFile]); + + entries.forEach(entry => { + assert(entry.changed === false, `the entry for ${entry.key} is initially unchanged`); + }); + + // this should result in a changed entry + shell.touch(goodFile); + fileCache = fCache.createFromFile(cacheLocation); + assert(fileCache.getFileDescriptor(badFile).changed === false, `the entry for ${badFile} is unchanged`); + assert(fileCache.getFileDescriptor(goodFile).changed === true, `the entry for ${goodFile} is changed`); + }); + + it("should not detect changes using a file's modification time when set to 'content'", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + + doDelete(cacheLocation); + + eslint = new FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + + // specifying cache true the cache will be created + cache: true, + cacheLocation, + cacheStrategy: "content", + 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")); + + await eslint.lintFiles([badFile, goodFile]); + let fileCache = fCache.createFromFile(cacheLocation, true); + let entries = fileCache.normalizeEntries([badFile, goodFile]); + + entries.forEach(entry => { + assert(entry.changed === false, `the entry for ${entry.key} is initially unchanged`); + }); + + // this should NOT result in a changed entry + shell.touch(goodFile); + fileCache = fCache.createFromFile(cacheLocation, true); + entries = fileCache.normalizeEntries([badFile, goodFile]); + entries.forEach(entry => { + assert(entry.changed === false, `the entry for ${entry.key} remains unchanged`); + }); + }); + + it("should detect changes using a file's contents when set to 'content'", async () => { + const cacheLocation = getFixturePath(".eslintcache"); + + doDelete(cacheLocation); + + eslint = new FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + + // specifying cache true the cache will be created + cache: true, + cacheLocation, + cacheStrategy: "content", + 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 goodFileCopy = path.resolve(`${path.dirname(goodFile)}`, "test-file-copy.js"); + + shell.cp(goodFile, goodFileCopy); + + await eslint.lintFiles([badFile, goodFileCopy]); + let fileCache = fCache.createFromFile(cacheLocation, true); + const entries = fileCache.normalizeEntries([badFile, goodFileCopy]); + + entries.forEach(entry => { + assert(entry.changed === false, `the entry for ${entry.key} is initially unchanged`); + }); + + // this should result in a changed entry + shell.sed("-i", "abc", "xzy", goodFileCopy); + fileCache = fCache.createFromFile(cacheLocation, true); + assert(fileCache.getFileDescriptor(badFile).changed === false, `the entry for ${badFile} is unchanged`); + assert(fileCache.getFileDescriptor(goodFileCopy).changed === true, `the entry for ${goodFileCopy} is changed`); + }); + }); + }); + + describe("processors", () => { + + it("should return two messages when executing with config file that specifies preloaded processor", async () => { + eslint = new FlatESLint({ + overrideConfigFile: true, + overrideConfig: [ + { + plugins: { + test: { + processors: { + txt: { + preprocess(text) { + return [text]; + }, + postprocess(messages) { + return messages[0]; + } + } + } + } + }, + processor: "test/txt", + rules: { + "no-console": 2, + "no-unused-vars": 2 + } + }, + { + files: ["**/*.txt/*.txt"] + } + ], + + 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 run processors when calling lintFiles with config file that specifies preloaded processor", async () => { + eslint = new FlatESLint({ + overrideConfigFile: true, + overrideConfig: [ + { + plugins: { + test: { + processors: { + txt: { + preprocess(text) { + return [text.replace("a()", "b()")]; + }, + postprocess(messages) { + messages[0][0].ruleId = "post-processed"; + return messages[0]; + } + } + } + } + }, + processor: "test/txt", + rules: { + "no-console": 2, + "no-unused-vars": 2 + } + }, + { + files: ["**/*.txt/*.txt"] + } + ], + 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 lintText with config file that specifies preloaded processor", async () => { + eslint = new FlatESLint({ + overrideConfigFile: true, + overrideConfig: [ + { + plugins: { + test: { + processors: { + txt: { + preprocess(text) { + return [text.replace("a()", "b()")]; + }, + postprocess(messages) { + messages[0][0].ruleId = "post-processed"; + return messages[0]; + } + } + } + } + }, + processor: "test/txt", + rules: { + "no-console": 2, + "no-unused-vars": 2 + } + }, + { + files: ["**/*.txt/*.txt"] + } + ], + 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 processor resolves same extension but different content correctly", async () => { + let count = 0; + + eslint = new FlatESLint({ + overrideConfigFile: true, + overrideConfig: [ + { + plugins: { + test: { + processors: { + txt: { + preprocess(text) { + count++; + return [ + { + + // it will be run twice, and text will be as-is at the second time, then it will not run third time + text: text.replace("a()", "b()"), + filename: ".txt" + } + ]; + }, + postprocess(messages) { + messages[0][0].ruleId = "post-processed"; + return messages[0]; + } + } + } + } + }, + processor: "test/txt" + }, + { + files: ["**/*.txt/*.txt"], + rules: { + "no-console": 2, + "no-unused-vars": 2 + } + } + ], + extensions: ["txt"], + ignore: false + }); + const results = await eslint.lintText("function a() {console.log(\"Test\");}", { filePath: "tests/fixtures/processors/test/test-processor.txt" }); + + assert.strictEqual(count, 2); + 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 FlatESLint({ + overrideConfigFile: true, + overrideConfig: { + files: ["**/*.html"], + plugins: { + test: { processors: { html: HTML_PROCESSOR } } + }, + processor: "test/html", + rules: { + semi: 2 + } + }, + ignore: false, + fix: true + }); + 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 FlatESLint({ + overrideConfigFile: true, + overrideConfig: { + files: ["**/*.html"], + plugins: { + test: { processors: { html: Object.assign({ supportsAutofix: true }, HTML_PROCESSOR) } } + }, + processor: "test/html", + rules: { + semi: 2 + } + }, + extensions: ["js", "txt"], + ignore: false + }); + 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 FlatESLint({ + cwd: getFixturePath("cli-engine"), + overrideConfigFile: true + }); + }); + + 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\/\*\*\/\*\.js' 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("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 = { + "markdown": { ...processor, supportsAutofix: true }, + "non-fixable": processor + }; + `, + "node_modules/eslint-plugin-html/index.js": ` + const { defineProcessor } = require("pattern-processor"); + const processor = defineProcessor(${/ + + \`\`\` + ` + }; + + // unique directory for each test to avoid quirky disk-cleanup errors + let id; + + beforeEach(() => (id = Date.now().toString())); + afterEach(async () => fsp.rmdir(root, { recursive: true, force: true })); + + it("should lint only JavaScript blocks if '--ext' was not given.", async () => { + const teardown = createCustomTeardown({ + cwd: path.join(root, id), + files: { + ...commonFiles, + "eslint.config.js": `module.exports = [ + { + plugins: { + markdown: require("eslint-plugin-markdown"), + html: require("eslint-plugin-html") + } + }, + { + files: ["**/*.js"], + rules: { semi: "error" } + }, + { + files: ["**/*.md"], + processor: "markdown/markdown" + } + ];` + } + }); + + await teardown.prepare(); + eslint = new FlatESLint({ cwd: teardown.getPath() }); + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1, "Should have one result."); + assert.strictEqual(results[0].messages.length, 1, "Should have one message."); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); + assert.strictEqual(results[0].messages[0].line, 2, "Message should be on line 2."); + }); + + it("should fix only JavaScript blocks if '--ext' was not given.", async () => { + const teardown = createCustomTeardown({ + cwd: path.join(root, id), + files: { + ...commonFiles, + "eslint.config.js": `module.exports = [ + { + plugins: { + markdown: require("eslint-plugin-markdown") + } + }, + { + files: ["**/*.js"], + rules: { semi: "error" } + }, + { + files: ["**/*.md"], + processor: "markdown/markdown" + } + ];` + } + }); + + await teardown.prepare(); + eslint = new FlatESLint({ cwd: teardown.getPath(), 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 represented in config.", async () => { + const teardown = createCustomTeardown({ + cwd: path.join(root, id), + files: { + ...commonFiles, + "eslint.config.js": `module.exports = [ + { + plugins: { + markdown: require("eslint-plugin-markdown"), + html: require("eslint-plugin-html") + } + }, + { + files: ["**/*.js"], + rules: { semi: "error" } + }, + { + files: ["**/*.md"], + processor: "markdown/markdown" + }, + { + files: ["**/*.html"], + processor: "html/html" + } + ];` + } + }); + + await teardown.prepare(); + eslint = new FlatESLint({ cwd: teardown.getPath(), extensions: ["js", "html"] }); + const results = await eslint.lintFiles(["test.md"]); + + assert.strictEqual(results.length, 1, "Should have one result."); + assert.strictEqual(results[0].messages.length, 2, "Should have two messages."); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); // JS block + assert.strictEqual(results[0].messages[0].line, 2, "First error should be on line 2"); + assert.strictEqual(results[0].messages[1].ruleId, "semi"); // JS block in HTML block + assert.strictEqual(results[0].messages[1].line, 7, "Second error should be on line 7."); + }); + + it("should fix HTML blocks as well with multiple processors if represented in config.", async () => { + const teardown = createCustomTeardown({ + cwd: path.join(root, id), + files: { + ...commonFiles, + "eslint.config.js": `module.exports = [ + { + plugins: { + markdown: require("eslint-plugin-markdown"), + html: require("eslint-plugin-html") + } + }, + { + files: ["**/*.js"], + rules: { semi: "error" } + }, + { + files: ["**/*.md"], + processor: "markdown/markdown" + }, + { + files: ["**/*.html"], + processor: "html/html" + } + ];` + } + }); + + await teardown.prepare(); + eslint = new FlatESLint({ cwd: teardown.getPath(), 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 the config '**/*.html/*.js' to lint JavaScript blocks in HTML.", async () => { + const teardown = createCustomTeardown({ + cwd: path.join(root, id), + files: { + ...commonFiles, + "eslint.config.js": `module.exports = [ + { + plugins: { + markdown: require("eslint-plugin-markdown"), + html: require("eslint-plugin-html") + } + }, + { + files: ["**/*.js"], + rules: { semi: "error" } + }, + { + files: ["**/*.md"], + processor: "markdown/markdown" + }, + { + files: ["**/*.html"], + processor: "html/html" + }, + { + files: ["**/*.html/*.js"], + rules: { + semi: "off", + "no-console": "error" + } + } + + ];` + + } + }); + + await teardown.prepare(); + eslint = new FlatESLint({ cwd: teardown.getPath(), 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 () => { + const teardown = createCustomTeardown({ + cwd: path.join(root, id), + files: { + ...commonFiles, + "eslint.config.js": `module.exports = [ + { + plugins: { + markdown: require("eslint-plugin-markdown"), + html: require("eslint-plugin-html") + }, + rules: { semi: "error" } + }, + { + files: ["**/*.md"], + processor: "markdown/markdown" + }, + { + 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" + } + } + + ];` + } + }); + + await teardown.prepare(); + eslint = new FlatESLint({ cwd: teardown.getPath(), 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 () => { + const teardown = createCustomTeardown({ + cwd: path.join(root, id), + files: { + ...commonFiles, + "eslint.config.js": `module.exports = [ + { + plugins: { + markdown: require("eslint-plugin-markdown"), + html: require("eslint-plugin-html") + } + }, + { + files: ["**/*.md"], + processor: "markdown/unknown" + } + + ];` + } + }); + + await teardown.prepare(); + eslint = new FlatESLint({ cwd: teardown.getPath() }); + + await assert.rejects(async () => { + await eslint.lintFiles(["test.md"]); + }, /Key "processor": Could not find "unknown" in plugin "markdown"/u); + }); + + }); + + describe("glob pattern '[ab].js'", () => { + const root = getFixturePath("cli-engine/unmatched-glob"); + + let cleanup; + + beforeEach(() => { + cleanup = () => { }; + }); + + afterEach(() => cleanup()); + + it("should match '[ab].js' if existed.", async () => { + + const teardown = createCustomTeardown({ + cwd: root, + files: { + "a.js": "", + "b.js": "", + "ab.js": "", + "[ab].js": "", + "eslint.config.js": "module.exports = [];" + } + }); + + await teardown.prepare(); + cleanup = teardown.cleanup; + + eslint = new FlatESLint({ cwd: teardown.getPath() }); + 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 () => { + const teardown = createCustomTeardown({ + cwd: root, + files: { + "a.js": "", + "b.js": "", + "ab.js": "", + "eslint.config.js": "module.exports = [];" + } + }); + + await teardown.prepare(); + cleanup = teardown.cleanup; + eslint = new FlatESLint({ cwd: teardown.getPath() }); + 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"); + + let cleanup; + + beforeEach(() => { + cleanup = () => { }; + }); + + afterEach(() => cleanup()); + + it("should warn directive comments if 'noInlineConfig' was given.", async () => { + const teardown = createCustomTeardown({ + cwd: root, + files: { + "test.js": "/* globals foo */", + "eslint.config.js": "module.exports = [{ linterOptions: { noInlineConfig: true } }];" + } + }); + + await teardown.prepare(); + cleanup = teardown.cleanup; + eslint = new FlatESLint({ cwd: teardown.getPath() }); + + 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."); + }); + + }); + + describe("with 'reportUnusedDisableDirectives' setting", () => { + const root = getFixturePath("cli-engine/reportUnusedDisableDirectives"); + + let cleanup; + + beforeEach(() => { + cleanup = () => { }; + }); + + afterEach(() => cleanup()); + + it("should warn unused 'eslint-disable' comments if 'reportUnusedDisableDirectives' was given.", async () => { + const teardown = createCustomTeardown({ + cwd: root, + files: { + "test.js": "/* eslint-disable eqeqeq */", + "eslint.config.js": "module.exports = { linterOptions: { reportUnusedDisableDirectives: true } }" + } + }); + + + await teardown.prepare(); + cleanup = teardown.cleanup; + eslint = new FlatESLint({ cwd: teardown.getPath() }); + + 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 () => { + const teardown = createCustomTeardown({ + cwd: root, + files: { + "test.js": "/* eslint-disable eqeqeq */", + ".eslintrc.yml": "reportUnusedDisableDirectives: true" + } + }); + + await teardown.prepare(); + cleanup = teardown.cleanup; + + eslint = new FlatESLint({ + cwd: teardown.getPath(), + 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 () => { + const teardown = createCustomTeardown({ + cwd: root, + files: { + "test.js": "/* eslint-disable eqeqeq */", + ".eslintrc.yml": "reportUnusedDisableDirectives: true" + } + }); + + await teardown.prepare(); + cleanup = teardown.cleanup; + + eslint = new FlatESLint({ + cwd: teardown.getPath(), + 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')."); + }); + }); + }); + + it("should throw if non-boolean value is given to 'options.warnIgnored' option", async () => { + eslint = new FlatESLint(); + 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("Fix Types", () => { + + let eslint; + + it("should throw an error when an invalid fix type is specified", () => { + assert.throws(() => { + eslint = new FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + fix: true, + fixTypes: ["layou"] + }); + }, /'fixTypes' must be an array of any of "directive", "problem", "suggestion", and "layout"\./iu); + }); + + it("should not fix any rules when fixTypes is used without fix", async () => { + eslint = new FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + 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 FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + 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 FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + 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 FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + 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); + }); + + }); + + describe("isPathIgnored", () => { + it("should check if the given path is ignored", async () => { + const engine = new FlatESLint({ + 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 FlatESLint({ + 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 FlatESLint({ + ignore: false, + cwd: getFixturePath("cli-engine") + }); + + assert(await engine.isPathIgnored("node_modules/foo.js")); + }); + + describe("about the default ignore patterns", () => { + it("should always apply default ignore patterns if ignore option is true", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new FlatESLint({ 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 default ignore patterns if ignore option is is false", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new FlatESLint({ 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 FlatESLint({ + cwd, + overrideConfigFile: true, + ignorePatterns: "!/node_modules/package" + }); + + const result = await engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules", "package", "file.js")); + + assert(!result, "File should not be ignored"); + }); + + it("should allow subfolders of defaultPatterns to be unignored by ignorePath", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new FlatESLint({ + cwd, + overrideConfigFile: true, + ignorePath: getFixturePath("ignored-paths", ".eslintignoreWithUnignoredDefaults") + }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "node_modules", "package", "file.js"))); + }); + + it("should ignore .git directory", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new FlatESLint({ cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", ".git/bar"))); + }); + + it("should still ignore .git directory when ignore option disabled", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new FlatESLint({ ignore: false, cwd }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", ".git/bar"))); + }); + + it("should not ignore absolute paths containing '..'", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new FlatESLint({ 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 FlatESLint({ 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 FlatESLint({ 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 FlatESLint({ 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 FlatESLint({ 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 FlatESLint({ cwd }); + + // `${cwd}/.eslintignore` includes `sampleignorepattern`. + assert(await engine.isPathIgnored("sampleignorepattern")); + }); + + }); + + describe("with ignorePatterns option", () => { + it("should accept a string for options.ignorePatterns", async () => { + const cwd = getFixturePath("ignored-paths", "ignore-pattern"); + const engine = new FlatESLint({ + ignorePatterns: ["ignore-me.txt"], + cwd + }); + + assert(await engine.isPathIgnored("ignore-me.txt")); + }); + + it("should accept an array for options.ignorePattern", async () => { + const engine = new FlatESLint({ + ignorePatterns: ["a.js", "b.js"], + overrideConfigFile: true + }); + + assert(await engine.isPathIgnored("a.js"), "a.js should be ignored"); + assert(await engine.isPathIgnored("b.js"), "b.js should be ignored"); + assert(!await engine.isPathIgnored("c.js"), "c.js should not be ignored"); + }); + + 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 FlatESLint({ + 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 FlatESLint({ 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 filePath = getFixturePath("ignored-paths", "subdir", "undef.js"); + const engine = new FlatESLint({ + ignorePatterns: ["/undef.js"], + overrideConfigFile: true, + cwd + }); + + assert(!await engine.isPathIgnored(filePath)); + }); + + it("should return true for file matching a child of an ignore pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new FlatESLint({ 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 FlatESLint({ 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 FlatESLint({ 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 FlatESLint({ + overrideConfigFile: true, + ignorePatterns: ["**/*.js"], + cwd + }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo.js")), "foo.js should be ignored"); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar.js")), "foo/bar.js should be ignored"); + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar/baz.js")), "foo/bar/baz.js"); + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "foo.cjs")), "foo.cjs should not be ignored"); + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar.cjs")), "foo/bar.cjs should not be ignored"); + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "foo/bar/baz.cjs")), "foo/bar/baz.cjs should not be ignored"); + }); + }); + + describe("with ignorePath 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 FlatESLint({ 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 FlatESLint({ 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 FlatESLint({ ignorePath, cwd }); + + assert(await engine.isPathIgnored("../custom-name/foo.js")); + }); + + it("missing ignore file should throw error", done => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "not-a-directory", ".foobaz"); + const engine = new FlatESLint({ ignorePath, cwd }); + + engine.isPathIgnored("foo.js").then(() => { + assert.fail("missing file should not succeed"); + }).catch(error => { + assert(/Cannot read ignore file/u.test(error)); + done(); + }); + }); + + 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 FlatESLint({ 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"); + + // /undef.js in ignore file + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreForDifferentCwd"); + const engine = new FlatESLint({ ignorePath, cwd, overrideConfigFile: true }); + + assert(await engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/undef.js")), "subdir/undef.js should be ignored"); + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "subdir/subdir/undef.js")), "subdir/subdir/undef.js should not be ignored"); + }); + + 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 FlatESLint({ 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 FlatESLint({ + ignorePath, + cwd, + overrideConfig: { + files: ["**/*.txt"] + } + }); + + assert(await engine.isPathIgnored("subdir/blah.txt"), "subdir/blah.txt should be ignore"); + assert(await engine.isPathIgnored("blah.txt"), "blah.txt should be ignored"); + assert(await engine.isPathIgnored("subdir/bar.txt"), "subdir/bar.txt should be ignored"); + assert(!await engine.isPathIgnored("bar.txt"), "bar.txt should not be ignored"); + assert(!await engine.isPathIgnored("baz.txt"), "baz.txt should not be ignored"); + assert(!await engine.isPathIgnored("subdir/baz.txt"), "subdir/baz.txt should not be ignored"); + }); + + 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 FlatESLint({ 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 FlatESLint({ 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.", "Ignore file must have CRLF for test to pass."); + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", "crlf/.eslintignore"); + const engine = new FlatESLint({ 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 ignore a non-negated pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const ignorePath = getFixturePath("ignored-paths", ".eslintignoreWithNegation"); + const engine = new FlatESLint({ 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 FlatESLint({ ignorePath, cwd }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "negation", "unignore.js"))); + }); + }); + + describe("with ignorePath option and ignorePatterns option", () => { + it("should return false for ignored file when unignored with ignore pattern", async () => { + const cwd = getFixturePath("ignored-paths"); + const engine = new FlatESLint({ + ignorePath: getFixturePath("ignored-paths", ".eslintignoreForNegationTest"), + ignorePatterns: ["!undef.js"], + cwd + }); + + assert(!await engine.isPathIgnored(getFixturePath("ignored-paths", "undef.js"))); + }); + }); + + it("should throw if non-string value is given to 'filePath' parameter", async () => { + const eslint = new FlatESLint(); + + 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 FlatESLint(); + 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 FlatESLint(); + 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 FlatESLint(); + 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 FlatESLint({ + 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 FlatESLint({ + 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 FlatESLint({ + 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 FlatESLint({ + 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 FlatESLint({ + 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 custom formatter doesn't exist", async () => { + const engine = new FlatESLint(); + 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 FlatESLint(); + 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}.js\nError: Cannot find module '${fullFormatterPath}.js'`), "u")); + }); + + it("should throw if the required formatter exists but has an error", async () => { + const engine = new FlatESLint(); + const formatterPath = getFixturePath("formatters", "broken.js"); + + await assert.rejects(async () => { + await engine.loadFormatter(formatterPath); + + // for some reason, the error here contains multiple "there was a problem loading formatter" lines, so omitting + }, new RegExp(escapeStringRegExp("Error: Cannot find module 'this-module-does-not-exist'"), "u")); + }); + + it("should throw if a non-string formatter name is passed", async () => { + const engine = new FlatESLint(); + + 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 FlatESLint({ + overrideConfigFile: true, + overrideConfig: { + rules: { + quotes: "error", + "no-var": "error", + "eol-last": "error", + "no-unused-vars": "error" + } + } + }); + const results = await engine.lintText("var foo = 'bar';"); + const errorResults = FlatESLint.getErrorResults(results); + + assert.strictEqual(errorResults[0].messages.length, 4, "messages.length is wrong"); + assert.strictEqual(errorResults[0].errorCount, 4, "errorCount is wrong"); + assert.strictEqual(errorResults[0].fixableErrorCount, 3, "fixableErrorCount is wrong"); + assert.strictEqual(errorResults[0].fixableWarningCount, 0, "fixableWarningCount is wrong"); + assert.strictEqual(errorResults[0].messages[0].ruleId, "no-var"); + assert.strictEqual(errorResults[0].messages[0].severity, 2); + assert.strictEqual(errorResults[0].messages[1].ruleId, "no-unused-vars"); + assert.strictEqual(errorResults[0].messages[1].severity, 2); + assert.strictEqual(errorResults[0].messages[2].ruleId, "quotes"); + assert.strictEqual(errorResults[0].messages[2].severity, 2); + assert.strictEqual(errorResults[0].messages[3].ruleId, "eol-last"); + assert.strictEqual(errorResults[0].messages[3].severity, 2); + }); + + it("should not mutate passed report parameter", async () => { + process.chdir(originalDir); + const engine = new FlatESLint({ + overrideConfigFile: true, + overrideConfig: { + rules: { quotes: [1, "double"] } + } + }); + const results = await engine.lintText("var foo = 'bar';"); + const reportResultsLength = results[0].messages.length; + + FlatESLint.getErrorResults(results); + + assert.strictEqual(results[0].messages.length, reportResultsLength); + }); + + it("should report a warningCount of 0 when looking for errors only", async () => { + const engine = new FlatESLint({ + overrideConfigFile: true, + overrideConfig: { + rules: { + strict: ["error", "global"], + quotes: "error", + "no-var": "error", + "eol-last": "error", + "no-unused-vars": "error" + } + } + }); + const lintResults = await engine.lintText("var foo = 'bar';"); + const errorResults = FlatESLint.getErrorResults(lintResults); + + 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 FlatESLint({ + overrideConfigFile: true, + 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 = FlatESLint.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 FlatESLint({ + overrideConfigFile: true, + overrideConfig: { + rules: { quotes: [2, "double"] } + } + }); + const results = await engine.lintText("var foo = 'bar';"); + const errorResults = FlatESLint.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 FlatESLint({ + overrideConfigFile: true, + fix: true, + overrideConfig: { + rules: { + semi: 2, + "no-console": 2 + } + } + }); + const results = await engine.lintText("console.log('foo')"); + const errorResults = FlatESLint.getErrorResults(results); + + assert.strictEqual(errorResults[0].messages.length, 1); + assert.strictEqual(errorResults[0].output, "console.log('foo');"); + }); + }); + + describe("getRulesMetaForResults()", () => { + + it("should throw an error when results were not created from this instance", async () => { + const engine = new FlatESLint({ + overrideConfigFile: true + }); + + assert.throws(() => { + engine.getRulesMetaForResults([ + { + filePath: "path/to/file.js", + messages: [ + { + ruleId: "curly", + severity: 2, + message: "Expected { after 'if' condition.", + line: 2, + column: 1, + nodeType: "IfStatement" + }, + { + ruleId: "no-process-exit", + severity: 2, + message: "Don't use process.exit(); throw an error instead.", + line: 3, + column: 1, + nodeType: "CallExpression" + } + ], + errorCount: 2, + warningCount: 0, + fatalErrorCount: 0, + fixableErrorCount: 0, + fixableWarningCount: 0, + source: + "var err = doStuff();\nif (err) console.log('failed tests: ' + err);\nprocess.exit(1);\n" + } + ]); + }, /Results object was not created from this ESLint instance/u); + }); + + it("should return empty object when there are no linting errors", async () => { + const engine = new FlatESLint({ + overrideConfigFile: true + }); + + const rulesMeta = engine.getRulesMetaForResults([]); + + assert.strictEqual(Object.keys(rulesMeta).length, 0); + }); + + it("should return one rule meta when there is a linting error", async () => { + const engine = new FlatESLint({ + overrideConfigFile: true, + overrideConfig: { + rules: { + semi: 2 + } + } + }); + + const results = await engine.lintText("a", { filePath: "foo.js" }); + const rulesMeta = engine.getRulesMetaForResults(results); + + assert.strictEqual(rulesMeta.semi, coreRules.get("semi").meta); + }); + + it("should return multiple rule meta when there are multiple linting errors", async () => { + const engine = new FlatESLint({ + overrideConfigFile: true, + overrideConfig: { + rules: { + semi: 2, + quotes: [2, "double"] + } + } + }); + + const results = await engine.lintText("'a'"); + const rulesMeta = engine.getRulesMetaForResults(results); + + assert.strictEqual(rulesMeta.semi, coreRules.get("semi").meta); + assert.strictEqual(rulesMeta.quotes, coreRules.get("quotes").meta); + }); + + it("should return multiple rule meta when there are multiple linting errors from a plugin", async () => { + const nodePlugin = require("eslint-plugin-n"); + const engine = new FlatESLint({ + overrideConfigFile: true, + overrideConfig: { + plugins: { + n: nodePlugin + }, + rules: { + "n/no-new-require": 2, + semi: 2, + quotes: [2, "double"] + } + } + }); + + const results = await engine.lintText("new require('hi')"); + const rulesMeta = engine.getRulesMetaForResults(results); + + assert.strictEqual(rulesMeta.semi, coreRules.get("semi").meta); + assert.strictEqual(rulesMeta.quotes, coreRules.get("quotes").meta); + assert.strictEqual( + rulesMeta["n/no-new-require"], + nodePlugin.rules["no-new-require"].meta + ); + }); + }); + + describe("outputFixes()", () => { + afterEach(() => { + sinon.verifyAndRestore(); + }); + + it("should call fs.writeFile() for each result with output", async () => { + const fakeFS = { + writeFile: sinon.spy(() => Promise.resolve()) + }; + const spy = fakeFS.writeFile; + const { FlatESLint: localESLint } = proxyquire("../../../lib/eslint/flat-eslint", { + fs: { + promises: fakeFS + } + }); + + 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"), "First call was incorrect."); + assert(spy.secondCall.calledWithExactly(path.resolve("bar.js"), "baz"), "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 = { + writeFile: sinon.spy(() => Promise.resolve()) + }; + const spy = fakeFS.writeFile; + const { FlatESLint: localESLint } = proxyquire("../../../lib/eslint/flat-eslint", { + fs: { + promises: fakeFS + } + }); + 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, "Call count was wrong"); + assert(spy.firstCall.calledWithExactly(path.resolve("foo.js"), "bar"), "First call was incorrect."); + assert(spy.secondCall.calledWithExactly(path.resolve("bar.js"), "baz"), "Second call was incorrect."); + }); + + it("should throw if non object array is given to 'results' parameter", async () => { + await assert.rejects(() => FlatESLint.outputFixes(null), /'results' must be an array/u); + await assert.rejects(() => FlatESLint.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, + overrideConfigFile: true, + allowInlineConfig: false, + overrideConfig: { + rules: { + "eol-last": 0, + "no-alert": 1, + "no-trailing-spaces": 0, + strict: 0, + quotes: 0 + } + } + }; + const eslintCLI = new FlatESLint(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, + overrideConfigFile: true, + allowInlineConfig: true, + overrideConfig: { + rules: { + "eol-last": 0, + "no-alert": 1, + "no-trailing-spaces": 0, + strict: 0, + quotes: 0 + } + } + }; + const eslintCLI = new FlatESLint(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 FlatESLint({ overrideConfigFile: true, 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, + fix: { + range: [0, 20], + text: " " + }, + severity: 2, + nodeType: null + } + ], + errorCount: 1, + warningCount: 0, + fatalErrorCount: 0, + fixableErrorCount: 1, + fixableWarningCount: 0, + source: "/* eslint-disable */", + usedDeprecatedRules: [] + } + ] + ); + }); + }); + + describe("when retrieving version number", () => { + it("should return current version number", () => { + const eslintCLI = require("../../../lib/eslint/flat-eslint").FlatESLint; + const version = eslintCLI.version; + + assert.strictEqual(typeof version, "string"); + assert(parseInt(version[0], 10) >= 3); + }); + }); + + describe("mutability", () => { + + describe("rules", () => { + it("Loading rules in one instance doesn't mutate to another instance", async () => { + const filePath = getFixturePath("single-quoted.js"); + const engine1 = new FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true, + overrideConfig: { + plugins: { + example: { + rules: { + "example-rule"() { + return {}; + } + } + } + }, + rules: { "example/example-rule": 1 } + } + }); + const engine2 = new FlatESLint({ + cwd: path.join(fixtureDir, ".."), + overrideConfigFile: true + }); + 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, void 0, "example is not present for engine 2"); + }); + }); + }); + + describe("with ignores config", () => { + const root = getFixturePath("cli-engine/ignore-patterns"); + + describe("ignores can add an ignore pattern ('foo.js').", () => { + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + "eslint.config.js": `module.exports = { + ignores: ["**/foo.js"] + };`, + "foo.js": "", + "bar.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + + 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 FlatESLint({ cwd: getPath() }); + + 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 FlatESLint({ cwd: getPath() }); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar.js"), + path.join(root, "eslint.config.js"), + path.join(root, "subdir/bar.js") + ]); + }); + }); + + describe("ignores can add ignore patterns ('**/foo.js', '/bar.js').", () => { + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root + Date.now(), + files: { + "eslint.config.js": `module.exports = { + ignores: ["**/foo.js", "bar.js"] + };`, + "foo.js": "", + "bar.js": "", + "baz.js": "", + "subdir/foo.js": "", + "subdir/bar.js": "", + "subdir/baz.js": "" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("'isPathIgnored()' should return 'true' for 'foo.js'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + + 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 FlatESLint({ cwd: getPath() }); + + 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 FlatESLint({ cwd: getPath() }); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(getPath(), "baz.js"), + path.join(getPath(), "eslint.config.js"), + path.join(getPath(), "subdir/bar.js"), + path.join(getPath(), "subdir/baz.js") + ]); + }); + }); + + + /* + * These tests fail due to a bug in fast-flob that doesn't allow + * negated patterns inside of ignores. These tests won't work until + * this bug is fixed: + * https://github.com/mrmlnc/fast-glob/issues/356 + */ + xdescribe("ignorePatterns can unignore '/node_modules/foo'.", () => { + + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + "eslint.config.js": `module.exports = { + ignores: ["!**/node_modules/foo/**"] + };`, + "node_modules/foo/index.js": "", + "node_modules/foo/.dot.js": "", + "node_modules/bar/index.js": "", + "foo.js": "" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("'isPathIgnored()' should return 'false' for 'node_modules/foo/index.js'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + + assert.strictEqual(await engine.isPathIgnored("node_modules/foo/index.js"), false); + }); + + it("'isPathIgnored()' should return 'false' for 'node_modules/foo/.dot.js'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + + assert.strictEqual(await engine.isPathIgnored("node_modules/foo/.dot.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for 'node_modules/bar/index.js'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + + assert.strictEqual(await engine.isPathIgnored("node_modules/bar/index.js"), true); + }); + + it("'lintFiles()' should verify 'node_modules/foo/index.js'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "eslint.config.js"), + path.join(root, "foo.js"), + path.join(root, "node_modules/foo/index.js") + ]); + }); + }); + + xdescribe(".eslintignore can re-ignore files that are unignored by ignorePatterns.", () => { + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + "eslint.config.js": `module.exports = ${JSON.stringify({ + ignores: ["!.*"] + })}`, + ".eslintignore": ".foo*", + ".foo.js": "", + ".bar.js": "" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("'isPathIgnored()' should return 'true' for re-ignored '.foo.js'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + + assert.strictEqual(await engine.isPathIgnored(".foo.js"), true); + }); + + it("'isPathIgnored()' should return 'false' for unignored '.bar.js'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + + assert.strictEqual(await engine.isPathIgnored(".bar.js"), false); + }); + + it("'lintFiles()' should not lint re-ignored '.foo.js'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, ".bar.js"), + path.join(root, "eslint.config.js") + ]); + }); + }); + + xdescribe(".eslintignore can unignore files that are ignored by ignorePatterns.", () => { + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + "eslint.config.js": `module.exports = ${JSON.stringify({ + ignores: ["**/*.js"] + })}`, + ".eslintignore": "!foo.js", + "foo.js": "", + "bar.js": "" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("'isPathIgnored()' should return 'false' for unignored 'foo.js'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), false); + }); + + it("'isPathIgnored()' should return 'true' for ignored 'bar.js'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + + assert.strictEqual(await engine.isPathIgnored("bar.js"), true); + }); + + it("'lintFiles()' should verify unignored 'foo.js'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo.js") + ]); + }); + }); + + describe("ignores in a config file should not be used if ignore: false.", () => { + + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root, + files: { + "eslint.config.js": `module.exports = { + ignores: ["*.js"] + }`, + "foo.js": "" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("'isPathIgnored()' should return 'false' for 'foo.js'.", async () => { + const engine = new FlatESLint({ cwd: getPath(), ignore: false }); + + assert.strictEqual(await engine.isPathIgnored("foo.js"), false); + }); + + it("'lintFiles()' should verify 'foo.js'.", async () => { + const engine = new FlatESLint({ cwd: getPath(), ignore: false }); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "eslint.config.js"), + path.join(root, "foo.js") + ]); + }); + }); + + }); + + describe("config.files adds lint targets", () => { + const root = getFixturePath("cli-engine/additional-lint-targets"); + + + describe("if { files: 'foo/*.txt', ignores: '**/ignore.txt' } is present,", () => { + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root + 1, + files: { + "eslint.config.js": `module.exports = [{ + files: ["foo/*.txt"], + ignores: ["**/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": "" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("'lintFiles()' with a directory path should contain 'foo/test.txt'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + const filePaths = (await engine.lintFiles(".")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(getPath(), "bar/test.js"), + path.join(getPath(), "eslint.config.js"), + path.join(getPath(), "foo/test.js"), + path.join(getPath(), "foo/test.txt"), + path.join(getPath(), "test.js") + ]); + }); + + it("'lintFiles()' with a glob pattern '*.js' should not contain 'foo/test.txt'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + const filePaths = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(getPath(), "bar/test.js"), + path.join(getPath(), "eslint.config.js"), + path.join(getPath(), "foo/test.js"), + path.join(getPath(), "test.js") + ]); + }); + }); + + describe("if { files: 'foo/*.txt', ignores: '**/ignore.txt' } is present and subdirectory is passed,", () => { + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root + 2, + files: { + "eslint.config.js": `module.exports = [{ + files: ["foo/*.txt"], + ignores: ["**/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": "" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("'lintFiles()' with a directory path should contain 'foo/test.txt'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + const filePaths = (await engine.lintFiles("foo")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(getPath(), "foo/test.js"), + path.join(getPath(), "foo/test.txt") + ]); + }); + + it("'lintFiles()' with a glob pattern '*.js' should not contain 'foo/test.txt'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + const filePaths = (await engine.lintFiles("foo/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(getPath(), "foo/test.js") + ]); + }); + }); + + describe("if { files: 'foo/**/*.txt' } is present,", () => { + + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root + 3, + files: { + "eslint.config.js": `module.exports = [ + { + files: ["foo/**/*.txt"] + } + ]`, + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "test.js": "", + "test.txt": "" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("'lintFiles()' with a directory path should contain 'foo/test.txt' and 'foo/nested/test.txt'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + const filePaths = (await engine.lintFiles(".")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(getPath(), "bar/test.js"), + path.join(getPath(), "eslint.config.js"), + path.join(getPath(), "foo/nested/test.txt"), + path.join(getPath(), "foo/test.js"), + path.join(getPath(), "foo/test.txt"), + path.join(getPath(), "test.js") + ]); + }); + }); + + describe("if { files: 'foo/**/*' } is present,", () => { + + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: root + 4, + files: { + "eslint.config.js": `module.exports = [ + { + files: ["foo/**/*"] + } + ]`, + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "test.js": "", + "test.txt": "" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("'lintFiles()' with a directory path should NOT contain 'foo/test.txt' and 'foo/nested/test.txt'.", async () => { + const engine = new FlatESLint({ cwd: getPath() }); + const filePaths = (await engine.lintFiles(".")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(getPath(), "bar/test.js"), + path.join(getPath(), "eslint.config.js"), + path.join(getPath(), "foo/test.js"), + path.join(getPath(), "test.js") + ]); + }); + }); + + }); + + describe("'ignores', 'files' of the configuration that the '--config' option provided should be resolved from CWD.", () => { + const root = getFixturePath("cli-engine/config-and-overrides-files"); + + describe("if { files: 'foo/*.txt', ... } is present by '--config node_modules/myconf/eslint.config.js',", () => { + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: `${root}a1`, + files: { + "node_modules/myconf/eslint.config.js": `module.exports = [ + { + files: ["foo/*.js"], + rules: { + eqeqeq: "error" + } + } + ];`, + "node_modules/myconf/foo/test.js": "a == b", + "foo/test.js": "a == b" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("'lintFiles()' with 'foo/test.js' should use the files entry.", async () => { + const engine = new FlatESLint({ + overrideConfigFile: "node_modules/myconf/eslint.config.js", + cwd: getPath(), + ignore: 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(getPath(), "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, + fatalErrorCount: 0 + } + ]); + }); + + it("'lintFiles()' with 'node_modules/myconf/foo/test.js' should NOT use the files entry.", async () => { + const engine = new FlatESLint({ + overrideConfigFile: "node_modules/myconf/eslint.config.js", + cwd: getPath(), + ignore: 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(getPath(), "node_modules/myconf/foo/test.js"), + fixableErrorCount: 0, + fixableWarningCount: 0, + messages: [ + { + fatal: false, + message: "File ignored by default. Use \"--ignore-pattern '!node_modules/*'\" to override.", + severity: 1 + } + ], + usedDeprecatedRules: [], + warningCount: 1, + fatalErrorCount: 0 + } + ]); + }); + }); + + describe("if { files: '*', ignores: 'foo/*.txt', ... } is present by '--config bar/myconf/eslint.config.js',", () => { + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: `${root}a2`, + files: { + "bar/myconf/eslint.config.js": `module.exports = [ + { + files: ["**/*"], + ignores: ["foo/*.js"], + rules: { + eqeqeq: "error" + } + } + ]`, + "bar/myconf/foo/test.js": "a == b", + "foo/test.js": "a == b" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("'lintFiles()' with 'foo/test.js' should have no errors because no rules are enabled.", async () => { + const engine = new FlatESLint({ + overrideConfigFile: "bar/myconf/eslint.config.js", + cwd: getPath(), + ignore: 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(getPath(), "foo/test.js"), + fixableErrorCount: 0, + fixableWarningCount: 0, + messages: [], + usedDeprecatedRules: [], + warningCount: 0, + fatalErrorCount: 0 + } + ]); + }); + + it("'lintFiles()' with 'bar/myconf/foo/test.js' should have an error because eqeqeq is enabled.", async () => { + const engine = new FlatESLint({ + overrideConfigFile: "bar/myconf/eslint.config.js", + cwd: getPath(), + ignore: false + }); + const results = await engine.lintFiles("bar/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(getPath(), "bar/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, + fatalErrorCount: 0 + } + ]); + }); + }); + + // dependent on https://github.com/mrmlnc/fast-glob/issues/86 + xdescribe("if { ignores: 'foo/*.js', ... } is present by '--config node_modules/myconf/eslint.config.js',", () => { + const { prepare, cleanup, getPath } = createCustomTeardown({ + cwd: `${root}a3`, + files: { + "node_modules/myconf/eslint.config.js": `module.exports = { + ignores: ["**/eslint.config.js", "!node_modules/myconf", "foo/*.js"], + rules: { + eqeqeq: "error" + } + }`, + "node_modules/myconf/foo/test.js": "a == b", + "foo/test.js": "a == b" + } + }); + + beforeEach(prepare); + afterEach(cleanup); + + it("'lintFiles()' with '**/*.js' should iterate 'node_modules/myconf/foo/test.js' but not 'foo/test.js'.", async () => { + const engine = new FlatESLint({ + overrideConfigFile: "node_modules/myconf/eslint.config.js", + cwd: getPath() + }); + const files = (await engine.lintFiles("**/*.js")) + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(files, [ + path.join(getPath(), "node_modules/myconf/foo/test.js") + ]); + }); + }); + }); + +}); diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 73acdf64578..407194d47e7 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -7147,7 +7147,7 @@ var a = "test2"; it("should not modify a parser error message without a leading line: prefix", () => { linter.defineParser("no-line-error", testParsers.noLineError); - const messages = linter.verify(";", { parser: "no-line-error" }, "filename"); + const messages = linter.verify(";", { parser: "no-line-error" }, filename); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); @@ -7950,7 +7950,7 @@ describe("Linter with FlatConfigArray", () => { languageOptions: { parser: testParsers.lineError } - }, "filename"); + }, filename); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); @@ -7965,7 +7965,7 @@ describe("Linter with FlatConfigArray", () => { languageOptions: { parser: testParsers.noLineError } - }, "filename"); + }, filename); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); @@ -8278,7 +8278,7 @@ describe("Linter with FlatConfigArray", () => { it("should report an error when JSX code is encountered and JSX is not enabled", () => { const code = "var myDivElement =
;"; - const messages = linter.verify(code, {}, "filename"); + const messages = linter.verify(code, {}, filename); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 1); @@ -8299,7 +8299,7 @@ describe("Linter with FlatConfigArray", () => { } } } - }, "filename"); + }, filename); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 0); @@ -8318,7 +8318,7 @@ describe("Linter with FlatConfigArray", () => { } } - }, "filename"); + }, "filename.js"); const suppressedMessages = linter.getSuppressedMessages(); assert.strictEqual(messages.length, 0); @@ -8618,6 +8618,23 @@ describe("Linter with FlatConfigArray", () => { assert.strictEqual(suppressedMessages.length, 0); }); + it("should report ignored file when filename isn't matched in the config array", () => { + + const code = "foo()\n alert('test')"; + const config = { rules: { "no-mixed-spaces-and-tabs": 1, "eol-last": 1, semi: [1, "always"] } }; + + const messages = linter.verify(code, config, "filename.ts"); + + assert.strictEqual(messages.length, 1); + assert.deepStrictEqual(messages[0], { + ruleId: null, + severity: 1, + message: "No matching configuration found for filename.ts.", + line: 0, + column: 0 + }); + }); + describe("Plugins", () => { it("should not load rule definition when rule isn't used", () => { diff --git a/tests/lib/unsupported-api.js b/tests/lib/unsupported-api.js index dd88be5e69b..53b466adf06 100644 --- a/tests/lib/unsupported-api.js +++ b/tests/lib/unsupported-api.js @@ -23,6 +23,14 @@ describe("unsupported-api", () => { assert.isFunction(api.FileEnumerator); }); + it("should have FlatESLint exposed", () => { + assert.isFunction(api.FlatESLint); + }); + + it("should have FlatRuleTester exposed", () => { + assert.isFunction(api.FlatRuleTester); + }); + it("should have builtinRules exposed", () => { assert.instanceOf(api.builtinRules, LazyLoadingRuleMap); });