diff --git a/README.md b/README.md index 424d4869..1bbb84f5 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,7 @@ Then configure the rules you want to use under the rules section. Name | ✔️ | 🛠 | Description ----- | ----- | ----- | ----- [consistent-output](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/consistent-output.md) | | | Enforces consistent use of output assertions in rule tests +[fixer-return](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/fixer-return.md) | | | Enforces always return from a fixer function [no-deprecated-report-api](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-report-api.md) | ✔️ | 🛠 | Prohibits the deprecated `context.report(node, message)` API [no-identical-tests](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-identical-tests.md) | | 🛠 | Disallows identical tests [no-missing-placeholders](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-missing-placeholders.md) | ✔️ | | Disallows missing placeholders in rule report messages diff --git a/docs/rules/fixer-return.md b/docs/rules/fixer-return.md new file mode 100644 index 00000000..edb0b263 --- /dev/null +++ b/docs/rules/fixer-return.md @@ -0,0 +1,41 @@ +# Enforces always return from a fixer function (fixer-return) + +In a fixable rule, missing return from a fixer function will not apply fixes. + +## Rule Details + +This rule enforces that fixer functions always return a value. + +Examples of **incorrect** code for this rule: + +```js +/* eslint eslint-plugin/fixer-return: error */ +module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + fixer.foo(); + } + }); + } +}; +``` + +Examples of **correct** code for this rule: + +```js +/* eslint eslint-plugin/fixer-return: error */ +module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + return fixer.foo(); + } + }); + } +}; +``` + +## When Not To Use It + +If you don't want to enforce always return from a fixer function, do not enable this rule. diff --git a/lib/rules/fixer-return.js b/lib/rules/fixer-return.js new file mode 100644 index 00000000..e4b1ca68 --- /dev/null +++ b/lib/rules/fixer-return.js @@ -0,0 +1,106 @@ +/** + * @fileoverview Enforces always return from a fixer function + * @author 薛定谔的猫 + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const utils = require('../utils'); + +// ------------------------------------------------------------------------------ +// Rule Definition +// ------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: 'Expected fixer function to always return a value.', + category: 'Possible Errors', + recommended: false, + }, + fixable: null, + }, + + create (context) { + const message = 'Expected fixer function to always return a value.'; + let funcInfo = { + upper: null, + codePath: null, + hasReturn: false, + shouldCheck: false, + node: null, + }; + let contextIdentifiers; + + /** + * Checks whether or not the last code path segment is reachable. + * Then reports this function if the segment is reachable. + * + * If the last code path segment is reachable, there are paths which are not + * returned or thrown. + * + * @param {ASTNode} node - A node to check. + * @returns {void} + */ + function checkLastSegment (node) { + if (funcInfo.shouldCheck && funcInfo.codePath.currentSegments.some(segment => segment.reachable)) { + context.report({ + node, + loc: (node.id || node).loc.start, + message, + }); + } + } + + return { + Program (node) { + contextIdentifiers = utils.getContextIdentifiers(context, node); + }, + + // Stacks this function's information. + onCodePathStart (codePath, node) { + const parent = node.parent; + const shouldCheck = node.type === 'FunctionExpression' && + 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, + hasReturn: false, + shouldCheck, + node, + }; + }, + + // Pops this function's information. + onCodePathEnd () { + funcInfo = funcInfo.upper; + }, + + // Checks the return statement is valid. + ReturnStatement (node) { + if (funcInfo.shouldCheck) { + funcInfo.hasReturn = true; + + if (!node.argument) { + context.report({ + node, + message, + }); + } + } + }, + + // Reports a given function if the last path is reachable. + 'FunctionExpression:exit': checkLastSegment, + }; + }, +}; diff --git a/tests/lib/rules/fixer-return.js b/tests/lib/rules/fixer-return.js new file mode 100644 index 00000000..23ee5ca2 --- /dev/null +++ b/tests/lib/rules/fixer-return.js @@ -0,0 +1,62 @@ +/** + * @fileoverview enforces always return from a fixer function + * @author 薛定谔的猫 + */ + +'use strict'; + +// ------------------------------------------------------------------------------ +// Requirements +// ------------------------------------------------------------------------------ + +const rule = require('../../../lib/rules/fixer-return'); +const RuleTester = require('eslint').RuleTester; + +const ERROR = { message: 'Expected fixer function to always return a value.' }; + +// ------------------------------------------------------------------------------ +// Tests +// ------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ parserOptions: { ecmaVersion: 6 } }); +ruleTester.run('fixer-return', rule, { + valid: [ + ` + module.exports = { + create: function(context) { + context.report( { + fix: function(fixer) { + return fixer.foo(); + } + }); + } + }; + `, + ` + module.exports = { + create: function(context) { + context.report({ + fix: fixer => fixer.foo() + }); + } + }; + `, + ], + + invalid: [ + { + code: ` + module.exports = { + create: function(context) { + context.report({ + fix(fixer) { + fixer.foo(); + } + }); + } + }; + `, + errors: [ERROR], + }, + ], +});