Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new rule no-array-prototype-extensions #1461

Merged
merged 6 commits into from
Apr 8, 2022
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ module.exports = {
'no-unused-labels': 'off',
'no-unused-vars': 'off',
'no-useless-constructor': 'off',
'node/no-extraneous-import': 'off',
'node/no-missing-import': 'off',
'node/no-missing-require': 'off',
'node/no-unsupported-features/es-syntax': 'off',
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -103,6 +103,7 @@ Rules are grouped by category to help you understand their purpose. Each rule ha
|:--------|:------------|:---------------|:-----------|:---------------|
| [closure-actions](./docs/rules/closure-actions.md) | enforce usage of closure actions | ✅ | | |
| [new-module-imports](./docs/rules/new-module-imports.md) | enforce using "New Module Imports" from Ember RFC #176 | ✅ | | |
| [no-array-prototype-extensions](./docs/rules/no-array-prototype-extensions.md) | disallow usage of Ember's `Array` prototype extensions | | | |
| [no-function-prototype-extensions](./docs/rules/no-function-prototype-extensions.md) | disallow usage of Ember's `function` prototype extensions | ✅ | | |
| [no-mixins](./docs/rules/no-mixins.md) | disallow the usage of mixins | ✅ | | |
| [no-new-mixins](./docs/rules/no-new-mixins.md) | disallow the creation of new mixins | ✅ | | |
Expand Down
99 changes: 99 additions & 0 deletions docs/rules/no-array-prototype-extensions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
# no-array-prototype-extensions

Do not use Ember's `array` prototype extensions.

Use native array functions instead of `.filterBy`, `.toArray()` in Ember modules.

Use lodash helper functions instead of `.uniqBy()`, `sortBy()` in Ember modules.

Use immutable update style with `@tracked` properties or `TrackedArray` from `tracked-built-ins` instead of `.pushObject`, `removeObject` in Ember modules.

## Examples

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

```js
/** Helper functions **/
export default class SampleComponent extends Component {
abc = ['x', 'y', 'z', 'x'];

def = this.abc.without('x');
ghi = this.abc.uniq();
jkl = this.abc.toArray();
mno = this.abc.uniqBy('y');
pqr = this.abc.sortBy('z');
}
```

```js
/** Observable-based functions **/
import { action } from '@ember/object';

export default class SampleComponent extends Component {
abc = [];
@action
someAction(newItem) {
this.abc.pushObject('1');
}
}
```

Examples of **correct** code for this rule:

```js
/** Helper functions **/
import { uniqBy, sortBy } from 'lodash';

export default class SampleComponent extends Component {
abc = ['x', 'y', 'z', 'x'];

def = this.abc.filter((el) => el !== 'x');
ghi = [...new Set(this.abc)];
jkl = [...this.abc];
mno = uniqBy(this.abc, 'y');
pqr = sortBy(this.abc, 'z');
}
```

```js
/** Observable-based functions **/
/** Use immutable tracked property is OK **/
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';

export default class SampleComponent extends Component {
smilland marked this conversation as resolved.
Show resolved Hide resolved
@tracked abc = [];

@action
someAction(newItem) {
this.abc = [...abc, newItem];
}
}
```

```js
/** Observable-based functions **/
/** Use TrackedArray is OK **/
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';
import { TrackedArray } from 'tracked-built-ins';

export default class SampleComponent extends Component {
@tracked abc = new TrackedArray();

@action
someAction(newItem) {
abc.push(newItem);
}
}
```

## References
smilland marked this conversation as resolved.
Show resolved Hide resolved

* [Prototype extensions documentation](https://guides.emberjs.com/release/configuring-ember/disabling-prototype-extensions/)
* Array prototype extensions deprecation RFC
smilland marked this conversation as resolved.
Show resolved Hide resolved

## Related Rules
smilland marked this conversation as resolved.
Show resolved Hide resolved

* [no-function-prototype-extensions](no-function-prototype-extensions.md)
* [no-string-prototype-extensions](no-string-prototype-extensions.md)
131 changes: 131 additions & 0 deletions lib/rules/no-array-prototype-extensions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
'use strict';

const ERROR_MESSAGE = "Don't use Ember's array prototype extensions";

const EXTENSION_METHODS = new Set([
/** EmberArray */
'any',
'compact',
'filterBy',
'findBy',
'getEach',
'invoke',
'isAny',
'isEvery',
'mapBy',
'objectAt',
'objectsAt',
'reject',
'rejectBy',
'setEach',
'sortBy',
'toArray',
'uniq',
'uniqBy',
'without',
/** MutableArray */
smilland marked this conversation as resolved.
Show resolved Hide resolved
'addObject',
'addObjects',
'clear',
'insertAt',
'popObject',
'pushObject',
'pushObjects',
'removeAt',
'removeObject',
'removeObjects',
'reverseObjects',
'setObject',
'shiftObject',
'unshiftObject',
'unshiftObjects',
]);

const EXTENSION_PROPERTIES = new Set(['lastObject', 'firstObject']);
//----------------------------------------------------------------------------------------------
// General rule - Don't use Ember's array prototype extensions like .any(), .pushObject() or .firstObject
//----------------------------------------------------------------------------------------------

/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: "disallow usage of Ember's `Array` prototype extensions",
category: 'Deprecations',
recommended: false,
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/no-array-prototype-extensions.md',
},
fixable: null,
schema: [],
messages: {
main: ERROR_MESSAGE,
},
},

create(context) {
return {
/**
* Cover cases when `EXTENSION_METHODS` is getting called.
* Example: something.filterBy();
* @param {Object} node
*/
CallExpression(node) {
smilland marked this conversation as resolved.
Show resolved Hide resolved
// Skip case: filterBy();
if (node.callee.type !== 'MemberExpression') {
return;
}

// Skip case: this.filterBy();
if (node.callee.object.type === 'ThisExpression') {
return;
}

if (node.callee.property.type !== 'Identifier') {
return;
}

if (EXTENSION_METHODS.has(node.callee.property.name)) {
context.report({ node, messageId: 'main' });
}
},

/**
* Cover cases when `EXTENSION_PROPERTIES` is accessed like:
* foo.firstObject;
* bar.lastObject.bar;
* @param {Object} node
*/
MemberExpression(node) {
// Skip case when EXTENSION_PROPERTIES is accessed through callee.
// Example: something.firstObject()
if (node.parent.type === 'CallExpression') {
return;
}

if (node.property.type !== 'Identifier') {
return;
}
if (EXTENSION_PROPERTIES.has(node.property.name)) {
context.report({ node, messageId: 'main' });
}
},

/**
* Cover cases when `EXTENSION_PROPERTIES` is accessed through literals like:
* get(something, 'foo.firstObject');
* set(something, 'lastObject.bar', 'something');
* @param {Object} node
*/
Literal(node) {
// Generate regexp for extension properties.
// new RegExp(`${[...EXTENSION_PROPERTIES].map(prop => `(\.|^)${prop}(\.|$)`).join('|')}`) won't generate \. correctly
const regexp = /(\.|^)firstObject(\.|$)|(\.|^)lastObject(\.|$)/;

if (typeof node.value === 'string' && regexp.test(node.value)) {
context.report({ node, messageId: 'main' });
}
},
};
},
};
Loading