Skip to content

Commit ec64db5

Browse files
Merge pull request #2531 from johanrd/autofix/no-array-prototype-extensions
Restore autofix: `template-no-array-prototype-extensions`
2 parents 50b7a59 + 1b918ca commit ec64db5

4 files changed

Lines changed: 119 additions & 8 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,7 +206,7 @@ rules in templates can be disabled with eslint directives with mustache or html
206206
| [template-no-action-on-submit-button](docs/rules/template-no-action-on-submit-button.md) | disallow action attribute on submit buttons | | | |
207207
| [template-no-args-paths](docs/rules/template-no-args-paths.md) | disallow args.foo paths in templates, use @foo instead | | 🔧 | |
208208
| [template-no-arguments-for-html-elements](docs/rules/template-no-arguments-for-html-elements.md) | disallow @arguments on HTML elements | | | |
209-
| [template-no-array-prototype-extensions](docs/rules/template-no-array-prototype-extensions.md) | disallow usage of Ember Array prototype extensions | | | |
209+
| [template-no-array-prototype-extensions](docs/rules/template-no-array-prototype-extensions.md) | disallow usage of Ember Array prototype extensions | | 🔧 | |
210210
| [template-no-at-ember-render-modifiers](docs/rules/template-no-at-ember-render-modifiers.md) | disallow usage of @ember/render-modifiers | | | |
211211
| [template-no-bare-strings](docs/rules/template-no-bare-strings.md) | disallow bare strings in templates (require translation/localization) | | | |
212212
| [template-no-bare-yield](docs/rules/template-no-bare-yield.md) | disallow templates whose only meaningful content is a bare {{yield}} | | | |

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

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

3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
35
<!-- end auto-generated rule header -->
46

