Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New: Add overrides and files configuration options (refs #3611) #7177

Closed
wants to merge 1 commit into from

Conversation

CrabDude
Copy link

@CrabDude CrabDude commented Sep 17, 2016

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

Please check each item to ensure your pull request is ready:

  • I've read the pull request guide
  • I've included tests for my change
  • I've updated documentation for my change (if appropriate)

What changes did you make? (Give an overview)

  1. Added the ability to add configurations targeting specific files based on glob patterns:
{
    rules: {
        semi: "error"
    },
    overrides: [
        {
            files: ["foo/*.js"],
            extends: "eslint:recommended",
            rules: {
                semi: "warn"
            }
        }

    ]
}

This mostly follows @nzakas' micro-proposal, but with grunt-style aggregating glob pattern arrays.

  1. Importantly, this means files within the same directory no longer necessarily share the same config. Consequently, configs can no longer be cached per directory. To avoid performance degradation due to lack of directory config caching, a new caching strategy was implemented, which necessitated a refactoring of lib/config.js.

Specifically, a vector based caching was implemented, where a vector is an array of absolute config file paths within a hierarchy that apply to a given file, including separate entries for "overrides" sub-configs. This hierarchy can then be used to generate a hash for caching, or to merge the config hierarchy and then cached and returned from Config.prototype.getConfig(filePath).

Example
Given the following config hierarchy:

project-root
├── app
   ├── lib
      ├── foo.js
      ├── fooSpec.js
      ├── bazSpec.js
   ├── components
      ├── bar.js
      ├── barSpec.js
   ├── .eslintrc
├── server
   ├── server.js
   ├── serverSpec.js
├── .eslintrc

With corresponding configs:

// project-root/.eslintrc
{
  rules: { "no-unused-expression": 2, semi: 0 },
  overrides: [{
    files: "**/*Spec.js",
    rules: { "no-unused-expression": 0 }
  }]
} 
// project-root/app/.eslintrc
{
  env: { node: true },
  rules: { no-console: 1 },
  overrides: [
    {
      files: "lib/*Spec.js",
      rules: { "no-console": 0 }
    },
    {
      files: "components/*Spec.js",
      env: { browser: true }
    },
    {
      files: "**/*Spec.js",
      env: { "mocha": true }
    }
  ]
} 

For project-root/app/lib/fooSpec.js, where /project-root/ is the absolute path to project-root, the corresponding config vector and caching hash (included for illustration, but not important) would be:

// Config vector
[
  '/project-root/.eslintrc',
  '/project-root/.eslintrc:0',
  '/project-root/app/.eslintrc',
  '/project-root/app/.eslintrc:0',
  // Note the absence of sub-config 1 here
  '/project-root/app/.eslintrc:2'
]

// Config vector hash
'/project-root/.eslintrc,/project-root/.eslintrc:0,/project-root/app/.eslintrc,/project-root/app/.eslintrc:0,/project-root/app/.eslintrc:2'

And the resulting corresponding config value (cached at config.mergedCache[hash]):

{
  env: {
    node: true,
    mocha: true
  }
  rules: {
    "no-unused-expression": 0,
    "no-console": 0,
    semi: 0
  }
}

// Where the cache includes the properly expanded "env" values

And getConfig('project-root/app/lib/bazSpec.js') would load from cache.

Details
To maximize the performance benefits of refactoring the config loader caching layer, several other layers of caching (on the config instance) were implemented including config file contents caching, iterative config caching (a merged sub-vector cache), config hierarchy caching by vector and directory-based local config file hierarchy caching.

As a result, from the previous example, all of the files within project-root would load from various degrees of partial cache.

Results
The net performance gain from these caching improvements is a consistent 15% performance increase when run against the Pinterest web codebase regardless of whether an override config is utilized.

Is there anything you'd like reviewers to focus on?

Edge cases.

@lo1tuma

EDIT: Added an expanded example.

@eslintbot
Copy link

LGTM

@nzakas
Copy link
Member

nzakas commented Sep 19, 2016

@CrabDude wow, thanks for digging into this. These are some major changes that I don't have enough energy to get to today, but I did want to leave a note to let you know I'll definitely look at this during this week (hopefully Wednesday).

I do have a couple of questions in the meantime:

  1. How many overrides are applied to a file? Is is just the first matching one or all matching overrides?
  2. What happens if an override extends a config that also has overrides?

Thanks again!

@CrabDude
Copy link
Author

CrabDude commented Sep 19, 2016

@nzakas Thanks for your consideration.

I added an expanded example just before you commented, so you may not yet have seen it, but it illustrates that "overrides" sub-configs are treated as peers within the config hierarchy, so just as an arbitrary number of .eslintrc files can exist within a hierarchy, so too can an arbitrary number of overrides occur.

As a result of this implementation, ATM, the same config merging rules apply to sub-configs. So...

  1. All matching overrides w/n the hierarchy are applied and merged as full configs (as though there were an .eslintrc in an interstitial directory.
  2. Presently, (I need to push the fix for this) all configs are expanded as regular configs through loadConfig, so "plugsins", "extends", etc... should work, but since the "overrides" logic is manual on a per-config-file basis, nested "overrides" I assume will get merged by ConfigFile.applyExtends in loadConfig (I need to check on this also and some tests).

Thanks for the feedback. I'll add some more tests for the "extends", "plugins" and nested "extends" "overrides" cases.

@CrabDude
Copy link
Author

CrabDude commented Sep 20, 2016

Still working on a fix for nested overrides. Merging it with the fix for extends in overrides. Will push later today soon.

Copy link
Member

@nzakas nzakas left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Once again, thanks for doing this. I think overall you're heading in the right direction.

One thing I'd like to explore, though, is if we can pull some of the logic out of config.js and move it into either config-ops.js or config-file.js. The config.js file is really difficult to unit test, so we started breaking it apart into the smaller files (didn't quite finish).

For instance, it seems like the logic to say "given this config and this filename, give me a config that has overrides merged in could potentially live in config-ops.js (as it doesn't rely on the filesystem). And maybe the actual merging could take place in config-file.js somewhere. I'm open to feedback on this point, I just really want to keep config.js as small as possible because of the difficulty in unit testing.

@@ -703,6 +703,65 @@ module.exports = {
}
```

## Glob Pattern based Configuration
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe "Configuration Based on Glob Patterns"?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #8081.

@@ -703,6 +703,65 @@ module.exports = {
}
```

## Glob Pattern based Configuration

Sometimes a more fine-controlled configuration is necessary e.g. if the configuration for files within the same directory has to be different. Therefore you can provide configurations that will only apply to files that match a specific glob pattern (based on [minimatch](https://github.com/isaacs/minimatch)).
Copy link
Member

@nzakas nzakas Sep 22, 2016

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a way to not use minimatch? I'm concerned that the differences between minimatch and globs that we use on the command line will be confusing for users (why will one thing work on the command line but not in the config?).

See also: https://github.com/eslint/eslint/blob/master/lib/util/glob-util.js

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I had a look at using the existing glob code, but it seems to be a different use case (it uses node-glob, which is the filesystem-aware version of minimatch). In this case, we've already retrieved a list of candidate files from the filesystem, and we just use minimatch to filter them. Since node-glob uses minimatch under the hood, I think the results should be essentially the same, so I don't think there's too much potential for confusion.

this.ignore = options.ignore;
this.ignorePath = options.ignorePath;
this.cache = {};
this.mergedCache = {};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add some comments describing what each of these does? Lots of "caches" are hard to distinguish from one another.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Many comments added in #8081.



/**
* Get the vector of applicable configs from the hierarchy for a given file (glob matching occurs here).
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm unclear on what "vector" is in this context. Can you use a different word?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I tried to think of a better word for this but came up blank. I fell back to describing it in detail in comments in the JSDoc:

A vector is an array of config file paths, each optionally followed by one or more numbers that correspond to the indices of nested config blocks within the config file's overrides section.


/**
* Merges all configurations for a given config vector
* @param {Array} vector array of config file paths or relative override indices
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

An array of what? If strings, then use string[]. Otherwise, please use a type-specific array. (And same note about the word "vector", I just don't understand what that means.)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #8081.

@@ -54,6 +54,10 @@
"json-stable-stringify": "^1.0.0",
"levn": "^0.3.0",
"lodash": "^4.0.0",
"lodash.clonedeep": "^3.0.1",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We already include lodash as a whole, so there's no need to include individual methods.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in #8081.

if (this.useSpecificConfig) {
debug("Merging command line config file");
/**
* Get the local config hierarchy for a given directory.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Similar to previous note about adding comments, it's unclear what "local" means here. Can you be more specific in this comment?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

JSDoc has been rewritten and expanded in #8081.

@@ -48,6 +48,12 @@
".eslintrc": "{\n \"env\": {\n \"commonjs\": true\n }\n}\n"
}
},
"overrides": {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just double-checking: are you sure this won't interfere with already-existing tests?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All the tests still pass, so this seems to be OK! :)

@CrabDude
Copy link
Author

CrabDude commented Oct 2, 2016

Should've mentioned I've been OOO since 9/22. Will pick up where we left off this week.

@nzakas
Copy link
Member

nzakas commented Oct 31, 2016

Just a heads up: we have moved to a new CLA checker on pull requests. Even if you've previously signed our CLA, we will need to you sign the new one. To do so, look at the status checks for licence/cla and click the "Details" link. Sorry for the inconvenience.

@nzakas nzakas removed the CLA: Valid label Oct 31, 2016
@vitorbal
Copy link
Member

@CrabDude thanks again for all your hard work on this one! Definitely appreciated.

This seems like such an awesome change, it would be a shame to let it fall through the cracks. Is there anything that we could do to help move this forward?

@vitorbal vitorbal added core Relates to ESLint's core APIs and features evaluating The team will evaluate this issue to decide whether it meets the criteria for inclusion labels Nov 11, 2016
@CrabDude
Copy link
Author

@vitorbal Per #7177 (comment), it's feature complete, but needs to be refactored into more testable files / architecture, with the corresponding tests updated (and added). If you were so motivated, I suppose you could write some tests for the spec defined here, as it would help with the refactor.

Also, the encouragement helps. 🙂

@kaicataldo
Copy link
Member

@CrabDude Friendly ping again. Anything you need from us to help finish this up?

@CrabDude
Copy link
Author

CrabDude commented Jan 9, 2017

@kaicataldo While I still intend to get back to this, I'm not sure when that will be. If you or anyone wants to speed this up, please feel free to write tests that cover the implementation as documented above.

@nzakas
Copy link
Member

nzakas commented Jan 14, 2017

This is on my todo list for when I'm able to spend some extended time at a computer.

@smably
Copy link
Contributor

smably commented Feb 1, 2017

I rebased this onto latest master and fixed the conflicts (plus a couple of the really easy fixes from @nzakas' code review above): https://github.com/smably/eslint/tree/overrides

I haven't had a chance to really dig into the logic, and I don't know whether I will have the time to do it, but I hope this is at least a start for whoever takes this on.

@nzakas -- do you still intend to come back to this? If not, I might be able to dig a bit deeper into this sometime later this week.

@CrabDude
Copy link
Author

CrabDude commented Feb 1, 2017

@smably Thanks so much for wading into this! Definitely motivating to not be working on this alone.

I don't believe @nzakas ever had/has any plans to work on this. I plan to finish this, but had to come back to it. I should be able to actively start working on this again.

Would you be open to working on it together a bit? We can figure out how specifically you would like to contribute once I dive back in.

Off the top of my head there are 2 tasks:

  1. Refactor per @nzakas request
  2. Comprehensive tests

@not-an-aardvark
Copy link
Member

I don't believe @nzakas ever had/has any plans to work on this. I plan to finish this, but had to come back to it. I should be able to actively start working on this again.

Thanks! To clarify re. @nzakas's plans to work on this, I think @smably was referring to #7177 (comment).

@smably
Copy link
Contributor

smably commented Feb 2, 2017

@CrabDude Awesome, glad to hear you're able to pick this up again. I'd love to help, though I think it'll be a few days, at least, before I can dedicate any time to this. Keep me posted on how I can best contribute!

@smably
Copy link
Contributor

smably commented Feb 5, 2017

I had a look into splitting some of the logic out of config.js but ran into issues with caching support. config-file.js and config-ops.js are both collections of stateless helper functions, so I'm not sure how best to give them access to the caches (or whether that's even a good idea at all). One thought I had was to split all the caching-related logic out into a new file, config-cache.js, but I will defer to someone more experienced to advise on whether or not that is a good idea. I'm happy to help with the refactoring, but I might need a bit of help to get started, given the complexity of the caching logic.

In any case, I updated my version of the branch with a couple of other fixes and verified that all tests are still passing. Next time I have a chance to look at this, I'll see about adding some more comprehensive tests, though I do wonder whether they'll have to be rewritten anyway to support the refactor...

@CrabDude
Copy link
Author

CrabDude commented Feb 6, 2017

@smably The refactor should not affect the tests, as the tests should be written against the spec, as defined in my OP at the top. This is one of the reasons why I pointed to new tests that cover the spec as an easy task for anyone looking to contribute.

Thanks for the useful thoughts on the refactor, and proposing a config-cache.js.

@alberto
Copy link
Member

alberto commented Feb 7, 2017

@CrabDude I'm sorry, but we need you to sign our new CLA. Thanks!

@smably
Copy link
Contributor

smably commented Feb 15, 2017

Added a bunch of updates and opened a new PR: #8081.

@alberto
Copy link
Member

alberto commented Feb 19, 2017

@CrabDude I'm sorry for the insistence, but we need you to sign our new CLA. Even if you don't plan to finish it (we understand, life changes priorities) we would need your signature to be able to continue from here.

Thanks for your contribution!

smably pushed a commit to smably/eslint that referenced this pull request Feb 26, 2017
smably pushed a commit to smably/eslint that referenced this pull request Feb 27, 2017
smably pushed a commit to smably/eslint that referenced this pull request Mar 13, 2017
@kaicataldo
Copy link
Member

Closing this as it seems abandoned and the same work is being in #8081. Please feel free to reopen if that's not the case @CrabDude, and thanks for contributing :)

@kaicataldo kaicataldo closed this Apr 20, 2017
smably pushed a commit to smably/eslint that referenced this pull request Apr 25, 2017
smably pushed a commit to smably/eslint that referenced this pull request May 7, 2017
@CrabDude
Copy link
Author

@alberto Not sure if it's still relevant, but signed.

@alberto
Copy link
Member

alberto commented Jun 30, 2017

Yes, thank you @CrabDude!

@eslint-deprecated eslint-deprecated bot locked and limited conversation to collaborators Feb 6, 2018
@eslint-deprecated eslint-deprecated bot added the archived due to age This issue has been archived; please open a new issue for any further discussion label Feb 6, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
archived due to age This issue has been archived; please open a new issue for any further discussion core Relates to ESLint's core APIs and features evaluating The team will evaluate this issue to decide whether it meets the criteria for inclusion
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

10 participants