Skip to content

Commit 4003efd

Browse files
Merge pull request #2534 from johanrd/autofix-complex/no-obscure-array-access
Restore autofix: `template-no-obscure-array-access`
2 parents 87775b8 + ee679ec commit 4003efd

4 files changed

Lines changed: 111 additions & 16 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -239,7 +239,7 @@ rules in templates can be disabled with eslint directives with mustache or html
239239
| [template-no-negated-comparison](docs/rules/template-no-negated-comparison.md) | disallow negated comparisons in templates | | | |
240240
| [template-no-negated-condition](docs/rules/template-no-negated-condition.md) | disallow negated conditions in if/unless | | | |
241241
| [template-no-nested-splattributes](docs/rules/template-no-nested-splattributes.md) | disallow nested ...attributes usage | | | |
242-
| [template-no-obscure-array-access](docs/rules/template-no-obscure-array-access.md) | disallow obscure array access patterns like `[email protected]` | | | |
242+
| [template-no-obscure-array-access](docs/rules/template-no-obscure-array-access.md) | disallow obscure array access patterns like `[email protected]` | | 🔧 | |
243243
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
244244
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
245245
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |

docs/rules/template-no-obscure-array-access.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
# ember/template-no-obscure-array-access
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
Disallow obscure array access patterns in templates.

lib/rules/template-no-obscure-array-access.js

