Skip to content

Commit 8015768

Browse files
medusalixfiskersindresorhus
authored
Add prefer-default-parameters rule (#632)
Co-authored-by: fisker <[email protected]> Co-authored-by: Sindre Sorhus <[email protected]>
1 parent 94ae87a commit 8015768

File tree

5 files changed

+904
-0
lines changed

5 files changed

+904
-0
lines changed
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# Prefer default parameters over reassignment
2+
3+
Instead of reassigning a function parameter, default parameters should be used. The `foo = foo || 123` statement evaluates to `123` when `foo` is falsy, possibly leading to confusing behavior, whereas default parameters only apply when passed an `undefined` value. This rule only reports reassignments to literal values.
4+
5+
This rule is fixable.
6+
7+
## Fail
8+
9+
```js
10+
function abc(foo) {
11+
foo = foo || 'bar';
12+
}
13+
```
14+
15+
```js
16+
function abc(foo) {
17+
const bar = foo || 'bar';
18+
}
19+
```
20+
21+
## Pass
22+
23+
```js
24+
function abc(foo = 'bar') {}
25+
```
26+
27+
```js
28+
function abc(foo) {
29+
foo = foo || bar();
30+
}
31+
```

index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ module.exports = {
5757
'unicorn/prefer-array-find': 'error',
5858
'unicorn/prefer-dataset': 'error',
5959
'unicorn/prefer-date-now': 'error',
60+
'unicorn/prefer-default-parameters': 'error',
6061
'unicorn/prefer-event-key': 'error',
6162
// TODO: Enable this by default when targeting Node.js 12.
6263
'unicorn/prefer-flat-map': 'off',

readme.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ Configure it in `package.json`.
7272
"unicorn/prefer-array-find": "error",
7373
"unicorn/prefer-dataset": "error",
7474
"unicorn/prefer-date-now": "error",
75+
"unicorn/prefer-default-parameters": "error",
7576
"unicorn/prefer-event-key": "error",
7677
"unicorn/prefer-flat-map": "error",
7778
"unicorn/prefer-includes": "error",
@@ -141,6 +142,7 @@ Configure it in `package.json`.
141142
- [prefer-array-find](docs/rules/prefer-array-find.md) - Prefer `.find(…)` over the first element from `.filter(…)`. *(partly fixable)*
142143
- [prefer-dataset](docs/rules/prefer-dataset.md) - Prefer using `.dataset` on DOM elements over `.setAttribute(…)`. *(fixable)*
143144
- [prefer-date-now](docs/rules/prefer-date-now.md) - Prefer `Date.now()` to get the number of milliseconds since the Unix Epoch. *(fixable)*
145+
- [prefer-default-parameters](docs/rules/prefer-default-parameters.md) - Prefer default parameters over reassignment. *(fixable)*
144146
- [prefer-event-key](docs/rules/prefer-event-key.md) - Prefer `KeyboardEvent#key` over `KeyboardEvent#keyCode`. *(partly fixable)*
145147
- [prefer-flat-map](docs/rules/prefer-flat-map.md) - Prefer `.flatMap(…)` over `.map(…).flat()`. *(fixable)*
146148
- [prefer-includes](docs/rules/prefer-includes.md) - Prefer `.includes()` over `.indexOf()` when checking for existence or non-existence. *(fixable)*

rules/prefer-default-parameters.js

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
'use strict';
2+
const {findVariable} = require('eslint-utils');
3+
const getDocumentationUrl = require('./utils/get-documentation-url');
4+
5+
const MESSAGE_ID = 'preferDefaultParameters';
6+
const MESSAGE_ID_SUGGEST = 'preferDefaultParametersSuggest';
7+
8+
const assignmentSelector = [
9+
'ExpressionStatement',
10+
'[expression.type="AssignmentExpression"]'
11+
].join('');
12+
13+
const declarationSelector = [
14+
'VariableDeclaration',
15+
'[declarations.0.type="VariableDeclarator"]'
16+
].join('');
17+
18+
const isDefaultExpression = (left, right) =>
19+
left &&
20+
right &&
21+
left.type === 'Identifier' &&
22+
right.type === 'LogicalExpression' &&
23+
(right.operator === '||' || right.operator === '??') &&
24+
right.left.type === 'Identifier' &&
25+
right.right.type === 'Literal';
26+
27+
const containsCallExpression = (source, node) => {
28+
if (!node) {
29+
return false;
30+
}
31+
32+
if (node.type === 'CallExpression') {
33+
return true;
34+
}
35+
36+
for (const key of source.visitorKeys[node.type]) {
37+
const value = node[key];
38+
39+
if (Array.isArray(value)) {
40+
for (const element of value) {
41+
if (containsCallExpression(source, element)) {
42+
return true;
43+
}
44+
}
45+
} else if (containsCallExpression(source, value)) {
46+
return true;
47+
}
48+
}
49+
50+
return false;
51+
};
52+
53+
const hasSideEffects = (source, function_, node) => {
54+
for (const element of function_.body.body) {
55+
if (element === node) {
56+
break;
57+
}
58+
59+
// Function call before default-assignment
60+
if (containsCallExpression(source, element)) {
61+
return true;
62+
}
63+
}
64+
65+
return false;
66+
};
67+
68+
const hasExtraReferences = (assignment, references, left) => {
69+
// Parameter is referenced prior to default-assignment
70+
if (assignment && references[0].identifier !== left) {
71+
return true;
72+
}
73+
74+
// Old parameter is still referenced somewhere else
75+
if (!assignment && references.length > 1) {
76+
return true;
77+
}
78+
79+
return false;
80+
};
81+
82+
const isLastParameter = (parameters, parameter) => {
83+
const lastParameter = parameters[parameters.length - 1];
84+
85+
// See 'default-param-last' rule
86+
return parameter && parameter === lastParameter;
87+
};
88+
89+
const needsParentheses = (source, function_) => {
90+
if (function_.type !== 'ArrowFunctionExpression' || function_.params.length > 1) {
91+
return false;
92+
}
93+
94+
const [parameter] = function_.params;
95+
const before = source.getTokenBefore(parameter);
96+
const after = source.getTokenAfter(parameter);
97+
98+
return !after || !before || before.value !== '(' || after.value !== ')';
99+
};
100+
101+
const fixDefaultExpression = (fixer, source, node) => {
102+
const {line} = source.getLocFromIndex(node.range[0]);
103+
const {column} = source.getLocFromIndex(node.range[1]);
104+
const nodeText = source.getText(node);
105+
const lineText = source.lines[line - 1];
106+
const isOnlyNodeOnLine = lineText.trim() === nodeText;
107+
const endsWithWhitespace = lineText[column] === ' ';
108+
109+
if (isOnlyNodeOnLine) {
110+
return fixer.removeRange([
111+
source.getIndexFromLoc({line, column: 0}),
112+
source.getIndexFromLoc({line: line + 1, column: 0})
113+
]);
114+
}
115+
116+
if (endsWithWhitespace) {
117+
return fixer.removeRange([
118+
node.range[0],
119+
node.range[1] + 1
120+
]);
121+
}
122+
123+
return fixer.removeRange(node.range);
124+
};
125+
126+
const create = context => {
127+
const source = context.getSourceCode();
128+
const functionStack = [];
129+
130+
const checkExpression = (node, left, right, assignment) => {
131+
const currentFunction = functionStack[functionStack.length - 1];
132+
133+
if (!currentFunction || !isDefaultExpression(left, right)) {
134+
return;
135+
}
136+
137+
const {name: firstId} = left;
138+
const {
139+
left: {name: secondId},
140+
right: {raw: literal}
141+
} = right;
142+
143+
// Parameter is reassigned to a different identifier
144+
if (assignment && firstId !== secondId) {
145+
return;
146+
}
147+
148+
const {references} = findVariable(context.getScope(), secondId);
149+
const {params} = currentFunction;
150+
const parameter = params.find(parameter =>
151+
parameter.type === 'Identifier' &&
152+
parameter.name === secondId
153+
);
154+
155+
if (
156+
hasSideEffects(source, currentFunction, node) ||
157+
hasExtraReferences(assignment, references, left) ||
158+
!isLastParameter(params, parameter)
159+
) {
160+
return;
161+
}
162+
163+
const replacement = needsParentheses(source, currentFunction) ?
164+
`(${firstId} = ${literal})` :
165+
`${firstId} = ${literal}`;
166+
167+
context.report({
168+
node,
169+
messageId: MESSAGE_ID,
170+
suggest: [{
171+
messageId: MESSAGE_ID_SUGGEST,
172+
fix: fixer => [
173+
fixer.replaceText(parameter, replacement),
174+
fixDefaultExpression(fixer, source, node)
175+
]
176+
}]
177+
});
178+
};
179+
180+
return {
181+
':function': node => {
182+
functionStack.push(node);
183+
},
184+
':function:exit': () => {
185+
functionStack.pop();
186+
},
187+
[assignmentSelector]: node => {
188+
const {left, right} = node.expression;
189+
190+
checkExpression(node, left, right, true);
191+
},
192+
[declarationSelector]: node => {
193+
const {id, init} = node.declarations[0];
194+
195+
checkExpression(node, id, init, false);
196+
}
197+
};
198+
};
199+
200+
module.exports = {
201+
create,
202+
meta: {
203+
type: 'suggestion',
204+
docs: {
205+
url: getDocumentationUrl(__filename)
206+
},
207+
fixable: 'code',
208+
messages: {
209+
[MESSAGE_ID]: 'Prefer default parameters over reassignment.',
210+
[MESSAGE_ID_SUGGEST]: 'Replace reassignment with default parameter.'
211+
}
212+
}
213+
};

0 commit comments

Comments
 (0)