Skip to content

Commit b3ea4a3

Browse files
Merge pull request #2487 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-obscure-array-access
Extract rule: template-no-obscure-array-access
2 parents 25a995d + 8bf8402 commit b3ea4a3

4 files changed

Lines changed: 312 additions & 19 deletions

File tree

README.md

Lines changed: 20 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -192,25 +192,26 @@ rules in templates can be disabled with eslint directives with mustache or html
192192

193193
### Best Practices
194194

195-
| Name                                       | Description | 💼 | 🔧 | 💡 |
196-
| :----------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | :- | :- | :- |
197-
| [template-builtin-component-arguments](docs/rules/template-builtin-component-arguments.md) | disallow setting certain attributes on builtin components | | | |
198-
| [template-no-action-modifiers](docs/rules/template-no-action-modifiers.md) | disallow usage of {{action}} modifiers | | | |
199-
| [template-no-arguments-for-html-elements](docs/rules/template-no-arguments-for-html-elements.md) | disallow @arguments on HTML elements | | | |
200-
| [template-no-array-prototype-extensions](docs/rules/template-no-array-prototype-extensions.md) | disallow usage of Ember Array prototype extensions | | | |
201-
| [template-no-block-params-for-html-elements](docs/rules/template-no-block-params-for-html-elements.md) | disallow block params on HTML elements | | | |
202-
| [template-no-capital-arguments](docs/rules/template-no-capital-arguments.md) | disallow capital arguments (use lowercase @arg instead of @Arg) | | | |
203-
| [template-no-chained-this](docs/rules/template-no-chained-this.md) | disallow redundant `this.this` in templates | | 🔧 | |
204-
| [template-no-debugger](docs/rules/template-no-debugger.md) | disallow {{debugger}} in templates | | | |
205-
| [template-no-element-event-actions](docs/rules/template-no-element-event-actions.md) | disallow element event actions (use {{on}} modifier instead) | | | |
206-
| [template-no-inline-event-handlers](docs/rules/template-no-inline-event-handlers.md) | disallow DOM event handler attributes | | | |
207-
| [template-no-inline-styles](docs/rules/template-no-inline-styles.md) | disallow inline styles | | | |
208-
| [template-no-input-placeholder](docs/rules/template-no-input-placeholder.md) | disallow placeholder attribute on input elements | | | |
209-
| [template-no-input-tagname](docs/rules/template-no-input-tagname.md) | disallow tagName attribute on {{input}} helper | | | |
210-
| [template-no-log](docs/rules/template-no-log.md) | disallow {{log}} in templates | | | |
211-
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
212-
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
213-
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
195+
| Name                                       | Description | 💼 | 🔧 | 💡 |
196+
| :----------------------------------------------------------------------------------------------------- | :---------------------------------------------------------------------- | :- | :- | :- |
197+
| [template-builtin-component-arguments](docs/rules/template-builtin-component-arguments.md) | disallow setting certain attributes on builtin components | | | |
198+
| [template-no-action-modifiers](docs/rules/template-no-action-modifiers.md) | disallow usage of {{action}} modifiers | | | |
199+
| [template-no-arguments-for-html-elements](docs/rules/template-no-arguments-for-html-elements.md) | disallow @arguments on HTML elements | | | |
200+
| [template-no-array-prototype-extensions](docs/rules/template-no-array-prototype-extensions.md) | disallow usage of Ember Array prototype extensions | | | |
201+
| [template-no-block-params-for-html-elements](docs/rules/template-no-block-params-for-html-elements.md) | disallow block params on HTML elements | | | |
202+
| [template-no-capital-arguments](docs/rules/template-no-capital-arguments.md) | disallow capital arguments (use lowercase @arg instead of @Arg) | | | |
203+
| [template-no-chained-this](docs/rules/template-no-chained-this.md) | disallow redundant `this.this` in templates | | 🔧 | |
204+
| [template-no-debugger](docs/rules/template-no-debugger.md) | disallow {{debugger}} in templates | | | |
205+
| [template-no-element-event-actions](docs/rules/template-no-element-event-actions.md) | disallow element event actions (use {{on}} modifier instead) | | | |
206+
| [template-no-inline-event-handlers](docs/rules/template-no-inline-event-handlers.md) | disallow DOM event handler attributes | | | |
207+
| [template-no-inline-styles](docs/rules/template-no-inline-styles.md) | disallow inline styles | | | |
208+
| [template-no-input-placeholder](docs/rules/template-no-input-placeholder.md) | disallow placeholder attribute on input elements | | | |
209+
| [template-no-input-tagname](docs/rules/template-no-input-tagname.md) | disallow tagName attribute on {{input}} helper | | | |
210+
| [template-no-log](docs/rules/template-no-log.md) | disallow {{log}} in templates | | | |
211+
| [template-no-obscure-array-access](docs/rules/template-no-obscure-array-access.md) | disallow obscure array access patterns like `objectPath.@each.property` | | | |
212+
| [template-no-obsolete-elements](docs/rules/template-no-obsolete-elements.md) | disallow obsolete HTML elements | | | |
213+
| [template-no-outlet-outside-routes](docs/rules/template-no-outlet-outside-routes.md) | disallow {{outlet}} outside of route templates | | | |
214+
| [template-no-page-title-component](docs/rules/template-no-page-title-component.md) | disallow usage of ember-page-title component | | | |
214215

