diff --git a/lib/rules/fixer-return.js b/lib/rules/fixer-return.js index 0dd7fc02..40dfa5df 100644 --- a/lib/rules/fixer-return.js +++ b/lib/rules/fixer-return.js @@ -98,22 +98,12 @@ module.exports = { // Stacks this function's information. onCodePathStart (codePath, node) { - const parent = node.parent; - - // Whether we are inside the fixer function we care about. - const shouldCheck = ['FunctionExpression', 'ArrowFunctionExpression'].includes(node.type) && - parent.parent.type === 'ObjectExpression' && - parent.parent.parent.type === 'CallExpression' && - contextIdentifiers.has(parent.parent.parent.callee.object) && - parent.parent.parent.callee.property.name === 'report' && - utils.getReportInfo(parent.parent.parent.arguments).fix === node; - funcInfo = { upper: funcInfo, codePath, hasYieldWithFixer: false, hasReturnWithFixer: false, - shouldCheck, + shouldCheck: utils.isAutoFixerFunction(node, contextIdentifiers) || utils.isSuggestionFixerFunction(node, contextIdentifiers), node, }; }, diff --git a/lib/rules/prefer-replace-text.js b/lib/rules/prefer-replace-text.js index 9275883a..a39419d0 100644 --- a/lib/rules/prefer-replace-text.js +++ b/lib/rules/prefer-replace-text.js @@ -43,18 +43,10 @@ module.exports = { // Stacks this function's information. onCodePathStart (codePath, node) { - const parent = node.parent; - const shouldCheck = (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && - parent.parent.type === 'ObjectExpression' && - parent.parent.parent.type === 'CallExpression' && - contextIdentifiers.has(parent.parent.parent.callee.object) && - parent.parent.parent.callee.property.name === 'report' && - utils.getReportInfo(parent.parent.parent.arguments, context).fix === node; - funcInfo = { upper: funcInfo, codePath, - shouldCheck, + shouldCheck: utils.isAutoFixerFunction(node, contextIdentifiers) || utils.isSuggestionFixerFunction(node, contextIdentifiers), node, }; }, diff --git a/lib/utils.js b/lib/utils.js index 8ed164b4..24f699da 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -424,4 +424,44 @@ module.exports = { ), ]; }, + + /** + * Whether the provided node represents an autofixer function. + * @param {Node} node + * @param {Node[]} contextIdentifiers + * @returns {boolean} + */ + isAutoFixerFunction (node, contextIdentifiers) { + const parent = node.parent; + return ['FunctionExpression', 'ArrowFunctionExpression'].includes(node.type) && + parent.parent.type === 'ObjectExpression' && + parent.parent.parent.type === 'CallExpression' && + contextIdentifiers.has(parent.parent.parent.callee.object) && + parent.parent.parent.callee.property.name === 'report' && + module.exports.getReportInfo(parent.parent.parent.arguments).fix === node; + }, + + /** + * Whether the provided node represents a suggestion fixer function. + * @param {Node} node + * @param {Node[]} contextIdentifiers + * @returns {boolean} + */ + isSuggestionFixerFunction (node, contextIdentifiers) { + const parent = node.parent; + return (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') && + parent.type === 'Property' && + parent.key.type === 'Identifier' && + parent.key.name === 'fix' && + parent.parent.type === 'ObjectExpression' && + parent.parent.parent.type === 'ArrayExpression' && + parent.parent.parent.parent.type === 'Property' && + parent.parent.parent.parent.key.type === 'Identifier' && + parent.parent.parent.parent.key.name === 'suggest' && + parent.parent.parent.parent.parent.type === 'ObjectExpression' && + parent.parent.parent.parent.parent.parent.type === 'CallExpression' && + contextIdentifiers.has(parent.parent.parent.parent.parent.parent.callee.object) && + parent.parent.parent.parent.parent.parent.callee.property.name === 'report' && + module.exports.getReportInfo(parent.parent.parent.parent.parent.parent.arguments).suggest === parent.parent.parent; + }, }; diff --git a/tests/lib/rules/fixer-return.js b/tests/lib/rules/fixer-return.js index 11cf7ab6..4e4afec8 100644 --- a/tests/lib/rules/fixer-return.js +++ b/tests/lib/rules/fixer-return.js @@ -221,6 +221,55 @@ ruleTester.run('fixer-return', rule, { } }; `, + + // Suggestion + ` + module.exports = { + create: function(context) { + context.report( { + suggest: [ + { + fix: function(fixer) { + return fixer.foo(); + } + } + ] + }); + } + }; + `, + // Suggestion but wrong `suggest` key + ` + module.exports = { + create: function(context) { + context.report( { + notSuggest: [ + { + fix: function(fixer) { + fixer.foo(); + } + } + ] + }); + } + }; + `, + // Suggestion but wrong `fix` key + ` + module.exports = { + create: function(context) { + context.report( { + suggest: [ + { + notFix: function(fixer) { + fixer.foo(); + } + } + ] + }); + } + }; + `, ], invalid: [ @@ -239,6 +288,25 @@ ruleTester.run('fixer-return', rule, { `, errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 5, column: 24 }], }, + { + // Fix but missing return (suggestion) + code: ` + module.exports = { + create: function(context) { + context.report({ + suggest: [ + { + fix(fixer) { + fixer.foo(); + } + } + ] + }); + } + }; + `, + errors: [{ messageId: 'missingFix', type: 'FunctionExpression', line: 7, column: 36 }], + }, { // Fix but missing return (arrow function, report on arrow) code: ` @@ -254,6 +322,25 @@ ruleTester.run('fixer-return', rule, { `, errors: [{ messageId: 'missingFix', type: 'ArrowFunctionExpression', line: 5, endLine: 5, column: 34, endColumn: 36 }], }, + { + // Fix but missing return (arrow function, report on arrow, suggestion) + code: ` + module.exports = { + create: function(context) { + context.report({ + suggest: [ + { + fix: (fixer) => { + fixer.foo(); + } + } + ] + }); + } + }; + `, + errors: [{ messageId: 'missingFix', type: 'ArrowFunctionExpression', line: 7, endLine: 7, column: 46, endColumn: 48 }], + }, { // With no autofix (arrow function, explicit return, report on arrow) code: ` diff --git a/tests/lib/rules/prefer-replace-text.js b/tests/lib/rules/prefer-replace-text.js index 10777b26..1341c71a 100644 --- a/tests/lib/rules/prefer-replace-text.js +++ b/tests/lib/rules/prefer-replace-text.js @@ -17,8 +17,6 @@ const RuleTester = require('eslint').RuleTester; // ------------------------------------------------------------------------------ const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); -const ERROR = { messageId: 'useReplaceText', type: 'CallExpression' }; - ruleTester.run('prefer-placeholders', rule, { valid: [ @@ -52,6 +50,23 @@ ruleTester.run('prefer-placeholders', rule, { ` fixer.replaceTextRange([node.range[0], node.range[1]], ''); `, + + // Suggestion + ` + module.exports = { + create(context) { + context.report({ + suggest: [ + { + fix(fixer) { + return fixer.replaceTextRange([start, end], ''); + } + } + ] + }); + } + }; + `, ], invalid: [ @@ -67,7 +82,7 @@ ruleTester.run('prefer-placeholders', rule, { } }; `, - errors: [ERROR], + errors: [{ messageId: 'useReplaceText', type: 'CallExpression' }], }, { code: ` @@ -81,7 +96,7 @@ ruleTester.run('prefer-placeholders', rule, { } }; `, - errors: [ERROR], + errors: [{ messageId: 'useReplaceText', type: 'CallExpression' }], }, { code: ` @@ -95,7 +110,7 @@ ruleTester.run('prefer-placeholders', rule, { } }; `, - errors: [ERROR], + errors: [{ messageId: 'useReplaceText', type: 'CallExpression' }], }, { code: ` @@ -107,7 +122,7 @@ ruleTester.run('prefer-placeholders', rule, { } }; `, - errors: [ERROR], + errors: [{ messageId: 'useReplaceText', type: 'CallExpression' }], }, { code: ` @@ -121,7 +136,27 @@ ruleTester.run('prefer-placeholders', rule, { } }; `, - errors: [ERROR], + errors: [{ messageId: 'useReplaceText', type: 'CallExpression' }], + }, + + { + // Suggestion fixer + code: ` + module.exports = { + create(context) { + context.report({ + suggest: [ + { + fix(fixer) { + return fixer.replaceTextRange([node.range[0], node.range[1]], ''); + } + } + ] + }); + } + }; + `, + errors: [{ messageId: 'useReplaceText', type: 'CallExpression' }], }, ], }); diff --git a/tests/lib/utils.js b/tests/lib/utils.js index ea12dde9..9579ddef 100644 --- a/tests/lib/utils.js +++ b/tests/lib/utils.js @@ -471,4 +471,57 @@ describe('utils', () => { } }); }); + + describe('isAutoFixerFunction / isSuggestionFixerFunction', () => { + const CASES = { + // isAutoFixerFunction + 'context.report({ fix(fixer) {} });' (ast) { + return { expected: true, node: ast.body[0].expression.arguments[0].properties[0].value, context: ast.body[0].expression.callee.object, fn: utils.isAutoFixerFunction }; + }, + 'context.notReport({ fix(fixer) {} });' (ast) { + return { expected: false, node: ast.body[0].expression.arguments[0].properties[0].value, context: ast.body[0].expression.callee.object, fn: utils.isAutoFixerFunction }; + }, + 'context.report({ notFix(fixer) {} });' (ast) { + return { expected: false, node: ast.body[0].expression.arguments[0].properties[0].value, context: ast.body[0].expression.callee.object, fn: utils.isAutoFixerFunction }; + }, + 'notContext.report({ notFix(fixer) {} });' (ast) { + return { expected: false, node: ast.body[0].expression.arguments[0].properties[0].value, context: undefined, fn: utils.isAutoFixerFunction }; + }, + + // isSuggestionFixerFunction + 'context.report({ suggest: [{ fix(fixer) {} }] });' (ast) { + return { expected: true, node: ast.body[0].expression.arguments[0].properties[0].value.elements[0].properties[0].value, context: ast.body[0].expression.callee.object, fn: utils.isSuggestionFixerFunction }; + }, + 'context.notReport({ suggest: [{ fix(fixer) {} }] });' (ast) { + return { expected: false, node: ast.body[0].expression.arguments[0].properties[0].value.elements[0].properties[0].value, context: ast.body[0].expression.callee.object, fn: utils.isSuggestionFixerFunction }; + }, + 'context.report({ notSuggest: [{ fix(fixer) {} }] });' (ast) { + return { expected: false, node: ast.body[0].expression.arguments[0].properties[0].value.elements[0].properties[0].value, context: ast.body[0].expression.callee.object, fn: utils.isSuggestionFixerFunction }; + }, + 'context.report({ suggest: [{ notFix(fixer) {} }] });' (ast) { + return { expected: false, node: ast.body[0].expression.arguments[0].properties[0].value.elements[0].properties[0].value, context: ast.body[0].expression.callee.object, fn: utils.isSuggestionFixerFunction }; + }, + 'notContext.report({ suggest: [{ fix(fixer) {} }] });' (ast) { + return { expected: false, node: ast.body[0].expression.arguments[0].properties[0].value, context: undefined, fn: utils.isSuggestionFixerFunction }; + }, + }; + + Object.keys(CASES).forEach(ruleSource => { + it(ruleSource, () => { + const ast = espree.parse(ruleSource, { ecmaVersion: 6, range: true }); + + // Add parent to each node. + estraverse.traverse(ast, { + enter (node, parent) { + node.parent = parent; + }, + }); + + const testCase = CASES[ruleSource](ast); + const contextIdentifiers = new Set([testCase.context]); + const result = testCase.fn(testCase.node, contextIdentifiers); + assert.strictEqual(result, testCase.expected); + }); + }); + }); });