Skip to content

Commit ed983a2

Browse files
smillandhanli
andauthored
New Rule no-array-prototype-extensions (#1461)
* New Rule `no-array-prototype-extensions` * Change wording * Address feedbacks * Address more from feedbacks, more comments * Fix lint * Fix md lint Co-authored-by: hanli <[email protected]>
1 parent 43a6aee commit ed983a2

File tree

7 files changed

+536
-0
lines changed

7 files changed

+536
-0
lines changed

.eslintrc.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ module.exports = {
186186
'no-unused-labels': 'off',
187187
'no-unused-vars': 'off',
188188
'no-useless-constructor': 'off',
189+
'node/no-extraneous-import': 'off',
189190
'node/no-missing-import': 'off',
190191
'node/no-missing-require': 'off',
191192
'node/no-unsupported-features/es-syntax': 'off',

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,7 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
103103
|:--------|:------------|:---------------|:-----------|:---------------|
104104
| [closure-actions](./docs/rules/closure-actions.md) | enforce usage of closure actions || | |
105105
| [new-module-imports](./docs/rules/new-module-imports.md) | enforce using "New Module Imports" from Ember RFC #176 || | |
106+
| [no-array-prototype-extensions](./docs/rules/no-array-prototype-extensions.md) | disallow usage of Ember's `Array` prototype extensions | | | |
106107
| [no-function-prototype-extensions](./docs/rules/no-function-prototype-extensions.md) | disallow usage of Ember's `function` prototype extensions || | |
107108
| [no-mixins](./docs/rules/no-mixins.md) | disallow the usage of mixins || | |
108109
| [no-new-mixins](./docs/rules/no-new-mixins.md) | disallow the creation of new mixins || | |
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# no-array-prototype-extensions
2+
3+
Do not use Ember's `array` prototype extensions.
4+
5+
Use native array functions instead of `.filterBy`, `.toArray()` in Ember modules.
6+
7+
Use lodash helper functions instead of `.uniqBy()`, `sortBy()` in Ember modules.
8+
9+
Use immutable update style with `@tracked` properties or `TrackedArray` from `tracked-built-ins` instead of `.pushObject`, `removeObject` in Ember modules.
10+
11+
## Examples
12+
13+
Examples of **incorrect** code for this rule:
14+
15+
```js
16+
/** Helper functions **/
17+
import Component from '@glimmer/component';
18+
19+
export default class SampleComponent extends Component {
20+
abc = ['x', 'y', 'z', 'x'];
21+
22+
def = this.abc.without('x');
23+
ghi = this.abc.uniq();
24+
jkl = this.abc.toArray();
25+
mno = this.abc.uniqBy('y');
26+
pqr = this.abc.sortBy('z');
27+
}
28+
```
29+
30+
```js
31+
/** Observable-based functions **/
32+
import Component from '@glimmer/component';
33+
import { action } from '@ember/object';
34+
35+
export default class SampleComponent extends Component {
36+
abc = [];
37+
@action
38+
someAction(newItem) {
39+
this.abc.pushObject('1');
40+
}
41+
}
42+
```
43+
44+
Examples of **correct** code for this rule:
45+
46+
```js
47+
/** Helper functions **/
48+
import Component from '@glimmer/component';
49+
import { uniqBy, sortBy } from 'lodash';
50+
51+
export default class SampleComponent extends Component {
52+
abc = ['x', 'y', 'z', 'x'];
53+
54+
def = this.abc.filter((el) => el !== 'x');
55+
ghi = [...new Set(this.abc)];
56+
jkl = [...this.abc];
57+
mno = uniqBy(this.abc, 'y');
58+
pqr = sortBy(this.abc, 'z');
59+
}
60+
```
61+
62+
```js
63+
/** Observable-based functions **/
64+
/** Use immutable tracked property is OK **/
65+
import Component from '@glimmer/component';
66+
import { action } from '@ember/object';
67+
import { tracked } from '@glimmer/tracking';
68+
69+
export default class SampleComponent extends Component {
70+
@tracked abc = [];
71+
72+
@action
73+
someAction(newItem) {
74+
this.abc = [...abc, newItem];
75+
}
76+
}
77+
```
78+
79+
```js
80+
/** Observable-based functions **/
81+
/** Use TrackedArray is OK **/
82+
import Component from '@glimmer/component';
83+
import { action } from '@ember/object';
84+
import { tracked } from '@glimmer/tracking';
85+
import { TrackedArray } from 'tracked-built-ins';
86+
87+
export default class SampleComponent extends Component {
88+
@tracked abc = new TrackedArray();
89+
90+
@action
91+
someAction(newItem) {
92+
abc.push(newItem);
93+
}
94+
}
95+
```
96+
97+
## References
98+
99+
* [EmberArray](https://api.emberjs.com/ember/release/classes/EmberArray)
100+
* [MutableArray](https://api.emberjs.com/ember/release/classes/MutableArray)
101+
* [Prototype extensions documentation](https://guides.emberjs.com/release/configuring-ember/disabling-prototype-extensions/)
102+
* Array prototype extensions deprecation RFC
103+
104+
## Related Rules
105+
106+
* [no-function-prototype-extensions](no-function-prototype-extensions.md)
107+
* [no-string-prototype-extensions](no-string-prototype-extensions.md)

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,3 +39,8 @@ export default Component.extend({
3939
}
4040
});
4141
```
42+
43+
## Related Rules
44+
45+
* [no-array-prototype-extensions](no-array-prototype-extensions.md)
46+
* [no-string-prototype-extensions](no-string-prototype-extensions.md)

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,3 +65,8 @@ dasherize('myString');
6565

6666
* [Prototype extensions documentation](https://guides.emberjs.com/release/configuring-ember/disabling-prototype-extensions/)
6767
* [String prototype extensions deprecation RFC](https://emberjs.github.io/rfcs/0236-deprecation-ember-string.html#string-prototype-extensions)
68+
69+
## Related Rules
70+
71+
* [no-array-prototype-extensions](no-array-prototype-extensions.md)
72+
* [no-function-prototype-extensions](no-function-prototype-extensions.md)
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
'use strict';
2+
3+
const ERROR_MESSAGE = "Don't use Ember's array prototype extensions";
4+
5+
const EXTENSION_METHODS = new Set([
6+
/**
7+
* https://api.emberjs.com/ember/release/classes/EmberArray
8+
* EmberArray methods excluding native functions like reduce, filter etc.
9+
* */
10+
'any',
11+
'compact',
12+
'filterBy',
13+
'findBy',
14+
'getEach',
15+
'invoke',
16+
'isAny',
17+
'isEvery',
18+
'mapBy',
19+
'objectAt',
20+
'objectsAt',
21+
'reject',
22+
'rejectBy',
23+
'setEach',
24+
'sortBy',
25+
'toArray',
26+
'uniq',
27+
'uniqBy',
28+
'without',
29+
/**
30+
* https://api.emberjs.com/ember/release/classes/MutableArray
31+
* MutableArray methods excluding `replace` since it's part of string native functions
32+
* */
33+
'addObject',
34+
'addObjects',
35+
'clear',
36+
'insertAt',
37+
'popObject',
38+
'pushObject',
39+
'pushObjects',
40+
'removeAt',
41+
'removeObject',
42+
'removeObjects',
43+
'reverseObjects',
44+
'setObject',
45+
'shiftObject',
46+
'unshiftObject',
47+
'unshiftObjects',
48+
]);
49+
50+
/**
51+
* https://api.emberjs.com/ember/release/classes/EmberArray
52+
* EmberArray properties excluding native props: [], length.
53+
* */
54+
const EXTENSION_PROPERTIES = new Set(['lastObject', 'firstObject']);
55+
//----------------------------------------------------------------------------------------------
56+
// General rule - Don't use Ember's array prototype extensions like .any(), .pushObject() or .firstObject
57+
//----------------------------------------------------------------------------------------------
58+
59+
/** @type {import('eslint').Rule.RuleModule} */
60+
module.exports = {
61+
meta: {
62+
type: 'suggestion',
63+
docs: {
64+
description: "disallow usage of Ember's `Array` prototype extensions",
65+
category: 'Deprecations',
66+
recommended: false,
67+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-array-prototype-extensions.md',
68+
},
69+
fixable: null,
70+
schema: [],
71+
messages: {
72+
main: ERROR_MESSAGE,
73+
},
74+
},
75+
76+
create(context) {
77+
return {
78+
/**
79+
* Cover cases when `EXTENSION_METHODS` is getting called.
80+
* Example: something.filterBy();
81+
* @param {Object} node
82+
*/
83+
CallExpression(node) {
84+
// Skip case: filterBy();
85+
if (node.callee.type !== 'MemberExpression') {
86+
return;
87+
}
88+
89+
// Skip case: this.filterBy();
90+
if (node.callee.object.type === 'ThisExpression') {
91+
return;
92+
}
93+
94+
if (node.callee.property.type !== 'Identifier') {
95+
return;
96+
}
97+
98+
if (EXTENSION_METHODS.has(node.callee.property.name)) {
99+
context.report({ node, messageId: 'main' });
100+
}
101+
},
102+
103+
/**
104+
* Cover cases when `EXTENSION_PROPERTIES` is accessed like:
105+
* foo.firstObject;
106+
* bar.lastObject.bar;
107+
* @param {Object} node
108+
*/
109+
MemberExpression(node) {
110+
// Skip case when EXTENSION_PROPERTIES is accessed through callee.
111+
// Example: something.firstObject()
112+
if (node.parent.type === 'CallExpression') {
113+
return;
114+
}
115+
116+
if (node.property.type !== 'Identifier') {
117+
return;
118+
}
119+
if (EXTENSION_PROPERTIES.has(node.property.name)) {
120+
context.report({ node, messageId: 'main' });
121+
}
122+
},
123+
124+
/**
125+
* Cover cases when `EXTENSION_PROPERTIES` is accessed through literals like:
126+
* get(something, 'foo.firstObject');
127+
* set(something, 'lastObject.bar', 'something');
128+
* @param {Object} node
129+
*/
130+
Literal(node) {
131+
// Generate regexp for extension properties.
132+
// new RegExp(`${[...EXTENSION_PROPERTIES].map(prop => `(\.|^)${prop}(\.|$)`).join('|')}`) won't generate \. correctly
133+
const regexp = /(\.|^)firstObject(\.|$)|(\.|^)lastObject(\.|$)/;
134+
135+
if (typeof node.value === 'string' && regexp.test(node.value)) {
136+
context.report({ node, messageId: 'main' });
137+
}
138+
},
139+
};
140+
},
141+
};

0 commit comments

Comments
 (0)