Skip to content

Commit

Permalink
New: extends in glob-based config (fixes #8813) (#11554)
Browse files Browse the repository at this point in the history
  • Loading branch information
mysticatea committed May 24, 2019
1 parent ec105b2 commit 54e6eda
Show file tree
Hide file tree
Showing 7 changed files with 287 additions and 46 deletions.
68 changes: 37 additions & 31 deletions conf/config-schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,13 @@

const baseConfigProperties = {
env: { type: "object" },
extends: { $ref: "#/definitions/stringOrStrings" },
globals: { type: "object" },
overrides: {
type: "array",
items: { $ref: "#/definitions/overrideConfig" },
additionalItems: false
},
parser: { type: ["string", "null"] },
parserOptions: { type: "object" },
plugins: { type: "array" },
Expand All @@ -17,54 +23,54 @@ const baseConfigProperties = {
ecmaFeatures: { type: "object" } // deprecated; logs a warning when used
};

const overrideProperties = Object.assign(
{},
baseConfigProperties,
{
files: {
const configSchema = {
definitions: {
stringOrStrings: {
oneOf: [
{ type: "string" },
{
type: "array",
items: { type: "string" },
minItems: 1
additionalItems: false
}
]
},
excludedFiles: {
stringOrStringsRequired: {
oneOf: [
{ type: "string" },
{
type: "array",
items: { type: "string" }
items: { type: "string" },
additionalItems: false,
minItems: 1
}
]
}
}
);
},

// Config at top-level.
objectConfig: {
type: "object",
properties: {
root: { type: "boolean" },
...baseConfigProperties
},
additionalProperties: false
},

const topLevelConfigProperties = Object.assign(
{},
baseConfigProperties,
{
extends: { type: ["string", "array"] },
root: { type: "boolean" },
overrides: {
type: "array",
items: {
type: "object",
properties: overrideProperties,
required: ["files"],
additionalProperties: false
}
// Config in `overrides`.
overrideConfig: {
type: "object",
properties: {
excludedFiles: { $ref: "#/definitions/stringOrStrings" },
files: { $ref: "#/definitions/stringOrStringsRequired" },
...baseConfigProperties
},
required: ["files"],
additionalProperties: false
}
}
);
},

const configSchema = {
type: "object",
properties: topLevelConfigProperties,
additionalProperties: false
$ref: "#/definitions/objectConfig"
};

module.exports = configSchema;
5 changes: 3 additions & 2 deletions docs/user-guide/configuring.md
Original file line number Diff line number Diff line change
Expand Up @@ -775,10 +775,11 @@ module.exports = {

### How it works

* Glob pattern overrides can only be configured within config files (`.eslintrc.*` or `package.json`).
* The patterns are applied against the file path relative to the directory of the config file. For example, if your config file has the path `/Users/john/workspace/any-project/.eslintrc.js` and the file you want to lint has the path `/Users/john/workspace/any-project/lib/util.js`, then the pattern provided in `.eslintrc.js` will be executed against the relative path `lib/util.js`.
* Glob pattern overrides have higher precedence than the regular configuration in the same config file. Multiple overrides within the same config are applied in order. That is, the last override block in a config file always has the highest precedence.
* A glob specific configuration works almost the same as any other ESLint config. Override blocks can contain any configuration options that are valid in a regular config, with the exception of `extends`, `overrides`, and `root`.
* A glob specific configuration works almost the same as any other ESLint config. Override blocks can contain any configuration options that are valid in a regular config, with the exception of `root`.
* A glob specific configuration can have `extends` setting, but the `root` property in the extended configs is ignored.
* Nested `overrides` setting will be applied only if the glob patterns of both of the parent config and the child config matched. This is the same when the extended configs have `overrides` setting.
* Multiple glob patterns can be provided within a single override block. A file must match at least one of the supplied patterns for the configuration to apply.
* Override blocks can also specify patterns to exclude from matches. If a file matches any of the excluded patterns, the configuration won't apply.

Expand Down
8 changes: 8 additions & 0 deletions lib/cli-engine/config-array-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -500,6 +500,14 @@ class ConfigArrayFactory {
*/
element.criteria = OverrideTester.and(criteria, element.criteria);

/*
* Remove `root` property to ignore `root` settings which came from
* `extends` in `overrides`.
*/
if (element.criteria) {
element.root = void 0;
}

yield element;
}
}
Expand Down
2 changes: 2 additions & 0 deletions lib/shared/types.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,10 @@ module.exports = {};
* @typedef {Object} OverrideConfigData
* @property {Record<string, boolean>} [env] The environment settings.
* @property {string | string[]} [excludedFiles] The glob pattarns for excluded files.
* @property {string | string[]} [extends] The path to other config files or the package name of shareable configs.
* @property {string | string[]} files The glob pattarns for target files.
* @property {Record<string, GlobalConf>} [globals] The global variable settings.
* @property {OverrideConfigData[]} [overrides] The override settings per kind of files.
* @property {string} [parser] The path to a parser or the package name of a parser.
* @property {ParserOptions} [parserOptions] The parser options.
* @property {string[]} [plugins] The plugin specifiers.
Expand Down
217 changes: 217 additions & 0 deletions tests/lib/cli-engine/config-array-factory.js
Original file line number Diff line number Diff line change
Expand Up @@ -1162,6 +1162,28 @@ describe("ConfigArrayFactory", () => {
const { ConfigArrayFactory } = defineConfigArrayFactoryWithInMemoryFileSystem({
cwd: () => tempDir,
files: {
"node_modules/eslint-config-foo/index.js": `
module.exports = {
rules: { eqeqeq: "error" }
}
`,
"node_modules/eslint-config-has-overrides/index.js": `
module.exports = {
rules: { eqeqeq: "error" },
overrides: [
{
files: ["**/foo/**/*.js"],
rules: { eqeqeq: "off" }
}
]
}
`,
"node_modules/eslint-config-root/index.js": `
module.exports = {
root: true,
rules: { eqeqeq: "error" }
}
`
}
});

Expand Down Expand Up @@ -1207,6 +1229,201 @@ describe("ConfigArrayFactory", () => {
});
});
});

describe("if a config in 'overrides' property had 'extends' property, the returned value", () => {
let configArray;

beforeEach(() => {
configArray = create({
rules: { regular: 1 },
overrides: [
{
files: "*.xxx",
extends: "foo",
rules: { override: 1 }
}
]
});
});

it("should have three elements.", () => {
assert.strictEqual(configArray.length, 3);
});

it("should have the given config data at the first element.", () => {
assertConfigArrayElement(configArray[0], {
rules: { regular: 1 }
});
});

it("should have the config data of 'overrides[0] » eslint-config-foo' at the second element.", () => {
assertConfigArrayElement(configArray[1], {
name: "#overrides[0] » eslint-config-foo",
filePath: path.join(tempDir, "node_modules/eslint-config-foo/index.js"),
criteria: OverrideTester.create(["*.xxx"], [], tempDir),
rules: { eqeqeq: "error" }
});
});

it("should have the config data of 'overrides[0]' at the third element.", () => {
assertConfigArrayElement(configArray[2], {
name: "#overrides[0]",
criteria: OverrideTester.create(["*.xxx"], [], tempDir),
rules: { override: 1 }
});
});
});

describe("if a config in 'overrides' property had 'extends' property and the shareable config has 'overrides' property, the returned value", () => {
let configArray;

beforeEach(() => {
configArray = create({
rules: { regular: 1 },
overrides: [
{
files: "*.xxx",
extends: "has-overrides",
rules: { override: 1 }
}
]
});
});

it("should have four elements.", () => {
assert.strictEqual(configArray.length, 4);
});

it("should have the given config data at the first element.", () => {
assertConfigArrayElement(configArray[0], {
rules: { regular: 1 }
});
});

it("should have the config data of 'overrides[0] » eslint-config-has-overrides' at the second element.", () => {
assertConfigArrayElement(configArray[1], {
name: "#overrides[0] » eslint-config-has-overrides",
filePath: path.join(tempDir, "node_modules/eslint-config-has-overrides/index.js"),
criteria: OverrideTester.create(["*.xxx"], [], tempDir),
rules: { eqeqeq: "error" }
});
});

it("should have the config data of 'overrides[0] » eslint-config-has-overrides#overrides[0]' at the third element.", () => {
assertConfigArrayElement(configArray[2], {
name: "#overrides[0] » eslint-config-has-overrides#overrides[0]",
filePath: path.join(tempDir, "node_modules/eslint-config-has-overrides/index.js"),
criteria: OverrideTester.and(
OverrideTester.create(["*.xxx"], [], tempDir),
OverrideTester.create(["**/foo/**/*.js"], [], tempDir)
),
rules: { eqeqeq: "off" }
});
});

it("should have the config data of 'overrides[0]' at the fourth element.", () => {
assertConfigArrayElement(configArray[3], {
name: "#overrides[0]",
criteria: OverrideTester.create(["*.xxx"], [], tempDir),
rules: { override: 1 }
});
});
});

describe("if a config in 'overrides' property had 'overrides' property, the returned value", () => {
let configArray;

beforeEach(() => {
configArray = create({
rules: { regular: 1 },
overrides: [
{
files: "*.xxx",
rules: { override: 1 },
overrides: [
{
files: "*.yyy",
rules: { override: 2 }
}
]
}
]
});
});

it("should have three elements.", () => {
assert.strictEqual(configArray.length, 3);
});

it("should have the given config data at the first element.", () => {
assertConfigArrayElement(configArray[0], {
rules: { regular: 1 }
});
});

it("should have the config data of 'overrides[0]' at the second element.", () => {
assertConfigArrayElement(configArray[1], {
name: "#overrides[0]",
criteria: OverrideTester.create(["*.xxx"], [], tempDir),
rules: { override: 1 }
});
});

it("should have the config data of 'overrides[0].overrides[0]' at the third element.", () => {
assertConfigArrayElement(configArray[2], {
name: "#overrides[0]#overrides[0]",
criteria: OverrideTester.and(
OverrideTester.create(["*.xxx"], [], tempDir),
OverrideTester.create(["*.yyy"], [], tempDir)
),
rules: { override: 2 }
});
});
});

describe("if a config in 'overrides' property had 'root' property, the returned value", () => {
let configArray;

beforeEach(() => {
configArray = create({
rules: { regular: 1 },
overrides: [
{
files: "*.xxx",
extends: "root",
rules: { override: 1 }
}
]
});
});

it("should have three elements.", () => {
assert.strictEqual(configArray.length, 3);
});

it("should have the given config data at the first element.", () => {
assertConfigArrayElement(configArray[0], {
rules: { regular: 1 }
});
});

it("should have the config data of 'overrides[0] » eslint-config-root' at the second element; it doesn't have 'root' property.", () => {
assertConfigArrayElement(configArray[1], {
name: "#overrides[0] » eslint-config-root",
filePath: path.join(tempDir, "node_modules/eslint-config-root/index.js"),
criteria: OverrideTester.create(["*.xxx"], [], tempDir),
rules: { eqeqeq: "error" }
});
});

it("should have the config data of 'overrides[0]' at the third element.", () => {
assertConfigArrayElement(configArray[2], {
name: "#overrides[0]",
criteria: OverrideTester.create(["*.xxx"], [], tempDir),
rules: { override: 1 }
});
});
});
});

describe("additional plugin pool", () => {
Expand Down
Loading

0 comments on commit 54e6eda

Please sign in to comment.