From 164079d1fc7e07796e332af38302135d061f0c32 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 11 Nov 2019 18:33:57 +0800 Subject: [PATCH 1/4] New: support specifying extensions in the config (fixes #10828) --- conf/config-schema.js | 1 + lib/cli-engine/cli-engine.js | 7 ++- lib/cli-engine/config-array-factory.js | 2 + lib/cli-engine/config-array/config-array.js | 2 + .../config-array/extracted-config.js | 6 +++ lib/cli-engine/file-enumerator.js | 40 ++++++++------ lib/shared/types.js | 1 + tests/fixtures/configurations/extensions.json | 3 ++ tests/fixtures/extensions/extensions-js.js | 1 + tests/fixtures/extensions/extensions-ts.ts | 1 + tests/lib/cli-engine/cli-engine.js | 26 ++++++++- tests/lib/cli-engine/config-array-factory.js | 19 +++++++ .../cli-engine/config-array/config-array.js | 7 +++ .../config-array/extracted-config.js | 4 ++ tests/lib/cli-engine/file-enumerator.js | 53 +++++++++++++++++++ 15 files changed, 154 insertions(+), 19 deletions(-) create mode 100644 tests/fixtures/configurations/extensions.json create mode 100644 tests/fixtures/extensions/extensions-js.js create mode 100644 tests/fixtures/extensions/extensions-ts.ts diff --git a/conf/config-schema.js b/conf/config-schema.js index 164f0b4219f..52026fbf2b9 100644 --- a/conf/config-schema.js +++ b/conf/config-schema.js @@ -8,6 +8,7 @@ const baseConfigProperties = { env: { type: "object" }, extends: { $ref: "#/definitions/stringOrStrings" }, + extensions: { type: "array", items: "string" }, globals: { type: "object" }, overrides: { type: "array", diff --git a/lib/cli-engine/cli-engine.js b/lib/cli-engine/cli-engine.js index 8afd262708f..c049e72a786 100644 --- a/lib/cli-engine/cli-engine.js +++ b/lib/cli-engine/cli-engine.js @@ -789,7 +789,7 @@ class CLIEngine { fix, allowInlineConfig, reportUnusedDisableDirectives, - extensionRegExp: fileEnumerator.extensionRegExp, + extensionRegExp: fileEnumerator.getExtensionRegExp(config, filePath), linter }); @@ -878,7 +878,10 @@ class CLIEngine { fix, allowInlineConfig, reportUnusedDisableDirectives, - extensionRegExp: fileEnumerator.extensionRegExp, + extensionRegExp: fileEnumerator.getExtensionRegExp( + config, + resolvedFilename || path.join(cwd, "__placeholder__.js") + ), linter })); } diff --git a/lib/cli-engine/config-array-factory.js b/lib/cli-engine/config-array-factory.js index cf529b6ee63..401b2aa22f0 100644 --- a/lib/cli-engine/config-array-factory.js +++ b/lib/cli-engine/config-array-factory.js @@ -525,6 +525,7 @@ class ConfigArrayFactory { { env, extends: extend, + extensions, globals, noInlineConfig, parser: parserName, @@ -568,6 +569,7 @@ class ConfigArrayFactory { // Config data. criteria: null, env, + extensions, globals, noInlineConfig, parser, diff --git a/lib/cli-engine/config-array/config-array.js b/lib/cli-engine/config-array/config-array.js index 089ff305a2b..873fb878108 100644 --- a/lib/cli-engine/config-array/config-array.js +++ b/lib/cli-engine/config-array/config-array.js @@ -53,6 +53,7 @@ const { ExtractedConfig } = require("./extracted-config"); * @property {string} filePath The path to the source file of this config element. * @property {InstanceType|null} criteria The tester for the `files` and `excludedFiles` of this config element. * @property {Record|undefined} env The environment settings. + * @property {string[]|undefined} extensions The extensions to match files for directory patterns. * @property {Record|undefined} globals The global variable settings. * @property {boolean|undefined} noInlineConfig The flag that disables directive comments. * @property {DependentParser|undefined} parser The parser loader. @@ -262,6 +263,7 @@ function createConfig(instance, indices) { // Merge others. mergeWithoutOverwrite(config.env, element.env); + config.extensions = config.extensions.concat(element.extensions || []); mergeWithoutOverwrite(config.globals, element.globals); mergeWithoutOverwrite(config.parserOptions, element.parserOptions); mergeWithoutOverwrite(config.settings, element.settings); diff --git a/lib/cli-engine/config-array/extracted-config.js b/lib/cli-engine/config-array/extracted-config.js index 66858313ba6..1e4ea621674 100644 --- a/lib/cli-engine/config-array/extracted-config.js +++ b/lib/cli-engine/config-array/extracted-config.js @@ -41,6 +41,12 @@ class ExtractedConfig { */ this.env = {}; + /** + * Extensions to match files for directory patterns. + * @type {string[]} + */ + this.extensions = []; + /** * Global variables. * @type {Record} diff --git a/lib/cli-engine/file-enumerator.js b/lib/cli-engine/file-enumerator.js index 38f55de039d..a626955c5ba 100644 --- a/lib/cli-engine/file-enumerator.js +++ b/lib/cli-engine/file-enumerator.js @@ -89,7 +89,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 {string[]} baseExtensions The array to check 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 {IgnoredPaths} ignoredPathsWithDotfiles The ignored paths but don't include dot files. @@ -198,17 +198,7 @@ class FileEnumerator { internalSlotsMap.set(this, { configArrayFactory, cwd, - extensionRegExp: new RegExp( - `.\\.(?:${extensions - .map(ext => escapeRegExp( - ext.startsWith(".") - ? ext.slice(1) - : ext - )) - .join("|") - })$`, - "u" - ), + baseExtensions: extensions, globInputPaths, ignoreFlag: ignore, ignoredPaths, @@ -221,10 +211,27 @@ class FileEnumerator { /** * The `RegExp` object that tests if a file path has the allowed file extensions. - * @type {RegExp} + * @param {ConfigArray} [configArray] ESLint configuration. + * @param {string} [filePath] The absolute path to the target file. + * @returns {RegExp} The RegExp to test if a string ends with specific file extensions. */ - get extensionRegExp() { - return internalSlotsMap.get(this).extensionRegExp; + getExtensionRegExp(configArray, filePath) { + let extensions = internalSlotsMap.get(this).baseExtensions; + + if (configArray) { + const config = configArray.extractConfig(filePath); + + extensions = extensions.concat(config.extensions || []); + } + + // Use set to deduplicate. + const exts = new Set( + extensions + .filter(Boolean) + .map(ext => escapeRegExp(ext.startsWith(".") ? ext.slice(1) : ext)) + ); + + return new RegExp(`.\\.${Array.from(exts).join("|")}$`, "u"); } /** @@ -386,7 +393,7 @@ class FileEnumerator { return; } debug(`Enter the directory: ${directoryPath}`); - const { configArrayFactory, extensionRegExp } = internalSlotsMap.get(this); + const { configArrayFactory } = internalSlotsMap.get(this); /** @type {ConfigArray|null} */ let config = null; @@ -401,6 +408,7 @@ class FileEnumerator { if (!config) { config = configArrayFactory.getConfigArrayForFile(filePath); } + const extensionRegExp = this.getExtensionRegExp(config, filePath); const ignored = this._isIgnoredFile(filePath, options); const flag = ignored ? IGNORED_SILENTLY : NONE; const matched = options.selector diff --git a/lib/shared/types.js b/lib/shared/types.js index 12bd0aed8c6..6faad20d508 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -29,6 +29,7 @@ module.exports = {}; * @typedef {Object} ConfigData * @property {Record} [env] The environment settings. * @property {string | string[]} [extends] The path to other config files or the package name of shareable configs. + * @property {string[]} [extensions] The array of filename extensions that should be checked for code. * @property {Record} [globals] The global variable settings. * @property {boolean} [noInlineConfig] The flag that disables directive comments. * @property {OverrideConfigData[]} [overrides] The override settings per kind of files. diff --git a/tests/fixtures/configurations/extensions.json b/tests/fixtures/configurations/extensions.json new file mode 100644 index 00000000000..180151af1e2 --- /dev/null +++ b/tests/fixtures/configurations/extensions.json @@ -0,0 +1,3 @@ +{ + "extensions": ["ts"] +} diff --git a/tests/fixtures/extensions/extensions-js.js b/tests/fixtures/extensions/extensions-js.js new file mode 100644 index 00000000000..581311da634 --- /dev/null +++ b/tests/fixtures/extensions/extensions-js.js @@ -0,0 +1 @@ +function fn() {} diff --git a/tests/fixtures/extensions/extensions-ts.ts b/tests/fixtures/extensions/extensions-ts.ts new file mode 100644 index 00000000000..581311da634 --- /dev/null +++ b/tests/fixtures/extensions/extensions-ts.ts @@ -0,0 +1 @@ +function fn() {} diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index 2cdc81301bf..af1f790e7c9 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -633,7 +633,6 @@ describe("CLIEngine", () => { assert.strictEqual(report.results[0].source, "var foo = 'bar'"); }); - it("should not return a `source` property when no errors or warnings are present", () => { engine = new CLIEngine({ useEslintrc: false, @@ -753,6 +752,7 @@ describe("CLIEngine", () => { assert.strictEqual(report.messages[0].message, "OK"); }); }); + it("should warn when deprecated rules are found in a config", () => { engine = new CLIEngine({ cwd: originalDir, @@ -1157,6 +1157,30 @@ describe("CLIEngine", () => { assert.strictEqual(report.results[0].messages.length, 0); }); + it("should process files with specifying extensions in configuration file", () => { + + engine = new CLIEngine({ + cwd: path.join(fixtureDir, "extensions"), + configFile: getFixturePath("configurations", "extensions.json") + }); + + let report = engine.executeOnFiles(["."]); + + assert.lengthOf(report.results, 2); + assert.lengthOf(report.results[0].messages, 0); + assert.lengthOf(report.results[1].messages, 0); + + engine = new CLIEngine({ + cwd: path.join(fixtureDir, "extensions"), + configFile: getFixturePath("configurations", "es6.json") + }); + + report = engine.executeOnFiles(["."]); + + assert.lengthOf(report.results, 1); + assert.lengthOf(report.results[0].messages, 0); + }); + it("should return zero messages when given a config with environment set to browser", () => { engine = new CLIEngine({ diff --git a/tests/lib/cli-engine/config-array-factory.js b/tests/lib/cli-engine/config-array-factory.js index 8b86b0f84ff..8eed8ea3c7d 100644 --- a/tests/lib/cli-engine/config-array-factory.js +++ b/tests/lib/cli-engine/config-array-factory.js @@ -29,6 +29,7 @@ function assertConfigArrayElement(actual, providedExpected) { filePath: "", criteria: null, env: void 0, + extensions: void 0, globals: void 0, noInlineConfig: void 0, parser: void 0, @@ -54,6 +55,7 @@ function assertConfigArrayElement(actual, providedExpected) { function assertConfig(actual, providedExpected) { const expected = { env: {}, + extensions: [], globals: {}, noInlineConfig: void 0, parser: null, @@ -461,6 +463,23 @@ describe("ConfigArrayFactory", () => { }); }); + describe("if the config data had 'extensions' property, the returned value", () => { + const extensions = ["ts"]; + let configArray; + + beforeEach(() => { + configArray = create({ extensions }); + }); + + it("should have an element.", () => { + assert.strictEqual(configArray.length, 1); + }); + + it("should have the 'extensions' value in the element.", () => { + assertConfigArrayElement(configArray[0], { extensions }); + }); + }); + describe("if the config data had 'globals' property, the returned value", () => { const globals = { window: "readonly" }; let configArray; diff --git a/tests/lib/cli-engine/config-array/config-array.js b/tests/lib/cli-engine/config-array/config-array.js index 1d4c34708d1..33f9923677f 100644 --- a/tests/lib/cli-engine/config-array/config-array.js +++ b/tests/lib/cli-engine/config-array/config-array.js @@ -424,6 +424,7 @@ describe("ConfigArray", () => { assert.deepStrictEqual(result, { configNameOfNoInlineConfig: "", env: {}, + extensions: [], globals: {}, noInlineConfig: void 0, parser: null, @@ -456,6 +457,7 @@ describe("ConfigArray", () => { assert.deepStrictEqual(result, { configNameOfNoInlineConfig: "", env: {}, + extensions: [], globals: {}, noInlineConfig: void 0, parser: null, @@ -550,6 +552,7 @@ describe("ConfigArray", () => { ecmaFeatures: { jsx: true } }, env: { browser: true }, + extensions: ["ts"], globals: { foo: false } }, { @@ -563,6 +566,7 @@ describe("ConfigArray", () => { ecmaFeatures: { globalReturn: true } }, env: { browser: false }, + extensions: ["vue"], globals: { foo: true } } ]; @@ -582,6 +586,7 @@ describe("ConfigArray", () => { env: { browser: false }, + extensions: ["vue", "ts"], globals: { foo: true }, @@ -623,6 +628,7 @@ describe("ConfigArray", () => { ecmaFeatures: { jsx: true } }, env: { browser: true }, + extensions: ["ts"], globals: { foo: false } }); assert.deepStrictEqual(config[1], { @@ -636,6 +642,7 @@ describe("ConfigArray", () => { ecmaFeatures: { globalReturn: true } }, env: { browser: false }, + extensions: ["vue"], globals: { foo: true } }); }); diff --git a/tests/lib/cli-engine/config-array/extracted-config.js b/tests/lib/cli-engine/config-array/extracted-config.js index 9d700477b5e..7b3a0be6d03 100644 --- a/tests/lib/cli-engine/config-array/extracted-config.js +++ b/tests/lib/cli-engine/config-array/extracted-config.js @@ -21,6 +21,10 @@ describe("'ExtractedConfig' class", () => { assert.deepStrictEqual(config.env, {}); }); + it("should have 'extensions' property.", () => { + assert.deepStrictEqual(config.extensions, []); + }); + it("should have 'globals' property.", () => { assert.deepStrictEqual(config.globals, {}); }); diff --git a/tests/lib/cli-engine/file-enumerator.js b/tests/lib/cli-engine/file-enumerator.js index d379ac520d6..78bc182a565 100644 --- a/tests/lib/cli-engine/file-enumerator.js +++ b/tests/lib/cli-engine/file-enumerator.js @@ -9,6 +9,7 @@ const path = require("path"); const os = require("os"); const { assert } = require("chai"); const sh = require("shelljs"); +const { ConfigArrayFactory } = require("../../../lib/cli-engine/config-array-factory"); const { CascadingConfigArrayFactory } = require("../../../lib/cli-engine/cascading-config-array-factory"); const { IgnoredPaths } = require("../../../lib/cli-engine/ignored-paths"); @@ -476,4 +477,56 @@ describe("FileEnumerator", () => { }); }); }); + + describe("'getExtensionRegExp()' method should return correct regex", () => { + const { FileEnumerator } = require("../../../lib/cli-engine/file-enumerator"); + + /** + * Generate extension regex with reading from configuration. + * @param {import("../../../lib/shared/types").ConfigData} [config] ESLint configuration. + * @returns {RegExp} The RegExp to test if a string ends with specific file extensions. + */ + function readFromConfig(config = {}) { + const configArrayFactory = new ConfigArrayFactory(); + const filePath = path.join(process.cwd(), "foo.js"); + const configArray = configArrayFactory.create(config, { filePath }); + const fileEnumerator = new FileEnumerator(); + + return fileEnumerator.getExtensionRegExp(configArray, filePath); + } + + it("should return default regex without configuration", () => { + const fileEnumerator = new FileEnumerator(); + const regex = fileEnumerator.getExtensionRegExp(); + + assert.match("foo.js", regex); + assert.notMatch("foo.ts", regex); + }); + + it("should read from configuration", () => { + const regex = readFromConfig({ extensions: ["ts"] }); + + assert.match("foo.js", regex); + assert.match("foo.ts", regex); + }); + + it("should deduplicate extensions", () => { + const regex = readFromConfig({ extensions: ["js", "js"] }); + + assert.strictEqual(regex.source, ".\\.js$"); + }); + + it("should strip prefixed dot", () => { + const regex = readFromConfig({ extensions: [".ts"] }); + + assert.strictEqual(regex.source, ".\\.js|ts$"); + }); + + it("should allow missing in configuration", () => { + const regex = readFromConfig(); + + assert.match("foo.js", regex); + assert.notMatch("foo.ts", regex); + }); + }); }); From 51a3f4b19e8993f4381b8dca9a0e6b37db724a32 Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 11 Nov 2019 19:03:07 +0800 Subject: [PATCH 2/4] Docs: add usage --- docs/user-guide/configuring.md | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md index e68294205c1..95d017572e3 100644 --- a/docs/user-guide/configuring.md +++ b/docs/user-guide/configuring.md @@ -10,6 +10,7 @@ ESLint is designed to be completely configurable, meaning you can turn off every There are several pieces of information that can be configured: * **Environments** - which environments your script is designed to run in. Each environment brings with it a certain set of predefined global variables. +* **Extensions** - files with specified extensions will be linted. * **Globals** - the additional global variables your script accesses during execution. * **Rules** - which rules are enabled and at what error level. @@ -129,6 +130,24 @@ 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`. +## Specifying Extensions + +By default, ESLint only lints files whose names end with `.js`. If you want to let ESLint lint files whose names end with other extensions, you can specify it by passing an argument at CLI with `--ext`. + +However, sometimes you may want this behavior can be defined in configuration file, which will be able to be shared so you don't need to specify it at CLI. + +Adding `extensions` property in configuration file will let ESLint lint files whose names end with those extensions without specifying it at CLI: + +```json +{ + "extensions": ["ts"] +} +``` + +Note that you don't need to add prefix to extension with dot (`.`). + +With the configuration above, files whose names end with `.js` or `.ts` will be linted. + ## Specifying Environments An environment defines global variables that are predefined. The available environments are: From 2a89af3b97fa9e5ce6e04c55257940f0c0c42f9a Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 11 Nov 2019 22:49:42 +0800 Subject: [PATCH 3/4] Update: make sure .eslintignore be over this feature --- docs/user-guide/configuring.md | 2 ++ tests/lib/cli-engine/cli-engine.js | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md index 95d017572e3..a9ae009609f 100644 --- a/docs/user-guide/configuring.md +++ b/docs/user-guide/configuring.md @@ -136,6 +136,8 @@ By default, ESLint only lints files whose names end with `.js`. If you want to l However, sometimes you may want this behavior can be defined in configuration file, which will be able to be shared so you don't need to specify it at CLI. +The ignoring configuration (`.eslintignore`) precedences this configuration. If `.eslintignore` contains the additional target files, those files will still be ignored. + Adding `extensions` property in configuration file will let ESLint lint files whose names end with those extensions without specifying it at CLI: ```json diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index af1f790e7c9..40119622060 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -1181,6 +1181,20 @@ describe("CLIEngine", () => { assert.lengthOf(report.results[0].messages, 0); }); + it("should ignoring configuration precedences extensions", () => { + + engine = new CLIEngine({ + cwd: path.join(fixtureDir, "extensions"), + configFile: getFixturePath("configurations", "extensions.json"), + ignorePattern: "*.ts" + }); + + const report = engine.executeOnFiles(["."]); + + assert.lengthOf(report.results, 1); + assert.lengthOf(report.results[0].messages, 0); + }); + it("should return zero messages when given a config with environment set to browser", () => { engine = new CLIEngine({ From 2316fc43c9d722430f0ad9e14771e62b4ef1574e Mon Sep 17 00:00:00 2001 From: Pig Fang Date: Mon, 11 Nov 2019 23:00:16 +0800 Subject: [PATCH 4/4] Chore: add more tests --- tests/lib/cli-engine/cli-engine.js | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/lib/cli-engine/cli-engine.js b/tests/lib/cli-engine/cli-engine.js index 40119622060..adf7b0e6d71 100644 --- a/tests/lib/cli-engine/cli-engine.js +++ b/tests/lib/cli-engine/cli-engine.js @@ -1195,6 +1195,22 @@ describe("CLIEngine", () => { assert.lengthOf(report.results[0].messages, 0); }); + it("should treat glob patterns over extensions defined in configuration", () => { + + engine = new CLIEngine({ + cwd: path.join(fixtureDir, "extensions"), + + // This config doesn't specify about extensions. + configFile: getFixturePath("configurations", "es6.json") + }); + + const report = engine.executeOnFiles(["*.*"]); + + assert.lengthOf(report.results, 2); + assert.lengthOf(report.results[0].messages, 0); + assert.lengthOf(report.results[1].messages, 0); + }); + it("should return zero messages when given a config with environment set to browser", () => { engine = new CLIEngine({