Lines changed: 73 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,30 @@
1+
const DIGIT_REGEXP = /^\[?\d+]?$/;
2+
3+
/**
4+
* Extract the target path and string key from a path with numeric segments.
5+
* E.g. "this.list.0.name" → ["this.list", "0.name"]
6+
* E.g. "this.list.[0]" → ["this.list", "0"]
7+
* E.g. "@foo.0.bar" → ["@foo", "0.bar"]
8+
*
9+
* @param {string} original The original path string
10+
* @returns {[string, string]} A tuple of [targetPath, stringKey]
11+
*/
12+
function getHelperParams(original) {
13+
const parts = original.split('.');
14+
const firstDigitIndex = parts.findIndex((part) => DIGIT_REGEXP.test(part));
15+
16+
if (firstDigitIndex === -1) {
17+
return null;
18+
}
19+
20+
const targetPath = parts.slice(0, firstDigitIndex).join('.');
21+
// Strip brackets from digit segments: [0] → 0
22+
const keyParts = parts.slice(firstDigitIndex).map((part) => part.replace(/^\[(\d+)]$/, '$1'));
23+
const stringKey = keyParts.join('.');
24+
25+
return [targetPath, stringKey];
26+
}
27+
128
/** @type {import('eslint').Rule.RuleModule} */
229
module.exports = {
330
meta: {
@@ -8,6 +35,7 @@ module.exports = {
835
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-obscure-array-access.md',
936
templateMode: 'both',
1037
},
38+
fixable: 'code',
1139
schema: [],
1240
messages: {
1341
noObscureArrayAccess:
@@ -26,7 +54,7 @@ module.exports = {
2654
GlimmerPathExpression(node) {
2755
const path = node.original;
2856
const sourcePath = context.sourceCode.getText(node);
29-
// Check for @each or [] in paths
57+
// Check for @each or [] in paths — these are structural and not autofixable
3058
if (path && (path.includes('.@each.') || path.includes('.[].'))) {
3159
context.report({
3260
node,
@@ -37,13 +65,57 @@ module.exports = {
3765
}
3866
// Check for numeric path segments (e.g., foo.0.bar) or bracket notation (e.g., foo.[0])
3967
if (node.tail && node.tail.some((segment) => /^\d+$/.test(segment))) {
68+
const params = getHelperParams(path);
4069
context.report({
4170
node,
4271
messageId: 'noObscureArrayAccess',
4372
data: { path: sourcePath },
73+
fix: params ? buildFix(context, node, params) : undefined,
4474
});
4575
}
4676
},
4777
};
4878
},
4979
};
80+
81+
/**
82+
* Build a fix function for a numeric path expression.
83+
*
84+
* @param {import('eslint').Rule.RuleContext} context
85+
* @param {import('estree').Node} node The GlimmerPathExpression node
86+
* @param {[string, string]} params The [targetPath, stringKey] tuple
87+
* @returns {(fixer: import('eslint').Rule.RuleFixer) => import('eslint').Rule.Fix}
88+
*/
89+
function buildFix(context, node, params) {
90+
const [target, key] = params;
91+
const parent = node.parent;
92+
93+
// Case 1: PathExpression is the path of a MustacheStatement (e.g., {{this.list.0.name}})
94+
// Replace the entire mustache inner content with: get target "key"
95+
if (
96+
parent &&
97+
parent.type === 'GlimmerMustacheStatement' &&
98+
parent.path === node &&
99+
parent.params.length === 0 &&
100+
(!parent.hash || parent.hash.pairs.length === 0)
101+
) {
102+
return (fixer) => {
103+
// The mustache is {{path}} — replace just the path portion
104+
// The source text of the MustacheStatement includes {{ and }}
105+
const mustacheSource = context.sourceCode.getText(parent);
106+
const isTriple = mustacheSource.startsWith('{{{');
107+
const openLen = isTriple ? 3 : 2;
108+
const closeLen = isTriple ? 3 : 2;
109+
const innerStart = parent.range[0] + openLen;
110+
const innerEnd = parent.range[1] - closeLen;
111+
112+
return fixer.replaceTextRange([innerStart, innerEnd], `get ${target} "${key}"`);
113+
};
114+
}
115+
116+
// Case 2: PathExpression is a param or hash value or any other context
117+
// Wrap with (get target "key")
118+
return (fixer) => {
119+
return fixer.replaceText(node, `(get ${target} "${key}")`);
120+
};
121+
}

tests/lib/rules/template-no-obscure-array-access.js

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -40,37 +40,58 @@ ruleTester.run('template-no-obscure-array-access', rule, {
4040

4141
{
4242
code: '<template><Foo @onClick={{fn this.func @foo.0.bar}} /></template>',
43-
output: null,
43+
output: '<template><Foo @onClick={{fn this.func (get @foo "0.bar")}} /></template>',
4444
errors: [{ messageId: 'noObscureArrayAccess' }],
4545
},
4646
{
4747
code: '<template>{{foo bar=this.list.[0]}}</template>',
48-
output: null,
48+
output: '<template>{{foo bar=(get this.list "0")}}</template>',
4949
errors: [{ messageId: 'noObscureArrayAccess' }],
5050
},
5151
{
5252
code: '<template>{{foo bar=@list.[1]}}</template>',
53-
output: null,
53+
output: '<template>{{foo bar=(get @list "1")}}</template>',
5454
errors: [{ messageId: 'noObscureArrayAccess' }],
5555
},
5656
{
5757
code: '<template>{{this.list.[0]}}</template>',
58-
output: null,
58+
output: '<template>{{get this.list "0"}}</template>',
5959
errors: [{ messageId: 'noObscureArrayAccess' }],
6060
},
6161
{
6262
code: '<template>{{this.list.[0].name}}</template>',
63-
output: null,
63+
output: '<template>{{get this.list "0.name"}}</template>',
6464
errors: [{ messageId: 'noObscureArrayAccess' }],
6565
},
6666
{
6767
code: '<template><Foo @bar={{this.list.[0]}} /></template>',
68-
output: null,
68+
output: '<template><Foo @bar={{get this.list "0"}} /></template>',
6969
errors: [{ messageId: 'noObscureArrayAccess' }],
7070
},
7171
{
7272
code: '<template><Foo @bar={{this.list.[0].name.[1].foo}} /></template>',
73-
output: null,
73+
output: '<template><Foo @bar={{get this.list "0.name.1.foo"}} /></template>',
74+
errors: [{ messageId: 'noObscureArrayAccess' }],
75+
},
76+
// Regression: ember-template-lint#2926 — multi-line must NOT duplicate lines
77+
{
78+
code: `<template><Component
79+
title="<error>"
80+
id={{@model.0.id}}
81+
as |value|
82+
></Component></template>`,
83+
output: `<template><Component
84+
title="<error>"
85+
id={{get @model "0.id"}}
86+
as |value|
87+
></Component></template>`,
88+
errors: [{ messageId: 'noObscureArrayAccess' }],
89+
},
90+
// Regression: ember-template-lint#2924 — must NOT eat preceding params
91+
{
92+
code: '<template><Button @onClick={{fn this.myFunc @row.0.sha256}}>Button</Button></template>',
93+
output:
94+
'<template><Button @onClick={{fn this.myFunc (get @row "0.sha256")}}>Button</Button></template>',
7495
errors: [{ messageId: 'noObscureArrayAccess' }],
7596
},
7697
],
@@ -102,7 +123,7 @@ hbsRuleTester.run('template-no-obscure-array-access', rule, {
102123
invalid: [
103124
{
104125
code: '<Foo @onClick={{fn this.func @foo.0.bar}} />',
105-
output: null,
126+
output: '<Foo @onClick={{fn this.func (get @foo "0.bar")}} />',
106127
errors: [
107128
{
108129
message:
@@ -112,7 +133,7 @@ hbsRuleTester.run('template-no-obscure-array-access', rule, {
112133
},
113134
{
114135
code: '{{foo bar=this.list.[0]}}',
115-
output: null,
136+
output: '{{foo bar=(get this.list "0")}}',
116137
errors: [
117138
{
118139
message:
@@ -122,7 +143,7 @@ hbsRuleTester.run('template-no-obscure-array-access', rule, {
122143
},
123144
{
124145
code: '{{foo bar=@list.[1]}}',
125-
output: null,
146+
output: '{{foo bar=(get @list "1")}}',
126147
errors: [
127148
{
128149
message:
@@ -132,7 +153,7 @@ hbsRuleTester.run('template-no-obscure-array-access', rule, {
132153
},
133154
{
134155
code: '{{this.list.[0]}}',
135-
output: null,
156+
output: '{{get this.list "0"}}',
136157
errors: [
137158
{
138159
message:
@@ -142,7 +163,7 @@ hbsRuleTester.run('template-no-obscure-array-access', rule, {
142163
},
143164
{
144165
code: '{{this.list.[0].name}}',
145-
output: null,
166+
output: '{{get this.list "0.name"}}',
146167
errors: [
147168
{
148169
message:
@@ -152,7 +173,7 @@ hbsRuleTester.run('template-no-obscure-array-access', rule, {
152173
},
153174
{
154175
code: '<Foo @bar={{this.list.[0]}} />',
155-
output: null,
176+
output: '<Foo @bar={{get this.list "0"}} />',
156177
errors: [
157178
{
158179
message:
@@ -162,7 +183,7 @@ hbsRuleTester.run('template-no-obscure-array-access', rule, {
162183
},
163184
{
164185
code: '<Foo @bar={{this.list.[0].name.[1].foo}} />',
165-
output: null,
186+
output: '<Foo @bar={{get this.list "0.name.1.foo"}} />',
166187
errors: [
167188
{
168189
message:

0 commit comments

Comments
 (0)