diff --git a/conf/default-cli-options.js b/conf/default-cli-options.js index 0f7b377fb36..e09a829d17c 100644 --- a/conf/default-cli-options.js +++ b/conf/default-cli-options.js @@ -12,7 +12,7 @@ module.exports = { useEslintrc: true, envs: [], globals: [], - extensions: [".js"], + extensions: null, ignore: true, ignorePath: void 0, cache: false, diff --git a/docs/user-guide/command-line-interface.md b/docs/user-guide/command-line-interface.md index 89f42744eb3..9a6e43a74e8 100644 --- a/docs/user-guide/command-line-interface.md +++ b/docs/user-guide/command-line-interface.md @@ -127,19 +127,19 @@ Examples: #### `--ext` -This option allows you to specify which file extensions ESLint will use when searching for JavaScript files in the directories you specify. -By default, it uses `.js` as the only file extension. +This option allows you to specify which file extensions ESLint will use when searching for target files in the directories you specify. +By default, ESLint lints `*.js` files and the files that match the `overrides` entries of your configuration. Examples: - # Use only .js2 extension - eslint . --ext .js2 + # Use only .ts extension + eslint . --ext .ts - # Use both .js and .js2 - eslint . --ext .js --ext .js2 + # Use both .js and .ts + eslint . --ext .js --ext .ts - # Also use both .js and .js2 - eslint . --ext .js,.js2 + # Also use both .js and .ts + eslint . --ext .js,.ts **Note:** `--ext` is only used when the arguments are directories. If you use glob patterns or file names, then `--ext` is ignored. diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md index 93cd5f4c882..deb25484fc7 100644 --- a/docs/user-guide/configuring.md +++ b/docs/user-guide/configuring.md @@ -125,7 +125,7 @@ Processors may make named code blocks such as `0.js` and `1.js`. ESLint handles } ``` -ESLint checks the file extension of named code blocks then ignores those if [`--ext` CLI option](../user-guide/command-line-interface.md#--ext) didn't include the file extension. Be sure to specify the `--ext` option if you wanted to lint named code blocks other than `*.js`. +ESLint checks the file path of named code blocks then ignores those if any `overrides` entry didn't match the file path. Be sure to make `overrides` entry if you wanted to lint named code blocks other than `*.js`. ## Specifying Environments @@ -990,6 +990,12 @@ In your `.eslintrc.json`: } ``` +### Specifying Target Files to Lint + +If you specified directories with CLI (e.g., `eslint lib`), ESLint searches target files in the directory to lint. The target files are `*.js` or the files that match any of `overrides` entries (but exclude entries that are any of `files` end with `*`). + +If you specified the [`--ext`](./command-line-interface#ext) command line option along with directories, the target files are only the files that have specified file extensions regardless of `overrides` entries. + ## Comments in Configuration Files Both the JSON and YAML configuration file formats support comments (`package.json` files should not include them). You can use JavaScript-style comments or YAML-style comments in either type of file and ESLint will safely ignore them. This allows your configuration files to be more human-friendly. For example: @@ -1007,10 +1013,6 @@ Both the JSON and YAML configuration file formats support comments (`package.jso } ``` -## Specifying File extensions to Lint - -Currently the sole method for telling ESLint which file extensions to lint is by specifying a comma separated list of extensions using the [`--ext`](./command-line-interface#ext) command line option. Note this flag only takes effect in conjunction with directories, and will be ignored if used with filenames or glob patterns. - ## Ignoring Files and Directories ### `ignorePatterns` in config files diff --git a/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index ca03bec0638..f91dea4c448 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -106,6 +106,7 @@ function createBaseConfigArray({ */ if (rulePaths && rulePaths.length > 0) { baseConfigArray.push({ + type: "config", name: "--rulesdir", filePath: "", plugins: { diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index 22336a91de2..6a0352399b7 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -57,7 +57,7 @@ const validFixTypes = new Set(["problem", "suggestion", "layout"]); * @property {string} configFile The configuration file to use. * @property {string} cwd The value to use for the current working directory. * @property {string[]} envs An array of environments to load. - * @property {string[]} extensions An array of file extensions to check. + * @property {string[]|null} extensions An array of file extensions to check. * @property {boolean|Function} fix Execute in autofix mode. If a function, should return a boolean. * @property {string[]} fixTypes Array of rule types to apply fixes for. * @property {string[]} globals An array of global variables to declare. @@ -201,7 +201,7 @@ function calculateStatsPerRun(results) { * @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 {RegExp} config.extensionRegExp The `RegExp` object that tests if a file path has the allowed file extensions. + * @param {FileEnumerator} config.fileEnumerator The file enumerator to check if a path is a target or not. * @param {Linter} config.linter The linter instance to verify. * @returns {LintResult} The result of linting. * @private @@ -214,7 +214,7 @@ function verifyText({ fix, allowInlineConfig, reportUnusedDisableDirectives, - extensionRegExp, + fileEnumerator, linter }) { const filePath = providedFilePath || ""; @@ -238,13 +238,11 @@ function verifyText({ /** * Check if the linter should adopt a given code block or not. - * Currently, the linter adopts code blocks if the name matches `--ext` option. - * In the future, `overrides` in the configuration would affect the adoption (https://github.com/eslint/rfcs/pull/20). * @param {string} blockFilename The virtual filename of a code block. * @returns {boolean} `true` if the linter should adopt the code block. */ filterCodeBlock(blockFilename) { - return extensionRegExp.test(blockFilename); + return fileEnumerator.isTargetPath(blockFilename); } } ); @@ -704,7 +702,7 @@ class CLIEngine { return patterns.filter(Boolean); } - const extensions = options.extensions.map(ext => ext.replace(/^\./u, "")); + const extensions = (options.extensions || [".js"]).map(ext => ext.replace(/^\./u, "")); const dirSuffix = `/**/*.{${extensions.join(",")}}`; return patterns.filter(Boolean).map(pathname => { @@ -803,7 +801,7 @@ class CLIEngine { fix, allowInlineConfig, reportUnusedDisableDirectives, - extensionRegExp: fileEnumerator.extensionRegExp, + fileEnumerator, linter }); @@ -891,7 +889,7 @@ class CLIEngine { fix, allowInlineConfig, reportUnusedDisableDirectives, - extensionRegExp: fileEnumerator.extensionRegExp, + fileEnumerator, linter })); } diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index 2f6595355eb..997a7e15318 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -565,7 +565,7 @@ class ConfigArrayFactory { */ *_normalizeESLintIgnoreData(ignorePatterns, filePath, name) { const elements = this._normalizeObjectConfigData( - { ignorePatterns }, + { type: "ignore", ignorePatterns }, filePath, name ); @@ -667,6 +667,7 @@ class ConfigArrayFactory { root, rules, settings, + type = "config", overrides: overrideList = [] }, filePath, @@ -698,6 +699,7 @@ class ConfigArrayFactory { yield { // Debug information. + type, name, filePath, @@ -1047,6 +1049,7 @@ class ConfigArrayFactory { if (processorId.startsWith(".")) { yield* this._normalizeObjectConfigData( { + type: "implicit-processor", files: [`*${processorId}`], processor: `${pluginId}/${processorId}` }, diff --git a/lib/cli-engine/config-array/config-array.js b/lib/cli-engine/config-array/config-array.js index 4fae8deaca1..02f73b43e65 100644 --- a/lib/cli-engine/config-array/config-array.js +++ b/lib/cli-engine/config-array/config-array.js @@ -454,6 +454,25 @@ class ConfigArray extends Array { return cache.get(cacheKey); } + + /** + * Check if a given path is an additional lint target. + * @param {string} filePath The absolute path to the target file. + * @returns {boolean} `true` if the file is an additional lint target. + */ + isAdditionalTargetPath(filePath) { + for (const { criteria, type } of this) { + if ( + type === "config" && + criteria && + !criteria.endsWithWildcard && + criteria.test(filePath) + ) { + return true; + } + } + return false; + } } const exportObject = { diff --git a/lib/cli-engine/config-array/override-tester.js b/lib/cli-engine/config-array/override-tester.js index 67c8a6ed8a0..e7ba1202f1c 100644 --- a/lib/cli-engine/config-array/override-tester.js +++ b/lib/cli-engine/config-array/override-tester.js @@ -96,14 +96,22 @@ class OverrideTester { static create(files, excludedFiles, basePath) { const includePatterns = normalizePatterns(files); const excludePatterns = normalizePatterns(excludedFiles); - const allPatterns = includePatterns.concat(excludePatterns); + let endsWithWildcard = false; - if (allPatterns.length === 0) { + if (includePatterns.length === 0) { return null; } // Rejects absolute paths or relative paths to parents. - for (const pattern of allPatterns) { + for (const pattern of includePatterns) { + if (path.isAbsolute(pattern) || pattern.includes("..")) { + throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`); + } + if (pattern.endsWith("*")) { + endsWithWildcard = true; + } + } + for (const pattern of excludePatterns) { if (path.isAbsolute(pattern) || pattern.includes("..")) { throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`); } @@ -112,7 +120,11 @@ class OverrideTester { const includes = toMatcher(includePatterns); const excludes = toMatcher(excludePatterns); - return new OverrideTester([{ includes, excludes }], basePath); + return new OverrideTester( + [{ includes, excludes }], + basePath, + endsWithWildcard + ); } /** @@ -125,28 +137,44 @@ class OverrideTester { */ static and(a, b) { if (!b) { - return a && new OverrideTester(a.patterns, a.basePath); + return a && new OverrideTester( + a.patterns, + a.basePath, + a.endsWithWildcard + ); } if (!a) { - return new OverrideTester(b.patterns, b.basePath); + return new OverrideTester( + b.patterns, + b.basePath, + b.endsWithWildcard + ); } assert.strictEqual(a.basePath, b.basePath); - return new OverrideTester(a.patterns.concat(b.patterns), a.basePath); + return new OverrideTester( + a.patterns.concat(b.patterns), + a.basePath, + a.endsWithWildcard || b.endsWithWildcard + ); } /** * Initialize this instance. * @param {Pattern[]} patterns The matchers. * @param {string} basePath The base path. + * @param {boolean} endsWithWildcard If `true` then a pattern ends with `*`. */ - constructor(patterns, basePath) { + constructor(patterns, basePath, endsWithWildcard = false) { /** @type {Pattern[]} */ this.patterns = patterns; /** @type {string} */ this.basePath = basePath; + + /** @type {boolean} */ + this.endsWithWildcard = endsWithWildcard; } /** diff --git a/lib/cli-engine/file-enumerator.js b/lib/cli-engine/file-enumerator.js index f6f4987ea02..16e5182ad8a 100644 --- a/lib/cli-engine/file-enumerator.js +++ b/lib/cli-engine/file-enumerator.js @@ -88,7 +88,7 @@ const IGNORED = 2; * @typedef {Object} FileEnumeratorInternalSlots * @property {CascadingConfigArrayFactory} configArrayFactory The factory for config arrays. * @property {string} cwd The base directory to start lookup. - * @property {RegExp} extensionRegExp The RegExp to test if a string ends with specific file extensions. + * @property {RegExp|null} extensionRegExp The RegExp to test if a string ends with specific file extensions. * @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} ignoreFlag The flag to check ignored files. * @property {(filePath:string, dot:boolean) => boolean} defaultIgnores The default predicate function to ignore files. @@ -142,6 +142,27 @@ function readdirSafeSync(directoryPath) { } } +/** + * Create a `RegExp` object to detect extensions. + * @param {string[] | null} extensions The extensions to create. + * @returns {RegExp | null} The created `RegExp` object or null. + */ +function createExtensionRegExp(extensions) { + if (extensions) { + const normalizedExts = extensions.map(ext => escapeRegExp( + ext.startsWith(".") + ? ext.slice(1) + : ext + )); + + return new RegExp( + `.\\.(?:${normalizedExts.join("|")})$`, + "u" + ); + } + return null; +} + /** * The error type when no files match a glob. */ @@ -188,7 +209,7 @@ class FileEnumerator { constructor({ cwd = process.cwd(), configArrayFactory = new CascadingConfigArrayFactory({ cwd }), - extensions = [".js"], + extensions = null, globInputPaths = true, errorOnUnmatchedPattern = true, ignore = true @@ -197,17 +218,7 @@ class FileEnumerator { configArrayFactory, cwd, defaultIgnores: IgnorePattern.createDefaultIgnore(cwd), - extensionRegExp: new RegExp( - `.\\.(?:${extensions - .map(ext => escapeRegExp( - ext.startsWith(".") - ? ext.slice(1) - : ext - )) - .join("|") - })$`, - "u" - ), + extensionRegExp: createExtensionRegExp(extensions), globInputPaths, errorOnUnmatchedPattern, ignoreFlag: ignore @@ -215,11 +226,36 @@ class FileEnumerator { } /** - * The `RegExp` object that tests if a file path has the allowed file extensions. - * @type {RegExp} + * Check if a given file is target or not. + * @param {string} filePath The path to a candidate file. + * @param {ConfigArray} [providedConfig] Optional. The configuration for the file. + * @returns {boolean} `true` if the file is a target. */ - get extensionRegExp() { - return internalSlotsMap.get(this).extensionRegExp; + isTargetPath(filePath, providedConfig) { + const { + configArrayFactory, + extensionRegExp + } = internalSlotsMap.get(this); + + // If `--ext` option is present, use it. + if (extensionRegExp) { + return extensionRegExp.test(filePath); + } + + // `.js` file is target by default. + if (filePath.endsWith(".js")) { + return true; + } + + // use `overrides[].files` to check additional targets. + const config = + providedConfig || + configArrayFactory.getConfigArrayForFile( + filePath, + { ignoreNotFoundError: true } + ); + + return config.isAdditionalTargetPath(filePath); } /** @@ -380,7 +416,7 @@ class FileEnumerator { */ *_iterateFilesRecursive(directoryPath, options) { debug(`Enter the directory: ${directoryPath}`); - const { configArrayFactory, extensionRegExp } = internalSlotsMap.get(this); + const { configArrayFactory } = internalSlotsMap.get(this); /** @type {ConfigArray|null} */ let config = null; @@ -403,17 +439,18 @@ class FileEnumerator { { ignoreNotFoundError: true } ); } - const ignored = this._isIgnoredFile(filePath, { ...options, config }); - const flag = ignored ? IGNORED_SILENTLY : NONE; const matched = options.selector // Started with a glob pattern; choose by the pattern. ? options.selector.match(filePath) // Started with a directory path; choose by file extensions. - : extensionRegExp.test(filePath); + : this.isTargetPath(filePath, config); if (matched) { + const ignored = this._isIgnoredFile(filePath, { ...options, config }); + const flag = ignored ? IGNORED_SILENTLY : NONE; + debug(`Yield: ${entry.name}${ignored ? " but ignored" : ""}`); yield { config: configArrayFactory.getConfigArrayForFile(filePath), diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index 910200be395..03a0a79f6ae 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -3329,6 +3329,39 @@ describe("CLIEngine", () => { engine.executeOnFiles(["test.md"]); }, /ESLint configuration of processor in '\.eslintrc\.json' is invalid: 'markdown\/unknown' was not found\./u); }); + + it("should lint HTML blocks as well with multiple processors if 'overrides[].files' is present.", () => { + CLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + ...commonFiles, + ".eslintrc.json": JSON.stringify({ + plugins: ["markdown", "html"], + rules: { semi: "error" }, + overrides: [ + { + files: "*.html", + processor: "html/.html" + }, + { + files: "*.md", + processor: "markdown/.md" + } + ] + }) + } + }).CLIEngine; + engine = new CLIEngine({ cwd: root }); + + const { results } = engine.executeOnFiles(["test.md"]); + + assert.strictEqual(results.length, 1); + assert.strictEqual(results[0].messages.length, 2); + assert.strictEqual(results[0].messages[0].ruleId, "semi"); // JS block + assert.strictEqual(results[0].messages[0].line, 2); + assert.strictEqual(results[0].messages[1].ruleId, "semi"); // JS block in HTML block + assert.strictEqual(results[0].messages[1].line, 7); + }); }); describe("MODULE_NOT_FOUND error handling", () => { @@ -5451,4 +5484,232 @@ describe("CLIEngine", () => { }); }); + + describe("'overrides[].files' adds lint targets", () => { + const root = getFixturePath("cli-engine/additional-lint-targets"); + let InMemoryCLIEngine; + + describe("if { files: 'foo/*.txt', excludedFiles: '**/ignore.txt' } is present,", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + overrides: [ + { + files: "foo/*.txt", + excludedFiles: "**/ignore.txt" + } + ] + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "foo/ignore.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "bar/ignore.txt": "", + "test.js": "", + "test.txt": "", + "ignore.txt": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' with a directory path should contain 'foo/test.txt'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles(".") + .results + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "test.js") + ]); + }); + + it("'executeOnFiles()' with a glob pattern '*.js' should not contain 'foo/test.txt'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles("**/*.js") + .results + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/test.js"), + path.join(root, "test.js") + ]); + }); + }); + + describe("if { files: 'foo/**/*.txt' } is present,", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + overrides: [ + { + files: "foo/**/*.txt" + } + ] + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "test.js": "", + "test.txt": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' with a directory path should contain 'foo/test.txt' and 'foo/nested/test.txt'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles(".") + .results + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/nested/test.txt"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "test.js") + ]); + }); + }); + + describe("if { files: 'foo/**/*' } is present,", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + ".eslintrc.json": JSON.stringify({ + overrides: [ + { + files: "foo/**/*" + } + ] + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "test.js": "", + "test.txt": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' with a directory path should NOT contain 'foo/test.txt' and 'foo/nested/test.txt'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles(".") + .results + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/test.js"), + path.join(root, "test.js") + ]); + }); + }); + + describe("if { files: 'foo/**/*.txt' } is present in a shareable config,", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-config-foo/index.js": `module.exports = ${JSON.stringify({ + overrides: [ + { + files: "foo/**/*.txt" + } + ] + })}`, + ".eslintrc.json": JSON.stringify({ + extends: "foo" + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "test.js": "", + "test.txt": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' with a directory path should contain 'foo/test.txt' and 'foo/nested/test.txt'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles(".") + .results + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/nested/test.txt"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "test.js") + ]); + }); + }); + + describe("if { files: 'foo/**/*.txt' } is present in a plugin config,", () => { + beforeEach(() => { + InMemoryCLIEngine = defineCLIEngineWithInMemoryFileSystem({ + cwd: () => root, + files: { + "node_modules/eslint-plugin-foo/index.js": `exports.configs = ${JSON.stringify({ + bar: { + overrides: [ + { + files: "foo/**/*.txt" + } + ] + } + })}`, + ".eslintrc.json": JSON.stringify({ + extends: "plugin:foo/bar" + }), + "foo/nested/test.txt": "", + "foo/test.js": "", + "foo/test.txt": "", + "bar/test.js": "", + "bar/test.txt": "", + "test.js": "", + "test.txt": "" + } + }).CLIEngine; + }); + + it("'executeOnFiles()' with a directory path should contain 'foo/test.txt' and 'foo/nested/test.txt'.", () => { + const engine = new InMemoryCLIEngine(); + const filePaths = engine.executeOnFiles(".") + .results + .map(r => r.filePath) + .sort(); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "bar/test.js"), + path.join(root, "foo/nested/test.txt"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "test.js") + ]); + }); + }); + }); }); diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js index 1fb5661fb4d..15823ca6799 100644 --- a/tests/lib/cli-engine/config-array-factory.js +++ b/tests/lib/cli-engine/config-array-factory.js @@ -40,6 +40,7 @@ function assertConfigArrayElement(actual, providedExpected) { root: void 0, rules: void 0, settings: void 0, + type: "config", ...providedExpected };