diff --git a/docs/developer-guide/nodejs-api.md b/docs/developer-guide/nodejs-api.md index f5ec84616bf..1ffb6737def 100644 --- a/docs/developer-guide/nodejs-api.md +++ b/docs/developer-guide/nodejs-api.md @@ -108,7 +108,7 @@ The most important method on `Linter` is `verify()`, which initiates linting of * `preprocess` - (optional) A function that [Processors in Plugins](/docs/developer-guide/working-with-plugins.md#processors-in-plugins) documentation describes as the `preprocess` method. * `postprocess` - (optional) A function that [Processors in Plugins](/docs/developer-guide/working-with-plugins.md#processors-in-plugins) documentation describes as the `postprocess` method. * `filterCodeBlock` - (optional) A function that decides which code blocks the linter should adopt. The function receives two arguments. The first argument is the virtual filename of a code block. The second argument is the text of the code block. If the function returned `true` then the linter adopts the code block. If the function was omitted, the linter adopts only `*.js` code blocks. If you provided a `filterCodeBlock` function, it overrides this default behavior, so the linter doesn't adopt `*.js` code blocks automatically. - * `disableFixes` - (optional) when set to `true`, the linter doesn't make the `fix` property of the lint result. + * `disableFixes` - (optional) when set to `true`, the linter doesn't make either the `fix` or `suggestions` property of the lint result. * `allowInlineConfig` - (optional) set to `false` to disable inline comments from changing ESLint rules. * `reportUnusedDisableDirectives` - (optional) when set to `true`, adds reported errors for unused `eslint-disable` directives when no problems would be reported in the disabled area anyway. @@ -170,6 +170,7 @@ The information available for each linting message is: * `endColumn` - the end column of the range on which the error occurred (this property is omitted if it's not range). * `endLine` - the end line of the range on which the error occurred (this property is omitted if it's not range). * `fix` - an object describing the fix for the problem (this property is omitted if no fix is available). +* `suggestions` - an array of objects describing possible lint fixes for editors to programmatically enable (see details in the [Working with Rules docs](./working-with-rules.md#providing-suggestions)). Linting message objects have a deprecated `source` property. This property **will be removed** from linting messages in an upcoming breaking release. If you depend on this property, you should now use the `SourceCode` instance provided by the linter. @@ -437,9 +438,23 @@ The return value is an object containing the results of the linting operation. H column: 13, nodeType: "ExpressionStatement", fix: { range: [12, 12], text: ";" } + }, { + ruleId: "no-useless-escape", + severity: 1, + message: "disallow unnecessary escape characters", + line: 1, + column: 10, + nodeType: "ExpressionStatement", + suggestions: [{ + desc: "Remove unnecessary escape. This maintains the current functionality.", + fix: { range: [9, 10], text: "" } + }, { + desc: "Escape backslash to include it in the RegExp.", + fix: { range: [9, 9], text: "\\" } + }] }], errorCount: 1, - warningCount: 0, + warningCount: 1, fixableErrorCount: 1, fixableWarningCount: 0, source: "\"use strict\"\n" @@ -865,6 +880,7 @@ In addition to the properties above, invalid test cases can also have the follow * `column` (number): The 1-based column number of the reported location * `endLine` (number): The 1-based line number of the end of the reported location * `endColumn` (number): The 1-based column number of the end of the reported location + * `suggestions` (array): An array of objects with suggestion details to check. See [Testing Suggestions](#testing-suggestions) for details If a string is provided as an error instead of an object, the string is used to assert the `message` of the error. * `output` (string, optional): Asserts the output that will be produced when using this rule for a single pass of autofixing (e.g. with the `--fix` command line flag). If this is `null`, asserts that none of the reported problems suggest autofixes. @@ -880,6 +896,31 @@ Any additional properties of a test case will be passed directly to the linter a If a valid test case only uses the `code` property, it can optionally be provided as a string containing the code, rather than an object with a `code` key. +#### Testing Suggestions + +Suggestions can be tested by defining a `suggestions` key on an errors object. The options to check for the suggestions are the following (all are optional): + * `desc` (string): The suggestion `desc` value + * `messageId` (string): The suggestion `messageId` value for suggestions that use `messageId`s + * `output` (string): A code string representing the result of applying the suggestion fix to the input code + +Example: + +```js +ruleTester.run("my-rule-for-no-foo", rule, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + messageId: "renameFoo", + output: "var bar;" + }] + }] + }] +}) +``` + ### Customizing RuleTester `RuleTester` depends on two functions to run tests: `describe` and `it`. These functions can come from various places: diff --git a/docs/developer-guide/unit-tests.md b/docs/developer-guide/unit-tests.md index 9b9bd440744..281c3967454 100644 --- a/docs/developer-guide/unit-tests.md +++ b/docs/developer-guide/unit-tests.md @@ -12,10 +12,10 @@ This automatically starts Mocha and runs all tests in the `tests` directory. You If you want to quickly run just one test, you can do so by running Mocha directly and passing in the filename. For example: - npm test:cli tests/lib/rules/no-wrap-func.js + npm run test:cli tests/lib/rules/no-wrap-func.js Running individual tests is useful when you're working on a specific bug and iterating on the solution. You should be sure to run `npm test` before submitting a pull request. ## More Control on Unit Testing -`npm test:cli` is an alias of the Mocha cli in `./node_modules/.bin/mocha`. [Options](https://mochajs.org/#command-line-usage) are available to be provided to help to better control the test to run. +`npm run test:cli` is an alias of the Mocha cli in `./node_modules/.bin/mocha`. [Options](https://mochajs.org/#command-line-usage) are available to be provided to help to better control the test to run. diff --git a/docs/developer-guide/working-with-rules.md b/docs/developer-guide/working-with-rules.md index c5d8808049e..945061f1ff0 100644 --- a/docs/developer-guide/working-with-rules.md +++ b/docs/developer-guide/working-with-rules.md @@ -62,6 +62,7 @@ The source file for a rule exports an object with the following properties. * `category` (string) specifies the heading under which the rule is listed in the [rules index](../rules/) * `recommended` (boolean) is whether the `"extends": "eslint:recommended"` property in a [configuration file](../user-guide/configuring.md#extending-configuration-files) enables the rule * `url` (string) specifies the URL at which the full documentation can be accessed + * `suggestion` (boolean) specifies whether rules can return suggestions (defaults to false if omitted) In a custom rule or plugin, you can omit `docs` or include any properties that you need in it. @@ -344,6 +345,79 @@ Best practices for fixes: * This fixer can just select a quote type arbitrarily. If it guesses wrong, the resulting code will be automatically reported and fixed by the [`quotes`](/docs/rules/quotes.md) rule. +### Providing Suggestions + +In some cases fixes aren't appropriate to be automatically applied, for example, if a fix potentially changes functionality or if there are multiple valid ways to fix a rule depending on the implementation intent (see the best practices for [applying fixes](#applying-fixes) listed above). In these cases, there is an alternative `suggest` option on `context.report()` that allows other tools, such as editors, to expose helpers for users to manually apply a suggestion. + +In order to provide suggestions, use the `suggest` key in the report argument with an array of suggestion objects. The suggestion objects represent individual suggestions that could be applied and require either a `desc` key string that describes what applying the suggestion would do or a `messageId` key (see [below](#suggestion-messageids)), and a `fix` key that is a function defining the suggestion result. This `fix` function follows the same API as regular fixes (described above in [applying fixes](#applying-fixes)). + +```js +context.report({ + node: node, + message: "Unnecessary escape character: \\{{character}}.", + data: { character }, + suggest: [ + { + desc: "Remove the `\\`. This maintains the current functionality.", + fix: function(fixer) { + return fixer.removeRange(range); + } + }, + { + desc: "Replace the `\\` with `\\\\` to include the actual backslash character.", + fix: function(fixer) { + return fixer.insertTextBeforeRange(range, "\\"); + } + } + ] +}); +``` + +Note: Suggestions will be applied as a stand-alone change, without triggering multipass fixes. Each suggestion should focus on a singular change in the code and should not try to conform to user defined styles. For example, if a suggestion is adding a new statement into the codebase, it should not try to match correct indentation, or confirm to user preferences on presence/absence of semicolumns. All of those things can be corrected by multipass autofix when the user triggers it. + +Best practices for suggestions: + +1. Don't try to do too much and suggest large refactors that could introduce a lot of breaking changes. +1. As noted above, don't try to conform to user-defined styles. + +#### Suggestion `messageId`s + +Instead of using a `desc` key for suggestions a `messageId` can be used instead. This works the same way as `messageId`s for the overall error (see [messageIds](#messageIds)). Here is an example of how to use it in a rule: + +```js +module.exports = { + meta: { + messages: { + unnecessaryEscape: "Unnecessary escape character: \\{{character}}.", + removeEscape: "Remove the `\\`. This maintains the current functionality.", + escapeBackslash: "Replace the `\\` with `\\\\` to include the actual backslash character." + } + }, + create: function(context) { + // ... + context.report({ + node: node, + messageId: 'unnecessaryEscape', + data: { character }, + suggest: [ + { + messageId: "removeEscape", + fix: function(fixer) { + return fixer.removeRange(range); + } + }, + { + messageId: "escapeBackslash", + fix: function(fixer) { + return fixer.insertTextBeforeRange(range, "\\"); + } + } + ] + }); + } +}; +``` + ### context.options Some rules require options in order to function correctly. These options appear in configuration (`.eslintrc`, command line, or in comments). For example: diff --git a/lib/linter/report-translator.js b/lib/linter/report-translator.js index 8c9ed007a25..eef5165585b 100644 --- a/lib/linter/report-translator.js +++ b/lib/linter/report-translator.js @@ -26,6 +26,7 @@ const interpolate = require("./interpolate"); * @property {Object} [data] Optional data to use to fill in placeholders in the * message. * @property {Function} [fix] The function to call that creates a fix command. + * @property {Array<{desc?: string, messageId?: string, fix: Function}>} suggest Suggestion descriptions and functions to create a the associated fixes. */ /** @@ -34,14 +35,15 @@ const interpolate = require("./interpolate"); * @property {string} ruleId * @property {(0|1|2)} severity * @property {(string|undefined)} message - * @property {(string|undefined)} messageId + * @property {(string|undefined)} [messageId] * @property {number} line * @property {number} column - * @property {(number|undefined)} endLine - * @property {(number|undefined)} endColumn + * @property {(number|undefined)} [endLine] + * @property {(number|undefined)} [endColumn] * @property {(string|null)} nodeType * @property {string} source - * @property {({text: string, range: (number[]|null)}|null)} fix + * @property {({text: string, range: (number[]|null)}|null)} [fix] + * @property {Array<{text: string, range: (number[]|null)}|null>} [suggestions] */ //------------------------------------------------------------------------------ @@ -182,6 +184,29 @@ function normalizeFixes(descriptor, sourceCode) { return fix; } +/** + * Gets an array of suggestion objects from the given descriptor. + * @param {MessageDescriptor} descriptor The report descriptor. + * @param {SourceCode} sourceCode The source code object to get text between fixes. + * @param {Object} messages Object of meta messages for the rule. + * @returns {Array} The suggestions for the descriptor + */ +function mapSuggestions(descriptor, sourceCode, messages) { + if (!descriptor.suggest || !Array.isArray(descriptor.suggest)) { + return []; + } + + return descriptor.suggest.map(suggestInfo => { + const computedDesc = suggestInfo.desc || messages[suggestInfo.messageId]; + + return { + ...suggestInfo, + desc: interpolate(computedDesc, suggestInfo.data), + fix: normalizeFixes(suggestInfo, sourceCode) + }; + }); +} + /** * Creates information about the report from a descriptor * @param {Object} options Information about the problem @@ -192,6 +217,7 @@ function normalizeFixes(descriptor, sourceCode) { * @param {string} [options.messageId] The error message ID. * @param {{start: SourceLocation, end: (SourceLocation|null)}} options.loc Start and end location * @param {{text: string, range: (number[]|null)}} options.fix The fix object + * @param {Array<{text: string, range: (number[]|null)}>} options.suggestions The array of suggestions objects * @returns {function(...args): ReportInfo} Function that returns information about the report */ function createProblem(options) { @@ -221,9 +247,47 @@ function createProblem(options) { problem.fix = options.fix; } + if (options.suggestions && options.suggestions.length > 0) { + problem.suggestions = options.suggestions; + } + return problem; } +/** + * Validates that suggestions are properly defined. Throws if an error is detected. + * @param {Array<{ desc?: string, messageId?: string }>} suggest The incoming suggest data. + * @param {Object} messages Object of meta messages for the rule. + * @returns {void} + */ +function validateSuggestions(suggest, messages) { + if (suggest && Array.isArray(suggest)) { + suggest.forEach(suggestion => { + if (suggestion.messageId) { + const { messageId } = suggestion; + + if (!messages) { + throw new TypeError(`context.report() called with a suggest option with a messageId '${messageId}', but no messages were present in the rule metadata.`); + } + + if (!messages[messageId]) { + throw new TypeError(`context.report() called with a suggest option with a messageId '${messageId}' which is not present in the 'messages' config: ${JSON.stringify(messages, null, 2)}`); + } + + if (suggestion.desc) { + throw new TypeError("context.report() called with a suggest option that defines both a 'messageId' and an 'desc'. Please only pass one."); + } + } else if (!suggestion.desc) { + throw new TypeError("context.report() called with a suggest option that doesn't have either a `desc` or `messageId`"); + } + + if (typeof suggestion.fix !== "function") { + throw new TypeError(`context.report() called with a suggest option without a fix function. See: ${suggestion}`); + } + }); + } +} + /** * Returns a function that converts the arguments of a `context.report` call from a rule into a reported * problem for the Node.js API. @@ -242,17 +306,17 @@ module.exports = function createReportTranslator(metadata) { */ return (...args) => { const descriptor = normalizeMultiArgReportCall(...args); + const messages = metadata.messageIds; assertValidNodeInfo(descriptor); let computedMessage; if (descriptor.messageId) { - if (!metadata.messageIds) { + if (!messages) { throw new TypeError("context.report() called with a messageId, but no messages were present in the rule metadata."); } const id = descriptor.messageId; - const messages = metadata.messageIds; if (descriptor.message) { throw new TypeError("context.report() called with a message and a messageId. Please only pass one."); @@ -267,6 +331,7 @@ module.exports = function createReportTranslator(metadata) { throw new TypeError("Missing `message` property in report() call; add a message that describes the linting problem."); } + validateSuggestions(descriptor.suggest, messages); return createProblem({ ruleId: metadata.ruleId, @@ -275,7 +340,8 @@ module.exports = function createReportTranslator(metadata) { message: interpolate(computedMessage, descriptor.data), messageId: descriptor.messageId, loc: normalizeReportLoc(descriptor), - fix: metadata.disableFixes ? null : normalizeFixes(descriptor, metadata.sourceCode) + fix: metadata.disableFixes ? null : normalizeFixes(descriptor, metadata.sourceCode), + suggestions: metadata.disableFixes ? [] : mapSuggestions(descriptor, metadata.sourceCode, messages) }); }; }; diff --git a/lib/rule-tester/rule-tester.js b/lib/rule-tester/rule-tester.js index e57cd09bd87..b4b3b2bec86 100644 --- a/lib/rule-tester/rule-tester.js +++ b/lib/rule-tester/rule-tester.js @@ -349,7 +349,7 @@ class RuleTester { filename = item.filename; } - if (Object.prototype.hasOwnProperty.call(item, "options")) { + if (hasOwnProperty(item, "options")) { assert(Array.isArray(item.options), "options must be an array"); config.rules[ruleName] = [1].concat(item.options); } else { @@ -577,21 +577,55 @@ class RuleTester { assert.strictEqual(message.nodeType, error.type, `Error type should be ${error.type}, found ${message.nodeType}`); } - if (Object.prototype.hasOwnProperty.call(error, "line")) { + if (hasOwnProperty(error, "line")) { assert.strictEqual(message.line, error.line, `Error line should be ${error.line}`); } - if (Object.prototype.hasOwnProperty.call(error, "column")) { + if (hasOwnProperty(error, "column")) { assert.strictEqual(message.column, error.column, `Error column should be ${error.column}`); } - if (Object.prototype.hasOwnProperty.call(error, "endLine")) { + if (hasOwnProperty(error, "endLine")) { assert.strictEqual(message.endLine, error.endLine, `Error endLine should be ${error.endLine}`); } - if (Object.prototype.hasOwnProperty.call(error, "endColumn")) { + if (hasOwnProperty(error, "endColumn")) { assert.strictEqual(message.endColumn, error.endColumn, `Error endColumn should be ${error.endColumn}`); } + + if (hasOwnProperty(error, "suggestions")) { + + // Support asserting there are no suggestions + if (!error.suggestions) { + assert.strictEqual(message.suggestions, error.suggestions, `Error should have no suggestions on error with message: "${message.message}"`); + } else { + assert.strictEqual(Array.isArray(message.suggestions), true, `Error should have an array of suggestions. Instead received "${message.suggestions}" on error with message: "${message.message}"`); + assert.strictEqual(message.suggestions.length, error.suggestions.length, `Error should have ${error.suggestions.length} suggestions. Instead found ${message.suggestions.length} suggestions`); + + error.suggestions.forEach((expectedSuggestion, index) => { + const actualSuggestion = message.suggestions[index]; + + /** + * Tests equality of a suggestion key if that key is defined in the expected output. + * @param {string} key Key to validate from the suggestion object + * @returns {void} + */ + function assertSuggestionKeyEquals(key) { + if (hasOwnProperty(expectedSuggestion, key)) { + assert.deepStrictEqual(actualSuggestion[key], expectedSuggestion[key], `Error suggestion at index: ${index} should have desc of: "${actualSuggestion[key]}"`); + } + } + assertSuggestionKeyEquals("desc"); + assertSuggestionKeyEquals("messageId"); + + if (hasOwnProperty(expectedSuggestion, "output")) { + const codeWithAppliedSuggestion = SourceCodeFixer.applyFixes(item.code, [actualSuggestion]).output; + + assert.strictEqual(codeWithAppliedSuggestion, expectedSuggestion.output, `Expected the applied suggestion fix to match the test suggestion output for suggestion at index: ${index} on error with message: "${message.message}"`); + } + }); + } + } } else { // Message was an unexpected type @@ -600,7 +634,7 @@ class RuleTester { } } - if (Object.prototype.hasOwnProperty.call(item, "output")) { + if (hasOwnProperty(item, "output")) { if (item.output === null) { assert.strictEqual( result.output, diff --git a/lib/rules/no-useless-escape.js b/lib/rules/no-useless-escape.js index ebfe4cba38a..8057e44ddab 100644 --- a/lib/rules/no-useless-escape.js +++ b/lib/rules/no-useless-escape.js @@ -85,7 +85,14 @@ module.exports = { description: "disallow unnecessary escape characters", category: "Best Practices", recommended: true, - url: "https://eslint.org/docs/rules/no-useless-escape" + url: "https://eslint.org/docs/rules/no-useless-escape", + suggestion: true + }, + + messages: { + unnecessaryEscape: "Unnecessary escape character: \\{{character}}.", + removeEscape: "Remove the `\\`. This maintains the current functionality.", + escapeBackslash: "Replace the `\\` with `\\\\` to include the actual backslash character." }, schema: [] @@ -103,6 +110,8 @@ module.exports = { */ function report(node, startOffset, character) { const start = sourceCode.getLocFromIndex(sourceCode.getIndexFromLoc(node.loc.start) + startOffset); + const rangeStart = sourceCode.getIndexFromLoc(node.loc.start) + startOffset; + const range = [rangeStart, rangeStart + 1]; context.report({ node, @@ -110,8 +119,22 @@ module.exports = { start, end: { line: start.line, column: start.column + 1 } }, - message: "Unnecessary escape character: \\{{character}}.", - data: { character } + messageId: "unnecessaryEscape", + data: { character }, + suggest: [ + { + messageId: "removeEscape", + fix(fixer) { + return fixer.removeRange(range); + } + }, + { + messageId: "escapeBackslash", + fix(fixer) { + return fixer.insertTextBeforeRange(range, "\\"); + } + } + ] }); } diff --git a/lib/shared/types.js b/lib/shared/types.js index 57740b6c951..a5bd0200e27 100644 --- a/lib/shared/types.js +++ b/lib/shared/types.js @@ -92,6 +92,14 @@ module.exports = {}; * @property {string} message The error message. * @property {string|null} ruleId The ID of the rule which makes this message. * @property {0|1|2} severity The severity of this message. + * @property {Array<{desc?: string, messageId?: string, fix: {range: [number, number], text: string}}>} [suggestions] Information for suggestions. + */ + +/** + * @typedef {Object} SuggestionResult + * @property {string} desc A short description. + * @property {string} [messageId] Id referencing a message for the description. + * @property {{ text: string, range: number[] }} fix fix result info */ /** diff --git a/tests/fixtures/testers/rule-tester/suggestions.js b/tests/fixtures/testers/rule-tester/suggestions.js new file mode 100644 index 00000000000..df97e6dd138 --- /dev/null +++ b/tests/fixtures/testers/rule-tester/suggestions.js @@ -0,0 +1,60 @@ +"use strict"; + +module.exports.basic = { + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + message: "Avoid using identifiers named 'foo'.", + suggest: [{ + desc: "Rename identifier 'foo' to 'bar'", + fix: fixer => fixer.replaceText(node, 'bar') + }] + }); + } + } + }; + } +}; + +module.exports.withMessageIds = { + meta: { + messages: { + avoidFoo: "Avoid using identifiers named '{{ name }}'.", + unused: "An unused key", + renameFoo: "Rename identifier 'foo' to '{{ newName }}'" + } + }, + create(context) { + return { + Identifier(node) { + if (node.name === "foo") { + context.report({ + node, + messageId: "avoidFoo", + data: { + name: "foo" + }, + suggest: [{ + messageId: "renameFoo", + data: { + newName: "bar" + }, + fix: fixer => fixer.replaceText(node, "bar") + }, { + messageId: "renameFoo", + data: { + newName: "baz" + }, + fix: fixer => fixer.replaceText(node, "baz") + }] + }); + } + } + }; + } +}; + + diff --git a/tests/lib/linter/linter.js b/tests/lib/linter/linter.js index 4d04eb34ddc..e444aea920c 100644 --- a/tests/lib/linter/linter.js +++ b/tests/lib/linter/linter.js @@ -4351,6 +4351,86 @@ describe("Linter", () => { }); }); + describe("suggestions", () => { + it("provides suggestion information for tools to use", () => { + linter.defineRule("rule-with-suggestions", context => ({ + Program(node) { + context.report({ + node, + message: "Incorrect spacing", + suggest: [{ + desc: "Insert space at the beginning", + fix: fixer => fixer.insertTextBefore(node, " ") + }, { + desc: "Insert space at the end", + fix: fixer => fixer.insertTextAfter(node, " ") + }] + }); + } + })); + + const messages = linter.verify("var a = 1;", { rules: { "rule-with-suggestions": "error" } }); + + assert.deepStrictEqual(messages[0].suggestions, [{ + desc: "Insert space at the beginning", + fix: { + range: [0, 0], + text: " " + } + }, { + desc: "Insert space at the end", + fix: { + range: [10, 10], + text: " " + } + }]); + }); + + it("supports messageIds for suggestions", () => { + linter.defineRule("rule-with-suggestions", { + meta: { + messages: { + suggestion1: "Insert space at the beginning", + suggestion2: "Insert space at the end" + } + }, + create: context => ({ + Program(node) { + context.report({ + node, + message: "Incorrect spacing", + suggest: [{ + messageId: "suggestion1", + fix: fixer => fixer.insertTextBefore(node, " ") + }, { + messageId: "suggestion2", + fix: fixer => fixer.insertTextAfter(node, " ") + }] + }); + } + }) + }); + + const messages = linter.verify("var a = 1;", { rules: { "rule-with-suggestions": "error" } }); + + assert.deepStrictEqual(messages[0].suggestions, [{ + messageId: "suggestion1", + desc: "Insert space at the beginning", + fix: { + range: [0, 0], + text: " " + } + }, { + messageId: "suggestion2", + desc: "Insert space at the end", + fix: { + range: [10, 10], + text: " " + } + }]); + }); + }); + describe("mutability", () => { let linter1 = null; let linter2 = null; @@ -4602,6 +4682,19 @@ describe("Linter", () => { }); }); + it("does not include suggestions in autofix results", () => { + const fixResult = linter.verifyAndFix("var foo = /\\#/", { + rules: { + semi: 2, + "no-useless-escape": 2 + } + }); + + assert.strictEqual(fixResult.output, "var foo = /\\#/;"); + assert.strictEqual(fixResult.fixed, true); + assert.strictEqual(fixResult.messages[0].suggestions.length > 0, true); + }); + it("does not apply autofixes when fix argument is `false`", () => { const fixResult = linter.verifyAndFix("var a", { rules: { diff --git a/tests/lib/linter/report-translator.js b/tests/lib/linter/report-translator.js index 2a3cda6c7e2..a79400cf7e4 100644 --- a/tests/lib/linter/report-translator.js +++ b/tests/lib/linter/report-translator.js @@ -40,7 +40,7 @@ describe("createReportTranslator", () => { ); } - let node, location, message, translateReport; + let node, location, message, translateReport, suggestion1, suggestion2; beforeEach(() => { const sourceCode = createSourceCode("foo\nbar"); @@ -48,7 +48,18 @@ describe("createReportTranslator", () => { node = sourceCode.ast.body[0]; location = sourceCode.ast.body[1].loc.start; message = "foo"; - translateReport = createReportTranslator({ ruleId: "foo-rule", severity: 2, sourceCode, messageIds: { testMessage: message } }); + suggestion1 = "First suggestion"; + suggestion2 = "Second suggestion {{interpolated}}"; + translateReport = createReportTranslator({ + ruleId: "foo-rule", + severity: 2, + sourceCode, + messageIds: { + testMessage: message, + suggestion1, + suggestion2 + } + }); }); describe("old-style call with location", () => { @@ -91,7 +102,14 @@ describe("createReportTranslator", () => { node, loc: location, message, - fix: () => ({ range: [1, 2], text: "foo" }) + fix: () => ({ range: [1, 2], text: "foo" }), + suggest: [{ + desc: "suggestion 1", + fix: () => ({ range: [2, 3], text: "s1" }) + }, { + desc: "suggestion 2", + fix: () => ({ range: [3, 4], text: "s2" }) + }] }; assert.deepStrictEqual( @@ -106,10 +124,18 @@ describe("createReportTranslator", () => { fix: { range: [1, 2], text: "foo" - } + }, + suggestions: [{ + desc: "suggestion 1", + fix: { range: [2, 3], text: "s1" } + }, { + desc: "suggestion 2", + fix: { range: [3, 4], text: "s2" } + }] } ); }); + it("should translate the messageId into a message", () => { const reportDescriptor = { node, @@ -135,6 +161,7 @@ describe("createReportTranslator", () => { } ); }); + it("should throw when both messageId and message are provided", () => { const reportDescriptor = { node, @@ -150,6 +177,7 @@ describe("createReportTranslator", () => { "context.report() called with a message and a messageId. Please only pass one." ); }); + it("should throw when an invalid messageId is provided", () => { const reportDescriptor = { node, @@ -164,6 +192,7 @@ describe("createReportTranslator", () => { /^context\.report\(\) called with a messageId of '[^']+' which is not present in the 'messages' config:/u ); }); + it("should throw when no message is provided", () => { const reportDescriptor = { node }; @@ -173,7 +202,118 @@ describe("createReportTranslator", () => { "Missing `message` property in report() call; add a message that describes the linting problem." ); }); + + it("should support messageIds for suggestions and output resulting descriptions", () => { + const reportDescriptor = { + node, + loc: location, + message, + suggest: [{ + messageId: "suggestion1", + fix: () => ({ range: [2, 3], text: "s1" }) + }, { + messageId: "suggestion2", + data: { interpolated: "'interpolated value'" }, + fix: () => ({ range: [3, 4], text: "s2" }) + }] + }; + + assert.deepStrictEqual( + translateReport(reportDescriptor), + { + ruleId: "foo-rule", + severity: 2, + message: "foo", + line: 2, + column: 1, + nodeType: "ExpressionStatement", + suggestions: [{ + messageId: "suggestion1", + desc: "First suggestion", + fix: { range: [2, 3], text: "s1" } + }, { + messageId: "suggestion2", + data: { interpolated: "'interpolated value'" }, + desc: "Second suggestion 'interpolated value'", + fix: { range: [3, 4], text: "s2" } + }] + } + ); + }); + + it("should throw when a suggestion defines both a desc and messageId", () => { + const reportDescriptor = { + node, + loc: location, + message, + suggest: [{ + desc: "The description", + messageId: "suggestion1", + fix: () => ({ range: [2, 3], text: "s1" }) + }] + }; + + assert.throws( + () => translateReport(reportDescriptor), + TypeError, + "context.report() called with a suggest option that defines both a 'messageId' and an 'desc'. Please only pass one." + ); + }); + + it("should throw when a suggestion uses an invalid messageId", () => { + const reportDescriptor = { + node, + loc: location, + message, + suggest: [{ + messageId: "noMatchingMessage", + fix: () => ({ range: [2, 3], text: "s1" }) + }] + }; + + assert.throws( + () => translateReport(reportDescriptor), + TypeError, + /^context\.report\(\) called with a suggest option with a messageId '[^']+' which is not present in the 'messages' config:/u + ); + }); + + it("should throw when a suggestion does not provide either a desc or messageId", () => { + const reportDescriptor = { + node, + loc: location, + message, + suggest: [{ + fix: () => ({ range: [2, 3], text: "s1" }) + }] + }; + + assert.throws( + () => translateReport(reportDescriptor), + TypeError, + "context.report() called with a suggest option that doesn't have either a `desc` or `messageId`" + ); + }); + + it("should throw when a suggestion does not provide a fix function", () => { + const reportDescriptor = { + node, + loc: location, + message, + suggest: [{ + desc: "The description", + fix: false + }] + }; + + assert.throws( + () => translateReport(reportDescriptor), + TypeError, + /^context\.report\(\) called with a suggest option without a fix function. See:/u + ); + }); }); + describe("combining autofixes", () => { it("should merge fixes to one if 'fix' function returns an array of fixes.", () => { const reportDescriptor = { @@ -346,7 +486,73 @@ describe("createReportTranslator", () => { } ); }); + }); + + describe("suggestions", () => { + it("should support multiple suggestions.", () => { + const reportDescriptor = { + node, + loc: location, + message, + suggest: [{ + desc: "A first suggestion for the issue", + fix: () => [{ range: [1, 2], text: "foo" }] + }, { + desc: "A different suggestion for the issue", + fix: () => [{ range: [1, 3], text: "foobar" }] + }] + }; + + assert.deepStrictEqual( + translateReport(reportDescriptor), + { + ruleId: "foo-rule", + severity: 2, + message: "foo", + line: 2, + column: 1, + nodeType: "ExpressionStatement", + suggestions: [{ + desc: "A first suggestion for the issue", + fix: { range: [1, 2], text: "foo" } + }, { + desc: "A different suggestion for the issue", + fix: { range: [1, 3], text: "foobar" } + }] + } + ); + }); + + it("should merge suggestion fixes to one if 'fix' function returns an array of fixes.", () => { + const reportDescriptor = { + node, + loc: location, + message, + suggest: [{ + desc: "A suggestion for the issue", + fix: () => [{ range: [1, 2], text: "foo" }, { range: [4, 5], text: "bar" }] + }] + }; + assert.deepStrictEqual( + translateReport(reportDescriptor), + { + ruleId: "foo-rule", + severity: 2, + message: "foo", + line: 2, + column: 1, + nodeType: "ExpressionStatement", + suggestions: [{ + desc: "A suggestion for the issue", + fix: { + range: [1, 5], + text: "fooo\nbar" + } + }] + } + ); + }); }); describe("message interpolation", () => { diff --git a/tests/lib/rule-tester/rule-tester.js b/tests/lib/rule-tester/rule-tester.js index 71e381c70db..6ab341ccc7e 100644 --- a/tests/lib/rule-tester/rule-tester.js +++ b/tests/lib/rule-tester/rule-tester.js @@ -959,6 +959,161 @@ describe("RuleTester", () => { }, "Error must specify 'messageId' if 'data' is used."); }); + describe("suggestions", () => { + it("should pass with valid suggestions", () => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [ + "var boo;" + ], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "var bar;" + }] + }] + }] + }); + }); + + it("should pass with suggestions on multiple lines", () => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [], + invalid: [ + { + code: "function foo() {\n var foo = 1;\n}", + errors: [{ + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "function bar() {\n var foo = 1;\n}" + }] + }, { + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "function foo() {\n var bar = 1;\n}" + }] + }] + } + ] + }); + }); + + it("should pass with suggestions using messageIds", () => { + ruleTester.run("suggestions-messageIds", require("../../fixtures/testers/rule-tester/suggestions").withMessageIds, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + messageId: "renameFoo", + output: "var bar;" + }, { + desc: "Rename identifier 'foo' to 'baz'", + output: "var baz;" + }] + }] + }] + }); + }); + + it("should support explicitly expecting no suggestions", () => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/no-eval"), { + valid: [], + invalid: [{ + code: "eval('var foo');", + errors: [{ + suggestions: void 0 + }] + }] + }); + }); + + it("should fail when expecting no suggestions and there are suggestions", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: void 0 + }] + }] + }); + }, "Error should have no suggestions on error with message: \"Avoid using identifiers named 'foo'.\""); + }); + + it("should fail when testing for suggestions that don't exist", () => { + assert.throws(() => { + ruleTester.run("no-var", require("../../fixtures/testers/rule-tester/no-var"), { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + messageId: "this-does-not-exist" + }] + }] + }] + }); + }, "Error should have an array of suggestions. Instead received \"undefined\" on error with message: \"Bad var.\""); + }); + + it("should fail when there are a different number of suggestions", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "var bar;" + }, { + desc: "Rename identifier 'foo' to 'baz'", + output: "var baz;" + }] + }] + }] + }); + }, "Error should have 2 suggestions. Instead found 1 suggestions"); + }); + + it("should fail when the suggestion description doesn't match", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + desc: "not right", + output: "var baz;" + }] + }] + }] + }); + }, "Error suggestion at index: 0 should have desc of: \"Rename identifier 'foo' to 'bar'\""); + }); + + it("should fail when the resulting suggestion output doesn't match", () => { + assert.throws(() => { + ruleTester.run("suggestions-basic", require("../../fixtures/testers/rule-tester/suggestions").basic, { + valid: [], + invalid: [{ + code: "var foo;", + errors: [{ + suggestions: [{ + desc: "Rename identifier 'foo' to 'bar'", + output: "var baz;" + }] + }] + }] + }); + }, "Expected the applied suggestion fix to match the test suggestion output"); + }); + }); + describe("naming test cases", () => { /** diff --git a/tests/lib/rules/no-useless-escape.js b/tests/lib/rules/no-useless-escape.js index 31f8969d3ef..9321385b55b 100644 --- a/tests/lib/rules/no-useless-escape.js +++ b/tests/lib/rules/no-useless-escape.js @@ -127,163 +127,851 @@ ruleTester.run("no-useless-escape", rule, { ], invalid: [ - { code: "var foo = /\\#/;", errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\#.", type: "Literal" }] }, - { code: "var foo = /\\;/;", errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\;.", type: "Literal" }] }, - { code: "var foo = \"\\'\";", errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\'.", type: "Literal" }] }, - { code: "var foo = \"\\#/\";", errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\#.", type: "Literal" }] }, - { code: "var foo = \"\\a\"", errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\a.", type: "Literal" }] }, - { code: "var foo = \"\\B\";", errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\B.", type: "Literal" }] }, - { code: "var foo = \"\\@\";", errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\@.", type: "Literal" }] }, - { code: "var foo = \"foo \\a bar\";", errors: [{ line: 1, column: 16, endColumn: 17, message: "Unnecessary escape character: \\a.", type: "Literal" }] }, - { code: "var foo = '\\\"';", errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\\".", type: "Literal" }] }, - { code: "var foo = '\\#';", errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\#.", type: "Literal" }] }, - { code: "var foo = '\\$';", errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\$.", type: "Literal" }] }, - { code: "var foo = '\\p';", errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\p.", type: "Literal" }] }, + { + code: "var foo = /\\#/;", + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\#.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = /#/;" + }, { + messageId: "escapeBackslash", + output: "var foo = /\\\\#/;" + }] + }] + }, + { + code: "var foo = /\\;/;", + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\;.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = /;/;" + }, { + messageId: "escapeBackslash", + output: "var foo = /\\\\;/;" + }] + }] + }, + { + code: "var foo = \"\\'\";", + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\'.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = \"'\";" + }, { + messageId: "escapeBackslash", + output: "var foo = \"\\\\'\";" + }] + }] + }, + { + code: "var foo = \"\\#/\";", + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\#.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = \"#/\";" + }, { + messageId: "escapeBackslash", + output: "var foo = \"\\\\#/\";" + }] + }] + }, + { + code: "var foo = \"\\a\"", + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\a.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = \"a\"" + }, { + messageId: "escapeBackslash", + output: "var foo = \"\\\\a\"" + }] + }] + }, + { + code: "var foo = \"\\B\";", + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\B.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = \"B\";" + }, { + messageId: "escapeBackslash", + output: "var foo = \"\\\\B\";" + }] + }] + }, + { + code: "var foo = \"\\@\";", + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\@.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = \"@\";" + }, { + messageId: "escapeBackslash", + output: "var foo = \"\\\\@\";" + }] + }] + }, + { + code: "var foo = \"foo \\a bar\";", + errors: [{ + line: 1, + column: 16, + endColumn: 17, + message: "Unnecessary escape character: \\a.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = \"foo a bar\";" + }, { + messageId: "escapeBackslash", + output: "var foo = \"foo \\\\a bar\";" + }] + }] + }, + { + code: "var foo = '\\\"';", + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\\".", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = '\"';" + }, { + messageId: "escapeBackslash", + output: "var foo = '\\\\\"';" + }] + }] + }, + { + code: "var foo = '\\#';", + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\#.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = '#';" + }, { + messageId: "escapeBackslash", + output: "var foo = '\\\\#';" + }] + }] + }, + { + code: "var foo = '\\$';", + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\$.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = '$';" + }, { + messageId: "escapeBackslash", + output: "var foo = '\\\\$';" + }] + }] + }, + { + code: "var foo = '\\p';", + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\p.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = 'p';" + }, { + messageId: "escapeBackslash", + output: "var foo = '\\\\p';" + }] + }] + }, { code: "var foo = '\\p\\a\\@';", errors: [ - { line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\p.", type: "Literal" }, - { line: 1, column: 14, endColumn: 15, message: "Unnecessary escape character: \\a.", type: "Literal" }, - { line: 1, column: 16, endColumn: 17, message: "Unnecessary escape character: \\@.", type: "Literal" } + { + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\p.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = 'p\\a\\@';" + }, { + messageId: "escapeBackslash", + output: "var foo = '\\\\p\\a\\@';" + }] + }, + { + line: 1, + column: 14, + endColumn: 15, + message: "Unnecessary escape character: \\a.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = '\\pa\\@';" + }, { + messageId: "escapeBackslash", + output: "var foo = '\\p\\\\a\\@';" + }] + }, + { + line: 1, + column: 16, + endColumn: 17, + message: "Unnecessary escape character: \\@.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = '\\p\\a@';" + }, { + messageId: "escapeBackslash", + output: "var foo = '\\p\\a\\\\@';" + }] + } ] }, { code: "", parserOptions: { ecmaFeatures: { jsx: true } }, - errors: [{ line: 1, column: 13, endColumn: 14, message: "Unnecessary escape character: \\d.", type: "Literal" }] + errors: [{ + line: 1, + column: 13, + endColumn: 14, + message: "Unnecessary escape character: \\d.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "" + }, { + messageId: "escapeBackslash", + output: "" + }] + }] + }, + { + code: "var foo = '\\`';", + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\`.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = '`';" + }, { + messageId: "escapeBackslash", + output: "var foo = '\\\\`';" + }] + }] + }, + { + code: "var foo = `\\\"`;", + parserOptions: { ecmaVersion: 6 }, + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\\".", + type: "TemplateElement", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = `\"`;" + }, { + messageId: "escapeBackslash", + output: "var foo = `\\\\\"`;" + }] + }] + }, + { + code: "var foo = `\\'`;", + parserOptions: { ecmaVersion: 6 }, + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\'.", + type: "TemplateElement", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = `'`;" + }, { + messageId: "escapeBackslash", + output: "var foo = `\\\\'`;" + }] + }] + }, + { + code: "var foo = `\\#`;", + parserOptions: { ecmaVersion: 6 }, + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\#.", + type: "TemplateElement", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = `#`;" + }, { + messageId: "escapeBackslash", + output: "var foo = `\\\\#`;" + }] + }] }, - { code: "var foo = '\\`';", errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\`.", type: "Literal" }] }, - { code: "var foo = `\\\"`;", parserOptions: { ecmaVersion: 6 }, errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\\".", type: "TemplateElement" }] }, - { code: "var foo = `\\'`;", parserOptions: { ecmaVersion: 6 }, errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\'.", type: "TemplateElement" }] }, - { code: "var foo = `\\#`;", parserOptions: { ecmaVersion: 6 }, errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\#.", type: "TemplateElement" }] }, { code: "var foo = '\\`foo\\`';", errors: [ - { line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\`.", type: "Literal" }, - { line: 1, column: 17, endColumn: 18, message: "Unnecessary escape character: \\`.", type: "Literal" } + { + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\`.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = '`foo\\`';" + }, { + messageId: "escapeBackslash", + output: "var foo = '\\\\`foo\\`';" + }] + }, + { + line: 1, + column: 17, + endColumn: 18, + message: "Unnecessary escape character: \\`.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = '\\`foo`';" + }, { + messageId: "escapeBackslash", + output: "var foo = '\\`foo\\\\`';" + }] + } ] }, { code: "var foo = `\\\"${foo}\\\"`;", parserOptions: { ecmaVersion: 6 }, errors: [ - { line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\\".", type: "TemplateElement" }, - { line: 1, column: 20, endColumn: 21, message: "Unnecessary escape character: \\\".", type: "TemplateElement" } + { + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\\".", + type: "TemplateElement", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = `\"${foo}\\\"`;" + }, { + messageId: "escapeBackslash", + output: "var foo = `\\\\\"${foo}\\\"`;" + }] + }, + { + line: 1, + column: 20, + endColumn: 21, + message: "Unnecessary escape character: \\\".", + type: "TemplateElement", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = `\\\"${foo}\"`;" + }, { + messageId: "escapeBackslash", + output: "var foo = `\\\"${foo}\\\\\"`;" + }] + } ] }, { code: "var foo = `\\'${foo}\\'`;", parserOptions: { ecmaVersion: 6 }, errors: [ - { line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\'.", type: "TemplateElement" }, - { line: 1, column: 20, endColumn: 21, message: "Unnecessary escape character: \\'.", type: "TemplateElement" } + { + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\'.", + type: "TemplateElement", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = `'${foo}\\'`;" + }, { + messageId: "escapeBackslash", + output: "var foo = `\\\\'${foo}\\'`;" + }] + }, + { + line: 1, + column: 20, + endColumn: 21, + message: "Unnecessary escape character: \\'.", + type: "TemplateElement", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = `\\'${foo}'`;" + }, { + messageId: "escapeBackslash", + output: "var foo = `\\'${foo}\\\\'`;" + }] + } ] }, { code: "var foo = `\\#${foo}`;", parserOptions: { ecmaVersion: 6 }, - errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\#.", type: "TemplateElement" }] + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\#.", + type: "TemplateElement", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = `#${foo}`;" + }, { + messageId: "escapeBackslash", + output: "var foo = `\\\\#${foo}`;" + }] + }] }, { code: "let foo = '\\ ';", parserOptions: { ecmaVersion: 6 }, - errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\ .", type: "Literal" }] + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\ .", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "let foo = ' ';" + }, { + messageId: "escapeBackslash", + output: "let foo = '\\\\ ';" + }] + }] }, { code: "let foo = /\\ /;", parserOptions: { ecmaVersion: 6 }, - errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\ .", type: "Literal" }] + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\ .", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: "let foo = / /;" + }, { + messageId: "escapeBackslash", + output: "let foo = /\\\\ /;" + }] + }] }, { code: "var foo = `\\$\\{{${foo}`;", parserOptions: { ecmaVersion: 6 }, errors: [ - { line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\$.", type: "TemplateElement" } + { + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\$.", + type: "TemplateElement", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = `$\\{{${foo}`;" + }, { + messageId: "escapeBackslash", + output: "var foo = `\\\\$\\{{${foo}`;" + }] + } ] }, { code: "var foo = `\\$a${foo}`;", parserOptions: { ecmaVersion: 6 }, errors: [ - { line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\$.", type: "TemplateElement" } + { + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\$.", + type: "TemplateElement", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = `$a${foo}`;" + }, { + messageId: "escapeBackslash", + output: "var foo = `\\\\$a${foo}`;" + }] + } ] }, { code: "var foo = `a\\{{${foo}`;", parserOptions: { ecmaVersion: 6 }, errors: [ - { line: 1, column: 13, endColumn: 14, message: "Unnecessary escape character: \\{.", type: "TemplateElement" } + { + line: 1, + column: 13, + endColumn: 14, + message: "Unnecessary escape character: \\{.", + type: "TemplateElement", + suggestions: [{ + messageId: "removeEscape", + output: "var foo = `a{{${foo}`;" + }, { + messageId: "escapeBackslash", + output: "var foo = `a\\\\{{${foo}`;" + }] + } ] }, { code: String.raw`var foo = /[ab\-]/`, - errors: [{ line: 1, column: 15, endColumn: 16, message: "Unnecessary escape character: \\-.", type: "Literal" }] + errors: [{ + line: 1, + column: 15, + endColumn: 16, + message: "Unnecessary escape character: \\-.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /[ab-]/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /[ab\\-]/` + }] + }] }, { code: String.raw`var foo = /[\-ab]/`, - errors: [{ line: 1, column: 13, endColumn: 14, message: "Unnecessary escape character: \\-.", type: "Literal" }] + errors: [{ + line: 1, + column: 13, + endColumn: 14, + message: "Unnecessary escape character: \\-.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /[-ab]/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /[\\-ab]/` + }] + }] }, { code: String.raw`var foo = /[ab\?]/`, - errors: [{ line: 1, column: 15, endColumn: 16, message: "Unnecessary escape character: \\?.", type: "Literal" }] + errors: [{ + line: 1, + column: 15, + endColumn: 16, + message: "Unnecessary escape character: \\?.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /[ab?]/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /[ab\\?]/` + }] + }] }, { code: String.raw`var foo = /[ab\.]/`, - errors: [{ line: 1, column: 15, endColumn: 16, message: "Unnecessary escape character: \\..", type: "Literal" }] + errors: [{ + line: 1, + column: 15, + endColumn: 16, + message: "Unnecessary escape character: \\..", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /[ab.]/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /[ab\\.]/` + }] + }] }, { code: String.raw`var foo = /[a\|b]/`, - errors: [{ line: 1, column: 14, endColumn: 15, message: "Unnecessary escape character: \\|.", type: "Literal" }] + errors: [{ + line: 1, + column: 14, + endColumn: 15, + message: "Unnecessary escape character: \\|.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /[a|b]/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /[a\\|b]/` + }] + }] }, { code: String.raw`var foo = /\-/`, - errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\-.", type: "Literal" }] + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\-.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /-/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /\\-/` + }] + }] }, { code: String.raw`var foo = /[\-]/`, - errors: [{ line: 1, column: 13, endColumn: 14, message: "Unnecessary escape character: \\-.", type: "Literal" }] + errors: [{ + line: 1, + column: 13, + endColumn: 14, + message: "Unnecessary escape character: \\-.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /[-]/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /[\\-]/` + }] + }] }, { code: String.raw`var foo = /[ab\$]/`, - errors: [{ line: 1, column: 15, endColumn: 16, message: "Unnecessary escape character: \\$.", type: "Literal" }] + errors: [{ + line: 1, + column: 15, + endColumn: 16, + message: "Unnecessary escape character: \\$.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /[ab$]/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /[ab\\$]/` + }] + }] }, { code: String.raw`var foo = /[\(paren]/`, - errors: [{ line: 1, column: 13, endColumn: 14, message: "Unnecessary escape character: \\(.", type: "Literal" }] + errors: [{ + line: 1, + column: 13, + endColumn: 14, + message: "Unnecessary escape character: \\(.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /[(paren]/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /[\\(paren]/` + }] + }] }, { code: String.raw`var foo = /[\[]/`, - errors: [{ line: 1, column: 13, endColumn: 14, message: "Unnecessary escape character: \\[.", type: "Literal" }] + errors: [{ + line: 1, + column: 13, + endColumn: 14, + message: "Unnecessary escape character: \\[.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /[[]/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /[\\[]/` + }] + }] }, { code: String.raw`var foo = /[\/]/`, // A character class containing '/' - errors: [{ line: 1, column: 13, endColumn: 14, message: "Unnecessary escape character: \\/.", type: "Literal" }] + errors: [{ + line: 1, + column: 13, + endColumn: 14, + message: "Unnecessary escape character: \\/.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /[/]/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /[\\/]/` + }] + }] }, { code: String.raw`var foo = /[\B]/`, - errors: [{ line: 1, column: 13, endColumn: 14, message: "Unnecessary escape character: \\B.", type: "Literal" }] + errors: [{ + line: 1, + column: 13, + endColumn: 14, + message: "Unnecessary escape character: \\B.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /[B]/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /[\\B]/` + }] + }] }, { code: String.raw`var foo = /[a][\-b]/`, - errors: [{ line: 1, column: 16, endColumn: 17, message: "Unnecessary escape character: \\-.", type: "Literal" }] + errors: [{ + line: 1, + column: 16, + endColumn: 17, + message: "Unnecessary escape character: \\-.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /[a][-b]/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /[a][\\-b]/` + }] + }] }, { code: String.raw`var foo = /\-[]/`, - errors: [{ line: 1, column: 12, endColumn: 13, message: "Unnecessary escape character: \\-.", type: "Literal" }] + errors: [{ + line: 1, + column: 12, + endColumn: 13, + message: "Unnecessary escape character: \\-.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /-[]/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /\\-[]/` + }] + }] }, { code: String.raw`var foo = /[a\^]/`, - errors: [{ line: 1, column: 14, endColumn: 15, message: "Unnecessary escape character: \\^.", type: "Literal" }] + errors: [{ + line: 1, + column: 14, + endColumn: 15, + message: "Unnecessary escape character: \\^.", + type: "Literal", + suggestions: [{ + messageId: "removeEscape", + output: String.raw`var foo = /[a^]/` + }, { + messageId: "escapeBackslash", + output: String.raw`var foo = /[a\\^]/` + }] + }] }, { code: "`multiline template\nliteral with useless \\escape`", parserOptions: { ecmaVersion: 6 }, - errors: [{ line: 2, column: 22, endColumn: 23, message: "Unnecessary escape character: \\e.", type: "TemplateElement" }] + errors: [{ + line: 2, + column: 22, + endColumn: 23, + message: "Unnecessary escape character: \\e.", + type: "TemplateElement", + suggestions: [{ + messageId: "removeEscape", + output: "`multiline template\nliteral with useless escape`" + }, { + messageId: "escapeBackslash", + output: "`multiline template\nliteral with useless \\\\escape`" + }] + }] }, { code: "`\\a```", parserOptions: { ecmaVersion: 6 }, - errors: [{ line: 1, column: 2, endColumn: 3, message: "Unnecessary escape character: \\a.", type: "TemplateElement" }] + errors: [{ + line: 1, + column: 2, + endColumn: 3, + message: "Unnecessary escape character: \\a.", + type: "TemplateElement", + suggestions: [{ + messageId: "removeEscape", + output: "`a```" + }, { + messageId: "escapeBackslash", + output: "`\\\\a```" + }] + }] } ] });