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 --fix-type option to CLI (fixes #10855) #10912

Merged
merged 25 commits into from Nov 6, 2018
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
e173dbc
New: Add --fix-type option to CLI (fixes #10855)
nzakas Sep 19, 2018
f50690f
Chore: Update rule types
nzakas Oct 2, 2018
9105a5f
Docs: Update Nodejs API docs with new CLIEngine option
nzakas Oct 3, 2018
bc6be27
Chore: Fix lint errors
nzakas Oct 3, 2018
67c9581
Chore: Update options message for --fix-type
nzakas Oct 3, 2018
01ed312
CLIEngine validates fixType values
nzakas Oct 3, 2018
a40c6d9
Check for rule.meta when filtering fixes
nzakas Oct 3, 2018
febe03f
Call getRules() just once when filtering fixes
nzakas Oct 3, 2018
dfc80d3
Set no-multiline-str to suggestion
nzakas Oct 3, 2018
9293d08
Add check for missing rule info in rule-types.json
nzakas Oct 4, 2018
98bc478
Account for late definition of rules in CLIEngine
nzakas Oct 4, 2018
b5782f9
Add ruleType data to doc page for rules
nzakas Oct 4, 2018
a35500a
Remove duplicate test
nzakas Oct 5, 2018
047a242
Update spacing rules to be of type style
nzakas Oct 5, 2018
2b31a12
Fix lint issues in update-rule-types.js
nzakas Oct 17, 2018
bc74300
Change ruleType to rule_type in doc page front matter
nzakas Oct 19, 2018
19c0f65
Update rule type mappings based on feedback
nzakas Oct 19, 2018
9ee371f
Update rules to use 'layout' instead of 'style'
nzakas Oct 29, 2018
c5feaad
Fix update-rule-types script and prefer-arrow-callback
nzakas Oct 29, 2018
4873de4
Fix lint error
nzakas Oct 29, 2018
c866717
Update rule types based on latest feedback
nzakas Nov 2, 2018
6ce2222
Fix bug CLIEngine
nzakas Nov 2, 2018
1b546cf
Update new rule issue template with category info
nzakas Nov 5, 2018
7e80da5
Final update of rule types
nzakas Nov 5, 2018
5e1c448
Refactor CLIEngine piece based on feedback
nzakas Nov 5, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/ISSUE_TEMPLATE/NEW_RULE.md
Expand Up @@ -21,9 +21,9 @@ about: Propose a new rule to be added to ESLint

**What category of rule is this? (place an "X" next to just one item)**

[ ] Enforces code style
[ ] Warns about a potential error
[ ] Suggests an alternate way of doing something
[ ] Warns about a potential error (problem)
[ ] Suggests an alternate way of doing something (suggestion)
[ ] Enforces code style (layout)
[ ] Other (please specify:)

**Provide 2-3 code examples that this rule will warn about:**
Expand Down
24 changes: 22 additions & 2 deletions Makefile.js
Expand Up @@ -708,6 +708,7 @@ target.gensite = function(prereleaseVersion) {
ruleName = path.basename(filename, ".md"),
filePath = path.join("docs", path.relative("tmp", filename));
let text = cat(filename),
ruleType = "",
title;

process.stdout.write(`> Updating files (Steps 4-9): ${i}/${length} - ${filePath + " ".repeat(30)}\r`);
Expand All @@ -726,8 +727,11 @@ target.gensite = function(prereleaseVersion) {
const ruleDocsContent = textSplit.slice(1).join("\n");

text = `${ruleHeading}${isRecommended ? RECOMMENDED_TEXT : ""}${isFixable ? FIXABLE_TEXT : ""}\n${ruleDocsContent}`;

title = `${ruleName} - Rules`;

if (rule && rule.meta) {
ruleType = `rule_type: ${rule.meta.type}`;
}
} else {

// extract the title from the file itself
Expand All @@ -744,6 +748,7 @@ target.gensite = function(prereleaseVersion) {
`title: ${title}`,
"layout: doc",
`edit_link: https://github.com/eslint/eslint/edit/master/${filePath}`,
ruleType,
nzakas marked this conversation as resolved.
Show resolved Hide resolved
"---",
"<!-- Note: No pull requests accepted for this file. See README.md in the root directory for details. -->",
"",
Expand Down Expand Up @@ -863,7 +868,7 @@ target.checkRuleFiles = function() {
echo("Validating rules");

const eslintRecommended = require("./conf/eslint-recommended").rules;

const ruleTypes = require("./tools/rule-types.json");
const ruleFiles = find("lib/rules/").filter(fileType("js"));
let errors = 0;

Expand All @@ -880,6 +885,15 @@ target.checkRuleFiles = function() {
return Object.prototype.hasOwnProperty.call(eslintRecommended, basename);
}

/**
* Check if basename is present in rule-types.json file.
* @returns {boolean} true if present
* @private
*/
function isInRuleTypes() {
return Object.prototype.hasOwnProperty.call(ruleTypes, basename);
}

/**
* Check if id is present in title
* @param {string} id id to check for
Expand Down Expand Up @@ -918,6 +932,12 @@ target.checkRuleFiles = function() {
errors++;
}

// check for recommended configuration
if (!isInRuleTypes()) {
console.error("Missing setting for %s in tools/rule-types.json", basename);
errors++;
}

// check for tests
if (!test("-f", `tests/lib/rules/${basename}.js`)) {
console.error("Missing tests for rule %s", basename);
Expand Down
3 changes: 2 additions & 1 deletion docs/developer-guide/nodejs-api.md
Expand Up @@ -347,7 +347,8 @@ The `CLIEngine` is a constructor, and you can create a new instance by passing i
* `cwd` - Path to a directory that should be considered as the current working directory.
* `envs` - An array of environments to load (default: empty array). Corresponds to `--env`.
* `extensions` - An array of filename extensions that should be checked for code. The default is an array containing just `".js"`. Corresponds to `--ext`. It is only used in conjunction with directories, not with filenames or glob patterns.
* `fix` - This can be a boolean or a function which will be provided each linting message and should return a boolean. True indicates that fixes should be included with the output report, and that errors and warnings should not be listed if they can be fixed. However, the files on disk will not be changed. To persist changes to disk, call [`outputFixes()`](#cliengineoutputfixes).
* `fix` - A boolean or a function (default: `false`). If a function, it will be passed each linting message and should return a boolean indicating whether the fix should be included with the output report (errors and warnings will not be listed if fixed). Files on disk are never changed regardless of the value of `fix`. To persist changes to disk, call [`outputFixes()`](#cliengineoutputfixes).
* `fixTypes` - An array of rule types for which fixes should be applied (default: `null`). This array acts like a filter, only allowing rules of the given types to apply fixes. Possible array values are `"problem"`, `"suggestion"`, and `"layout"`.
* `globals` - An array of global variables to declare (default: empty array). Corresponds to `--global`.
* `ignore` - False disables use of `.eslintignore`, `ignorePath` and `ignorePattern` (default: true). Corresponds to `--no-ignore`.
* `ignorePath` - The ignore file to use instead of `.eslintignore` (default: null). Corresponds to `--ignore-path`.
Expand Down
7 changes: 7 additions & 0 deletions docs/developer-guide/working-with-rules.md
Expand Up @@ -26,6 +26,8 @@ Here is the basic format of the source file for a rule:

module.exports = {
meta: {
type: "suggestion",
nzakas marked this conversation as resolved.
Show resolved Hide resolved

docs: {
description: "disallow unnecessary semicolons",
category: "Possible Errors",
Expand All @@ -49,6 +51,11 @@ The source file for a rule exports an object with the following properties.

`meta` (object) contains metadata for the rule:

* `type` (string) indicates the type of rule, which is one of `"problem"`, `"suggestion"`, or `"layout"`:
* `"problem"` means the rule is identifying code that either will cause an error or may cause a confusing behavior. Developers should consider this a high priority to resolve.
* `"suggestion"` means the rule is identifying something that could be done in a better way but no errors will occur if the code isn't changed.
nzakas marked this conversation as resolved.
Show resolved Hide resolved
* `"layout"` means the rule cares primarily about whitespace, semicolons, commas, and parentheses, all the parts of the program that determine how the code looks rather than how it executes. These rules work on parts of the code that aren't specified in the AST.

* `docs` (object) is required for core rules of ESLint:

* `description` (string) provides the short description of the rule in the [rules index](../rules/)
Expand Down
19 changes: 19 additions & 0 deletions docs/user-guide/command-line-interface.md
Expand Up @@ -46,6 +46,7 @@ Specifying rules and plugins:
Fixing problems:
--fix Automatically fix problems
--fix-dry-run Automatically fix problems without saving the changes to the file system
--fix-type Array Specify the types of fixes to apply (problem, suggestion, layout)

Ignoring files:
--ignore-path path::String Specify path of ignore file
Expand Down Expand Up @@ -233,6 +234,24 @@ getSomeText | eslint --stdin --fix-dry-run --format=json

This flag can be useful for integrations (e.g. editor plugins) which need to autofix text from the command line without saving it to the filesystem.

#### `--fix-type`

This option allows you to specify the type of fixes to apply when using either `--fix` or `--fix-dry-run`. The three types of fixes are:

1. `problem` - fix potential errors in the code
Copy link
Member

Choose a reason for hiding this comment

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

Bikeshed: I think we currently use the term "problem" to mean "a warning or error reported by ESLint". For example, the default formatter outputs a message along the lines of ✖ 1 problem (1 error, 0 warnings).

I'm wondering if using the term "problem" to refer to potential errors could lead to confusion, because when a stylistic rule creates a report, it's also called a "problem" with the current terminology. Is there a term we could use other than "problem" to describe reports that address potential errors in code? (We've also used "messages" in some places to refer to reported errors/warnings, although this is also confusing because it sometimes refers to the text associated with a problem rather than the problem itself.)

Copy link
Member Author

Choose a reason for hiding this comment

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

Yeah, I also went back and forth on this, and believe that "problem" is the correct term. Some of the other terms we use:

  • "error" is a severity level, so definitely can't use that
  • We use "messages" in the output data structure as a generic catchall for everything, so we don't need to worry about that.
  • As best I can tell, "problem" is only ever used in the default formatter output. My feeling is that this isn't too confusing, but if others feel it is, I'd suggest changing the term to "messages" to echo what is in the data structure.

Copy link
Member

Choose a reason for hiding this comment

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

Should we change the stylish formatter? We could use "issue" there, for example, whereas "issue" doesn't work as well for the rule type (IMO).

(I'm aware "issue" also has meaning in GitHub, but I'm not worried about confusion between lint output and GitHub at this point.)

Copy link
Member Author

Choose a reason for hiding this comment

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

I wouldn't want to change the formatter as part of this PR, as I think that's a breaking change (we never know who is parsing the output). I'm open to changing it, though as I said, I also think it's fine if we don't.

Copy link
Member

Choose a reason for hiding this comment

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

problem feels better to me than messages because messages sounds all-encompassing - it's not immediately clear how messages differs from suggestion or style

1. `suggestion` - apply fixes to the code that improve it
1. `layout` - apply fixes that do not change the program structure (AST)

You can specify one or more fix type on the command line. Here are some examples:

```
eslint --fix --fix-type suggestion .
eslint --fix --fix-type suggestion --fix-type problem .
eslint --fix --fix-type suggestion,layout .
```

This option is helpful if you are using another program to format your code but you would still like ESLint to apply other types of fixes.

### Ignoring files

#### `--ignore-path`
Expand Down
57 changes: 55 additions & 2 deletions lib/cli-engine.js
Expand Up @@ -33,6 +33,7 @@ const fs = require("fs"),

const debug = require("debug")("eslint:cli-engine");
const resolver = new ModuleResolver();
const validFixTypes = new Set(["problem", "suggestion", "layout"]);

//------------------------------------------------------------------------------
// Typedefs
Expand All @@ -50,6 +51,7 @@ const resolver = new ModuleResolver();
* @property {string[]} envs An array of environments to load.
* @property {string[]} extensions An array of file extensions to check.
* @property {boolean|Function} fix Execute in autofix mode. If a function, should return a boolean.
* @property {string[]} fixTypes Array of rule types to apply fixes for.
* @property {string[]} globals An array of global variables to declare.
* @property {boolean} ignore False disables use of .eslintignore.
* @property {string} ignorePath The ignore file to use instead of .eslintignore.
Expand Down Expand Up @@ -86,6 +88,21 @@ const resolver = new ModuleResolver();
// Helpers
//------------------------------------------------------------------------------

/**
* Determines if each fix type in an array is supported by ESLint and throws
* an error if not.
* @param {string[]} fixTypes An array of fix types to check.
* @returns {void}
* @throws {Error} If an invalid fix type is found.
*/
function validateFixTypes(fixTypes) {
for (const fixType of fixTypes) {
if (!validFixTypes.has(fixType)) {
throw new Error(`Invalid fix type "${fixType}" found.`);
}
}
}

/**
* It will calculate the error and warning count for collection of messages per file
* @param {Object[]} messages - Collection of messages
Expand Down Expand Up @@ -176,7 +193,6 @@ function processText(text, configHelper, filename, fix, allowInlineConfig, repor
}

const autofixingEnabled = typeof fix !== "undefined" && (!processor || processor.supportsAutofix);

const fixedResult = linter.verifyAndFix(text, config, {
filename: effectiveFilename,
allowInlineConfig,
Expand All @@ -185,7 +201,6 @@ function processText(text, configHelper, filename, fix, allowInlineConfig, repor
preprocess: processor && (rawText => processor.preprocess(rawText, effectiveFilename)),
postprocess: processor && (problemLists => processor.postprocess(problemLists, effectiveFilename))
});

const stats = calculateStatsPerFile(fixedResult.messages);

const result = {
Expand Down Expand Up @@ -456,6 +471,33 @@ class CLIEngine {
*/
this._lintResultCache = new LintResultCache(cacheFile, this.config);
}

// setup special filter for fixes
if (this.options.fix && this.options.fixTypes && this.options.fixTypes.length > 0) {

debug(`Using fix types ${this.options.fixTypes}`);

// throw an error if any invalid fix types are found
validateFixTypes(this.options.fixTypes);

// convert to Set for faster lookup
const fixTypes = new Set(this.options.fixTypes);

// save original value of options.fix in case it's a function
const originalFix = (typeof this.options.fix === "function")
? this.options.fix : () => this.options.fix;

// create a cache of rules (but don't populate until needed)
this._rulesCache = null;

this.options.fix = lintResult => {
const rule = this._rulesCache.get(lintResult.ruleId);
const matches = rule.meta && fixTypes.has(rule.meta.type);

return matches && originalFix(lintResult);
};
}

}

getRules() {
Expand Down Expand Up @@ -560,6 +602,11 @@ class CLIEngine {
}
}

// if there's a cache, populate it
if ("_rulesCache" in this) {
this._rulesCache = this.getRules();
}

debug(`Processing ${fileInfo.filename}`);

const { result, config } = processFile(fileInfo.filename, configHelper, options, this.linter);
Expand Down Expand Up @@ -630,6 +677,12 @@ class CLIEngine {
}
usedDeprecatedRules = [];
} else {

// if there's a cache, populate it
if ("_rulesCache" in this) {
not-an-aardvark marked this conversation as resolved.
Show resolved Hide resolved
this._rulesCache = this.getRules();
}

const { result, config } = processText(
text,
configHelper,
Expand Down
7 changes: 6 additions & 1 deletion lib/cli.js
Expand Up @@ -64,6 +64,7 @@ function translateOptions(cliOptions) {
cacheFile: cliOptions.cacheFile,
cacheLocation: cliOptions.cacheLocation,
fix: (cliOptions.fix || cliOptions.fixDryRun) && (cliOptions.quiet ? quietFixPredicate : true),
fixTypes: cliOptions.fixType,
allowInlineConfig: cliOptions.inlineConfig,
reportUnusedDisableDirectives: cliOptions.reportUnusedDisableDirectives
};
Expand Down Expand Up @@ -187,8 +188,12 @@ const cli = {
return 2;
}

const engine = new CLIEngine(translateOptions(currentOptions));
if (currentOptions.fixType && !currentOptions.fix && !currentOptions.fixDryRun) {
log.error("The --fix-type option requires either --fix or --fix-dry-run.");
return 2;
}

const engine = new CLIEngine(translateOptions(currentOptions));
const report = useStdin ? engine.executeOnText(text, currentOptions.stdinFilename, true) : engine.executeOnFiles(files);

if (currentOptions.fix) {
Expand Down
5 changes: 5 additions & 0 deletions lib/options.js
Expand Up @@ -97,6 +97,11 @@ module.exports = optionator({
default: false,
description: "Automatically fix problems without saving the changes to the file system"
},
{
option: "fix-type",
type: "Array",
description: "Specify the types of fixes to apply (problem, suggestion, layout)"
},
{
heading: "Ignoring files"
},
Expand Down
4 changes: 4 additions & 0 deletions lib/rules/accessor-pairs.js
Expand Up @@ -72,12 +72,15 @@ function isPropertyDescriptor(node) {

module.exports = {
meta: {
type: "suggestion",

docs: {
description: "enforce getter and setter pairs in objects",
category: "Best Practices",
recommended: false,
url: "https://eslint.org/docs/rules/accessor-pairs"
},

schema: [{
type: "object",
properties: {
Expand All @@ -90,6 +93,7 @@ module.exports = {
},
additionalProperties: false
}],

messages: {
getter: "Getter is not present.",
setter: "Setter is not present."
Expand Down
5 changes: 5 additions & 0 deletions lib/rules/array-bracket-newline.js
Expand Up @@ -13,13 +13,17 @@ const astUtils = require("../util/ast-utils");

module.exports = {
meta: {
type: "layout",

docs: {
description: "enforce linebreaks after opening and before closing array brackets",
category: "Stylistic Issues",
recommended: false,
url: "https://eslint.org/docs/rules/array-bracket-newline"
},

fixable: "whitespace",

schema: [
{
oneOf: [
Expand All @@ -42,6 +46,7 @@ module.exports = {
]
}
],

messages: {
unexpectedOpeningLinebreak: "There should be no linebreak after '['.",
unexpectedClosingLinebreak: "There should be no linebreak before ']'.",
Expand Down
5 changes: 5 additions & 0 deletions lib/rules/array-bracket-spacing.js
Expand Up @@ -12,13 +12,17 @@ const astUtils = require("../util/ast-utils");

module.exports = {
meta: {
type: "layout",

docs: {
description: "enforce consistent spacing inside array brackets",
category: "Stylistic Issues",
recommended: false,
url: "https://eslint.org/docs/rules/array-bracket-spacing"
},

fixable: "whitespace",

schema: [
{
enum: ["always", "never"]
Expand All @@ -39,6 +43,7 @@ module.exports = {
additionalProperties: false
}
],

messages: {
unexpectedSpaceAfter: "There should be no space after '{{tokenValue}}'.",
unexpectedSpaceBefore: "There should be no space before '{{tokenValue}}'.",
Expand Down
2 changes: 2 additions & 0 deletions lib/rules/array-callback-return.js
Expand Up @@ -141,6 +141,8 @@ function isCallbackOfArrayMethod(node) {

module.exports = {
meta: {
type: "problem",

docs: {
description: "enforce `return` statements in callbacks of array methods",
category: "Best Practices",
Expand Down
4 changes: 4 additions & 0 deletions lib/rules/array-element-newline.js
Expand Up @@ -13,13 +13,17 @@ const astUtils = require("../util/ast-utils");

module.exports = {
meta: {
type: "layout",

docs: {
description: "enforce line breaks after each array element",
category: "Stylistic Issues",
recommended: false,
url: "https://eslint.org/docs/rules/array-element-newline"
},

fixable: "whitespace",

schema: [
{
oneOf: [
Expand Down