Skip to content

Commit 65cfb2c

Browse files
authored
Fix: Improve detection of fix functions that never return a fix in fixer-return rule (#143)
Behavior changes: 1. Catch fix functions that have a return statement but never actually return a fix (i.e. fix functions that only ever return undefined or similar values that are obviously not fixes). 2. Allow fix functions to have some code paths that do not return a fix (it's allowed for a fix function to bail out / return early and avoid fixing in some situations). I would consider these both bug fixes to reduce false positives and false negatives.
1 parent f136e4c commit 65cfb2c

File tree

4 files changed

+316
-57
lines changed

4 files changed

+316
-57
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ Then configure the rules you want to use under the rules section.
4747
Name | ✔️ | 🛠 | Description
4848
----- | ----- | ----- | -----
4949
[consistent-output](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/consistent-output.md) | | | enforce consistent use of output assertions in rule tests
50-
[fixer-return](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/fixer-return.md) | ✔️ | | require fixer function to always return a value.
50+
[fixer-return](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/fixer-return.md) | ✔️ | | require fixer function to return a fix
5151
[meta-property-ordering](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/meta-property-ordering.md) | | 🛠 | enforce the order of meta properties
5252
[no-deprecated-context-methods](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-context-methods.md) | | 🛠 | disallow usage of deprecated methods on rule context objects
5353
[no-deprecated-report-api](https://github.com/not-an-aardvark/eslint-plugin-eslint-plugin/blob/master/docs/rules/no-deprecated-report-api.md) | ✔️ | 🛠 | disallow use of the deprecated context.report() API

docs/rules/fixer-return.md

+20-7
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
# Enforces always return from a fixer function (fixer-return)
1+
# Require fixer function to return a fix (fixer-return)
22

33
✔️ The `"extends": "plugin:eslint-plugin/recommended"` property in a configuration file enables this rule.
44

5-
In a fixable rule, missing return from a fixer function will not apply fixes.
5+
In a [fixable](https://eslint.org/docs/developer-guide/working-with-rules#applying-fixes) rule, a fixer function is useless if it never returns anything.
66

77
## Rule Details
88

9-
This rule enforces that fixer functions always return a value.
9+
This rule enforces that a fixer function returns a fix in at least one situation.
1010

1111
Examples of **incorrect** code for this rule:
1212

@@ -17,7 +17,7 @@ module.exports = {
1717
create (context) {
1818
context.report({
1919
fix (fixer) {
20-
fixer.foo();
20+
fixer.insertTextAfter(node, 'foo');
2121
},
2222
});
2323
},
@@ -33,13 +33,26 @@ module.exports = {
3333
create (context) {
3434
context.report({
3535
fix (fixer) {
36-
return fixer.foo();
36+
return fixer.insertTextAfter(node, 'foo');
3737
},
3838
});
3939
},
4040
};
4141
```
4242

43-
## When Not To Use It
43+
```js
44+
/* eslint eslint-plugin/fixer-return: error */
4445

45-
If you don't want to enforce always return from a fixer function, do not enable this rule.
46+
module.exports = {
47+
create (context) {
48+
context.report({
49+
fix (fixer) {
50+
if (foo) {
51+
return; // no autofix in this situation
52+
}
53+
return fixer.insertTextAfter(node, 'foo');
54+
},
55+
});
56+
},
57+
};
58+
```

lib/rules/fixer-return.js

+53-28
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* @fileoverview Enforces always return from a fixer function
2+
* @fileoverview Require fixer function to return a fix
33
* @author 薛定谔的猫<[email protected]>
44
*/
55

@@ -10,6 +10,7 @@
1010
// ------------------------------------------------------------------------------
1111

1212
const utils = require('../utils');
13+
const { getStaticValue } = require('eslint-utils');
1314

1415
// ------------------------------------------------------------------------------
1516
// Rule Definition
@@ -19,50 +20,79 @@ module.exports = {
1920
meta: {
2021
type: 'problem',
2122
docs: {
22-
description: 'require fixer function to always return a value.',
23+
description: 'require fixer function to return a fix',
2324
category: 'Possible Errors',
2425
recommended: true,
2526
},
2627
fixable: null,
2728
schema: [],
29+
messages: {
30+
missingFix: 'Fixer function never returned a fix.',
31+
},
2832
},
2933

3034
create (context) {
31-
const message = 'Expected fixer function to always return a value.';
3235
let funcInfo = {
3336
upper: null,
3437
codePath: null,
35-
hasReturn: false,
36-
hasYield: false,
38+
hasReturnWithFixer: false,
39+
hasYieldWithFixer: false,
3740
shouldCheck: false,
3841
node: null,
3942
};
4043
let contextIdentifiers;
4144

4245
/**
43-
* Checks whether or not the last code path segment is reachable.
44-
* Then reports this function if the segment is reachable.
45-
*
46-
* If the last code path segment is reachable, there are paths which are not
47-
* returned or thrown.
46+
* As we exit the fix() function, ensure we have returned or yielded a real fix by this point.
47+
* If not, report the function as a violation.
4848
*
4949
* @param {ASTNode} node - A node to check.
5050
* @returns {void}
5151
*/
5252
function checkLastSegment (node) {
5353
if (
5454
funcInfo.shouldCheck &&
55-
funcInfo.codePath.currentSegments.some(segment => segment.reachable) &&
56-
(!node.generator || !funcInfo.hasYield)
55+
(
56+
(node.generator && !funcInfo.hasYieldWithFixer) || // Generator function never yielded a fix
57+
(!node.generator && !funcInfo.hasReturnWithFixer) // Non-generator function never returned a fix
58+
)
5759
) {
5860
context.report({
5961
node,
6062
loc: (node.id || node).loc.start,
61-
message,
63+
messageId: 'missingFix',
6264
});
6365
}
6466
}
6567

68+
/**
69+
* Check if a returned/yielded node is likely to be a fix or not.
70+
* A fix is an object created by fixer.replaceText() for example and returned by the fix function.
71+
* @param {ASTNode} node - node to check
72+
* @param {Context} context
73+
* @returns {boolean}
74+
*/
75+
function isFix (node) {
76+
if (node.type === 'ArrayExpression' && node.elements.length === 0) {
77+
// An empty array is not a fix.
78+
return false;
79+
}
80+
81+
const staticValue = getStaticValue(node, context.getScope());
82+
if (!staticValue) {
83+
// If we can't find a static value, assume it's a real fix value.
84+
return true;
85+
}
86+
87+
if (Array.isArray(staticValue.value)) {
88+
// If the static value is an array, then consider it a fix since fixes could have been added to it after creation.
89+
return true;
90+
}
91+
92+
// Any other static values (booleans, numbers, etc) are not fixes.
93+
return false;
94+
}
95+
6696
return {
6797
Program (node) {
6898
contextIdentifiers = utils.getContextIdentifiers(context, node);
@@ -71,6 +101,8 @@ module.exports = {
71101
// Stacks this function's information.
72102
onCodePathStart (codePath, node) {
73103
const parent = node.parent;
104+
105+
// Whether we are inside the fixer function we care about.
74106
const shouldCheck = node.type === 'FunctionExpression' &&
75107
parent.parent.type === 'ObjectExpression' &&
76108
parent.parent.parent.type === 'CallExpression' &&
@@ -81,8 +113,8 @@ module.exports = {
81113
funcInfo = {
82114
upper: funcInfo,
83115
codePath,
84-
hasYield: false,
85-
hasReturn: false,
116+
hasYieldWithFixer: false,
117+
hasReturnWithFixer: false,
86118
shouldCheck,
87119
node,
88120
};
@@ -94,27 +126,20 @@ module.exports = {
94126
},
95127

96128
// Yield in generators
97-
YieldExpression () {
98-
if (funcInfo.shouldCheck) {
99-
funcInfo.hasYield = true;
129+
YieldExpression (node) {
130+
if (funcInfo.shouldCheck && node.argument && isFix(node.argument)) {
131+
funcInfo.hasYieldWithFixer = true;
100132
}
101133
},
102134

103135
// Checks the return statement is valid.
104136
ReturnStatement (node) {
105-
if (funcInfo.shouldCheck) {
106-
funcInfo.hasReturn = true;
107-
108-
if (!node.argument) {
109-
context.report({
110-
node,
111-
message,
112-
});
113-
}
137+
if (funcInfo.shouldCheck && node.argument && isFix(node.argument)) {
138+
funcInfo.hasReturnWithFixer = true;
114139
}
115140
},
116141

117-
// Reports a given function if the last path is reachable.
142+
// Ensure the current fixer function returned or yielded a fix.
118143
'FunctionExpression:exit': checkLastSegment,
119144
};
120145
},

0 commit comments

Comments
 (0)