57
💼 This rule is enabled in the following [configs](https://github.com/ember-cli/eslint-plugin-ember#-configurations): `strict-gjs`, `strict-gts`.

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

Lines changed: 58 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,20 @@ function isGetHelperWithMatchedLiteral(node, matchedStr) {
5959
return false;
6060
}
6161

62+
/**
63+
* Build a `get` helper call string from a path containing `firstObject`.
64+
* e.g. "this.items.firstObject" => { target: "this.items", key: "0" }
65+
* e.g. "this.items.firstObject.name" => { target: "this.items", key: "0.name" }
66+
*/
67+
function buildGetHelperParts(originalPath) {
68+
const parts = originalPath.split('.');
69+
const firstObjectIndex = parts.indexOf(FIRST_OBJECT_PROP_NAME);
70+
const target = parts.slice(0, firstObjectIndex).join('.');
71+
const afterParts = ['0', ...parts.slice(firstObjectIndex + 1)];
72+
const key = afterParts.join('.');
73+
return { target, key };
74+
}
75+
6276
/** @type {import('eslint').Rule.RuleModule} */
6377
module.exports = {
6478
meta: {
@@ -70,7 +84,7 @@ module.exports = {
7084
strictGts: true,
7185
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-array-prototype-extensions.md',
7286
},
73-
fixable: null,
87+
fixable: 'code',
7488
schema: [],
7589
messages: {
7690
lastObject: ERROR_MESSAGES.LAST_OBJECT,
@@ -79,6 +93,8 @@ module.exports = {
7993
},
8094

8195
create(context) {
96+
const sourceCode = context.sourceCode || context.getSourceCode();
97+
8298
return {
8399
GlimmerPathExpression(node) {
84100
if (!node.original) {
@@ -96,14 +112,50 @@ module.exports = {
96112
});
97113
}
98114

99-
// Handle firstObject
100-
if (
101-
!isAllowed(node.original, FIRST_OBJECT_PROP_NAME) ||
102-
isGetHelperWithMatchedLiteral(node, FIRST_OBJECT_PROP_NAME)
103-
) {
115+
// Handle firstObject — with autofix
116+
const isGetWithFirstObject = isGetHelperWithMatchedLiteral(node, FIRST_OBJECT_PROP_NAME);
117+
118+
if (!isAllowed(node.original, FIRST_OBJECT_PROP_NAME) || isGetWithFirstObject) {
104119
context.report({
105120
node,
106121
messageId: 'firstObject',
122+
fix(fixer) {
123+
const parent = node.parent;
124+
125+
// Case 1: get helper with firstObject in string literal
126+
// e.g. {{get @list "items.firstObject"}} => {{get @list "items.0"}}
127+
if (isGetWithFirstObject) {
128+
const literalNode = parent.params[1];
129+
const literalText = sourceCode.getText(literalNode);
130+
// Detect quote character used in source
131+
const quote = literalText[0];
132+
const literalValue = literalNode.value || literalNode.original;
133+
const newValue = literalValue
134+
.split('.')
135+
.map((part) => (part === FIRST_OBJECT_PROP_NAME ? '0' : part))
136+
.join('.');
137+
return fixer.replaceText(literalNode, `${quote}${newValue}${quote}`);
138+
}
139+
140+
// Case 2: PathExpression is the path of a MustacheStatement
141+
// e.g. {{this.items.firstObject}} => {{get this.items "0"}}
142+
// e.g. {{this.items.firstObject.name}} => {{get this.items "0.name"}}
143+
if (parent && parent.type === 'GlimmerMustacheStatement' && parent.path === node) {
144+
const { target, key } = buildGetHelperParts(node.original);
145+
const newText = `{{get ${target} "${key}"}}`;
146+
return fixer.replaceText(parent, newText);
147+
}
148+
149+
// Case 3: PathExpression used as a subexpression argument or other context
150+
// e.g. {{helper this.items.firstObject}} => {{helper (get this.items "0")}}
151+
if (parent) {
152+
const { target, key } = buildGetHelperParts(node.original);
153+
const newText = `(get ${target} "${key}")`;
154+
return fixer.replaceText(node, newText);
155+
}
156+
157+
return null;
158+
},
107159
});
108160
}
109161
},

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

Lines changed: 58 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,15 +17,72 @@ ruleTester.run('template-no-array-prototype-extensions', rule, {
1717
],
1818

1919
invalid: [
20+
// firstObject — basic path in MustacheStatement
2021
{
2122
code: '<template>{{this.items.firstObject}}</template>',
22-
output: null,
23+
output: '<template>{{get this.items "0"}}</template>',
24+
errors: [{ messageId: 'firstObject' }],
25+
},
26+
// firstObject — path with trailing property
27+
{
28+
code: '<template>{{this.items.firstObject.name}}</template>',
29+
output: '<template>{{get this.items "0.name"}}</template>',
2330
errors: [{ messageId: 'firstObject' }],
2431
},
32+
// firstObject — deeper path
33+
{
34+
code: '<template>{{this.model.items.firstObject}}</template>',
35+
output: '<template>{{get this.model.items "0"}}</template>',
36+
errors: [{ messageId: 'firstObject' }],
37+
},
38+
// firstObject — in get helper string literal
39+
{
40+
code: '<template>{{get @model "items.firstObject"}}</template>',
41+
output: '<template>{{get @model "items.0"}}</template>',
42+
errors: [{ messageId: 'firstObject' }],
43+
},
44+
// firstObject — in get helper string literal with trailing property
45+
{
46+
code: '<template>{{get @model "items.firstObject.name"}}</template>',
47+
output: '<template>{{get @model "items.0.name"}}</template>',
48+
errors: [{ messageId: 'firstObject' }],
49+
},
50+
// firstObject — in hash argument context
51+
{
52+
code: '<template>{{foo bar=this.list.firstObject}}</template>',
53+
output: '<template>{{foo bar=(get this.list "0")}}</template>',
54+
errors: [{ messageId: 'firstObject' }],
55+
},
56+
// firstObject — @arg prefix path
57+
{
58+
code: '<template><Foo @bar={{@list.firstObject}} /></template>',
59+
output: '<template><Foo @bar={{get @list "0"}} /></template>',
60+
errors: [{ messageId: 'firstObject' }],
61+
},
62+
// firstObject — deeper path with trailing properties
63+
{
64+
code: '<template><Foo @bar={{this.list.firstObject.name.foo}} /></template>',
65+
output: '<template><Foo @bar={{get this.list "0.name.foo"}} /></template>',
66+
errors: [{ messageId: 'firstObject' }],
67+
},
68+
// firstObject — subexpression param context
69+
{
70+
code: '<template><div data-test={{eq this.list.firstObject.abc "def"}}>Hello</div></template>',
71+
output:
72+
'<template><div data-test={{eq (get this.list "0.abc") "def"}}>Hello</div></template>',
73+
errors: [{ messageId: 'firstObject' }],
74+
},
75+
// lastObject — no fix available
2576
{
2677
code: '<template>{{this.users.lastObject}}</template>',
2778
output: null,
2879
errors: [{ messageId: 'lastObject' }],
2980
},
81+
// lastObject — deeper path, no fix
82+
{
83+
code: '<template>{{this.users.lastObject.name}}</template>',
84+
output: null,
85+
errors: [{ messageId: 'lastObject' }],
86+
},
3087
],
3188
});

0 commit comments

Comments
 (0)