@mizdra/eslint-interactive
Advanced tools
Comparing version 1.2.0 to 2.0.0
import { ESLint, Rule } from 'eslint'; | ||
import { DisplayMode } from './types'; | ||
declare type CachedESLintOptions = { | ||
@@ -13,7 +14,8 @@ rulePaths?: string[]; | ||
printResults(results: ESLint.LintResult[]): void; | ||
showErrorAndWarningMessages(results: ESLint.LintResult[], ruleIds: string[]): Promise<void>; | ||
showProblems(formatterName: string, displayMode: DisplayMode, results: ESLint.LintResult[], ruleIds: string[]): Promise<void>; | ||
fix(ruleIds: string[]): Promise<void>; | ||
disable(results: ESLint.LintResult[], ruleIds: string[], description?: string): Promise<void>; | ||
applySuggestion(results: ESLint.LintResult[], ruleIds: string[], filterScript: string): Promise<void>; | ||
} | ||
export {}; | ||
//# sourceMappingURL=eslint.d.ts.map |
@@ -46,3 +46,3 @@ "use strict"; | ||
}, | ||
rulePaths: [...((_a = defaultOptions.rulePaths) !== null && _a !== void 0 ? _a : []), path_1.join(__dirname, './rules')], | ||
rulePaths: [...((_a = defaultOptions.rulePaths) !== null && _a !== void 0 ? _a : []), path_1.join(__dirname, 'rules')], | ||
// NOTE: add-disable-comment に関するエラーだけ fix したいのでフィルタしている | ||
@@ -53,2 +53,17 @@ fix: (message) => message.ruleId === 'add-disable-comment', | ||
} | ||
function createApplySuggestionESLint(defaultOptions, results, ruleIds, filterScript) { | ||
var _a; | ||
const eslint = new eslint_1.ESLint({ | ||
...defaultOptions, | ||
overrideConfig: { | ||
rules: { | ||
'apply-suggestion': [2, { results, ruleIds, filterScript }], | ||
}, | ||
}, | ||
rulePaths: [...((_a = defaultOptions.rulePaths) !== null && _a !== void 0 ? _a : []), path_1.join(__dirname, 'rules')], | ||
// NOTE: apply-suggestion に関するエラーだけ fix したいのでフィルタしている | ||
fix: (message) => message.ruleId === 'apply-suggestion', | ||
}); | ||
return eslint; | ||
} | ||
class CachedESLint { | ||
@@ -75,7 +90,12 @@ constructor(patterns, options) { | ||
} | ||
async showErrorAndWarningMessages(results, ruleIds) { | ||
async showProblems(formatterName, displayMode, results, ruleIds) { | ||
const eslint = new eslint_1.ESLint(this.defaultOptions); | ||
const formatter = await eslint.loadFormatter('codeframe'); | ||
const formatter = await eslint.loadFormatter(formatterName); | ||
const resultText = formatter.format(filterResultsByRuleId(results, ruleIds)); | ||
await node_pager_1.default(resultText); | ||
if (displayMode === 'withPager') { | ||
await node_pager_1.default(resultText); | ||
} | ||
else { | ||
console.log(resultText); | ||
} | ||
} | ||
@@ -99,4 +119,9 @@ async fix(ruleIds) { | ||
} | ||
async applySuggestion(results, ruleIds, filterScript) { | ||
const eslint = createApplySuggestionESLint(this.defaultOptions, results, ruleIds, filterScript); | ||
const newResults = await eslint.lintFiles(this.patterns); | ||
await eslint_1.ESLint.outputFixes(newResults); | ||
} | ||
} | ||
exports.CachedESLint = CachedESLint; | ||
//# sourceMappingURL=eslint.js.map |
@@ -1,3 +0,5 @@ | ||
export declare const ERROR_COLOR: "red"; | ||
export declare const WARNING_COLOR: "yellow"; | ||
import { Color } from 'chalk'; | ||
export declare const FAILED_COLOR: typeof Color; | ||
export declare const ERROR_COLOR: typeof Color; | ||
export declare const WARNING_COLOR: typeof Color; | ||
//# sourceMappingURL=colors.d.ts.map |
"use strict"; | ||
/* istanbul ignore file */ | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.WARNING_COLOR = exports.ERROR_COLOR = void 0; | ||
exports.WARNING_COLOR = exports.ERROR_COLOR = exports.FAILED_COLOR = void 0; | ||
exports.FAILED_COLOR = 'redBright'; | ||
exports.ERROR_COLOR = 'red'; | ||
exports.WARNING_COLOR = 'yellow'; | ||
//# sourceMappingURL=colors.js.map |
@@ -9,2 +9,5 @@ "use strict"; | ||
const colors_1 = require("./colors"); | ||
function pluralize(word, count) { | ||
return count > 1 ? `${word}s` : word; | ||
} | ||
const formatByFiles = (results) => { | ||
@@ -27,14 +30,21 @@ let errorCount = 0; | ||
const fileCount = passCount + failureCount; | ||
const summaryLineArray = [ | ||
chalk_1.default.bold(`${fileCount} file(s) checked.`), | ||
chalk_1.default.bold(`${passCount} passed.`), | ||
chalk_1.default.bold(`${failureCount} failed.`), | ||
]; | ||
if (warningCount || errorCount) { | ||
summaryLineArray.push(chalk_1.default[colors_1.ERROR_COLOR].bold(`${errorCount} file(s)`)); | ||
summaryLineArray.push(chalk_1.default[colors_1.WARNING_COLOR].bold(`${warningCount} file(s).`)); | ||
const problemCount = errorCount + warningCount; | ||
let summary = ''; | ||
summary += `- ${fileCount} ${pluralize('file', fileCount)}`; | ||
summary += ' ('; | ||
summary += `${passCount} ${pluralize('file', passCount)} passed`; | ||
summary += ', '; | ||
summary += chalk_1.default[colors_1.FAILED_COLOR](`${failureCount} ${pluralize('file', failureCount)} failed`); | ||
summary += ') checked.\n'; | ||
if (problemCount > 0) { | ||
summary += `- ${problemCount} ${pluralize('problem', problemCount)}`; | ||
summary += ' ('; | ||
summary += chalk_1.default[colors_1.ERROR_COLOR](`${errorCount} ${pluralize('error', errorCount)}`); | ||
summary += ', '; | ||
summary += chalk_1.default[colors_1.WARNING_COLOR](`${warningCount} ${pluralize('warning', warningCount)}`); | ||
summary += ') found.'; | ||
} | ||
return summaryLineArray.join(' '); | ||
return chalk_1.default.bold(summary); | ||
}; | ||
exports.formatByFiles = formatByFiles; | ||
//# sourceMappingURL=format-by-files.js.map |
@@ -15,13 +15,13 @@ "use strict"; | ||
const table = new cli_table_1.default({ | ||
head: ['Rule', 'Error (fixable)', 'Warning (fixable)'], | ||
head: ['Rule', 'Error (fixable/suggest-applicable)', 'Warning (fixable/suggest-applicable)'], | ||
}); | ||
ruleStatistics.forEach((ruleStatistic) => { | ||
var _a; | ||
const { ruleId, errorCount, warningCount, fixableErrorCount, fixableWarningCount } = ruleStatistic; | ||
const { ruleId, errorCount, warningCount, fixableErrorCount, fixableWarningCount, suggestApplicableErrorCount, suggestApplicableWarningCount, } = ruleStatistic; | ||
const ruleMetaData = data === null || data === void 0 ? void 0 : data.rulesMeta[ruleId]; | ||
const ruleCell = ((_a = ruleMetaData === null || ruleMetaData === void 0 ? void 0 : ruleMetaData.docs) === null || _a === void 0 ? void 0 : _a.url) ? terminal_link_1.default(ruleId, ruleMetaData === null || ruleMetaData === void 0 ? void 0 : ruleMetaData.docs.url) : ruleId; | ||
let errorCell = `${errorCount} (${fixableErrorCount})`; | ||
let errorCell = `${errorCount} (${fixableErrorCount}/${suggestApplicableErrorCount})`; | ||
if (errorCount > 0) | ||
errorCell = chalk_1.default[colors_1.ERROR_COLOR].bold(errorCell); | ||
let warningCell = `${warningCount} (${fixableWarningCount})`; | ||
let warningCell = `${warningCount} (${fixableWarningCount}/${suggestApplicableWarningCount})`; | ||
if (warningCount > 0) | ||
@@ -28,0 +28,0 @@ warningCell = chalk_1.default[colors_1.WARNING_COLOR].bold(warningCell); |
@@ -11,12 +11,18 @@ "use strict"; | ||
let fixableWarningCount = 0; | ||
let suggestApplicableErrorCount = 0; | ||
let suggestApplicableWarningCount = 0; | ||
for (const message of messages) { | ||
if (message.severity === 2) { | ||
errorCount++; | ||
if (message.fix !== undefined) | ||
if (message.fix) | ||
fixableErrorCount++; | ||
if (message.suggestions && message.suggestions.length > 0) | ||
suggestApplicableErrorCount++; | ||
} | ||
else if (message.severity === 1) { | ||
warningCount++; | ||
if (message.fix !== undefined) | ||
if (message.fix) | ||
fixableWarningCount++; | ||
if (message.suggestions && message.suggestions.length > 0) | ||
suggestApplicableWarningCount++; | ||
} | ||
@@ -30,2 +36,4 @@ } | ||
fixableWarningCount, | ||
suggestApplicableErrorCount, | ||
suggestApplicableWarningCount, | ||
}; | ||
@@ -32,0 +40,0 @@ } |
@@ -10,2 +10,3 @@ "use strict"; | ||
const yargs_1 = __importDefault(require("yargs/yargs")); | ||
const actions_1 = require("./actions"); | ||
const eslint_1 = require("./eslint"); | ||
@@ -28,3 +29,8 @@ const prompt_1 = require("./prompt"); | ||
}) | ||
.nargs('ext', 1).argv; | ||
.nargs('ext', 1) | ||
.option('format', { | ||
type: 'string', | ||
describe: 'Specify the format to be used for the `Display problem messages` action', | ||
default: 'codeframe', | ||
}).argv; | ||
// NOTE: convert `string` type because yargs convert `'10'` (`string` type) into `10` (`number` type) | ||
@@ -35,2 +41,3 @@ // and `lintFiles` only accepts `string[]`. | ||
const extensions = (_b = argv.ext) === null || _b === void 0 ? void 0 : _b.map((extension) => extension.toString()).flatMap((extension) => extension.split(',')); | ||
const formatterName = argv.format; | ||
const eslint = new eslint_1.CachedESLint(patterns, { rulePaths, extensions }); | ||
@@ -60,4 +67,5 @@ // eslint-disable-next-line no-constant-condition | ||
continue selectRule; | ||
if (action === 'showMessages') { | ||
await eslint.showErrorAndWarningMessages(results, selectedRuleIds); | ||
if (action === 'displayMessages') { | ||
const displayMode = await prompt_1.promptToInputDisplayMode(); | ||
await eslint.showProblems(formatterName, displayMode, results, selectedRuleIds); | ||
continue selectAction; | ||
@@ -78,2 +86,6 @@ } | ||
} | ||
else if (action === 'applySuggestion') { | ||
await actions_1.doApplySuggestionAction(eslint, results, selectedRuleIds); | ||
break selectRule; | ||
} | ||
} | ||
@@ -80,0 +92,0 @@ } |
@@ -1,6 +0,8 @@ | ||
import { Action } from './types'; | ||
import { Action, DisplayMode } from './types'; | ||
export declare function promptToInputRuleIds(ruleIdsInResults: string[]): Promise<string[]>; | ||
export declare function promptToInputAction(): Promise<Action>; | ||
export declare function promptToInputDisplayMode(): Promise<DisplayMode>; | ||
export declare function promptToInputDescription(): Promise<string | undefined>; | ||
export declare function promptToInputContinue(): Promise<boolean>; | ||
export declare function promptToInputReuseFilterScript(): Promise<boolean>; | ||
//# sourceMappingURL=prompt.d.ts.map |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.promptToInputContinue = exports.promptToInputDescription = exports.promptToInputAction = exports.promptToInputRuleIds = void 0; | ||
exports.promptToInputReuseFilterScript = exports.promptToInputContinue = exports.promptToInputDescription = exports.promptToInputDisplayMode = exports.promptToInputAction = exports.promptToInputRuleIds = void 0; | ||
const enquirer_1 = require("enquirer"); | ||
@@ -10,3 +10,3 @@ async function promptToInputRuleIds(ruleIdsInResults) { | ||
type: 'multiselect', | ||
message: 'Which rule(s) would you like to apply action?', | ||
message: 'Which rules would you like to apply action?', | ||
choices: ruleIdsInResults, | ||
@@ -23,7 +23,8 @@ }, | ||
type: 'select', | ||
message: 'Which action do you want to apply?', | ||
message: 'Which action do you want to do?', | ||
choices: [ | ||
{ name: 'showMessages', message: 'Show error/warning messages' }, | ||
{ name: 'fix', message: 'Fix error/warning' }, | ||
{ name: 'disable', message: 'Disable error/warning for with `// eslint-disable-next-line`' }, | ||
{ name: 'displayMessages', message: 'Display problem messages' }, | ||
{ name: 'fix', message: 'Fix problems' }, | ||
{ name: 'disable', message: 'Disable problems with `// eslint-disable-next-line`' }, | ||
{ name: 'applySuggestion', message: 'Apply suggestion (experimental, only for experts)' }, | ||
{ name: 'reselectRules', message: 'Reselect rules' }, | ||
@@ -36,2 +37,17 @@ ], | ||
exports.promptToInputAction = promptToInputAction; | ||
async function promptToInputDisplayMode() { | ||
const { displayMode } = await enquirer_1.prompt([ | ||
{ | ||
name: 'displayMode', | ||
type: 'select', | ||
message: 'What format do you want to display the problem message in?', | ||
choices: [ | ||
{ name: 'withPager', message: 'Display with pager' }, | ||
{ name: 'withoutPager', message: 'Display without pager' }, | ||
], | ||
}, | ||
]); | ||
return displayMode; | ||
} | ||
exports.promptToInputDisplayMode = promptToInputDisplayMode; | ||
async function promptToInputDescription() { | ||
@@ -60,2 +76,14 @@ const { description } = await enquirer_1.prompt([ | ||
exports.promptToInputContinue = promptToInputContinue; | ||
async function promptToInputReuseFilterScript() { | ||
const { reuseFilterScript } = await enquirer_1.prompt([ | ||
{ | ||
name: 'reuseFilterScript', | ||
type: 'confirm', | ||
message: 'Do you want to reuse a previously edited filter script?', | ||
initial: true, | ||
}, | ||
]); | ||
return reuseFilterScript; | ||
} | ||
exports.promptToInputReuseFilterScript = promptToInputReuseFilterScript; | ||
//# sourceMappingURL=prompt.js.map |
@@ -1,2 +0,3 @@ | ||
export declare type Action = 'showMessages' | 'fix' | 'reselectRules'; | ||
export declare type Action = 'displayMessages' | 'fix' | 'disable' | 'applySuggestion' | 'reselectRules'; | ||
export declare type DisplayMode = 'withPager' | 'withoutPager'; | ||
export declare type RuleStatistic = { | ||
@@ -8,3 +9,5 @@ ruleId: string; | ||
fixableWarningCount: number; | ||
suggestApplicableErrorCount: number; | ||
suggestApplicableWarningCount: number; | ||
}; | ||
//# sourceMappingURL=types.d.ts.map |
{ | ||
"name": "@mizdra/eslint-interactive", | ||
"description": "The CLI tool to run `eslint --fix` for each rule", | ||
"version": "1.2.0", | ||
"version": "2.0.0", | ||
"repository": "https://github.com/mizdra/eslint-interactive.git", | ||
@@ -12,12 +12,15 @@ "author": "mizdra <pp.mizdra@gmail.com>", | ||
"dev": "tsc-watch -p tsconfig.src.json --onSuccess 'bin/eslint-interactive fixtures --ruledir fixtures/rules --ext .js,jsx,.mjs'", | ||
"check": "run-s -c check:*", | ||
"check:tsc": "run-s -c check:tsc:*", | ||
"check:tsc:src": "tsc -p tsconfig.src.json --noEmit", | ||
"check:tsc:test": "tsc -p tsconfig.test.json --noEmit", | ||
"check:eslint": "eslint src test", | ||
"lint": "run-s -c lint:*", | ||
"lint:tsc": "run-s -c lint:tsc:*", | ||
"lint:tsc:src": "tsc -p tsconfig.src.json --noEmit", | ||
"lint:tsc:test": "tsc -p tsconfig.test.json --noEmit", | ||
"lint:eslint": "eslint --ignore-pattern '/fixtures' .", | ||
"lint:prettier": "prettier --check .", | ||
"test": "jest --colors" | ||
}, | ||
"prettier": "@mizdra/prettier-config-mizdra", | ||
"renovate": "github>mizdra/renovate-config-mizdra", | ||
"devDependencies": { | ||
"@jest/types": "^26.6.2", | ||
"@mizdra/eslint-config-mizdra": "^0.7.0", | ||
"@jest/types": "^27.1.0", | ||
"@mizdra/eslint-config-mizdra": "^1.0.0", | ||
"@mizdra/prettier-config-mizdra": "^0.3.0", | ||
@@ -27,15 +30,15 @@ "@types/cli-table": "^0.3.0", | ||
"@types/estree": "^0.0.50", | ||
"@types/jest": "^27.0.1", | ||
"@types/node": "^14.14.10", | ||
"@types/terminal-link": "^1.2.0", | ||
"@types/yargs": "^15.0.12", | ||
"@types/yargs": "^16.0.4", | ||
"@typescript-eslint/eslint-plugin": "^4.10.0", | ||
"@typescript-eslint/parser": "^4.10.0", | ||
"eslint": "^7.16.0", | ||
"eslint-config-prettier": "^7.1.0", | ||
"eslint-config-prettier": "^8.3.0", | ||
"eslint-plugin-import": "^2.22.1", | ||
"eslint-plugin-prettier": "^3.3.0", | ||
"jest": "^26.6.3", | ||
"jest": "^27.1.0", | ||
"npm-run-all": "^4.1.5", | ||
"prettier": "2.2.1", | ||
"ts-jest": "^26.4.4", | ||
"ts-jest": "^27.0.5", | ||
"tsc-watch": "^4.2.9", | ||
@@ -54,6 +57,6 @@ "typescript": "^4.1.2" | ||
"peerDependencies": { | ||
"eslint": ">=3.0" | ||
"eslint": ">=7.0" | ||
}, | ||
"engines": { | ||
"node": ">=12.0" | ||
"node": ">=14.0" | ||
}, | ||
@@ -71,6 +74,6 @@ "publishConfig": { | ||
"files": [ | ||
"local-rules", | ||
"src", | ||
"dist" | ||
"dist", | ||
"static" | ||
] | ||
} |
@@ -13,26 +13,25 @@ # eslint-interactive | ||
The default ESLint output contains a lot of useful information for developers, such as the source of the error and hints for fixing it. While this works for many use cases, it does not work well in situations where many errors are reported. For example, when introducing ESLint into a project, or when making big changes to the `.eslintrc` of a project. In these situations, the output of ESLint can be quite large, making it difficult for developers to analyze the output. It is also difficult for the developer to fix errors because many types of errors are mixed up in the output. | ||
The default ESLint output contains a lot of useful messages for developers, such as the source of the error and hints for fixing it. While this works for many use cases, it does not work well in situations where many messages are reported. For example, when introducing ESLint into a project, or when making big changes to the `.eslintrc` of a project. In these situations, the output of ESLint can be quite large, making it difficult for developers to analyze the output. It is also difficult for the developer to fix messages mechanically, because messages of many rules are mixed up in the output. | ||
In such an error-prone situation, I think two things are important: | ||
In such the above situation, I think two things are important: | ||
- Show a summary of all errors so that the whole picture can be easily understood | ||
- Showing the details of each error will confuse developers. | ||
- Provide an efficient way to fix many errors | ||
- `eslint --fix` is one of the best ways to fix errors efficiently, but it auto-fixes all rule errors at once. | ||
- Show a summary of all problems (called _"warnings"_ or _"errors"_ in ESLint) so that the whole picture can be easily understood | ||
- Showing the details of each problem will confuse developers. | ||
- Provide an efficient way to fix many problems | ||
- `eslint --fix` is one of the best ways to fix problems efficiently, but it auto-fixes all rule problems at once. | ||
- Depending on the rule, auto-fix may affect the behavior of the code, so auto-fix should be done with care. | ||
- Therefore, it is desirable to provide a way to auto-fix in smaller units than `eslint --fix`. | ||
So, I created a tool called `eslint-interactive` which wraps ESLint. This tool groups all errors by rule and outputs the number of errors per rule in a formatted format. In addition to the breakdown of _warnings_ and _errors_ per rule, it also outputs the number of fixable errors and other hints to help developers fix errors. You can also specify a number of rules to display raw ESLint error messages or to auto-fix. | ||
So, I created a tool called `eslint-interactive` which wraps ESLint. This tool groups all problems by rule and outputs formatted number of problems per rule. In addition to the breakdown of problems per rule, it also outputs the number of fixable problems and other hints to help developers fix problems. | ||
## What's the difference between [eslint-nibble](https://github.com/IanVS/eslint-nibble)? | ||
Also, You can perform the following actions for each rule: | ||
A tool similar to `eslint-interactive` is [eslint-nibble](https://github.com/IanVS/eslint-nibble). Both tools solve the same problem, but `eslint-interactive` has some features that `eslint-nibble` does not have. For example, `eslint-interactive` prints the number of fixable errors per rule, while `eslint-nibble` does not. Also, `eslint-interactive` has various tricks to speed up the cycle of auto-fixing per-rule, but `eslint-nibble` auto-fixes once and terminates the process every time, so it is not as fast as `eslint- interactive`. | ||
- Display raw ESLint problem messages | ||
- Apply auto-fix | ||
- Add disable comment (`// eslint-disable-next-line <rule-name>`) | ||
- Apply suggestion | ||
I think these features are very important to solve the aforementioned problem. At first, I thought of implementing these features in `eslint-nibble`, but it required a major rewrite of the code, so I implemented it as a new tool `eslint-interactive`. Although `eslint-interactive` is a tool independent of `eslint-nibble`, it is influenced by the ideas of `eslint-nibble` and inherits some of its code. That's why you can find the names of [@IanVS](https://github.com/IanVS) and others in [the license of `eslint-interactive`](https://github.com/mizdra/eslint-interactive/blob/master/LICENSE). | ||
Thanks, [@IanVS](https://github.com/IanVS). | ||
## Installation | ||
```bash | ||
```console | ||
$ npm i -g eslint @mizdra/eslint-interactive | ||
@@ -47,3 +46,3 @@ $ eslint-interactive --help | ||
```bash | ||
```console | ||
$ # Show help | ||
@@ -58,2 +57,4 @@ $ eslint-interactive --help | ||
--ext Specify JavaScript file extensions [array] | ||
--format Specify the format to be used for the `Display problem messages` | ||
action [string] [default: "codeframe"] | ||
@@ -69,7 +70,20 @@ | ||
## Future Work | ||
## Differences from related works | ||
- [ ] Support `--no-pager` option | ||
- [ ] Print the url of rule's documentation | ||
### [eslint-nibble](https://github.com/IanVS/eslint-nibble) | ||
A tool similar to `eslint-interactive` is [eslint-nibble](https://github.com/IanVS/eslint-nibble). Both tools solve the same problem, but `eslint-interactive` has some features that `eslint-nibble` does not have. For example, `eslint-interactive` prints the number of fixable problems per rule, while `eslint-nibble` does not. Also, `eslint-interactive` has various tricks to speed up the cycle of auto-fixing per-rule, but `eslint-nibble` auto-fixes once and terminates the process every time, so it is not as fast as `eslint-interactive`. | ||
I think these features are very important to solve the aforementioned problem. At first, I thought of implementing these features in `eslint-nibble`, but it required a major rewrite of the code, so I implemented it as a new tool `eslint-interactive`. Although `eslint-interactive` is a tool independent of `eslint-nibble`, it is influenced by the ideas of `eslint-nibble` and inherits some of its code. That's why you can find the names of [@IanVS](https://github.com/IanVS) and others in [the license of `eslint-interactive`](https://github.com/mizdra/eslint-interactive/blob/master/LICENSE). | ||
Thanks, [@IanVS](https://github.com/IanVS). | ||
### [suppress-eslint-errors](https://github.com/amanda-mitchell/suppress-eslint-errors) | ||
[suppress-eslint-errors](https://github.com/amanda-mitchell/suppress-eslint-errors) is an excellent tool to add comments for disable mechanically. Just like `eslint-interactive`, it allows you to add disable comments for each rule and leave the purpose of disable as a comment. There is no functional difference between the two, but there is a difference in the API used to insert the comments. | ||
`suppress-eslint-errors` uses [`jscodeshift`](https://github.com/facebook/jscodeshift) to insert comments. `jscodeshift` modifies the file in parallel, so `suppress-eslint-errors` has the advantage of being able to insert comments faster. However, `jscodeshift` cannot reuse the AST of ESLint, so you need to reparse the code in `jscodeshift`. This means that you have to pass `jscodeshift` the information it needs to parse your code (parser type, parser options). In fact, `suppress-eslint-errors` requires `--extensions` and `--parser` command line option. Normally, users specify the parsing options in `.eslintrc`, so passing these options may seem cumbersome. Also, due to the difference in the way ESLint and `jscodeshift` parse, it may not be possible to insert comments correctly. | ||
On the other hand, `eslint-interactive` uses [`ESLint.outputFixes`](https://eslint.org/docs/developer-guide/nodejs-api#-eslintoutputfixesresults) to insert comments. It uses ESLint's API to do everything from parsing the code to inserting the comments, so it works as expected in many cases. Also, `eslint-interactive` will parse the code using the parsing options specified in `.eslintrc`. Therefore, comments can be inserted without any additional command line options. By the way, comment insertion is slower than `suppress-eslint-errors` because, unlike `suppress-eslint-errors`, it cannot modify files in parallel. However, this limitation may be improved when ESLint supports parallel processing in the near future. | ||
## For Contributors | ||
@@ -81,3 +95,3 @@ | ||
- `yarn run dev`: Run for development | ||
- `yarn run check`: Try static-checking | ||
- `yarn run lint`: Try static-checking | ||
- `yarn run test`: Run tests | ||
@@ -84,0 +98,0 @@ |
@@ -7,2 +7,4 @@ import { tmpdir } from 'os'; | ||
import { DisableTarget, Option } from './rules/add-disable-comment'; | ||
import { ApplySuggestionOption } from './rules/apply-suggestion'; | ||
import { DisplayMode } from './types'; | ||
import { groupBy } from './util/array'; | ||
@@ -48,3 +50,3 @@ import { notEmpty } from './util/filter'; | ||
}, | ||
rulePaths: [...(defaultOptions.rulePaths ?? []), join(__dirname, './rules')], | ||
rulePaths: [...(defaultOptions.rulePaths ?? []), join(__dirname, 'rules')], | ||
// NOTE: add-disable-comment に関するエラーだけ fix したいのでフィルタしている | ||
@@ -56,2 +58,22 @@ fix: (message) => message.ruleId === 'add-disable-comment', | ||
function createApplySuggestionESLint( | ||
defaultOptions: ESLint.Options, | ||
results: ESLint.LintResult[], | ||
ruleIds: string[], | ||
filterScript: string, | ||
): ESLint { | ||
const eslint = new ESLint({ | ||
...defaultOptions, | ||
overrideConfig: { | ||
rules: { | ||
'apply-suggestion': [2, { results, ruleIds, filterScript } as ApplySuggestionOption], | ||
}, | ||
}, | ||
rulePaths: [...(defaultOptions.rulePaths ?? []), join(__dirname, 'rules')], | ||
// NOTE: apply-suggestion に関するエラーだけ fix したいのでフィルタしている | ||
fix: (message) => message.ruleId === 'apply-suggestion', | ||
}); | ||
return eslint; | ||
} | ||
type CachedESLintOptions = { | ||
@@ -91,7 +113,16 @@ rulePaths?: string[]; | ||
async showErrorAndWarningMessages(results: ESLint.LintResult[], ruleIds: string[]): Promise<void> { | ||
async showProblems( | ||
formatterName: string, | ||
displayMode: DisplayMode, | ||
results: ESLint.LintResult[], | ||
ruleIds: string[], | ||
): Promise<void> { | ||
const eslint = new ESLint(this.defaultOptions); | ||
const formatter = await eslint.loadFormatter('codeframe'); | ||
const formatter = await eslint.loadFormatter(formatterName); | ||
const resultText = formatter.format(filterResultsByRuleId(results, ruleIds)); | ||
await pager(resultText); | ||
if (displayMode === 'withPager') { | ||
await pager(resultText); | ||
} else { | ||
console.log(resultText); | ||
} | ||
} | ||
@@ -119,2 +150,8 @@ | ||
} | ||
async applySuggestion(results: ESLint.LintResult[], ruleIds: string[], filterScript: string): Promise<void> { | ||
const eslint = createApplySuggestionESLint(this.defaultOptions, results, ruleIds, filterScript); | ||
const newResults = await eslint.lintFiles(this.patterns); | ||
await ESLint.outputFixes(newResults); | ||
} | ||
} |
/* istanbul ignore file */ | ||
export const ERROR_COLOR = 'red' as const; | ||
export const WARNING_COLOR = 'yellow' as const; | ||
import { Color } from 'chalk'; | ||
export const FAILED_COLOR: typeof Color = 'redBright'; | ||
export const ERROR_COLOR: typeof Color = 'red'; | ||
export const WARNING_COLOR: typeof Color = 'yellow'; |
import chalk from 'chalk'; | ||
import { ESLint } from 'eslint'; | ||
import { ERROR_COLOR, WARNING_COLOR } from './colors'; | ||
import { ERROR_COLOR, FAILED_COLOR, WARNING_COLOR } from './colors'; | ||
function pluralize(word: string, count: number): string { | ||
return count > 1 ? `${word}s` : word; | ||
} | ||
export const formatByFiles: ESLint.Formatter['format'] = (results) => { | ||
@@ -24,15 +28,22 @@ let errorCount = 0; | ||
const fileCount = passCount + failureCount; | ||
const problemCount = errorCount + warningCount; | ||
const summaryLineArray = [ | ||
chalk.bold(`${fileCount} file(s) checked.`), | ||
chalk.bold(`${passCount} passed.`), | ||
chalk.bold(`${failureCount} failed.`), | ||
]; | ||
let summary = ''; | ||
summary += `- ${fileCount} ${pluralize('file', fileCount)}`; | ||
summary += ' ('; | ||
summary += `${passCount} ${pluralize('file', passCount)} passed`; | ||
summary += ', '; | ||
summary += chalk[FAILED_COLOR](`${failureCount} ${pluralize('file', failureCount)} failed`); | ||
summary += ') checked.\n'; | ||
if (warningCount || errorCount) { | ||
summaryLineArray.push(chalk[ERROR_COLOR].bold(`${errorCount} file(s)`)); | ||
summaryLineArray.push(chalk[WARNING_COLOR].bold(`${warningCount} file(s).`)); | ||
if (problemCount > 0) { | ||
summary += `- ${problemCount} ${pluralize('problem', problemCount)}`; | ||
summary += ' ('; | ||
summary += chalk[ERROR_COLOR](`${errorCount} ${pluralize('error', errorCount)}`); | ||
summary += ', '; | ||
summary += chalk[WARNING_COLOR](`${warningCount} ${pluralize('warning', warningCount)}`); | ||
summary += ') found.'; | ||
} | ||
return summaryLineArray.join(' '); | ||
return chalk.bold(summary); | ||
}; |
@@ -11,13 +11,21 @@ import chalk from 'chalk'; | ||
const table = new Table({ | ||
head: ['Rule', 'Error (fixable)', 'Warning (fixable)'], | ||
head: ['Rule', 'Error (fixable/suggest-applicable)', 'Warning (fixable/suggest-applicable)'], | ||
}); | ||
ruleStatistics.forEach((ruleStatistic) => { | ||
const { ruleId, errorCount, warningCount, fixableErrorCount, fixableWarningCount } = ruleStatistic; | ||
const { | ||
ruleId, | ||
errorCount, | ||
warningCount, | ||
fixableErrorCount, | ||
fixableWarningCount, | ||
suggestApplicableErrorCount, | ||
suggestApplicableWarningCount, | ||
} = ruleStatistic; | ||
const ruleMetaData = data?.rulesMeta[ruleId]; | ||
const ruleCell = ruleMetaData?.docs?.url ? terminalLink(ruleId, ruleMetaData?.docs.url) : ruleId; | ||
let errorCell = `${errorCount} (${fixableErrorCount})`; | ||
let errorCell = `${errorCount} (${fixableErrorCount}/${suggestApplicableErrorCount})`; | ||
if (errorCount > 0) errorCell = chalk[ERROR_COLOR].bold(errorCell); | ||
let warningCell = `${warningCount} (${fixableWarningCount})`; | ||
let warningCell = `${warningCount} (${fixableWarningCount}/${suggestApplicableWarningCount})`; | ||
if (warningCount > 0) warningCell = chalk[WARNING_COLOR].bold(warningCell); | ||
@@ -24,0 +32,0 @@ table.push([ruleCell, errorCell, warningCell]); |
@@ -11,2 +11,4 @@ import { ESLint, Linter } from 'eslint'; | ||
let fixableWarningCount = 0; | ||
let suggestApplicableErrorCount = 0; | ||
let suggestApplicableWarningCount = 0; | ||
@@ -16,6 +18,8 @@ for (const message of messages) { | ||
errorCount++; | ||
if (message.fix !== undefined) fixableErrorCount++; | ||
if (message.fix) fixableErrorCount++; | ||
if (message.suggestions && message.suggestions.length > 0) suggestApplicableErrorCount++; | ||
} else if (message.severity === 1) { | ||
warningCount++; | ||
if (message.fix !== undefined) fixableWarningCount++; | ||
if (message.fix) fixableWarningCount++; | ||
if (message.suggestions && message.suggestions.length > 0) suggestApplicableWarningCount++; | ||
} | ||
@@ -30,2 +34,4 @@ } | ||
fixableWarningCount, | ||
suggestApplicableErrorCount, | ||
suggestApplicableWarningCount, | ||
}; | ||
@@ -32,0 +38,0 @@ } |
import chalk from 'chalk'; | ||
import ora from 'ora'; | ||
import yargs from 'yargs/yargs'; | ||
import { doApplySuggestionAction } from './actions'; | ||
import { CachedESLint } from './eslint'; | ||
import { promptToInputAction, promptToInputContinue, promptToInputDescription, promptToInputRuleIds } from './prompt'; | ||
import { | ||
promptToInputAction, | ||
promptToInputContinue, | ||
promptToInputDescription, | ||
promptToInputDisplayMode, | ||
promptToInputRuleIds, | ||
} from './prompt'; | ||
import { unique } from './util/array'; | ||
@@ -25,3 +32,8 @@ import { notEmpty } from './util/filter'; | ||
}) | ||
.nargs('ext', 1).argv; | ||
.nargs('ext', 1) | ||
.option('format', { | ||
type: 'string', | ||
describe: 'Specify the format to be used for the `Display problem messages` action', | ||
default: 'codeframe', | ||
}).argv; | ||
// NOTE: convert `string` type because yargs convert `'10'` (`string` type) into `10` (`number` type) | ||
@@ -35,2 +47,3 @@ // and `lintFiles` only accepts `string[]`. | ||
.flatMap((extension) => extension.split(',')); | ||
const formatterName = argv.format; | ||
@@ -69,4 +82,5 @@ const eslint = new CachedESLint(patterns, { rulePaths, extensions }); | ||
if (action === 'showMessages') { | ||
await eslint.showErrorAndWarningMessages(results, selectedRuleIds); | ||
if (action === 'displayMessages') { | ||
const displayMode = await promptToInputDisplayMode(); | ||
await eslint.showProblems(formatterName, displayMode, results, selectedRuleIds); | ||
continue selectAction; | ||
@@ -84,2 +98,5 @@ } else if (action === 'fix') { | ||
break selectRule; | ||
} else if (action === 'applySuggestion') { | ||
await doApplySuggestionAction(eslint, results, selectedRuleIds); | ||
break selectRule; | ||
} | ||
@@ -86,0 +103,0 @@ } |
import { prompt } from 'enquirer'; | ||
import { Action } from './types'; | ||
import { Action, DisplayMode } from './types'; | ||
@@ -9,3 +9,3 @@ export async function promptToInputRuleIds(ruleIdsInResults: string[]): Promise<string[]> { | ||
type: 'multiselect', | ||
message: 'Which rule(s) would you like to apply action?', | ||
message: 'Which rules would you like to apply action?', | ||
choices: ruleIdsInResults, | ||
@@ -24,7 +24,8 @@ }, | ||
type: 'select', | ||
message: 'Which action do you want to apply?', | ||
message: 'Which action do you want to do?', | ||
choices: [ | ||
{ name: 'showMessages', message: 'Show error/warning messages' }, | ||
{ name: 'fix', message: 'Fix error/warning' }, | ||
{ name: 'disable', message: 'Disable error/warning for with `// eslint-disable-next-line`' }, | ||
{ name: 'displayMessages', message: 'Display problem messages' }, | ||
{ name: 'fix', message: 'Fix problems' }, | ||
{ name: 'disable', message: 'Disable problems with `// eslint-disable-next-line`' }, | ||
{ name: 'applySuggestion', message: 'Apply suggestion (experimental, only for experts)' }, | ||
{ name: 'reselectRules', message: 'Reselect rules' }, | ||
@@ -37,2 +38,19 @@ ], | ||
export async function promptToInputDisplayMode(): Promise<DisplayMode> { | ||
const { displayMode } = await prompt<{ | ||
displayMode: DisplayMode; | ||
}>([ | ||
{ | ||
name: 'displayMode', | ||
type: 'select', | ||
message: 'What format do you want to display the problem message in?', | ||
choices: [ | ||
{ name: 'withPager', message: 'Display with pager' }, | ||
{ name: 'withoutPager', message: 'Display without pager' }, | ||
], | ||
}, | ||
]); | ||
return displayMode; | ||
} | ||
export async function promptToInputDescription(): Promise<string | undefined> { | ||
@@ -62,1 +80,13 @@ const { description } = await prompt<{ | ||
} | ||
export async function promptToInputReuseFilterScript(): Promise<boolean> { | ||
const { reuseFilterScript } = await prompt<{ reuseFilterScript: boolean }>([ | ||
{ | ||
name: 'reuseFilterScript', | ||
type: 'confirm', | ||
message: 'Do you want to reuse a previously edited filter script?', | ||
initial: true, | ||
}, | ||
]); | ||
return reuseFilterScript; | ||
} |
@@ -1,2 +0,3 @@ | ||
export type Action = 'showMessages' | 'fix' | 'reselectRules'; | ||
export type Action = 'displayMessages' | 'fix' | 'disable' | 'applySuggestion' | 'reselectRules'; | ||
export type DisplayMode = 'withPager' | 'withoutPager'; | ||
@@ -9,2 +10,4 @@ export type RuleStatistic = { | ||
fixableWarningCount: number; | ||
suggestApplicableErrorCount: number; | ||
suggestApplicableWarningCount: number; | ||
}; |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
Environment variable access
Supply chain riskPackage accesses environment variables, which may be a sign of credential stuffing or data theft.
Found 1 instance in 1 package
Filesystem access
Supply chain riskAccesses the file system, and could potentially read sensitive data.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
191422
87
1807
107
4
1