215216
### Components
216217

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
# ember/template-no-obscure-array-access
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallow obscure array access patterns in templates.
6+
7+
## Rule Details
8+
9+
This rule discourages the use of obscure array access patterns in templates, including:
10+
11+
- Numeric array index access like `{{list.[0]}}` or `{{list.[1].name}}`
12+
- `@each` property access like `{{items.@each.name}}`
13+
- `[]` property access like `{{items.[].property}}`
14+
15+
Using obscure expressions like `{{list.[1].name}}` is discouraged. This rule recommends the use of Ember's `get` helper as an alternative for accessing array values.
16+
17+
## Examples
18+
19+
Examples of **incorrect** code for this rule:
20+
21+
```gjs
22+
<template>
23+
<Foo @bar={{@list.[0]}} />
24+
</template>
25+
```
26+
27+
```gjs
28+
<template>
29+
{{@list.[1].name}}
30+
</template>
31+
```
32+
33+
```gjs
34+
<template>
35+
{{items.@each.name}}
36+
</template>
37+
```
38+
39+
```gjs
40+
<template>
41+
{{items.[].property}}
42+
</template>
43+
```
44+
45+
Examples of **correct** code for this rule:
46+
47+
```gjs
48+
<template>
49+
<Foo @bar={{get @list '0'}} />
50+
</template>
51+
```
52+
53+
```gjs
54+
<template>
55+
{{get @list '1.name'}}
56+
</template>
57+
```
58+
59+
```gjs
60+
<template>
61+
{{#each items as |item|}}
62+
{{item.name}}
63+
{{/each}}
64+
</template>
65+
```
66+
67+
## References
68+
69+
- [eslint-plugin-ember template-no-obscure-array-access](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-obscure-array-access.md)
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'suggestion',
5+
docs: {
6+
description: 'disallow obscure array access patterns like `objectPath.@each.property`',
7+
category: 'Best Practices',
8+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-obscure-array-access.md',
9+
templateMode: 'both',
10+
},
11+
schema: [],
12+
messages: {
13+
noObscureArrayAccess:
14+
'Unexpected obscure array access pattern "{{path}}". Use computed properties or helpers instead.',
15+
},
16+
originallyFrom: {
17+
name: 'ember-template-lint',
18+
rule: 'lib/rules/no-obscure-array-access.js',
19+
docs: 'docs/rule/no-obscure-array-access.md',
20+
tests: 'test/unit/rules/no-obscure-array-access-test.js',
21+
},
22+
},
23+
24+
create(context) {
25+
return {
26+
GlimmerPathExpression(node) {
27+
const path = node.original;
28+
const sourcePath = context.sourceCode.getText(node);
29+
// Check for @each or [] in paths
30+
if (path && (path.includes('.@each.') || path.includes('.[].'))) {
31+
context.report({
32+
node,
33+
messageId: 'noObscureArrayAccess',
34+
data: { path: sourcePath },
35+
});
36+
return;
37+
}
38+
// Check for numeric path segments (e.g., foo.0.bar) or bracket notation (e.g., foo.[0])
39+
if (node.tail && node.tail.some((segment) => /^\d+$/.test(segment))) {
40+
context.report({
41+
node,
42+
messageId: 'noObscureArrayAccess',
43+
data: { path: sourcePath },
44+
});
45+
}
46+
},
47+
};
48+
},
49+
};
Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
//------------------------------------------------------------------------------
2+
// Requirements
3+
//------------------------------------------------------------------------------
4+
5+
const rule = require('../../../lib/rules/template-no-obscure-array-access');
6+
const RuleTester = require('eslint').RuleTester;
7+
8+
const ruleTester = new RuleTester({
9+
parser: require.resolve('ember-eslint-parser'),
10+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
11+
});
12+
13+
// Note: @each and [] are not valid in standard Glimmer templates and cause parse errors
14+
// This rule is designed to catch edge cases or custom syntax if they were to exist
15+
// We'll use simpler valid/invalid cases that actually parse
16+
ruleTester.run('template-no-obscure-array-access', rule, {
17+
valid: [
18+
'<template>{{items}}</template>',
19+
'<template>{{this.items}}</template>',
20+
'<template>{{#each items as |item|}}{{item.name}}{{/each}}</template>',
21+
'<template>{{get items 0}}</template>',
22+
'<template>{{items.firstObject.name}}</template>',
23+
24+
"<template>{{foo bar=(get this 'list.0' )}}</template>",
25+
"<template><Foo @bar={{get this 'list.0'}} /></template>",
26+
"<template>{{get this 'list.0'}}</template>",
27+
'<template>{{foo bar @list}}</template>',
28+
'<template>Just a regular text in the template bar.[1] bar.1</template>',
29+
'<template><Foo foo="bar.[1]" /></template>',
30+
`<template><FooBar
31+
@subHeaderText={{if
32+
this.isFooBarV2Enabled
33+
"foobar"
34+
}}
35+
/></template>`,
36+
],
37+
invalid: [
38+
// Since @each and [] cause parse errors, this rule serves as documentation
39+
// In practice, the parser will catch these issues before the rule runs
40+
41+
{
42+
code: '<template><Foo @onClick={{fn this.func @foo.0.bar}} /></template>',
43+
output: null,
44+
errors: [{ messageId: 'noObscureArrayAccess' }],
45+
},
46+
{
47+
code: '<template>{{foo bar=this.list.[0]}}</template>',
48+
output: null,
49+
errors: [{ messageId: 'noObscureArrayAccess' }],
50+
},
51+
{
52+
code: '<template>{{foo bar=@list.[1]}}</template>',
53+
output: null,
54+
errors: [{ messageId: 'noObscureArrayAccess' }],
55+
},
56+
{
57+
code: '<template>{{this.list.[0]}}</template>',
58+
output: null,
59+
errors: [{ messageId: 'noObscureArrayAccess' }],
60+
},
61+
{
62+
code: '<template>{{this.list.[0].name}}</template>',
63+
output: null,
64+
errors: [{ messageId: 'noObscureArrayAccess' }],
65+
},
66+
{
67+
code: '<template><Foo @bar={{this.list.[0]}} /></template>',
68+
output: null,
69+
errors: [{ messageId: 'noObscureArrayAccess' }],
70+
},
71+
{
72+
code: '<template><Foo @bar={{this.list.[0].name.[1].foo}} /></template>',
73+
output: null,
74+
errors: [{ messageId: 'noObscureArrayAccess' }],
75+
},
76+
],
77+
});
78+
79+
const hbsRuleTester = new RuleTester({
80+
parser: require.resolve('ember-eslint-parser/hbs'),
81+
parserOptions: {
82+
ecmaVersion: 2022,
83+
sourceType: 'module',
84+
},
85+
});
86+
87+
hbsRuleTester.run('template-no-obscure-array-access', rule, {
88+
valid: [
89+
"{{foo bar=(get this 'list.0' )}}",
90+
"<Foo @bar={{get this 'list.0'}}",
91+
"{{get this 'list.0'}}",
92+
'{{foo bar @list}}',
93+
'Just a regular text in the template bar.[1] bar.1',
94+
'<Foo foo="bar.[1]" />',
95+
`<FooBar
96+
@subHeaderText={{if
97+
this.isFooBarV2Enabled
98+
"foobar"
99+
}}
100+
/>`,
101+
],
102+
invalid: [
103+
{
104+
code: '<Foo @onClick={{fn this.func @foo.0.bar}} />',
105+
output: null,
106+
errors: [
107+
{
108+
message:
109+
'Unexpected obscure array access pattern "@foo.0.bar". Use computed properties or helpers instead.',
110+
},
111+
],
112+
},
113+
{
114+
code: '{{foo bar=this.list.[0]}}',
115+
output: null,
116+
errors: [
117+
{
118+
message:
119+
'Unexpected obscure array access pattern "this.list.[0]". Use computed properties or helpers instead.',
120+
},
121+
],
122+
},
123+
{
124+
code: '{{foo bar=@list.[1]}}',
125+
output: null,
126+
errors: [
127+
{
128+
message:
129+
'Unexpected obscure array access pattern "@list.[1]". Use computed properties or helpers instead.',
130+
},
131+
],
132+
},
133+
{
134+
code: '{{this.list.[0]}}',
135+
output: null,
136+
errors: [
137+
{
138+
message:
139+
'Unexpected obscure array access pattern "this.list.[0]". Use computed properties or helpers instead.',
140+
},
141+
],
142+
},
143+
{
144+
code: '{{this.list.[0].name}}',
145+
output: null,
146+
errors: [
147+
{
148+
message:
149+
'Unexpected obscure array access pattern "this.list.[0].name". Use computed properties or helpers instead.',
150+
},
151+
],
152+
},
153+
{
154+
code: '<Foo @bar={{this.list.[0]}} />',
155+
output: null,
156+
errors: [
157+
{
158+
message:
159+
'Unexpected obscure array access pattern "this.list.[0]". Use computed properties or helpers instead.',
160+
},
161+
],
162+
},
163+
{
164+
code: '<Foo @bar={{this.list.[0].name.[1].foo}} />',
165+
output: null,
166+
errors: [
167+
{
168+
message:
169+
'Unexpected obscure array access pattern "this.list.[0].name.[1].foo". Use computed properties or helpers instead.',
170+
},
171+
],
172+
},
173+
],
174+
});

0 commit comments

Comments
 (0)