Skip to content

Commit 86bf60e

Browse files
committed
Extract rule: template-no-array-prototype-extensions
1 parent 688cf6b commit 86bf60e

4 files changed

Lines changed: 215 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -192,6 +192,7 @@ rules in templates can be disabled with eslint directives with mustache or html
192192
| [template-builtin-component-arguments](docs/rules/template-builtin-component-arguments.md) | disallow setting certain attributes on builtin components | | | |
193193
| [template-no-action-modifiers](docs/rules/template-no-action-modifiers.md) | disallow usage of {{action}} modifiers | | | |
194194
| [template-no-arguments-for-html-elements](docs/rules/template-no-arguments-for-html-elements.md) | disallow @arguments on HTML elements | | | |
195+
| [template-no-array-prototype-extensions](docs/rules/template-no-array-prototype-extensions.md) | disallow usage of Ember Array prototype extensions | | | |
195196
| [template-no-debugger](docs/rules/template-no-debugger.md) | disallow {{debugger}} in templates | | | |
196197
| [template-no-log](docs/rules/template-no-log.md) | disallow {{log}} in templates | | | |
197198

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
# ember/template-no-array-prototype-extensions
2+
3+
<!-- end auto-generated rule header -->
4+
5+
💼 This rule is enabled in the following [configs](https://github.com/ember-cli/eslint-plugin-ember#-configurations): `strict-gjs`, `strict-gts`.
6+
7+
Disallow usage of Ember Array prototype extensions.
8+
9+
Ember historically provided Array prototype extensions like `firstObject` and `lastObject`. These extensions are deprecated and should be replaced with native JavaScript array methods or computed properties.
10+
11+
## Rule Details
12+
13+
This rule disallows using Ember Array prototype extensions in templates:
14+
15+
- `firstObject`
16+
- `lastObject`
17+
- `@each`
18+
- `[]`
19+
20+
## Examples
21+
22+
### Incorrect ❌
23+
24+
```gjs
25+
<template>
26+
{{this.items.firstObject}}
27+
</template>
28+
```
29+
30+
```gjs
31+
<template>
32+
{{this.users.lastObject}}
33+
</template>
34+
```
35+
36+
```gjs
37+
<template>
38+
{{this.data.@each}}
39+
</template>
40+
```
41+
42+
### Correct ✅
43+
44+
```gjs
45+
<template>
46+
{{get this.items 0}}
47+
</template>
48+
```
49+
50+
```gjs
51+
<template>
52+
{{this.firstItem}}
53+
</template>
54+
```
55+
56+
```gjs
57+
<template>
58+
{{#each this.items as |item|}}
59+
{{item}}
60+
{{/each}}
61+
</template>
62+
```
63+
64+
## Related Rules
65+
66+
- [no-array-prototype-extensions](./no-array-prototype-extensions.md)
67+
68+
## References
69+
70+
- [Ember Deprecations - Array prototype extensions](https://deprecations.emberjs.com/v3.x/#toc_ember-array-prototype-extensions)
71+
- [eslint-plugin-ember template-no-array-prototype-extensions](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-array-prototype-extensions.md)
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
const FIRST_OBJECT_PROP_NAME = 'firstObject';
2+
const LAST_OBJECT_PROP_NAME = 'lastObject';
3+
4+
const ERROR_MESSAGES = {
5+
LAST_OBJECT: 'Array prototype extension property lastObject usage is disallowed.',
6+
FIRST_OBJECT:
7+
"Array prototype extension property firstObject usage is disallowed. Please use Ember's get helper instead, e.g. `(get @list '0')`.",
8+
};
9+
10+
/**
11+
* Check if the path should be allowed. `@firstObject.test`, `@lastObject`, and
12+
* `this.firstObject` are allowed (they are property names, not extensions).
13+
*/
14+
function isAllowed(originalStr, matchedStr) {
15+
// allow `@firstObject.test`, `@lastObject`
16+
if (originalStr.startsWith(`@${matchedStr}`)) {
17+
return true;
18+
}
19+
20+
const originalParts = originalStr.split('.');
21+
const matchStrIndex = originalParts.indexOf(matchedStr);
22+
23+
// if not found
24+
if (matchStrIndex === -1) {
25+
return true;
26+
}
27+
// allow this.firstObject (direct property, not extension)
28+
return !matchStrIndex || originalParts[matchStrIndex - 1] === 'this';
29+
}
30+
31+
/**
32+
* Check if current node is a `get` helper and its string literal contains matchedStr.
33+
* For example `{{get this 'list.firstObject'}}` returns true,
34+
* but `{{get this 'firstObject'}}` returns false (that's a direct property).
35+
*/
36+
function isGetHelperWithMatchedLiteral(node, matchedStr) {
37+
if (node.original !== 'get') {
38+
return false;
39+
}
40+
41+
const parent = node.parent;
42+
if (
43+
parent &&
44+
(parent.type === 'GlimmerMustacheStatement' || parent.type === 'GlimmerSubExpression') &&
45+
parent.params &&
46+
parent.params[1] &&
47+
parent.params[1].type === 'GlimmerStringLiteral'
48+
) {
49+
const literal = parent.params[1].value || parent.params[1].original;
50+
const parts = literal.split('.');
51+
const matchStrIndex = parts.indexOf(matchedStr);
52+
53+
// matchedStr is found and not the `{{get this 'firstObject'}}` case
54+
return (
55+
matchStrIndex !== -1 &&
56+
!(matchStrIndex === 0 && parent.params[0] && parent.params[0].original === 'this')
57+
);
58+
}
59+
return false;
60+
}
61+
62+
/** @type {import('eslint').Rule.RuleModule} */
63+
module.exports = {
64+
meta: {
65+
type: 'suggestion',
66+
docs: {
67+
description: 'disallow usage of Ember Array prototype extensions',
68+
category: 'Best Practices',
69+
strictGjs: true,
70+
strictGts: true,
71+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-array-prototype-extensions.md',
72+
},
73+
fixable: null,
74+
schema: [],
75+
messages: {
76+
lastObject: ERROR_MESSAGES.LAST_OBJECT,
77+
firstObject: ERROR_MESSAGES.FIRST_OBJECT,
78+
},
79+
},
80+
81+
create(context) {
82+
return {
83+
GlimmerPathExpression(node) {
84+
if (!node.original) {
85+
return;
86+
}
87+
88+
// Handle lastObject — no fixer available
89+
if (
90+
!isAllowed(node.original, LAST_OBJECT_PROP_NAME) ||
91+
isGetHelperWithMatchedLiteral(node, LAST_OBJECT_PROP_NAME)
92+
) {
93+
context.report({
94+
node,
95+
messageId: 'lastObject',
96+
});
97+
}
98+
99+
// Handle firstObject
100+
if (
101+
!isAllowed(node.original, FIRST_OBJECT_PROP_NAME) ||
102+
isGetHelperWithMatchedLiteral(node, FIRST_OBJECT_PROP_NAME)
103+
) {
104+
context.report({
105+
node,
106+
messageId: 'firstObject',
107+
});
108+
}
109+
},
110+
};
111+
},
112+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
const rule = require('../../../lib/rules/template-no-array-prototype-extensions');
2+
const RuleTester = require('eslint').RuleTester;
3+
4+
const ruleTester = new RuleTester({
5+
parser: require.resolve('ember-eslint-parser'),
6+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
7+
});
8+
9+
ruleTester.run('template-no-array-prototype-extensions', rule, {
10+
valid: [
11+
'<template>{{this.items.[0]}}</template>',
12+
'<template>{{get this.items 0}}</template>',
13+
'<template>{{this.users}}</template>',
14+
'<template>{{@items}}</template>',
15+
'<template>{{firstObject}}</template>',
16+
'<template>{{length}}</template>',
17+
],
18+
19+
invalid: [
20+
{
21+
code: '<template>{{this.items.firstObject}}</template>',
22+
output: null,
23+
errors: [{ messageId: 'firstObject' }],
24+
},
25+
{
26+
code: '<template>{{this.users.lastObject}}</template>',
27+
output: null,
28+
errors: [{ messageId: 'lastObject' }],
29+
},
30+
],
31+
});

0 commit comments

Comments
 (0)