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/lib/cli-engine/cascading-config-array-factory.js b/lib/cli-engine/cascading-config-array-factory.js index 52703a873bb..66c56d92fbc 100644 --- a/lib/cli-engine/cascading-config-array-factory.js +++ b/lib/cli-engine/cascading-config-array-factory.js @@ -105,6 +105,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 0b1c76bac68..949bbdadace 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); } } ); @@ -703,7 +701,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 => { @@ -802,7 +800,7 @@ class CLIEngine { fix, allowInlineConfig, reportUnusedDisableDirectives, - extensionRegExp: fileEnumerator.extensionRegExp, + fileEnumerator, linter }); @@ -890,7 +888,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 c444031bcb0..38f1d4bfddd 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -540,7 +540,7 @@ class ConfigArrayFactory { */ *_normalizeESLintIgnoreData(ignorePatterns, filePath, name) { const elements = this._normalizeObjectConfigData( - { ignorePatterns }, + { type: "ignore", ignorePatterns }, filePath, name ); @@ -642,6 +642,7 @@ class ConfigArrayFactory { root, rules, settings, + type = "config", overrides: overrideList = [] }, filePath, @@ -673,6 +674,7 @@ class ConfigArrayFactory { yield { // Debug information. + type, name, filePath, @@ -1022,6 +1024,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 b5a082b71a6..fa3e834ff73 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, ignore = true } = {}) { @@ -196,28 +217,43 @@ 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, ignoreFlag: ignore }); } /** - * 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); } /** @@ -376,7 +412,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; @@ -400,17 +436,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: ${filename}${ignored ? " but ignored" : ""}`); yield { config: configArrayFactory.getConfigArrayForFile(filePath), diff --git a/tests/lib/_utils.js b/tests/lib/_utils.js index 2ac119fb732..b8d38ab80b5 100644 --- a/tests/lib/_utils.js +++ b/tests/lib/_utils.js @@ -55,6 +55,26 @@ function supportMkdirRecursiveOption(fs, cwd) { }; } +/** + * Suppress the error of `existsSync` bug in metro. + * @param {import("fs")} fs The in-memory file system. + * @returns {void} + */ +function suppressExistsSyncBug(fs) { + const { existsSync } = fs; + + fs.existsSync = (...args) => { + try { + return existsSync(...args); + } catch (error) { + if (error.code === "ENOTDIR") { + return false; + } + throw error; + } + }; +} + /** * Define in-memory file system. * @param {Object} options The options. @@ -82,6 +102,7 @@ function defineInMemoryFs({ } supportMkdirRecursiveOption(fs, cwd); + suppressExistsSyncBug(fs); fs.mkdirSync(cwd(), { recursive: true }); /* diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index 1c2411c9776..e53008613c3 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", () => { @@ -5404,4 +5437,214 @@ 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); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "bar/test.js"), + 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); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo/test.js"), + path.join(root, "bar/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); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo/nested/test.txt"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "bar/test.js"), + 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); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo/test.js"), + path.join(root, "bar/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); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo/nested/test.txt"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "bar/test.js"), + 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); + + assert.deepStrictEqual(filePaths, [ + path.join(root, "foo/nested/test.txt"), + path.join(root, "foo/test.js"), + path.join(root, "foo/test.txt"), + path.join(root, "bar/test.js"), + 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 4f3ade1e60a..6b5003d8ed3 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 };