Skip to content

Commit add4bf4

Browse files
committed
Merge branch 'master' into dependabot/npm_and_yarn/eslint-plugin-unicorn-44.0.0
* master: Add `compact` to `filter` autofixer for `no-array-prototype-extensions` rule (#1611) build(deps-dev): bump release-it from 15.4.2 to 15.5.0 (#1612) build(deps-dev): bump @babel/plugin-proposal-decorators from 7.19.1 to 7.19.3 (#1602) build(deps-dev): bump @typescript-eslint/parser from 5.38.0 to 5.38.1 (#1604) build(deps-dev): bump jsdom from 20.0.0 to 20.0.1 (#1607) build(deps-dev): bump sort-package-json from 1.57.0 to 2.0.0 (#1606) Add `filterBy` to `filter` autofixer for `no-array-prototype-extensions` rule (#1610) Add `any` to `some` autofixer for `no-array-prototype-extensions` rule (#1609)
2 parents b281e71 + 7efb2ab commit add4bf4

File tree

6 files changed

+391
-310
lines changed

6 files changed

+391
-310
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -102,7 +102,7 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
102102
|:--------|:------------|:---------------|:-----------|:---------------|
103103
| [closure-actions](./docs/rules/closure-actions.md) | enforce usage of closure actions || | |
104104
| [new-module-imports](./docs/rules/new-module-imports.md) | enforce using "New Module Imports" from Ember RFC #176 || | |
105-
| [no-array-prototype-extensions](./docs/rules/no-array-prototype-extensions.md) | disallow usage of Ember's `Array` prototype extensions | | | |
105+
| [no-array-prototype-extensions](./docs/rules/no-array-prototype-extensions.md) | disallow usage of Ember's `Array` prototype extensions | | 🔧 | |
106106
| [no-function-prototype-extensions](./docs/rules/no-function-prototype-extensions.md) | disallow usage of Ember's `function` prototype extensions || | |
107107
| [no-mixins](./docs/rules/no-mixins.md) | disallow the usage of mixins || | |
108108
| [no-new-mixins](./docs/rules/no-new-mixins.md) | disallow the creation of new mixins || | |

docs/rules/no-array-prototype-extensions.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# no-array-prototype-extensions
22

3+
🔧 The `--fix` option on the [command line](https://eslint.org/docs/user-guide/command-line-interface#fixing-problems) can automatically fix some of the problems reported by this rule.
4+
35
By default, Ember extends certain native JavaScript objects with additional methods. This can lead to problems in some situations. One example is relying on these methods in an addon that is used inside an app that has the extensions disabled.
46

57
The prototype extensions for the `Array` object were deprecated in [RFC #848](https://rfcs.emberjs.com/id/0848-deprecate-array-prototype-extensions).

lib/rules/no-array-prototype-extensions.js

Lines changed: 116 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,13 @@
22

33
const { getName, getNodeOrNodeFromVariable } = require('../utils/utils');
44
const { isClassPropertyOrPropertyDefinition } = require('../utils/types');
5+
const { getImportIdentifier } = require('../utils/import');
56
const Stack = require('../utils/stack');
67

78
const ERROR_MESSAGE = "Don't use Ember's array prototype extensions";
9+
const TOKEN_TYPES = {
10+
PUNCTUATOR: 'Punctuator',
11+
};
812

913
const EXTENSION_METHODS = new Set([
1014
/**
@@ -145,6 +149,102 @@ function variableNameToWords(name) {
145149
.split(' ');
146150
}
147151

152+
/**
153+
* Returns the fixing object if it can be fixable otherwise returns an empty array.
154+
*
155+
* @param {Object} callExpressionNode The call expression AST node.
156+
* @param {Object} fixer The ESLint fixer object which will be used to apply fixes.
157+
* @param {Object} context The ESlint context object which contains some helper utils
158+
* @param {Object} [options] An object that contains additional information
159+
* @param {String} [options.importedGetName] The name of the imported get specifier from @ember/object package
160+
* @returns {Object|[]}
161+
*/
162+
function applyFix(callExpressionNode, fixer, context, options = {}) {
163+
const propertyName = callExpressionNode.callee.property.name;
164+
165+
switch (propertyName) {
166+
case 'any':
167+
return fixer.replaceText(callExpressionNode.callee.property, 'some');
168+
case 'compact': {
169+
const calleePropertyNode = callExpressionNode.callee.property;
170+
const sourceCode = context.getSourceCode();
171+
const callArgs = callExpressionNode.arguments;
172+
const fixes = [];
173+
174+
// Get the open parenthesis immediately after the callee name
175+
const openParenToken = sourceCode.getTokenAfter(calleePropertyNode, {
176+
filter(token) {
177+
return token.type === TOKEN_TYPES.PUNCTUATOR && token.value === '(';
178+
},
179+
});
180+
// Get the close parenthesis from the end of the callExpressionNode
181+
const closeParenToken = sourceCode.getLastToken(callExpressionNode, {
182+
filter(token) {
183+
return token.type === TOKEN_TYPES.PUNCTUATOR && token.value === ')';
184+
},
185+
});
186+
187+
if (openParenToken && closeParenToken && callArgs.length === 0) {
188+
fixes.push(
189+
fixer.replaceText(calleePropertyNode, 'filter'),
190+
// Replacing the content starting from open parenthesis to close parenthesis
191+
fixer.replaceTextRange(
192+
[openParenToken.range[0], closeParenToken.range[1]],
193+
'(item => item !== undefined && item !== null)'
194+
)
195+
);
196+
}
197+
return fixes;
198+
}
199+
case 'filterBy': {
200+
const callArgs = callExpressionNode.arguments;
201+
const sourceCode = context.getSourceCode();
202+
const hasSecondArg = callArgs.length > 1;
203+
const firstArg = callArgs[0];
204+
const secondArg = callArgs[1];
205+
// default to "get" if the `get` hasn't already been imported.
206+
const importedGetName = options.importedGetName ?? 'get';
207+
208+
const fixes = [
209+
fixer.replaceText(callExpressionNode.callee.property, 'filter'),
210+
// Replace the first argument with the necessary condition
211+
// If the filterBy contains two arguments, the property (first argument) value will be compared against the second argument
212+
// If the filterBy contains only one argument, the property's truthy value is used to filter
213+
fixer.replaceText(
214+
firstArg,
215+
hasSecondArg
216+
? `item => ${importedGetName}(item, ${sourceCode.getText(
217+
firstArg
218+
)}) === ${sourceCode.getText(secondArg)}`
219+
: `item => ${importedGetName}(item, ${sourceCode.getText(firstArg)})`
220+
),
221+
];
222+
223+
// Add `get` import statement only if it is not imported already
224+
if (!options.importedGetName) {
225+
fixes.push(
226+
fixer.insertTextBefore(
227+
sourceCode.ast,
228+
`import { ${importedGetName} } from '@ember/object';\n`
229+
)
230+
);
231+
}
232+
233+
if (hasSecondArg) {
234+
fixes.push(
235+
// Required to get rid of `, ` followed by the first argument since the second argument will be removed
236+
fixer.removeRange([firstArg.range[1], secondArg.range[0]]),
237+
fixer.remove(secondArg)
238+
);
239+
}
240+
241+
return fixes;
242+
}
243+
default:
244+
return [];
245+
}
246+
}
247+
148248
//----------------------------------------------------------------------------------------------
149249
// General rule - Don't use Ember's array prototype extensions like .any(), .pushObject() or .firstObject
150250
//----------------------------------------------------------------------------------------------
@@ -159,7 +259,7 @@ module.exports = {
159259
recommended: false,
160260
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-array-prototype-extensions.md',
161261
},
162-
fixable: null,
262+
fixable: 'code',
163263
schema: [],
164264
messages: {
165265
main: ERROR_MESSAGE,
@@ -169,11 +269,17 @@ module.exports = {
169269
create(context) {
170270
const sourceCode = context.getSourceCode();
171271
const { scopeManager } = sourceCode;
272+
let importedGetName;
172273

173274
// Track some information about the current class we're inside.
174275
const classStack = new Stack();
175276

176277
return {
278+
ImportDeclaration(node) {
279+
if (node.source.value === '@ember/object') {
280+
importedGetName = importedGetName || getImportIdentifier(node, '@ember/object', 'get');
281+
}
282+
},
177283
/**
178284
* Cover cases when `EXTENSION_METHODS` is getting called.
179285
* Example: something.filterBy();
@@ -253,7 +359,15 @@ module.exports = {
253359
}
254360

255361
if (EXTENSION_METHODS.has(node.callee.property.name)) {
256-
context.report({ node, messageId: 'main' });
362+
context.report({
363+
node,
364+
messageId: 'main',
365+
fix(fixer) {
366+
return applyFix(node, fixer, context, {
367+
importedGetName,
368+
});
369+
},
370+
});
257371
}
258372

259373
// Example: someArray.replace(1, 2, [1, 2, 3]);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@
9696
"prettier": "^2.1.1",
9797
"release-it": "^15.1.3",
9898
"release-it-lerna-changelog": "^5.0.0",
99-
"sort-package-json": "^1.52.0",
99+
"sort-package-json": "^2.0.0",
100100
"typescript": "^4.7.4"
101101
},
102102
"peerDependencies": {

tests/lib/rules/no-array-prototype-extensions.js

Lines changed: 81 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -233,10 +233,82 @@ ruleTester.run('no-array-prototype-extensions', rule, {
233233
output: null,
234234
errors: [{ messageId: 'main', type: 'CallExpression' }],
235235
},
236+
{
237+
// filterBy with two arguments
238+
code: `
239+
const arr = [];
240+
241+
function getAge() {
242+
return 16;
243+
}
244+
245+
arr.filterBy("age", getAge());
246+
`,
247+
output: `
248+
import { get } from '@ember/object';
249+
const arr = [];
250+
251+
function getAge() {
252+
return 16;
253+
}
254+
255+
arr.filter(item => get(item, "age") === getAge());
256+
`,
257+
errors: [{ messageId: 'main', type: 'CallExpression' }],
258+
},
259+
{
260+
// filterBy with one argument
261+
code: `
262+
const arr = [];
263+
264+
arr.filterBy("age");
265+
`,
266+
output: `
267+
import { get } from '@ember/object';
268+
const arr = [];
269+
270+
arr.filter(item => get(item, "age"));
271+
`,
272+
errors: [{ messageId: 'main', type: 'CallExpression' }],
273+
},
274+
{
275+
// filterBy with one argument and `get` import statement already imported
276+
code: `
277+
import { get as g } from '@ember/object';
278+
const arr = [];
279+
280+
arr.filterBy("age");
281+
`,
282+
output: `
283+
import { get as g } from '@ember/object';
284+
const arr = [];
285+
286+
arr.filter(item => g(item, "age"));
287+
`,
288+
errors: [{ messageId: 'main', type: 'CallExpression' }],
289+
},
290+
{
291+
// filterBy with one argument and `get` import statement imported from a different package
292+
code: `
293+
import { get as g } from 'dummy';
294+
const arr = [];
295+
296+
arr.filterBy("age");
297+
`,
298+
output: `
299+
import { get } from '@ember/object';
300+
import { get as g } from 'dummy';
301+
const arr = [];
302+
303+
arr.filter(item => get(item, "age"));
304+
`,
305+
errors: [{ messageId: 'main', type: 'CallExpression' }],
306+
},
236307
{
237308
// Set in variable name but not a Set function.
238-
code: 'set.filterBy()',
239-
output: null,
309+
code: 'set.filterBy("age", 18);',
310+
output: `import { get } from '@ember/object';
311+
set.filter(item => get(item, "age") === 18);`,
240312
errors: [{ messageId: 'main', type: 'CallExpression' }],
241313
},
242314
{
@@ -321,12 +393,18 @@ ruleTester.run('no-array-prototype-extensions', rule, {
321393
},
322394
{
323395
code: 'something.compact()',
396+
output: 'something.filter(item => item !== undefined && item !== null)',
397+
errors: [{ messageId: 'main', type: 'CallExpression' }],
398+
},
399+
{
400+
// When unexpected number of params are passed, skipping auto-fixing
401+
code: 'something.compact(1, getVal(), 3)',
324402
output: null,
325403
errors: [{ messageId: 'main', type: 'CallExpression' }],
326404
},
327405
{
328406
code: 'something.any()',
329-
output: null,
407+
output: 'something.some()',
330408
errors: [{ messageId: 'main', type: 'CallExpression' }],
331409
},
332410
{

0 commit comments

Comments
 (0)