Repository: eslint/eslint
Pull request #8081 (2017-02-15 00:18:53)

New: Add `overrides` and `files` configuration options (refs #3611)
**What is the purpose of this pull request? (put an "X" next to item)**

- [x] Documentation update
- [x] Other, please explain:


The ability to override config values based on glob pattern matching relative to the config path. #3611

**What changes did you make? (Give an overview)**

Starting from @CrabDude's work in #7177, I rebased the branch against the latest master, addressed several comments from the original PR, rewrote and expanded many of the JSDoc comments, tweaked documentation, and split as much logic as I could from `lib/config.js` into `lib/config/config-cache.js`, `lib/config/config-file.js`, and `lib/config/config-ops.js`.


**Is there anything you'd like reviewers to focus on?**

Validating the approach -- does the separation of logic make sense? Is this still on the right track? Any edge cases that need tests?

jsf-clabot: [![CLA assistant check](https://cla.js.foundation/pull/badge/not_signed)](https://cla.js.foundation/eslint/eslint?pullRequest=8081) <br/>Thank you for your submission, we really appreciate it. Like many open source projects, we ask that you sign our [Contributor License Agreement](https://cla.js.foundation/eslint/eslint?pullRequest=8081) before we can accept your contribution.<br/>
eslintbot: LGTM
mention-bot: @smably, thanks for your PR! By analyzing the history of the files in this pull request, we identified @gyandeeps, @nzakas and @sethmcl to be potential reviewers.
not-an-aardvark: Thanks! Sorry, looks like we lost track of this PR.

To make this easier to review, how difficult would it be to split up @CrabDude's commit and your improvements? It's currently a bit difficult to identify which parts are different from https://github.com/eslint/eslint/pull/7177.

Also, does this implement the changes that @nzakas requested in https://github.com/eslint/eslint/pull/7177#pullrequestreview-1184437?
smably: Hi @not-an-aardvark, no worries. Unfortunately it would be pretty difficult to split up, because I had to rewrite large parts of @CrabDude's work to ES6-ify it (the diff was pretty much useless because the indentation changed when I moved everything inside a class). I can see about trying to split it up, but I think it might be best to review this from scratch as the changes are pretty substantial (particularly when it comes to the caches). Let me know what you think.

I'll add some comments on @nzakas' review on the original PR. Sorry I couldn't maintain the original PR -- not sure if there's a way to do that on a PR from a personal fork.

If there is still potential for this to get merged, I'll definitely see if I can rebase and fix the conflict later today.
eslintbot: LGTM
smably: Added comments to the original PR and fixed the merge conflict.
eslintbot: Thanks for the pull request, @smably! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
smably: @not-an-aardvark Turned out I had I still had the branch with the unsquashed version of this locally. I've updated this PR with the contents of the unsquashed branch. The first commit is the existing work, with some updates to make it mergeable but no functional changes. The subsequent commits represent the new stuff I've changed or added.
not-an-aardvark: Thanks!

I'm planning to review this sometime in the next day or two. I'm not very familiar with the part of the codebase that this changes, so I'm hoping someone else can also review it.
eslintbot: Thanks for the pull request, @smably! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
ilyavolodin: Performance numbers look pretty good (although we are not testing for anything with overrides):

Before:

```
Single File:
  CPU Speed is 3500 with multiplier 13000000
  Performance Run #1:  5168.491679ms
  Performance Run #2:  3631.0344720000003ms
  Performance Run #3:  3644.98604ms
  Performance Run #4:  3630.521594ms
  Performance Run #5:  3626.206455ms

  Performance budget ok:  3631.0344720000003ms (limit: 3714.285714285714ms)


Multi Files (0 files):
  CPU Speed is 3500 with multiplier 39000000
  Performance Run #1:  13425.494453ms
  Performance Run #2:  13795.64753ms
  Performance Run #3:  13960.317346ms
  Performance Run #4:  13530.801789ms
  Performance Run #5:  13425.006736ms

  Performance budget exceeded: 13530.801789ms (limit: 11142.857142857143ms)
```

After:

```
Single File:
  CPU Speed is 3500 with multiplier 13000000
  Performance Run #1:  3705.140804ms
  Performance Run #2:  3714.937271ms
  Performance Run #3:  3817.76126ms
  Performance Run #4:  3948.772472ms
  Performance Run #5:  3707.20636ms

  Performance budget exceeded: 3714.937271ms (limit: 3714.285714285714ms)


Multi Files (0 files):
  CPU Speed is 3500 with multiplier 39000000
  Performance Run #1:  13963.964251ms
  Performance Run #2:  13807.104047ms
  Performance Run #3:  14118.498804ms
  Performance Run #4:  14127.543946ms
  Performance Run #5:  13701.669443ms

  Performance budget exceeded: 13963.964251ms (limit: 11142.857142857143ms)
```
eslintbot: Thanks for the pull request, @smably! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
smably: Thanks for the comments, @ilyavolodin and @not-an-aardvark! It's been a busy week, and I haven't had a chance to address all of the issues you identified, but I've pushed my first round of fixes and responded to some of the comments. Just wanted to confirm that I haven't forgotten about this, and further fixes are coming.
eslintbot: Thanks for the pull request, @smably! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
not-an-aardvark: @smably I see you've pushed some changes since this was last reviewed. Do you think this is ready to be reviewed again? (There's no rush -- I just want to make sure we're not accidentally deadlocked if you were waiting for a review and I was waiting for you to finish.)
smably: Hey @not-an-aardvark, thanks for the nudge. I fixed most of the issues but tests seems to be failing on Windows (I'm guess something path-related) and I've been procrastinating figuring out the issue because I don't have a Windows machine available. I'll see if I can find some time in the next week to do some debugging and update the thread with the status of this work.
not-an-aardvark: It looks like the issue is that you're using `Array.prototype.includes`, which only exists on Node 6+, but the build runs on Node 4+. Switching to `indexOf` should hopefully fix the problem.
eslintbot: Thanks for the pull request, @smably! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
eslintbot: Thanks for the pull request, @smably! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
smably: @not-an-aardvark Thanks for the catch. I fixed and rebased. Builds look OK now! When you get a chance, I'd appreciate another look over this.
eslintbot: Thanks for the pull request, @smably! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
eslintbot: Thanks for the pull request, @smably! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
smably: @not-an-aardvark Thanks for the comments! Fixed most of the issues you flagged, plus left a few comments in response.
eslintbot: Thanks for the pull request, @smably! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
eslintbot: Thanks for the pull request, @smably! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
smably: @not-an-aardvark Hey, I finally got around to fixing this up. Sorry that took a while -- had to do a very painful rebase that also broke a bunch of tests in the process. Unfortunately, I squashed a bunch of the commits to try to make the merge conflict fixing easier, so I apologize if the commits aren't all cleanly separated. Hopefully fixed all the issues raised in the last round of review, but there's been enough refactoring that this may deserve another full check.
not-an-aardvark: No worries. Sorry about that, there has been a lot of churn in the codebase in the past few months.
eslintbot: Thanks for the pull request, @smably! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
eslintbot: Thanks for the pull request, @smably! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
smably: @not-an-aardvark I think this one's ready for another review. Almost there, I hope!
eslintbot: Thanks for the pull request, @smably! I took a look to make sure it's ready for merging and found some changes are needed:


* The commit summary needs to begin with a tag (such as `Fix:` or `Update:`). Please check out our [guide](http://eslint.org/docs/developer-guide/contributing/pull-requests#step-2-make-your-changes) for how to properly format your commit summary and [update](http://eslint.org/docs/developer-guide/contributing/pull-requests#updating-the-commit-message) it on this pull request.


Can you please update the pull request to address these?

(More information can be found in our [pull request guide](http://eslint.org/docs/developer-guide/contributing/pull-requests).)
not-an-aardvark: Waiting another day to merge this in case anyone else on @eslint/eslint-team wants to review it.
smably: 🎉  Thank you for the guidance and very helpful review comments! This is my first open source contribution (of code, at least) and at first I was a little worried that I'd bitten off more than I could chew. But I think it came together quite nicely.
ilyavolodin: Awesome job @smably This is a very high profile feature that users have been asking for a while, so great job at taking on it!

Patch:
diff --git a/conf/config-schema.js b/conf/config-schema.js
new file mode 100644
index 000000000..4ef958c40
--- /dev/null
+++ b/conf/config-schema.js
@@ -0,0 +1,68 @@
+/**
+ * @fileoverview Defines a schema for configs.
+ * @author Sylvan Mably
+ */
+
+"use strict";
+
+const baseConfigProperties = {
+    env: { type: "object" },
+    globals: { type: "object" },
+    parser: { type: ["string", "null"] },
+    parserOptions: { type: "object" },
+    plugins: { type: "array" },
+    rules: { type: "object" },
+    settings: { type: "object" }
+};
+
+const overrideProperties = Object.assign(
+    {},
+    baseConfigProperties,
+    {
+        files: {
+            oneOf: [
+                { type: "string" },
+                {
+                    type: "array",
+                    items: { type: "string" },
+                    minItems: 1
+                }
+            ]
+        },
+        excludedFiles: {
+            oneOf: [
+                { type: "string" },
+                {
+                    type: "array",
+                    items: { type: "string" }
+                }
+            ]
+        }
+    }
+);
+
+const topLevelConfigProperties = Object.assign(
+    {},
+    baseConfigProperties,
+    {
+        extends: { type: ["string", "array"] },
+        root: { type: "boolean" },
+        overrides: {
+            type: "array",
+            items: {
+                type: "object",
+                properties: overrideProperties,
+                required: ["files"],
+                additionalProperties: false
+            }
+        }
+    }
+);
+
+const configSchema = {
+    type: "object",
+    properties: topLevelConfigProperties,
+    additionalProperties: false
+};
+
+module.exports = configSchema;
diff --git a/conf/config-schema.json b/conf/config-schema.json
deleted file mode 100644
index c3676bde9..000000000
--- a/conf/config-schema.json
+++ /dev/null
@@ -1,15 +0,0 @@
-{
-    "type": "object",
-    "properties": {
-        "root": { "type": "boolean" },
-        "globals": { "type": ["object"] },
-        "parser": { "type": ["string", "null"] },
-        "env": { "type": "object" },
-        "plugins": { "type": ["array"] },
-        "settings": { "type": "object" },
-        "extends": { "type": ["string", "array"] },
-        "rules": { "type": "object" },
-        "parserOptions": { "type": "object" }
-    },
-    "additionalProperties": false
-}
diff --git a/docs/rules/no-trailing-spaces.md b/docs/rules/no-trailing-spaces.md
index 46f61fa60..050e93c39 100644
--- a/docs/rules/no-trailing-spaces.md
+++ b/docs/rules/no-trailing-spaces.md
@@ -31,6 +31,8 @@ This rule has an object option:
 
 * `"skipBlankLines": false` (default) disallows trailing whitespace on empty lines
 * `"skipBlankLines": true` allows trailing whitespace on empty lines
+* `"ignoreComments": false` (default) disallows trailing whitespace in comment blocks
+* `"ignoreComments": true` allows trailing whitespace in comment blocks
 
 ### skipBlankLines
 
@@ -43,3 +45,19 @@ var foo = 0;
 var baz = 5;
 //•••••
 ```
+
+### ignoreComments
+
+Examples of **correct** code for this rule with the `{ "ignoreComments": true }` option:
+
+```js
+/*eslint no-trailing-spaces: ["error", { "ignoreComments": true }]*/
+
+//foo•
+//•••••
+/**
+ *•baz
+ *••
+ *•bar
+ */
+```
\ No newline at end of file
diff --git a/docs/user-guide/configuring.md b/docs/user-guide/configuring.md
index 86469a5af..ed1f573fa 100644
--- a/docs/user-guide/configuring.md
+++ b/docs/user-guide/configuring.md
@@ -713,6 +713,61 @@ module.exports = {
 }
 ```
 
+## Configuration Based on Glob Patterns
+
+Sometimes a more fine-controlled configuration is necessary, for example if the configuration for files within the same directory has to be different. Therefore you can provide configurations under the `overrides` key that will only apply to files that match specific glob patterns, using the same format you would pass on the command line (e.g., `app/**/*.test.js`).
+
+### 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`.
+* 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.
+
+### Relative glob patterns
+
+```
+project-root
+├── app
+│   ├── lib
+│   │   ├── foo.js
+│   │   ├── fooSpec.js
+│   ├── components
+│   │   ├── bar.js
+│   │   ├── barSpec.js
+│   ├── .eslintrc.json
+├── server
+│   ├── server.js
+│   ├── serverSpec.js
+├── .eslintrc.json
+```
+
+The config in `app/.eslintrc.json` defines the glob pattern `**/*Spec.js`. This pattern is relative to the base directory of `app/.eslintrc.json`. So, this pattern would match `app/lib/fooSpec.js` and `app/components/barSpec.js` but **NOT** `server/serverSpec.js`. If you defined the same pattern in the `.eslintrc.json` file within in the `project-root` folder, it would match all three of the `*Spec` files.
+
+### Example configuration
+
+In your `.eslintrc.json`:
+
+```json
+{
+  "rules": {
+    "quotes": [ 2, "double" ]
+  },
+
+  "overrides": [
+    {
+      "files": [ "bin/*.js", "lib/*.js" ],
+      "excludedFiles": "*.test.js",
+      "rules": {
+        "quotes": [ 2, "single" ]
+      }
+    }
+  ]
+}
+```
+
 ## 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:
diff --git a/lib/cli-engine.js b/lib/cli-engine.js
index ebca4cb81..8d5ab065f 100644
--- a/lib/cli-engine.js
+++ b/lib/cli-engine.js
@@ -398,15 +398,17 @@ class CLIEngine {
         this.options = options;
         this.linter = new Linter();
 
-        const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
-
-        /**
-         * Cache used to avoid operating on files that haven't changed since the
-         * last successful execution (e.g., file passed linting with no errors and
-         * no warnings).
-         * @type {Object}
-         */
-        this._fileCache = fileEntryCache.create(cacheFile);
+        if (options.cache) {
+            const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
+
+            /**
+             * Cache used to avoid operating on files that haven't changed since the
+             * last successful execution (e.g., file passed linting with no errors and
+             * no warnings).
+             * @type {Object}
+             */
+            this._fileCache = fileEntryCache.create(cacheFile);
+        }
 
         // load in additional rules
         if (this.options.rulePaths) {
@@ -495,6 +497,11 @@ class CLIEngine {
             fileCache = this._fileCache,
             configHelper = this.config;
         let prevConfig; // the previous configuration used
+        const cacheFile = getCacheFile(this.options.cacheLocation || this.options.cacheFile, this.options.cwd);
+
+        if (!options.cache && fs.existsSync(cacheFile)) {
+            fs.unlinkSync(cacheFile);
+        }
 
         /**
          * Calculates the hash of the config file used to validate a given file
@@ -570,8 +577,6 @@ class CLIEngine {
                     // move to the next file
                     return;
                 }
-            } else {
-                fileCache.destroy();
             }
 
             debug(`Processing ${filename}`);
diff --git a/lib/config.js b/lib/config.js
index 5c04ee4e1..c69d120ef 100644
--- a/lib/config.js
+++ b/lib/config.js
@@ -13,6 +13,7 @@ const path = require("path"),
     os = require("os"),
     ConfigOps = require("./config/config-ops"),
     ConfigFile = require("./config/config-file"),
+    ConfigCache = require("./config/config-cache"),
     Plugins = require("./config/plugins"),
     FileFinder = require("./file-finder"),
     isResolvable = require("is-resolvable");
@@ -24,149 +25,22 @@ const debug = require("debug")("eslint:config");
 //------------------------------------------------------------------------------
 
 const PERSONAL_CONFIG_DIR = os.homedir() || null;
+const SUBCONFIG_SEP = ":";
 
 //------------------------------------------------------------------------------
 // Helpers
 //------------------------------------------------------------------------------
 
 /**
- * Check if item is an javascript object
- * @param {*} item object to check for
- * @returns {boolean} True if its an object
- * @private
- */
-function isObject(item) {
-    return typeof item === "object" && !Array.isArray(item) && item !== null;
-}
-
-/**
- * Load and parse a JSON config object from a file.
- * @param {string|Object} configToLoad the path to the JSON config file or the config object itself.
- * @param {Config} configContext config instance object
- * @returns {Object} the parsed config object (empty object if there was a parse error)
- * @private
- */
-function loadConfig(configToLoad, configContext) {
-    let config = {},
-        filePath = "";
-
-    if (configToLoad) {
-
-        if (isObject(configToLoad)) {
-            config = configToLoad;
-
-            if (config.extends) {
-                config = ConfigFile.applyExtends(config, configContext, filePath);
-            }
-        } else {
-            filePath = configToLoad;
-            config = ConfigFile.load(filePath, configContext);
-        }
-
-    }
-    return config;
-}
-
-/**
- * Get personal config object from ~/.eslintrc.
- * @param {Config} configContext Plugin context for the config instance
- * @returns {Object} the personal config object (null if there is no personal config)
- * @private
- */
-function getPersonalConfig(configContext) {
-    let config;
-
-    if (PERSONAL_CONFIG_DIR) {
-
-        const filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR);
-
-        if (filename) {
-            debug("Using personal config");
-            config = loadConfig(filename, configContext);
-        }
-    }
-
-    return config || null;
-}
-
-/**
- * Determine if rules were explicitly passed in as options.
+ * Determines if any rules were explicitly passed in as options.
  * @param {Object} options The options used to create our configuration.
  * @returns {boolean} True if rules were passed in as options, false otherwise.
+ * @private
  */
 function hasRules(options) {
     return options.rules && Object.keys(options.rules).length > 0;
 }
 
-/**
- * Get a local config object.
- * @param {Config} thisConfig A Config object.
- * @param {string} directory The directory to start looking in for a local config file.
- * @returns {Object} The local config object, or an empty object if there is no local config.
- */
-function getLocalConfig(thisConfig, directory) {
-
-    const projectConfigPath = ConfigFile.getFilenameForDirectory(thisConfig.options.cwd);
-    const localConfigFiles = thisConfig.findLocalConfigFiles(directory);
-    let found,
-        config = {};
-
-    for (const localConfigFile of localConfigFiles) {
-
-        // Don't consider the personal config file in the home directory,
-        // except if the home directory is the same as the current working directory
-        if (path.dirname(localConfigFile) === PERSONAL_CONFIG_DIR && localConfigFile !== projectConfigPath) {
-            continue;
-        }
-
-        debug(`Loading ${localConfigFile}`);
-        const localConfig = loadConfig(localConfigFile, thisConfig);
-
-        // Don't consider a local config file found if the config is null
-        if (!localConfig) {
-            continue;
-        }
-
-        found = true;
-        debug(`Using ${localConfigFile}`);
-        config = ConfigOps.merge(localConfig, config);
-
-        // Check for root flag
-        if (localConfig.root === true) {
-            break;
-        }
-    }
-
-    if (!found && !thisConfig.useSpecificConfig) {
-
-        /*
-         * - Is there a personal config in the user's home directory? If so,
-         *   merge that with the passed-in config.
-         * - Otherwise, if no rules were manually passed in, throw and error.
-         * - Note: This function is not called if useEslintrc is false.
-         */
-        const personalConfig = getPersonalConfig(thisConfig);
-
-        if (personalConfig) {
-            config = ConfigOps.merge(config, personalConfig);
-        } else if (!hasRules(thisConfig.options) && !thisConfig.options.baseConfig) {
-
-            // No config file, no manual configuration, and no rules, so error.
-            const noConfigError = new Error("No ESLint configuration found.");
-
-            noConfigError.messageTemplate = "no-config-found";
-            noConfigError.messageData = {
-                directory,
-                filesExamined: localConfigFiles
-            };
-
-            throw noConfigError;
-        }
-    }
-
-    return config;
-}
-
 //------------------------------------------------------------------------------
 // API
 //------------------------------------------------------------------------------
@@ -177,7 +51,6 @@ function getLocalConfig(thisConfig, directory) {
 class Config {
 
     /**
-     * Config options
      * @param {Object} options Options to be passed in
      * @param {Linter} linterContext Linter instance object
      */
@@ -187,13 +60,21 @@ class Config {
         this.linterContext = linterContext;
         this.plugins = new Plugins(linterContext.environments, linterContext.rules);
 
+        this.options = options;
         this.ignore = options.ignore;
         this.ignorePath = options.ignorePath;
-        this.cache = {};
         this.parser = options.parser;
         this.parserOptions = options.parserOptions || {};
 
-        this.baseConfig = options.baseConfig ? loadConfig(options.baseConfig, this) : { rules: {} };
+        this.baseConfig = options.baseConfig
+            ? ConfigOps.merge({}, ConfigFile.loadObject(options.baseConfig, this))
+            : { rules: {} };
+        this.baseConfig.filePath = "";
+        this.baseConfig.baseDirectory = this.options.cwd;
+
+        this.configCache = new ConfigCache();
+        this.configCache.setConfig(this.baseConfig.filePath, this.baseConfig);
+        this.configCache.setMergedVectorConfig(this.baseConfig.filePath, this.baseConfig);
 
         this.useEslintrc = (options.useEslintrc !== false);
 
@@ -209,132 +90,275 @@ class Config {
          * If user declares "foo", convert to "foo:false".
          */
         this.globals = (options.globals || []).reduce((globals, def) => {
-            const parts = def.split(":");
+            const parts = def.split(SUBCONFIG_SEP);
 
             globals[parts[0]] = (parts.length > 1 && parts[1] === "true");
 
             return globals;
         }, {});
 
-        this.options = options;
-        this.loadConfigFile(options.configFile);
+        this.loadSpecificConfig(options.configFile);
+
+        // Empty values in configs don't merge properly
+        const cliConfigOptions = {
+            env: this.env,
+            rules: this.options.rules,
+            globals: this.globals,
+            parserOptions: this.parserOptions,
+            plugins: this.options.plugins
+        };
+
+        this.cliConfig = {};
+        Object.keys(cliConfigOptions).forEach(configKey => {
+            const value = cliConfigOptions[configKey];
+
+            if (value) {
+                this.cliConfig[configKey] = value;
+            }
+        });
     }
 
     /**
-     * Loads the config from the configuration file
-     * @param {string} configFile - patch to the config file
-     * @returns {undefined}
-     */
-    loadConfigFile(configFile) {
-        if (configFile) {
-            debug(`Using command line config ${configFile}`);
-            if (isResolvable(configFile) || isResolvable(`eslint-config-${configFile}`) || configFile.charAt(0) === "@") {
-                this.useSpecificConfig = loadConfig(configFile, this);
-            } else {
-                this.useSpecificConfig = loadConfig(path.resolve(this.options.cwd, configFile), this);
+    * Loads the config options from a config specified on the command line.
+    * @param {string} [config] A shareable named config or path to a config file.
+    * @returns {void}
+    */
+    loadSpecificConfig(config) {
+        if (config) {
+            debug(`Using command line config ${config}`);
+            const isNamedConfig =
+                isResolvable(config) ||
+                isResolvable(`eslint-config-${config}`) ||
+                config.charAt(0) === "@";
+
+            if (!isNamedConfig) {
+                config = path.resolve(this.options.cwd, config);
             }
+
+            this.specificConfig = ConfigFile.load(config, this);
         }
     }
 
     /**
-     * Build a config object merging the base config (conf/eslint-recommended),
-     * the environments config (conf/environments.js) and eventually the user
-     * config.
-     * @param {string} filePath a file in whose directory we start looking for a local config
-     * @returns {Object} config object
+     * Gets the personal config object from user's home directory.
+     * @returns {Object} the personal config object (null if there is no personal config)
+     * @private
      */
-    getConfig(filePath) {
-        const directory = filePath ? path.dirname(filePath) : this.options.cwd;
-        let config,
-            userConfig;
+    getPersonalConfig() {
+        if (typeof this.personalConfig === "undefined") {
+            let config;
 
-        debug(`Constructing config for ${filePath ? filePath : "text"}`);
+            if (PERSONAL_CONFIG_DIR) {
+                const filename = ConfigFile.getFilenameForDirectory(PERSONAL_CONFIG_DIR);
 
-        config = this.cache[directory];
-        if (config) {
-            debug("Using config from cache");
-            return config;
+                if (filename) {
+                    debug("Using personal config");
+                    config = ConfigFile.load(filename, this);
+                }
+            }
+            this.personalConfig = config || null;
         }
 
-        // Step 1: Determine user-specified config from .eslintrc.* and package.json files
+        return this.personalConfig;
+    }
+
+    /**
+     * Builds a hierarchy of config objects, including the base config, all local configs from the directory tree,
+     * and a config file specified on the command line, if applicable.
+     * @param {string} directory a file in whose directory we start looking for a local config
+     * @returns {Object[]} The config objects, in ascending order of precedence
+     * @private
+     */
+    getConfigHierarchy(directory) {
+        debug(`Constructing config file hierarchy for ${directory}`);
+
+        // Step 1: Always include baseConfig
+        let configs = [this.baseConfig];
+
+        // Step 2: Add user-specified config from .eslintrc.* and package.json files
         if (this.useEslintrc) {
             debug("Using .eslintrc and package.json files");
-            userConfig = getLocalConfig(this, directory);
+            configs = configs.concat(this.getLocalConfigHierarchy(directory));
         } else {
             debug("Not using .eslintrc or package.json files");
-            userConfig = {};
         }
 
-        // Step 2: Create a copy of the baseConfig
-        config = ConfigOps.merge({}, this.baseConfig);
+        // Step 3: Merge in command line config file
+        if (this.specificConfig) {
+            debug("Using command line config file");
+            configs.push(this.specificConfig);
+        }
 
-        // Step 3: Merge in the user-specified configuration from .eslintrc and package.json
-        config = ConfigOps.merge(config, userConfig);
+        return configs;
+    }
 
-        // Step 4: Merge in command line config file
-        if (this.useSpecificConfig) {
-            debug("Merging command line config file");
+    /**
+     * Gets a list of config objects extracted from local config files that apply to the current directory, in
+     * descending order, beginning with the config that is highest in the directory tree.
+     * @param {string} directory The directory to start looking in for local config files.
+     * @returns {Object[]} The shallow local config objects, in ascending order of precedence (closest to the current
+     * directory at the end), or an empty array if there are no local configs.
+     * @private
+     */
+    getLocalConfigHierarchy(directory) {
+        const localConfigFiles = this.findLocalConfigFiles(directory),
+            projectConfigPath = ConfigFile.getFilenameForDirectory(this.options.cwd),
+            searched = [],
+            configs = [];
 
-            config = ConfigOps.merge(config, this.useSpecificConfig);
-        }
+        for (const localConfigFile of localConfigFiles) {
+            const localConfigDirectory = path.dirname(localConfigFile);
+            const localConfigHierarchyCache = this.configCache.getHierarchyLocalConfigs(localConfigDirectory);
 
-        // Step 5: Merge in command line environments
-        debug("Merging command line environment settings");
-        config = ConfigOps.merge(config, { env: this.env });
+            if (localConfigHierarchyCache) {
+                const localConfigHierarchy = localConfigHierarchyCache.concat(configs.reverse());
 
-        // Step 6: Merge in command line rules
-        if (this.options.rules) {
-            debug("Merging command line rules");
-            config = ConfigOps.merge(config, { rules: this.options.rules });
-        }
+                this.configCache.setHierarchyLocalConfigs(searched, localConfigHierarchy);
+                return localConfigHierarchy;
+            }
+
+            // Don't consider the personal config file in the home directory,
+            // except if the home directory is the same as the current working directory
+            if (localConfigDirectory === PERSONAL_CONFIG_DIR && localConfigFile !== projectConfigPath) {
+                continue;
+            }
 
-        // Step 7: Merge in command line globals
-        config = ConfigOps.merge(config, { globals: this.globals });
+            debug(`Loading ${localConfigFile}`);
+            const localConfig = ConfigFile.load(localConfigFile, this);
 
-        // Only override parser if it is passed explicitly through the command line or if it's not
-        // defined yet (because the final object will at least have the parser key)
-        if (this.parser || !config.parser) {
-            config = ConfigOps.merge(config, {
-                parser: this.parser
-            });
-        }
+            // Ignore empty config files
+            if (!localConfig) {
+                continue;
+            }
 
-        if (this.parserOptions) {
-            config = ConfigOps.merge(config, {
-                parserOptions: this.parserOptions
-            });
-        }
+            debug(`Using ${localConfigFile}`);
+            configs.push(localConfig);
+            searched.push(localConfigDirectory);
 
-        // Step 8: Merge in command line plugins
-        if (this.options.plugins) {
-            debug("Merging command line plugins");
-            this.plugins.loadAll(this.options.plugins);
-            config = ConfigOps.merge(config, { plugins: this.options.plugins });
+            // Stop traversing if a config is found with the root flag set
+            if (localConfig.root) {
+                break;
+            }
         }
 
-        // Step 9: Apply environments to the config if present
-        if (config.env) {
-            config = ConfigOps.applyEnvironments(config, this.linterContext.environments);
+        if (!configs.length && !this.specificConfig) {
+
+            // Fall back on the personal config from ~/.eslintrc
+            debug("Using personal config file");
+            const personalConfig = this.getPersonalConfig();
+
+            if (personalConfig) {
+                configs.push(personalConfig);
+            } else if (!hasRules(this.options) && !this.options.baseConfig) {
+
+                // No config file, no manual configuration, and no rules, so error.
+                const noConfigError = new Error("No ESLint configuration found.");
+
+                noConfigError.messageTemplate = "no-config-found";
+                noConfigError.messageData = {
+                    directory,
+                    filesExamined: localConfigFiles
+                };
+
+                throw noConfigError;
+            }
         }
 
-        this.cache[directory] = config;
+        // Set the caches for the parent directories
+        this.configCache.setHierarchyLocalConfigs(searched, configs.reverse());
 
-        return config;
+        return configs;
     }
 
     /**
-     * Find local config files from directory and parent directories.
+     * Gets the vector of applicable configs and subconfigs from the hierarchy for a given file. A vector is an array of
+     * entries, each of which in an object specifying a config file path and an array of override indices corresponding
+     * to entries in the config file's overrides section whose glob patterns match the specified file path; e.g., the
+     * vector entry { configFile: '/home/john/app/.eslintrc', matchingOverrides: [0, 2] } would indicate that the main
+     * project .eslintrc file and its first and third override blocks apply to the current file.
+     * @param {string} filePath The file path for which to build the hierarchy and config vector.
+     * @returns {Array<Object>} config vector applicable to the specified path
+     * @private
+     */
+    getConfigVector(filePath) {
+        const directory = filePath ? path.dirname(filePath) : this.options.cwd;
+
+        return this.getConfigHierarchy(directory).map(config => {
+            const vectorEntry = {
+                filePath: config.filePath,
+                matchingOverrides: []
+            };
+
+            if (config.overrides) {
+                const relativePath = path.relative(config.baseDirectory, filePath || directory);
+
+                config.overrides.forEach((override, i) => {
+                    if (ConfigOps.pathMatchesGlobs(relativePath, override.files, override.excludedFiles)) {
+                        vectorEntry.matchingOverrides.push(i);
+                    }
+                });
+            }
+
+            return vectorEntry;
+        });
+    }
+
+    /**
+     * Finds local config files from the specified directory and its parent directories.
      * @param {string} directory The directory to start searching from.
      * @returns {GeneratorFunction} The paths of local config files found.
      */
     findLocalConfigFiles(directory) {
-
         if (!this.localConfigFinder) {
             this.localConfigFinder = new FileFinder(ConfigFile.CONFIG_FILES, this.options.cwd);
         }
 
         return this.localConfigFinder.findAllInDirectoryAndParents(directory);
     }
+
+    /**
+     * Builds the authoritative config object for the specified file path by merging the hierarchy of config objects
+     * that apply to the current file, including the base config (conf/eslint-recommended), the user's personal config
+     * from their homedir, all local configs from the directory tree, any specific config file passed on the command
+     * line, any configuration overrides set directly on the command line, and finally the environment configs
+     * (conf/environments).
+     * @param {string} filePath a file in whose directory we start looking for a local config
+     * @returns {Object} config object
+     */
+    getConfig(filePath) {
+        const vector = this.getConfigVector(filePath);
+        let config = this.configCache.getMergedConfig(vector);
+
+        if (config) {
+            debug("Using config from cache");
+            return config;
+        }
+
+        // Step 1: Merge in the filesystem configurations (base, local, and personal)
+        config = ConfigOps.getConfigFromVector(vector, this.configCache);
+
+        // Step 2: Merge in command line configurations
+        config = ConfigOps.merge(config, this.cliConfig);
+
+        if (this.cliConfig.plugins) {
+            this.plugins.loadAll(this.cliConfig.plugins);
+        }
+
+        // Step 3: Override parser only if it is passed explicitly through the command line
+        // or if it's not defined yet (because the final object will at least have the parser key)
+        if (this.parser || !config.parser) {
+            config = ConfigOps.merge(config, { parser: this.parser });
+        }
+
+        // Step 4: Apply environments to the config if present
+        if (config.env) {
+            config = ConfigOps.applyEnvironments(config, this.linterContext.environments);
+        }
+
+        this.configCache.setMergedConfig(vector, config);
+
+        return config;
+    }
 }
 
 module.exports = Config;
diff --git a/lib/config/config-cache.js b/lib/config/config-cache.js
new file mode 100644
index 000000000..0ffcae944
--- /dev/null
+++ b/lib/config/config-cache.js
@@ -0,0 +1,130 @@
+/**
+ * @fileoverview Responsible for caching config files
+ * @author Sylvan Mably
+ */
+
+"use strict";
+
+//------------------------------------------------------------------------------
+// Helpers
+//------------------------------------------------------------------------------
+
+/**
+ * Get a string hash for a config vector
+ * @param {Array<Object>} vector config vector to hash
+ * @returns {string} hash of the vector values
+ * @private
+ */
+function hash(vector) {
+    return JSON.stringify(vector);
+}
+
+//------------------------------------------------------------------------------
+// API
+//------------------------------------------------------------------------------
+
+/**
+ * Configuration caching class (not exported)
+ */
+class ConfigCache {
+
+    constructor() {
+        this.filePathCache = new Map();
+        this.localHierarchyCache = new Map();
+        this.mergedVectorCache = new Map();
+        this.mergedCache = new Map();
+    }
+
+    /**
+     * Gets a config object from the cache for the specified config file path.
+     * @param {string} configFilePath the absolute path to the config file
+     * @returns {Object|null} config object, if found in the cache, otherwise null
+     * @private
+     */
+    getConfig(configFilePath) {
+        return this.filePathCache.get(configFilePath);
+    }
+
+    /**
+     * Sets a config object in the cache for the specified config file path.
+     * @param {string} configFilePath the absolute path to the config file
+     * @param {Object} config the config object to add to the cache
+     * @returns {void}
+     * @private
+     */
+    setConfig(configFilePath, config) {
+        this.filePathCache.set(configFilePath, config);
+    }
+
+    /**
+     * Gets a list of hierarchy-local config objects that apply to the specified directory.
+     * @param {string} directory the path to the directory
+     * @returns {Object[]|null} a list of config objects, if found in the cache, otherwise null
+     * @private
+     */
+    getHierarchyLocalConfigs(directory) {
+        return this.localHierarchyCache.get(directory);
+    }
+
+    /**
+     * For each of the supplied parent directories, sets the list of config objects for that directory to the
+     * appropriate subset of the supplied parent config objects.
+     * @param {string[]} parentDirectories a list of parent directories to add to the config cache
+     * @param {Object[]} parentConfigs a list of config objects that apply to the lowest directory in parentDirectories
+     * @returns {void}
+     * @private
+     */
+    setHierarchyLocalConfigs(parentDirectories, parentConfigs) {
+        parentDirectories.forEach((localConfigDirectory, i) => {
+            const directoryParentConfigs = parentConfigs.slice(0, parentConfigs.length - i);
+
+            this.localHierarchyCache.set(localConfigDirectory, directoryParentConfigs);
+        });
+    }
+
+    /**
+     * Gets a merged config object corresponding to the supplied vector.
+     * @param {Array<Object>} vector the vector to find a merged config for
+     * @returns {Object|null} a merged config object, if found in the cache, otherwise null
+     * @private
+     */
+    getMergedVectorConfig(vector) {
+        return this.mergedVectorCache.get(hash(vector));
+    }
+
+    /**
+     * Sets a merged config object in the cache for the supplied vector.
+     * @param {Array<Object>} vector the vector to save a merged config for
+     * @param {Object} config the merged config object to add to the cache
+     * @returns {void}
+     * @private
+     */
+    setMergedVectorConfig(vector, config) {
+        this.mergedVectorCache.set(hash(vector), config);
+    }
+
+    /**
+     * Gets a merged config object corresponding to the supplied vector, including configuration options from outside
+     * the vector.
+     * @param {Array<Object>} vector the vector to find a merged config for
+     * @returns {Object|null} a merged config object, if found in the cache, otherwise null
+     * @private
+     */
+    getMergedConfig(vector) {
+        return this.mergedCache.get(hash(vector));
+    }
+
+    /**
+     * Sets a merged config object in the cache for the supplied vector, including configuration options from outside
+     * the vector.
+     * @param {Array<Object>} vector the vector to save a merged config for
+     * @param {Object} config the merged config object to add to the cache
+     * @returns {void}
+     * @private
+     */
+    setMergedConfig(vector, config) {
+        this.mergedCache.set(hash(vector), config);
+    }
+}
+
+module.exports = ConfigCache;
diff --git a/lib/config/config-file.js b/lib/config/config-file.js
index c9fcc9dff..832529952 100644
--- a/lib/config/config-file.js
+++ b/lib/config/config-file.js
@@ -418,7 +418,7 @@ function applyExtends(config, configContext, filePath, relativeTo) {
                 );
             }
             debug(`Loading ${parentPath}`);
-            return ConfigOps.merge(load(parentPath, configContext, false, relativeTo), previousValue);
+            return ConfigOps.merge(load(parentPath, configContext, relativeTo), previousValue);
         } catch (e) {
 
             /*
@@ -517,16 +517,12 @@ function resolve(filePath, relativeTo) {
 
 /**
  * Loads a configuration file from the given file path.
- * @param {string} filePath The filename or package name to load the configuration
- *      information from.
+ * @param {Object} resolvedPath The value from calling resolve() on a filename or package name.
  * @param {Config} configContext Plugins context
- * @param {boolean} [applyEnvironments=false] Set to true to merge in environment settings.
- * @param {string} [relativeTo] The path to resolve relative to.
  * @returns {Object} The configuration information.
  */
-function load(filePath, configContext, applyEnvironments, relativeTo) {
-    const resolvedPath = resolve(filePath, relativeTo),
-        dirname = path.dirname(resolvedPath.filePath),
+function loadFromDisk(resolvedPath, configContext) {
+    const dirname = path.dirname(resolvedPath.filePath),
         lookupPath = getLookupPath(dirname);
     let config = loadConfigFile(resolvedPath);
 
@@ -547,27 +543,60 @@ function load(filePath, configContext, applyEnvironments, relativeTo) {
         }
 
         // validate the configuration before continuing
-        validator.validate(config, filePath, configContext.linterContext.rules, configContext.linterContext.environments);
+        validator.validate(config, resolvedPath, configContext.linterContext.rules, configContext.linterContext.environments);
 
         /*
          * If an `extends` property is defined, it represents a configuration file to use as
          * a "parent". Load the referenced file and merge the configuration recursively.
          */
         if (config.extends) {
-            config = applyExtends(config, configContext, filePath, dirname);
+            config = applyExtends(config, configContext, resolvedPath.filePath, dirname);
         }
+    }
 
-        if (config.env && applyEnvironments) {
+    return config;
+}
 
-            // Merge in environment-specific globals and parserOptions.
-            config = ConfigOps.applyEnvironments(config, configContext.linterContext.environments);
-        }
+/**
+ * Loads a config object, applying extends if present.
+ * @param {Object} configObject a config object to load
+ * @returns {Object} the config object with extends applied if present, or the passed config if not
+ * @private
+ */
+function loadObject(configObject) {
+    return configObject.extends ? applyExtends(configObject, "") : configObject;
+}
+
+/**
+ * Loads a config object from the config cache based on its filename, falling back to the disk if the file is not yet
+ * cached.
+ * @param {string} filePath the path to the config file
+ * @param {Config} configContext Context for the config instance
+ * @param {string} [relativeTo] The path to resolve relative to.
+ * @returns {Object} the parsed config object (empty object if there was a parse error)
+ * @private
+ */
+function load(filePath, configContext, relativeTo) {
+    const resolvedPath = resolve(filePath, relativeTo);
+
+    const cachedConfig = configContext.configCache.getConfig(resolvedPath.filePath);
 
+    if (cachedConfig) {
+        return cachedConfig;
+    }
+
+    const config = loadFromDisk(resolvedPath, configContext);
+
+    if (config) {
+        config.filePath = resolvedPath.filePath;
+        config.baseDirectory = path.dirname(resolvedPath.filePath);
+        configContext.configCache.setConfig(resolvedPath.filePath, config);
     }
 
     return config;
 }
 
+
 //------------------------------------------------------------------------------
 // Public Interface
 //------------------------------------------------------------------------------
@@ -577,6 +606,7 @@ module.exports = {
     getBaseDir,
     getLookupPath,
     load,
+    loadObject,
     resolve,
     write,
     applyExtends,
diff --git a/lib/config/config-ops.js b/lib/config/config-ops.js
index e1d9a9013..d169e60dc 100644
--- a/lib/config/config-ops.js
+++ b/lib/config/config-ops.js
@@ -9,6 +9,9 @@
 // Requirements
 //------------------------------------------------------------------------------
 
+const minimatch = require("minimatch"),
+    path = require("path");
+
 const debug = require("debug")("eslint:config-ops");
 
 //------------------------------------------------------------------------------
@@ -174,7 +177,9 @@ module.exports = {
                 });
             }
             Object.keys(src).forEach(key => {
-                if (Array.isArray(src[key]) || Array.isArray(target[key])) {
+                if (key === "overrides") {
+                    dst[key] = (target[key] || []).concat(src[key] || []);
+                } else if (Array.isArray(src[key]) || Array.isArray(target[key])) {
                     dst[key] = deepmerge(target[key], src[key], key === "plugins" || key === "extends", isRule);
                 } else if (typeof src[key] !== "object" || !src[key] || key === "exported" || key === "astGlobals") {
                     dst[key] = src[key];
@@ -268,5 +273,111 @@ module.exports = {
      */
     isEverySeverityValid(config) {
         return Object.keys(config).every(ruleId => this.isValidSeverity(config[ruleId]));
+    },
+
+    /**
+     * Merges all configurations in a given config vector. A vector is an array of objects, each containing a config
+     * file path and a list of subconfig indices that match the current file path. All config data is assumed to be
+     * cached.
+     * @param {Array<Object>} vector list of config files and their subconfig indices that match the current file path
+     * @param {Object} configCache the config cache
+     * @returns {Object} config object
+     */
+    getConfigFromVector(vector, configCache) {
+
+        const cachedConfig = configCache.getMergedVectorConfig(vector);
+
+        if (cachedConfig) {
+            return cachedConfig;
+        }
+
+        debug("Using config from partial cache");
+
+        const subvector = Array.from(vector);
+        let nearestCacheIndex = subvector.length - 1,
+            partialCachedConfig;
+
+        while (nearestCacheIndex >= 0) {
+            partialCachedConfig = configCache.getMergedVectorConfig(subvector);
+            if (partialCachedConfig) {
+                break;
+            }
+            subvector.pop();
+            nearestCacheIndex--;
+        }
+
+        if (!partialCachedConfig) {
+            partialCachedConfig = {};
+        }
+
+        let finalConfig = partialCachedConfig;
+
+        // Start from entry immediately following nearest cached config (first uncached entry)
+        for (let i = nearestCacheIndex + 1; i < vector.length; i++) {
+            finalConfig = this.mergeVectorEntry(finalConfig, vector[i], configCache);
+            configCache.setMergedVectorConfig(vector.slice(0, i + 1), finalConfig);
+        }
+
+        return finalConfig;
+    },
+
+    /**
+     * Merges the config options from a single vector entry into the supplied config.
+     * @param {Object} config the base config to merge the vector entry's options into
+     * @param {Object} vectorEntry a single entry from a vector, consisting of a config file path and an array of
+     * matching override indices
+     * @param {Object} configCache the config cache
+     * @returns {Object} merged config object
+     */
+    mergeVectorEntry(config, vectorEntry, configCache) {
+        const vectorEntryConfig = Object.assign({}, configCache.getConfig(vectorEntry.filePath));
+        let mergedConfig = Object.assign({}, config),
+            overrides;
+
+        if (vectorEntryConfig.overrides) {
+            overrides = vectorEntryConfig.overrides.filter(
+                (override, overrideIndex) => vectorEntry.matchingOverrides.indexOf(overrideIndex) !== -1
+            );
+        } else {
+            overrides = [];
+        }
+
+        mergedConfig = this.merge(mergedConfig, vectorEntryConfig);
+
+        delete mergedConfig.overrides;
+
+        mergedConfig = overrides.reduce((lastConfig, override) => this.merge(lastConfig, override), mergedConfig);
+
+        if (mergedConfig.filePath) {
+            delete mergedConfig.filePath;
+            delete mergedConfig.baseDirectory;
+        } else if (mergedConfig.files) {
+            delete mergedConfig.files;
+        }
+
+        return mergedConfig;
+    },
+
+    /**
+     * Checks that the specified file path matches all of the supplied glob patterns.
+     * @param {string} filePath The file path to test patterns against
+     * @param {string|string[]} patterns One or more glob patterns, of which at least one should match the file path
+     * @param {string|string[]} [excludedPatterns] One or more glob patterns, of which none should match the file path
+     * @returns {boolean} True if all the supplied patterns match the file path, false otherwise
+     */
+    pathMatchesGlobs(filePath, patterns, excludedPatterns) {
+        const patternList = [].concat(patterns);
+        const excludedPatternList = [].concat(excludedPatterns || []);
+
+        patternList.concat(excludedPatternList).forEach(pattern => {
+            if (path.isAbsolute(pattern) || pattern.includes("..")) {
+                throw new Error(`Invalid override pattern (expected relative path not containing '..'): ${pattern}`);
+            }
+        });
+
+        const opts = { matchBase: true };
+
+        return patternList.some(pattern => minimatch(filePath, pattern, opts)) &&
+            !excludedPatternList.some(excludedPattern => minimatch(filePath, excludedPattern, opts));
     }
 };
diff --git a/lib/config/config-validator.js b/lib/config/config-validator.js
index 329a5087d..8754485f4 100644
--- a/lib/config/config-validator.js
+++ b/lib/config/config-validator.js
@@ -10,7 +10,7 @@
 //------------------------------------------------------------------------------
 
 const schemaValidator = require("is-my-json-valid"),
-    configSchema = require("../../conf/config-schema.json"),
+    configSchema = require("../../conf/config-schema.js"),
     util = require("util");
 
 const validators = {
@@ -170,7 +170,7 @@ function formatErrors(errors) {
 
             return `Property "${formattedField}" is the wrong type (expected ${formattedExpectedType} but got \`${formattedValue}\`)`;
         }
-        return `"${error.field.replace(/^(data\.)/, "")}" ${error.message}. Value: ${error.value}`;
+        return `"${error.field.replace(/^(data\.)/, "")}" ${error.message}. Value: ${JSON.stringify(error.value)}`;
     }).map(message => `\t- ${message}.\n`).join("");
 }
 
diff --git a/lib/rules/no-trailing-spaces.js b/lib/rules/no-trailing-spaces.js
index 471381f24..b5d2f8d1b 100644
--- a/lib/rules/no-trailing-spaces.js
+++ b/lib/rules/no-trailing-spaces.js
@@ -30,6 +30,9 @@ module.exports = {
                 properties: {
                     skipBlankLines: {
                         type: "boolean"
+                    },
+                    ignoreComments: {
+                        type: "boolean"
                     }
                 },
                 additionalProperties: false
@@ -45,7 +48,8 @@ module.exports = {
             NONBLANK = `${BLANK_CLASS}+$`;
 
         const options = context.options[0] || {},
-            skipBlankLines = options.skipBlankLines || false;
+            skipBlankLines = options.skipBlankLines || false,
+            ignoreComments = typeof options.ignoreComments === "undefined" || options.ignoreComments;
 
         /**
          * Report the error message
@@ -72,6 +76,22 @@ module.exports = {
             });
         }
 
+        /**
+         * Given a list of comment nodes, return the line numbers for those comments.
+         * @param {Array} comments An array of comment nodes.
+         * @returns {number[]} An array of line numbers containing comments.
+         */
+        function getCommentLineNumbers(comments) {
+            const lines = new Set();
+
+            comments.forEach(comment => {
+                for (let i = comment.loc.start.line; i <= comment.loc.end.line; i++) {
+                    lines.add(i);
+                }
+            });
+
+            return lines;
+        }
 
         //--------------------------------------------------------------------------
         // Public
@@ -87,7 +107,10 @@ module.exports = {
                 const re = new RegExp(NONBLANK),
                     skipMatch = new RegExp(SKIP_BLANK),
                     lines = sourceCode.lines,
-                    linebreaks = sourceCode.getText().match(astUtils.createGlobalLinebreakMatcher());
+                    linebreaks = sourceCode.getText().match(astUtils.createGlobalLinebreakMatcher()),
+                    comments = sourceCode.getAllComments(),
+                    commentLineNumbers = getCommentLineNumbers(comments);
+
                 let totalLength = 0,
                     fixRange = [];
 
@@ -125,7 +148,10 @@ module.exports = {
                         }
 
                         fixRange = [rangeStart, rangeEnd];
-                        report(node, location, fixRange);
+
+                        if (!ignoreComments || !commentLineNumbers.has(location.line)) {
+                            report(node, location, fixRange);
+                        }
                     }
 
                     totalLength += lineLength;
diff --git a/package.json b/package.json
index 26dbdd8c8..df699ab89 100644
--- a/package.json
+++ b/package.json
@@ -56,6 +56,7 @@
     "json-stable-stringify": "^1.0.1",
     "levn": "^0.3.0",
     "lodash": "^4.17.4",
+    "minimatch": "^3.0.2",
     "mkdirp": "^0.5.1",
     "natural-compare": "^1.4.0",
     "optionator": "^0.8.2",


Test patch:
diff --git a/tests/fixtures/config-hierarchy/file-structure.json b/tests/fixtures/config-hierarchy/file-structure.json
index 9ebe7de30..f840fead5 100644
--- a/tests/fixtures/config-hierarchy/file-structure.json
+++ b/tests/fixtures/config-hierarchy/file-structure.json
@@ -48,6 +48,12 @@
             ".eslintrc": "{\n    \"env\": {\n        \"commonjs\": true\n    }\n}\n"
         }
     },
+    "overrides": {
+        ".eslintrc": "{\n    \"rules\": {\n        \"quotes\": [2, \"double\"],\n        \"no-else-return\": 0,\n        \"no-unused-vars\": 1,\n        \"semi\": [1, \"never\"]\n    },\n    \"overrides\": [\n        {\n            \"files\": \"foo.js\",\n            \"rules\": {\n                \"quotes\": [2, \"single\"]\n            }\n        },\n        {\n            \"files\": \"bar.js\",\n            \"rules\": {\n                \"no-else-return\": 1\n            }\n        },\n        {\n            \"files\": \"**/*one.js\",\n            \"rules\": {\n                \"curly\": [\"error\", \"multi\", \"consistent\"]\n            }\n        },\n        {\n            \"files\": \"two/child-two.js\",\n            \"rules\": {\n                \"no-unused-vars\": 2,\n                \"no-console\": 1\n            }\n        }\n    ]\n}\n",
+        "two": {
+            ".eslintrc": "{\n    \"rules\": {\n        \"semi\": [2, \"never\"]\n    },\n    \"overrides\": [\n        {\n            \"files\": \"child-two.js\",\n            \"rules\": {\n                \"no-console\": 0\n            }\n        }\n    ]\n}\n"
+        }
+    },
     "packagejson": {
         ".eslintrc": "rules:\r\n    quotes: [2, \"double\"]\r\n",
         "package.json": "{\r\n  \"name\": \"\",\r\n  \"version\": \"\",\r\n  \"eslintConfig\": {\r\n    \"rules\": {\r\n      \"quotes\": [1, \"single\"]\r\n    }\r\n  }\r\n}\r\n",
diff --git a/tests/lib/cli-engine.js b/tests/lib/cli-engine.js
index f0f43e6e1..7529d8edf 100644
--- a/tests/lib/cli-engine.js
+++ b/tests/lib/cli-engine.js
@@ -77,7 +77,7 @@ describe("CLIEngine", () => {
         engine.config.plugins.define(examplePreprocessorName, require("../fixtures/processors/custom-processor"));
 
         // load the real file now so that it can consume the loaded plugins
-        engine.config.loadConfigFile(options.configFile);
+        engine.config.loadSpecificConfig(options.configFile);
 
         return engine;
     }
@@ -1324,6 +1324,8 @@ describe("CLIEngine", () => {
             fakeFS.realpathSync = function() {
                 throw new Error("this error should not happen");
             };
+            fakeFS.existsSync = fs.existsSync;
+            fakeFS.unlinkSync = fs.unlinkSync;
 
             engine = new LocalCLIEngine({
                 ignorePattern: "tests"
diff --git a/tests/lib/config.js b/tests/lib/config.js
index 1942a1c08..935c210bd 100644
--- a/tests/lib/config.js
+++ b/tests/lib/config.js
@@ -338,8 +338,8 @@ describe("Config", () => {
         it("should load the config file when there are JS-style comments in the text", () => {
             const configPath = path.resolve(__dirname, "..", "fixtures", "configurations", "comments.json"),
                 configHelper = new Config({ configFile: configPath }, linter),
-                semi = configHelper.useSpecificConfig.rules.semi,
-                strict = configHelper.useSpecificConfig.rules.strict;
+                semi = configHelper.specificConfig.rules.semi,
+                strict = configHelper.specificConfig.rules.strict;
 
             assert.equal(semi, 1);
             assert.equal(strict, 0);
@@ -349,8 +349,8 @@ describe("Config", () => {
         it("should load the config file when a YAML file is used", () => {
             const configPath = path.resolve(__dirname, "..", "fixtures", "configurations", "env-browser.yaml"),
                 configHelper = new Config({ configFile: configPath }, linter),
-                noAlert = configHelper.useSpecificConfig.rules["no-alert"],
-                noUndef = configHelper.useSpecificConfig.rules["no-undef"];
+                noAlert = configHelper.specificConfig.rules["no-alert"],
+                noUndef = configHelper.specificConfig.rules["no-undef"];
 
             assert.equal(noAlert, 0);
             assert.equal(noUndef, 2);
@@ -1081,6 +1081,287 @@ describe("Config", () => {
                 }, "No ESLint configuration found");
             });
         });
+
+
+        describe("with overrides", () => {
+
+            /**
+             * Returns the path inside of the fixture directory.
+             * @param {...string} pathSegments One or more path segments, in order of depth, shallowest first
+             * @returns {string} The path inside the fixture directory.
+             * @private
+             */
+            function getFakeFixturePath() {
+                const pathSegments = Array.from(arguments);
+
+                pathSegments.unshift("config-hierarchy");
+                pathSegments.unshift("fixtures");
+                pathSegments.unshift("eslint");
+                pathSegments.unshift(process.cwd());
+
+                return path.join.apply(path, pathSegments);
+            }
+
+            before(() => {
+                mockFs({
+                    eslint: {
+                        fixtures: {
+                            "config-hierarchy": DIRECTORY_CONFIG_HIERARCHY
+                        }
+                    }
+                });
+            });
+
+            after(() => {
+                mockFs.restore();
+            });
+
+            it("should merge override config when the pattern matches the file name", () => {
+                const config = new Config({ cwd: process.cwd() }, linter);
+                const targetPath = getFakeFixturePath("overrides", "foo.js");
+                const expected = {
+                    rules: {
+                        quotes: [2, "single"],
+                        "no-else-return": 0,
+                        "no-unused-vars": 1,
+                        semi: [1, "never"]
+                    }
+                };
+                const actual = config.getConfig(targetPath);
+
+                assertConfigsEqual(actual, expected);
+            });
+
+            it("should merge override config when the pattern matches the file path relative to the config file", () => {
+                const config = new Config({ cwd: process.cwd() }, linter);
+                const targetPath = getFakeFixturePath("overrides", "child", "child-one.js");
+                const expected = {
+                    rules: {
+                        curly: ["error", "multi", "consistent"],
+                        "no-else-return": 0,
+                        "no-unused-vars": 1,
+                        quotes: [2, "double"],
+                        semi: [1, "never"]
+                    }
+                };
+                const actual = config.getConfig(targetPath);
+
+                assertConfigsEqual(actual, expected);
+            });
+
+            it("should not merge override config when the pattern matches the absolute file path", () => {
+                const targetPath = getFakeFixturePath("overrides", "bar.js");
+                const resolvedPath = path.resolve(__dirname, "..", "fixtures", "config-hierarchy", "overrides", "bar.js");
+                const config = new Config({
+                    cwd: process.cwd(),
+                    baseConfig: {
+                        overrides: [{
+                            files: resolvedPath,
+                            rules: {
+                                quotes: [1, "double"]
+                            }
+                        }],
+                        useEslintrc: false
+                    }
+                }, linter);
+
+                assert.throws(() => config.getConfig(targetPath), /Invalid override pattern/);
+            });
+
+            it("should not merge override config when the pattern traverses up the directory tree", () => {
+                const targetPath = getFakeFixturePath("overrides", "bar.js");
+                const parentPath = "overrides/../**/*.js";
+
+                const config = new Config({
+                    cwd: process.cwd(),
+                    baseConfig: {
+                        overrides: [{
+                            files: parentPath,
+                            rules: {
+                                quotes: [1, "single"]
+                            }
+                        }],
+                        useEslintrc: false
+                    }
+                }, linter);
+
+                assert.throws(() => config.getConfig(targetPath), /Invalid override pattern/);
+            });
+
+            it("should merge all local configs (override and non-override) before non-local configs", () => {
+                const config = new Config({ cwd: process.cwd() }, linter);
+                const targetPath = getFakeFixturePath("overrides", "two", "child-two.js");
+                const expected = {
+                    rules: {
+                        "no-console": 0,
+                        "no-else-return": 0,
+                        "no-unused-vars": 2,
+                        quotes: [2, "double"],
+                        semi: [2, "never"]
+                    }
+                };
+                const actual = config.getConfig(targetPath);
+
+                assertConfigsEqual(actual, expected);
+            });
+
+            it("should apply overrides in parent .eslintrc over non-override rules in child .eslintrc", () => {
+                const targetPath = getFakeFixturePath("overrides", "three", "foo.js");
+                const config = new Config({
+                    cwd: getFakeFixturePath("overrides"),
+                    baseConfig: {
+                        overrides: [
+                            {
+                                files: "three/**/*.js",
+                                rules: {
+                                    "semi-style": [2, "last"]
+                                }
+                            }
+                        ]
+                    },
+                    useEslintrc: false
+                }, linter);
+                const expected = {
+                    rules: {
+                        "semi-style": [2, "last"]
+                    }
+                };
+                const actual = config.getConfig(targetPath);
+
+                assertConfigsEqual(actual, expected);
+            });
+
+            it("should apply overrides if all glob patterns match", () => {
+                const targetPath = getFakeFixturePath("overrides", "one", "child-one.js");
+                const config = new Config({
+                    cwd: getFakeFixturePath("overrides"),
+                    baseConfig: {
+                        overrides: [{
+                            files: ["one/**/*", "*.js"],
+                            rules: {
+                                quotes: [2, "single"]
+                            }
+                        }]
+                    },
+                    useEslintrc: false
+                }, linter);
+                const expected = {
+                    rules: {
+                        quotes: [2, "single"]
+                    }
+                };
+                const actual = config.getConfig(targetPath);
+
+                assertConfigsEqual(actual, expected);
+            });
+
+            it("should apply overrides even if some glob patterns do not match", () => {
+                const targetPath = getFakeFixturePath("overrides", "one", "child-one.js");
+                const config = new Config({
+                    cwd: getFakeFixturePath("overrides"),
+                    baseConfig: {
+                        overrides: [{
+                            files: ["one/**/*", "*two.js"],
+                            rules: {
+                                quotes: [2, "single"]
+                            }
+                        }]
+                    },
+                    useEslintrc: false
+                }, linter);
+                const expected = {
+                    rules: {
+                        quotes: [2, "single"]
+                    }
+                };
+                const actual = config.getConfig(targetPath);
+
+                assertConfigsEqual(actual, expected);
+            });
+
+            it("should not apply overrides if any excluded glob patterns match", () => {
+                const targetPath = getFakeFixturePath("overrides", "one", "child-one.js");
+                const config = new Config({
+                    cwd: getFakeFixturePath("overrides"),
+                    baseConfig: {
+                        overrides: [{
+                            files: "one/**/*",
+                            excludedFiles: ["two/**/*", "*one.js"],
+                            rules: {
+                                quotes: [2, "single"]
+                            }
+                        }]
+                    },
+                    useEslintrc: false
+                }, linter);
+                const expected = {
+                    rules: {}
+                };
+                const actual = config.getConfig(targetPath);
+
+                assertConfigsEqual(actual, expected);
+            });
+
+            it("should apply overrides if all excluded glob patterns fail to match", () => {
+                const targetPath = getFakeFixturePath("overrides", "one", "child-one.js");
+                const config = new Config({
+                    cwd: getFakeFixturePath("overrides"),
+                    baseConfig: {
+                        overrides: [{
+                            files: "one/**/*",
+                            excludedFiles: ["two/**/*", "*two.js"],
+                            rules: {
+                                quotes: [2, "single"]
+                            }
+                        }]
+                    },
+                    useEslintrc: false
+                }, linter);
+                const expected = {
+                    rules: {
+                        quotes: [2, "single"]
+                    }
+                };
+                const actual = config.getConfig(targetPath);
+
+                assertConfigsEqual(actual, expected);
+            });
+
+            it("should cascade", () => {
+                const targetPath = getFakeFixturePath("overrides", "foo.js");
+                const config = new Config({
+                    cwd: getFakeFixturePath("overrides"),
+                    baseConfig: {
+                        overrides: [
+                            {
+                                files: "foo.js",
+                                rules: {
+                                    semi: [2, "never"],
+                                    quotes: [2, "single"]
+                                }
+                            },
+                            {
+                                files: "foo.js",
+                                rules: {
+                                    semi: [2, "never"],
+                                    quotes: [2, "double"]
+                                }
+                            }
+                        ]
+                    },
+                    useEslintrc: false
+                }, linter);
+                const expected = {
+                    rules: {
+                        semi: [2, "never"],
+                        quotes: [2, "double"]
+                    }
+                };
+                const actual = config.getConfig(targetPath);
+
+                assertConfigsEqual(actual, expected);
+            });
+        });
     });
 
     describe("Plugin Environments", () => {
diff --git a/tests/lib/config/config-file.js b/tests/lib/config/config-file.js
index a8f6046d2..a9d2baa0d 100644
--- a/tests/lib/config/config-file.js
+++ b/tests/lib/config/config-file.js
@@ -16,7 +16,6 @@ const assert = require("chai").assert,
     os = require("os"),
     yaml = require("js-yaml"),
     shell = require("shelljs"),
-    environments = require("../../../conf/environments"),
     ConfigFile = require("../../../lib/config/config-file"),
     Linter = require("../../../lib/linter"),
     Config = require("../../../lib/config");
@@ -24,7 +23,7 @@ const assert = require("chai").assert,
 const userHome = os.homedir();
 const temp = require("temp").track();
 const proxyquire = require("proxyquire").noCallThru().noPreserveCache();
-const configContext = new Config({}, new Linter());
+let configContext;
 
 //------------------------------------------------------------------------------
 // Helpers
@@ -144,6 +143,10 @@ function createStubModuleResolver(mapping) {
 
 describe("ConfigFile", () => {
 
+    beforeEach(() => {
+        configContext = new Config({}, new Linter());
+    });
+
     describe("CONFIG_FILES", () => {
         it("should be present when imported", () => {
             assert.isTrue(Array.isArray(ConfigFile.CONFIG_FILES));
@@ -177,6 +180,8 @@ describe("ConfigFile", () => {
             }, configContext, "/whatever");
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(resolvedPath),
+                filePath: resolvedPath,
                 extends: "foo",
                 parserOptions: {},
                 env: { browser: true },
@@ -332,6 +337,8 @@ describe("ConfigFile", () => {
             }, configContext, "/whatever");
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(resolvedPaths[0]),
+                filePath: resolvedPaths[0],
                 extends: "foo",
                 parserOptions: {},
                 env: { browser: true },
@@ -346,13 +353,18 @@ describe("ConfigFile", () => {
 
         it("should apply extensions when specified from a JavaScript file", () => {
 
+            const extendsFile = ".eslintrc.js";
+            const filePath = getFixturePath("js/foo.js");
+
             const config = ConfigFile.applyExtends({
-                extends: ".eslintrc.js",
+                extends: extendsFile,
                 rules: { eqeqeq: 2 }
-            }, configContext, getFixturePath("js/foo.js"));
+            }, configContext, filePath);
 
             assert.deepEqual(config, {
-                extends: ".eslintrc.js",
+                baseDirectory: path.dirname(filePath),
+                filePath: path.join(path.dirname(filePath), extendsFile),
+                extends: extendsFile,
                 parserOptions: {},
                 env: {},
                 globals: {},
@@ -366,13 +378,18 @@ describe("ConfigFile", () => {
 
         it("should apply extensions when specified from a YAML file", () => {
 
+            const extendsFile = ".eslintrc.yaml";
+            const filePath = getFixturePath("yaml/foo.js");
+
             const config = ConfigFile.applyExtends({
-                extends: ".eslintrc.yaml",
+                extends: extendsFile,
                 rules: { eqeqeq: 2 }
-            }, configContext, getFixturePath("yaml/foo.js"));
+            }, configContext, filePath);
 
             assert.deepEqual(config, {
-                extends: ".eslintrc.yaml",
+                baseDirectory: path.dirname(filePath),
+                filePath: path.join(path.dirname(filePath), extendsFile),
+                extends: extendsFile,
                 parserOptions: {},
                 env: { browser: true },
                 globals: {},
@@ -385,13 +402,18 @@ describe("ConfigFile", () => {
 
         it("should apply extensions when specified from a JSON file", () => {
 
+            const extendsFile = ".eslintrc.json";
+            const filePath = getFixturePath("json/foo.js");
+
             const config = ConfigFile.applyExtends({
-                extends: ".eslintrc.json",
+                extends: extendsFile,
                 rules: { eqeqeq: 2 }
-            }, configContext, getFixturePath("json/foo.js"));
+            }, configContext, filePath);
 
             assert.deepEqual(config, {
-                extends: ".eslintrc.json",
+                baseDirectory: path.dirname(filePath),
+                filePath: path.join(path.dirname(filePath), extendsFile),
+                extends: extendsFile,
                 parserOptions: {},
                 env: {},
                 globals: {},
@@ -405,13 +427,18 @@ describe("ConfigFile", () => {
 
         it("should apply extensions when specified from a package.json file in a sibling directory", () => {
 
+            const extendsFile = "../package-json/package.json";
+            const filePath = getFixturePath("json/foo.js");
+
             const config = ConfigFile.applyExtends({
-                extends: "../package-json/package.json",
+                extends: extendsFile,
                 rules: { eqeqeq: 2 }
-            }, configContext, getFixturePath("json/foo.js"));
+            }, configContext, filePath);
 
             assert.deepEqual(config, {
-                extends: "../package-json/package.json",
+                baseDirectory: path.dirname(path.resolve(path.dirname(filePath), extendsFile)),
+                filePath: path.resolve(path.dirname(filePath), extendsFile),
+                extends: extendsFile,
                 parserOptions: {},
                 env: { es6: true },
                 globals: {},
@@ -437,9 +464,12 @@ describe("ConfigFile", () => {
         });
 
         it("should load information from a legacy file", () => {
-            const config = ConfigFile.load(getFixturePath("legacy/.eslintrc"), configContext);
+            const configFilePath = getFixturePath("legacy/.eslintrc");
+            const config = ConfigFile.load(configFilePath, configContext);
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 parserOptions: {},
                 env: {},
                 globals: {},
@@ -450,9 +480,12 @@ describe("ConfigFile", () => {
         });
 
         it("should load information from a JavaScript file", () => {
-            const config = ConfigFile.load(getFixturePath("js/.eslintrc.js"), configContext);
+            const configFilePath = getFixturePath("js/.eslintrc.js");
+            const config = ConfigFile.load(configFilePath, configContext);
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 parserOptions: {},
                 env: {},
                 globals: {},
@@ -469,9 +502,12 @@ describe("ConfigFile", () => {
         });
 
         it("should interpret parser module name when present in a JavaScript file", () => {
-            const config = ConfigFile.load(getFixturePath("js/.eslintrc.parser.js"), configContext);
+            const configFilePath = getFixturePath("js/.eslintrc.parser.js");
+            const config = ConfigFile.load(configFilePath, configContext);
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 parser: path.resolve(getFixturePath("js/node_modules/foo/index.js")),
                 parserOptions: {},
                 env: {},
@@ -483,9 +519,12 @@ describe("ConfigFile", () => {
         });
 
         it("should interpret parser path when present in a JavaScript file", () => {
-            const config = ConfigFile.load(getFixturePath("js/.eslintrc.parser2.js"), configContext);
+            const configFilePath = getFixturePath("js/.eslintrc.parser2.js");
+            const config = ConfigFile.load(configFilePath, configContext);
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 parser: path.resolve(getFixturePath("js/not-a-config.js")),
                 parserOptions: {},
                 env: {},
@@ -497,9 +536,12 @@ describe("ConfigFile", () => {
         });
 
         it("should interpret parser module name or path when parser is set to default parser in a JavaScript file", () => {
-            const config = ConfigFile.load(getFixturePath("js/.eslintrc.parser3.js"), configContext);
+            const configFilePath = getFixturePath("js/.eslintrc.parser3.js");
+            const config = ConfigFile.load(configFilePath, configContext);
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 parser: require.resolve("espree"),
                 parserOptions: {},
                 env: {},
@@ -511,9 +553,12 @@ describe("ConfigFile", () => {
         });
 
         it("should load information from a JSON file", () => {
-            const config = ConfigFile.load(getFixturePath("json/.eslintrc.json"), configContext);
+            const configFilePath = getFixturePath("json/.eslintrc.json");
+            const config = ConfigFile.load(configFilePath, configContext);
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 parserOptions: {},
                 env: {},
                 globals: {},
@@ -544,16 +589,26 @@ describe("ConfigFile", () => {
                 tmpFilePath = writeTempConfigFile(initialConfig, tmpFilename);
             let config = ConfigFile.load(tmpFilePath, configContext);
 
-            assert.deepEqual(config, initialConfig);
+            assert.deepEqual(config, Object.assign({}, initialConfig, {
+                baseDirectory: path.dirname(tmpFilePath),
+                filePath: tmpFilePath
+            }));
             writeTempConfigFile(updatedConfig, tmpFilename, path.dirname(tmpFilePath));
+            configContext = new Config({}, new Linter());
             config = ConfigFile.load(tmpFilePath, configContext);
-            assert.deepEqual(config, updatedConfig);
+            assert.deepEqual(config, Object.assign({}, updatedConfig, {
+                baseDirectory: path.dirname(tmpFilePath),
+                filePath: tmpFilePath
+            }));
         });
 
         it("should load information from a package.json file", () => {
-            const config = ConfigFile.load(getFixturePath("package-json/package.json"), configContext);
+            const configFilePath = getFixturePath("package-json/package.json");
+            const config = ConfigFile.load(configFilePath, configContext);
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 parserOptions: {},
                 env: { es6: true },
                 globals: {},
@@ -567,17 +622,6 @@ describe("ConfigFile", () => {
             }, /Cannot read config file/);
         });
 
-        it("should load information from a package.json file and apply environments", () => {
-            const config = ConfigFile.load(getFixturePath("package-json/package.json"), configContext, true);
-
-            assert.deepEqual(config, {
-                parserOptions: { ecmaVersion: 6 },
-                env: { es6: true },
-                globals: environments.es6.globals,
-                rules: {}
-            });
-        });
-
         it("should load fresh information from a package.json file", () => {
             const initialConfig = {
                     eslintConfig: {
@@ -603,10 +647,17 @@ describe("ConfigFile", () => {
                 tmpFilePath = writeTempConfigFile(initialConfig, tmpFilename);
             let config = ConfigFile.load(tmpFilePath, configContext);
 
-            assert.deepEqual(config, initialConfig.eslintConfig);
+            assert.deepEqual(config, Object.assign({}, initialConfig.eslintConfig, {
+                baseDirectory: path.dirname(tmpFilePath),
+                filePath: tmpFilePath
+            }));
             writeTempConfigFile(updatedConfig, tmpFilename, path.dirname(tmpFilePath));
+            configContext = new Config({}, new Linter());
             config = ConfigFile.load(tmpFilePath, configContext);
-            assert.deepEqual(config, updatedConfig.eslintConfig);
+            assert.deepEqual(config, Object.assign({}, updatedConfig.eslintConfig, {
+                baseDirectory: path.dirname(tmpFilePath),
+                filePath: tmpFilePath
+            }));
         });
 
         it("should load fresh information from a .eslintrc.js file", () => {
@@ -630,16 +681,26 @@ describe("ConfigFile", () => {
                 tmpFilePath = writeTempJsConfigFile(initialConfig, tmpFilename);
             let config = ConfigFile.load(tmpFilePath, configContext);
 
-            assert.deepEqual(config, initialConfig);
+            assert.deepEqual(config, Object.assign({}, initialConfig, {
+                baseDirectory: path.dirname(tmpFilePath),
+                filePath: tmpFilePath
+            }));
             writeTempJsConfigFile(updatedConfig, tmpFilename, path.dirname(tmpFilePath));
+            configContext = new Config({}, new Linter());
             config = ConfigFile.load(tmpFilePath, configContext);
-            assert.deepEqual(config, updatedConfig);
+            assert.deepEqual(config, Object.assign({}, updatedConfig, {
+                baseDirectory: path.dirname(tmpFilePath),
+                filePath: tmpFilePath
+            }));
         });
 
         it("should load information from a YAML file", () => {
-            const config = ConfigFile.load(getFixturePath("yaml/.eslintrc.yaml"), configContext);
+            const configFilePath = getFixturePath("yaml/.eslintrc.yaml");
+            const config = ConfigFile.load(configFilePath, configContext);
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 parserOptions: {},
                 env: { browser: true },
                 globals: {},
@@ -648,9 +709,12 @@ describe("ConfigFile", () => {
         });
 
         it("should load information from a YAML file", () => {
-            const config = ConfigFile.load(getFixturePath("yaml/.eslintrc.empty.yaml"), configContext);
+            const configFilePath = getFixturePath("yaml/.eslintrc.empty.yaml");
+            const config = ConfigFile.load(configFilePath, configContext);
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 parserOptions: {},
                 env: {},
                 globals: {},
@@ -658,21 +722,13 @@ describe("ConfigFile", () => {
             });
         });
 
-        it("should load information from a YAML file and apply environments", () => {
-            const config = ConfigFile.load(getFixturePath("yaml/.eslintrc.yaml"), configContext, true);
-
-            assert.deepEqual(config, {
-                parserOptions: {},
-                env: { browser: true },
-                globals: environments.browser.globals,
-                rules: {}
-            });
-        });
-
         it("should load information from a YML file", () => {
-            const config = ConfigFile.load(getFixturePath("yml/.eslintrc.yml"), configContext);
+            const configFilePath = getFixturePath("yml/.eslintrc.yml");
+            const config = ConfigFile.load(configFilePath, configContext);
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 parserOptions: {},
                 env: { node: true },
                 globals: {},
@@ -680,33 +736,28 @@ describe("ConfigFile", () => {
             });
         });
 
-        it("should load information from a YML file and apply environments", () => {
-            const config = ConfigFile.load(getFixturePath("yml/.eslintrc.yml"), configContext, true);
-
-            assert.deepEqual(config, {
-                parserOptions: { ecmaFeatures: { globalReturn: true } },
-                env: { node: true },
-                globals: environments.node.globals,
-                rules: {}
-            });
-        });
-
         it("should load information from a YML file and apply extensions", () => {
-            const config = ConfigFile.load(getFixturePath("extends/.eslintrc.yml"), configContext, true);
+            const configFilePath = getFixturePath("extends/.eslintrc.yml");
+            const config = ConfigFile.load(configFilePath, configContext);
 
             assert.deepEqual(config, {
-                extends: "../package-json/package.json",
-                parserOptions: { ecmaVersion: 6 },
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 env: { es6: true },
-                globals: environments.es6.globals,
+                extends: "../package-json/package.json",
+                globals: {},
+                parserOptions: {},
                 rules: { booya: 2 }
             });
         });
 
         it("should load information from `extends` chain.", () => {
-            const config = ConfigFile.load(getFixturePath("extends-chain/.eslintrc.json"), configContext);
+            const configFilePath = getFixturePath("extends-chain/.eslintrc.json");
+            const config = ConfigFile.load(configFilePath, configContext);
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 env: {},
                 extends: "a",
                 globals: {},
@@ -720,9 +771,12 @@ describe("ConfigFile", () => {
         });
 
         it("should load information from `extends` chain with relative path.", () => {
-            const config = ConfigFile.load(getFixturePath("extends-chain-2/.eslintrc.json"), configContext);
+            const configFilePath = getFixturePath("extends-chain-2/.eslintrc.json");
+            const config = ConfigFile.load(configFilePath, configContext);
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 env: {},
                 extends: "a",
                 globals: {},
@@ -735,9 +789,12 @@ describe("ConfigFile", () => {
         });
 
         it("should load information from `extends` chain in .eslintrc with relative path.", () => {
-            const config = ConfigFile.load(getFixturePath("extends-chain-2/relative.eslintrc.json"), configContext);
+            const configFilePath = getFixturePath("extends-chain-2/relative.eslintrc.json");
+            const config = ConfigFile.load(configFilePath, configContext);
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 env: {},
                 extends: "./node_modules/eslint-config-a/index.js",
                 globals: {},
@@ -750,10 +807,13 @@ describe("ConfigFile", () => {
         });
 
         it("should load information from `parser` in .eslintrc with relative path.", () => {
-            const config = ConfigFile.load(getFixturePath("extends-chain-2/parser.eslintrc.json"), configContext);
+            const configFilePath = getFixturePath("extends-chain-2/parser.eslintrc.json");
+            const config = ConfigFile.load(configFilePath, configContext);
             const parserPath = getFixturePath("extends-chain-2/parser.js");
 
             assert.deepEqual(config, {
+                baseDirectory: path.dirname(configFilePath),
+                filePath: configFilePath,
                 env: {},
                 globals: {},
                 parser: parserPath,
@@ -778,9 +838,12 @@ describe("ConfigFile", () => {
             });
 
             it("should load information from `extends` chain in .eslintrc with relative path.", () => {
-                const config = ConfigFile.load(path.join(fixturePath, "relative.eslintrc.json"), configContext);
+                const configFilePath = path.join(fixturePath, "relative.eslintrc.json");
+                const config = ConfigFile.load(configFilePath, configContext);
 
                 assert.deepEqual(config, {
+                    baseDirectory: path.dirname(configFilePath),
+                    filePath: configFilePath,
                     env: {},
                     extends: "./node_modules/eslint-config-a/index.js",
                     globals: {},
@@ -793,10 +856,13 @@ describe("ConfigFile", () => {
             });
 
             it("should load information from `parser` in .eslintrc with relative path.", () => {
-                const config = ConfigFile.load(path.join(fixturePath, "parser.eslintrc.json"), configContext);
+                const configFilePath = path.join(fixturePath, "parser.eslintrc.json");
+                const config = ConfigFile.load(configFilePath, configContext);
                 const parserPath = path.join(fixturePath, "parser.js");
 
                 assert.deepEqual(config, {
+                    baseDirectory: path.dirname(configFilePath),
+                    filePath: configFilePath,
                     env: {},
                     globals: {},
                     parser: parserPath,
@@ -818,9 +884,12 @@ describe("ConfigFile", () => {
                     }
                 });
 
-                const config = ConfigFile.load(getFixturePath("plugins/.eslintrc.yml"), stubConfig);
+                const configFilePath = getFixturePath("plugins/.eslintrc.yml");
+                const config = ConfigFile.load(configFilePath, stubConfig);
 
                 assert.deepEqual(config, {
+                    baseDirectory: path.dirname(configFilePath),
+                    filePath: configFilePath,
                     parserOptions: {},
                     env: { "test/bar": true },
                     globals: {},
@@ -834,9 +903,12 @@ describe("ConfigFile", () => {
 
         describe("even if config files have Unicode BOM,", () => {
             it("should read the JSON config file correctly.", () => {
-                const config = ConfigFile.load(getFixturePath("bom/.eslintrc.json"), configContext);
+                const configFilePath = getFixturePath("bom/.eslintrc.json");
+                const config = ConfigFile.load(configFilePath, configContext);
 
                 assert.deepEqual(config, {
+                    baseDirectory: path.dirname(configFilePath),
+                    filePath: configFilePath,
                     env: {},
                     globals: {},
                     parserOptions: {},
@@ -847,9 +919,12 @@ describe("ConfigFile", () => {
             });
 
             it("should read the YAML config file correctly.", () => {
-                const config = ConfigFile.load(getFixturePath("bom/.eslintrc.yaml"), configContext);
+                const configFilePath = getFixturePath("bom/.eslintrc.yaml");
+                const config = ConfigFile.load(configFilePath, configContext);
 
                 assert.deepEqual(config, {
+                    baseDirectory: path.dirname(configFilePath),
+                    filePath: configFilePath,
                     env: {},
                     globals: {},
                     parserOptions: {},
@@ -860,9 +935,12 @@ describe("ConfigFile", () => {
             });
 
             it("should read the config in package.json correctly.", () => {
-                const config = ConfigFile.load(getFixturePath("bom/package.json"), configContext);
+                const configFilePath = getFixturePath("bom/package.json");
+                const config = ConfigFile.load(configFilePath, configContext);
 
                 assert.deepEqual(config, {
+                    baseDirectory: path.dirname(configFilePath),
+                    filePath: configFilePath,
                     env: {},
                     globals: {},
                     parserOptions: {},
diff --git a/tests/lib/config/config-ops.js b/tests/lib/config/config-ops.js
index f0c72c488..ed95238d0 100644
--- a/tests/lib/config/config-ops.js
+++ b/tests/lib/config/config-ops.js
@@ -12,6 +12,7 @@ const assert = require("chai").assert,
     leche = require("leche"),
     environments = require("../../../conf/environments"),
     Environments = require("../../../lib/config/environments"),
+    ConfigCache = require("../../../lib/config/config-cache"),
     ConfigOps = require("../../../lib/config/config-ops");
 
 const envContext = new Environments();
@@ -475,7 +476,78 @@ describe("ConfigOps", () => {
             });
         });
 
+        describe("overrides", () => {
+            it("should combine the override entries in the correct order", () => {
+                const baseConfig = { overrides: [{ files: ["**/*Spec.js"], env: { mocha: true } }] };
+                const customConfig = { overrides: [{ files: ["**/*.jsx"], ecmaFeatures: { jsx: true } }] };
+                const expectedResult = {
+                    overrides: [
+                        { files: ["**/*Spec.js"], env: { mocha: true } },
+                        { files: ["**/*.jsx"], ecmaFeatures: { jsx: true } }
+                    ]
+                };
 
+                const result = ConfigOps.merge(baseConfig, customConfig);
+
+                assert.deepEqual(result, expectedResult);
+            });
+
+            it("should work if the base config doesn’t have an overrides property", () => {
+                const baseConfig = {};
+                const customConfig = { overrides: [{ files: ["**/*.jsx"], ecmaFeatures: { jsx: true } }] };
+                const expectedResult = {
+                    overrides: [
+                        { files: ["**/*.jsx"], ecmaFeatures: { jsx: true } }
+                    ]
+                };
+
+                const result = ConfigOps.merge(baseConfig, customConfig);
+
+                assert.deepEqual(result, expectedResult);
+            });
+
+            it("should work if the custom config doesn’t have an overrides property", () => {
+                const baseConfig = { overrides: [{ files: ["**/*Spec.js"], env: { mocha: true } }] };
+                const customConfig = {};
+                const expectedResult = {
+                    overrides: [
+                        { files: ["**/*Spec.js"], env: { mocha: true } }
+                    ]
+                };
+
+                const result = ConfigOps.merge(baseConfig, customConfig);
+
+                assert.deepEqual(result, expectedResult);
+            });
+
+            it("should work if overrides are null in the base config", () => {
+                const baseConfig = { overrides: null };
+                const customConfig = { overrides: [{ files: ["**/*.jsx"], ecmaFeatures: { jsx: true } }] };
+                const expectedResult = {
+                    overrides: [
+                        { files: ["**/*.jsx"], ecmaFeatures: { jsx: true } }
+                    ]
+                };
+
+                const result = ConfigOps.merge(baseConfig, customConfig);
+
+                assert.deepEqual(result, expectedResult);
+            });
+
+            it("should work if overrides are null in the custom config", () => {
+                const baseConfig = { overrides: [{ files: ["**/*Spec.js"], env: { mocha: true } }] };
+                const customConfig = { overrides: null };
+                const expectedResult = {
+                    overrides: [
+                        { files: ["**/*Spec.js"], env: { mocha: true } }
+                    ]
+                };
+
+                const result = ConfigOps.merge(baseConfig, customConfig);
+
+                assert.deepEqual(result, expectedResult);
+            });
+        });
     });
 
     describe("normalize()", () => {
@@ -794,4 +866,57 @@ describe("ConfigOps", () => {
 
     });
 
+    describe("getConfigFromVector()", () => {
+        let configCache;
+
+        beforeEach(() => {
+            configCache = new ConfigCache();
+        });
+
+        it("should get from merged vector cache when present", () => {
+            const vector = [
+                { filePath: "configFile1", matchingOverrides: [1] },
+                { filePath: "configFile2", matchingOverrides: [0, 1] }
+            ];
+            const merged = { merged: true };
+
+            configCache.setMergedVectorConfig(vector, merged);
+
+            const result = ConfigOps.getConfigFromVector(vector, configCache);
+
+            assert.deepEqual(result, merged);
+        });
+
+        it("should get from raw cached configs when no merged vectors are cached", () => {
+            const config = [
+                {
+                    rules: { foo1: "off" },
+                    overrides: [
+                        { files: "pattern1", rules: { foo1: "warn" } },
+                        { files: "pattern2", rules: { foo1: "error" } }
+                    ]
+                },
+                {
+                    rules: { foo2: "warn" },
+                    overrides: [
+                        { files: "pattern1", rules: { foo2: "error" } },
+                        { files: "pattern2", rules: { foo2: "off" } }
+                    ]
+                }
+            ];
+
+            configCache.setConfig("configFile1", config[0]);
+            configCache.setConfig("configFile2", config[1]);
+
+            const vector = [
+                { filePath: "configFile1", matchingOverrides: [1] },
+                { filePath: "configFile2", matchingOverrides: [0, 1] }
+            ];
+
+            const result = ConfigOps.getConfigFromVector(vector, configCache);
+
+            assert.equal(result.rules.foo1, "error");
+            assert.equal(result.rules.foo2, "off");
+        });
+    });
 });
diff --git a/tests/lib/config/config-validator.js b/tests/lib/config/config-validator.js
index 3349c07ef..d220fdb93 100644
--- a/tests/lib/config/config-validator.js
+++ b/tests/lib/config/config-validator.js
@@ -354,6 +354,50 @@ describe("Validator", () => {
             });
         });
 
+        describe("overrides", () => {
+            it("should not throw with an empty overrides array", () => {
+                const fn = validator.validate.bind(null, { overrides: [] }, "tests", linter.rules, linter.environments);
+
+                assert.doesNotThrow(fn);
+            });
+
+            it("should not throw with a valid overrides array", () => {
+                const fn = validator.validate.bind(null, { overrides: [{ files: "*", rules: {} }] }, "tests", linter.rules, linter.environments);
+
+                assert.doesNotThrow(fn);
+            });
+
+            it("should throw if override does not specify files", () => {
+                const fn = validator.validate.bind(null, { overrides: [{ rules: {} }] }, "tests", linter.rules, linter.environments);
+
+                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- \"overrides.0.files\" is required. Value: {\"rules\":{}}.\n");
+            });
+
+            it("should throw if override has an empty files array", () => {
+                const fn = validator.validate.bind(null, { overrides: [{ files: [] }] }, "tests", linter.rules, linter.environments);
+
+                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- \"overrides.0.files\" no (or more than one) schemas match. Value: [].\n");
+            });
+
+            it("should throw if override has nested overrides", () => {
+                const fn = validator.validate.bind(null, { overrides: [{ files: "*", overrides: [{ files: "*", rules: {} }] }] }, "tests", linter.rules, linter.environments);
+
+                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- Unexpected top-level property \"overrides[j].overrides\".\n");
+            });
+
+            it("should throw if override extends", () => {
+                const fn = validator.validate.bind(null, { overrides: [{ files: "*", extends: "eslint-recommended" }] }, "tests", linter.rules, linter.environments);
+
+                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- Unexpected top-level property \"overrides[j].extends\".\n");
+            });
+
+            it("should throw if override tries to set root", () => {
+                const fn = validator.validate.bind(null, { overrides: [{ files: "*", root: "true" }] }, "tests", linter.rules, linter.environments);
+
+                assert.throws(fn, "tests:\n\tESLint configuration is invalid:\n\t- Unexpected top-level property \"overrides[j].root\".\n");
+            });
+        });
+
     });
 
     describe("getRuleOptionsSchema", () => {
diff --git a/tests/lib/rules/no-trailing-spaces.js b/tests/lib/rules/no-trailing-spaces.js
index 44a6ba5b6..3cfcfda97 100644
--- a/tests/lib/rules/no-trailing-spaces.js
+++ b/tests/lib/rules/no-trailing-spaces.js
@@ -70,6 +70,18 @@ ruleTester.run("no-trailing-spaces", rule, {
             code: "let str = `${a}\n   \n${b}`;\n   \n   ",
             parserOptions: { ecmaVersion: 6 },
             options: [{ skipBlankLines: true }]
+        },
+        {
+            code: "// Trailing comment test. ",
+            options: [{ ignoreComments: true }]
+        },
+        {
+            code: "/* \nTrailing comments test. \n*/",
+            options: [{ ignoreComments: true }]
+        },
+        {
+            code: "#!/usr/bin/env node ",
+            options: [{ ignoreComments: true }]
         }
     ],
 
@@ -416,6 +428,66 @@ ruleTester.run("no-trailing-spaces", rule, {
                     column: 8
                 }
             ]
+        },
+
+        // Tests for ignoreComments flag.
+        {
+            code: "var foo = 'bar'; ",
+            output: "var foo = 'bar';",
+            options: [{ ignoreComments: true }],
+            errors: [
+                {
+                    message: "Trailing spaces not allowed.",
+                    type: "Program",
+                    line: 1,
+                    column: 17
+                }
+            ]
+        },
+        {
+            code: "// Trailing comment test. ",
+            output: "// Trailing comment test.",
+            options: [{ ignoreComments: false }],
+            errors: [
+                {
+                    message: "Trailing spaces not allowed.",
+                    type: "Program",
+                    line: 1,
+                    column: 26
+                }
+            ]
+        },
+        {
+            code: "/* \nTrailing comments test. \n*/",
+            output: "/*\nTrailing comments test.\n*/",
+            options: [{ ignoreComments: false }],
+            errors: [
+                {
+                    message: "Trailing spaces not allowed.",
+                    type: "Program",
+                    line: 1,
+                    column: 3
+                },
+                {
+                    message: "Trailing spaces not allowed.",
+                    type: "Program",
+                    line: 2,
+                    column: 24
+                }
+            ]
+        },
+        {
+            code: "#!/usr/bin/env node ",
+            output: "#!/usr/bin/env node",
+            options: [{ ignoreComments: false }],
+            errors: [
+                {
+                    message: "Trailing spaces not allowed.",
+                    type: "Program",
+                    line: 1,
+                    column: 20
+                }
+            ]
         }
     ]
 });