As of ESLint 4.9.0, there are several configuration options that we don't want to allow in config files. For example, we don't allow --rulesdir in config files (https://github.com/eslint/eslint/issues/8769) because the option is project-specific and it wouldn't make sense for shareable configs to use it. We also don't allow --report-unused-disable-directives in a config file because we don't want shareable configs to use the option, due to semver-related issues described in https://github.com/eslint/eslint/issues/9382. Options like --ext wouldn't work in config files because they need to be resolved before the list of files to lint is processed.
As an alternative to config files, users currently configure these behaviors as command-line flags. Unfortunately, specifying options with command-line flags is not ideal for user experience. The main issue is that other tools which run ESLint (such as editor integrations) don't have access to these command-line flags, so they typically require manual configuration by each user in order to work correctly when a project uses CLI flags. This creates a poor experience for users and discourages the use of project-specific rules, which is something I think we should be encouraging.
Existing workarounds
Existing workarounds to this problem include:
Using plugins instead of using --rulesdir. Unfortunately, as discussed in https://github.com/eslint/eslint/issues/8769, this solution is often inconvenient for project-specific rules. Plugins are currently required to be in node_modules/, and it's generally useful to develop custom rules in the same repo as the code that they lint.
Using "loopback" plugins like eslint-plugin-rulesdir, eslint-plugin-local, eslint-plugin-self, and eslint-plugin-local-rules for project-specific linting rules. These plugins reside in node_modules and export rule files from the project that loads the plugin. This allows rules to be loaded from the project repo. Unfortunately, these modules are also inconvenient to use (e.g. they generally require that a project create a new file in the root directory), and they also have footguns (they don't work when ESLint is installed globally).
I don't know of any workarounds for other command-line flags, such as --report-unused-disable-directives and --ext.
Proposed solution
I think we should modify CLIEngine to load default options from a package.json file in the CWD, if present.
Details
When a new instance of CLIEngine is created, it looks for a eslintCliOptions property in a package.json file in the CWD.
If no package.json file is found, or the package.json file has no eslintCliOptions property, the behavior is the same as the current behavior.
When an eslintCliOptions property is found, the values of that object are used as defaults for CLIEngine. For example, cliEngineOptions could contain "reportUnusedDisableDirectives": true, "rulePaths": ["tools/internal-rules"] to reflect the --report-unused-disable-directives and --rulesdir CLI options.
Those values are then used as defaults for that instance of CLIEngine, and are merged in with the other options passed to the constructor. Options in the constructor take precedence over options in package.json, so anything in eslintCliOptions could be overridden by actually using a command-line flag.
Advantages
This would allow projects to declare project-wide options for ESLint (such as reportUnusedDisableDirectives and rulePaths) in a manner that would allow the options to automatically get used by external integrations.
The options would not be stored in a config file, avoiding confusion about whether the options would be inheritable in shared configs.
I've listed these disadvantages in order of decreasing significance, in my opinion. I consider the first items on the list to be the most significant disadvantages.
This would increase the complexity of ESLint's option resolution behavior, which could lead to increased confusion if a user is wondering why ESLint is doing a particular thing.
Using the fix option for CLIEngine could be very confusing for users, because it is different from the --fix command-line flag. The fix option does not call CLIEngine.outputFixes, so it would effectively be the same as the --fix-dry-run command-line flag. In other words, ESLint would not report most autofixable errors, but the autofixes would not actually get written to the filesystem.
Potential solutions for this problem include:
Doing nothing, and leaving a warning in the docs about using fix in eslintCliOptions.
Adding a special case for fix when used in eslintCliOptions, such as outputting a warning or disallowing the property entirely.
Translating command-line options names into CLIEngine option names isn't always intuitive. For example, the --rulesdir command-line flag translates to rulePaths in CLIEngine. This could make CLIEngine configuration confusing for users, because they would need to read the CLIEngine docs when they didn't need to do so previously. Additionally, some CLI flags (e.g. --version) don't have an equivalent in CLIEngine at all. This problem could partially be resolved by doing type-checking at runtime, and disallowing unknown properties.
The rules option for CLIEngine would be redundant with simply specifying rules in a config file. To address this, we could either (a) have the option work anyway for consistency, or (b) add a special case for the rules option (probably to disallow it in eslintCliOptions).
This would slightly hurt performance because it would add a filesystem read whenever a CLIEngine instance is created. (A CLIEngine instance usually reads a lot of files during its lifetime anyway, so I don't think this would be a very big problem.)
This would result in ESLint using three different keys in package.json (eslintConfig, eslintIgnore, and eslintCliOptions), which could be mildly annoying to some users.
Problem
As of ESLint 4.9.0, there are several configuration options that we don't want to allow in config files. For example, we don't allow
--rulesdir
in config files (https://github.com/eslint/eslint/issues/8769) because the option is project-specific and it wouldn't make sense for shareable configs to use it. We also don't allow--report-unused-disable-directives
in a config file because we don't want shareable configs to use the option, due to semver-related issues described in https://github.com/eslint/eslint/issues/9382. Options like--ext
wouldn't work in config files because they need to be resolved before the list of files to lint is processed.As an alternative to config files, users currently configure these behaviors as command-line flags. Unfortunately, specifying options with command-line flags is not ideal for user experience. The main issue is that other tools which run ESLint (such as editor integrations) don't have access to these command-line flags, so they typically require manual configuration by each user in order to work correctly when a project uses CLI flags. This creates a poor experience for users and discourages the use of project-specific rules, which is something I think we should be encouraging.
Existing workarounds
Existing workarounds to this problem include:
--rulesdir
. Unfortunately, as discussed in https://github.com/eslint/eslint/issues/8769, this solution is often inconvenient for project-specific rules. Plugins are currently required to be innode_modules/
, and it's generally useful to develop custom rules in the same repo as the code that they lint.node_modules
and export rule files from the project that loads the plugin. This allows rules to be loaded from the project repo. Unfortunately, these modules are also inconvenient to use (e.g. they generally require that a project create a new file in the root directory), and they also have footguns (they don't work when ESLint is installed globally).I don't know of any workarounds for other command-line flags, such as
--report-unused-disable-directives
and--ext
.Proposed solution
I think we should modify
CLIEngine
to load default options from apackage.json
file in the CWD, if present.Details
eslintCliOptions
property in apackage.json
file in the CWD.package.json
file is found, or thepackage.json
file has noeslintCliOptions
property, the behavior is the same as the current behavior.eslintCliOptions
property is found, the values of that object are used as defaults forCLIEngine
. For example,cliEngineOptions
could contain"reportUnusedDisableDirectives": true, "rulePaths": ["tools/internal-rules"]
to reflect the--report-unused-disable-directives
and--rulesdir
CLI options.CLIEngine
, and are merged in with the other options passed to the constructor. Options in the constructor take precedence over options inpackage.json
, so anything ineslintCliOptions
could be overridden by actually using a command-line flag.Advantages
reportUnusedDisableDirectives
andrulePaths
) in a manner that would allow the options to automatically get used by external integrations.topLevelOptions
values in a config file, along the lines of what was discussed in https://github.com/eslint/eslint/issues/2715#issuecomment-305989282.Disadvantages
I've listed these disadvantages in order of decreasing significance, in my opinion. I consider the first items on the list to be the most significant disadvantages.
This would increase the complexity of ESLint's option resolution behavior, which could lead to increased confusion if a user is wondering why ESLint is doing a particular thing.
Using the
fix
option forCLIEngine
could be very confusing for users, because it is different from the--fix
command-line flag. Thefix
option does not callCLIEngine.outputFixes
, so it would effectively be the same as the--fix-dry-run
command-line flag. In other words, ESLint would not report most autofixable errors, but the autofixes would not actually get written to the filesystem.Potential solutions for this problem include:
fix
ineslintCliOptions
.fix
when used ineslintCliOptions
, such as outputting a warning or disallowing the property entirely.Translating command-line options names into
CLIEngine
option names isn't always intuitive. For example, the--rulesdir
command-line flag translates torulePaths
inCLIEngine
. This could makeCLIEngine
configuration confusing for users, because they would need to read theCLIEngine
docs when they didn't need to do so previously. Additionally, some CLI flags (e.g.--version
) don't have an equivalent inCLIEngine
at all. This problem could partially be resolved by doing type-checking at runtime, and disallowing unknown properties.The
rules
option forCLIEngine
would be redundant with simply specifying rules in a config file. To address this, we could either (a) have the option work anyway for consistency, or (b) add a special case for therules
option (probably to disallow it ineslintCliOptions
).This would slightly hurt performance because it would add a filesystem read whenever a
CLIEngine
instance is created. (ACLIEngine
instance usually reads a lot of files during its lifetime anyway, so I don't think this would be a very big problem.)This would result in ESLint using three different keys in
package.json
(eslintConfig
,eslintIgnore
, andeslintCliOptions
), which could be mildly annoying to some users.