Skip to content

Commit be6400b

Browse files
committed
Extract rule: template-no-dynamic-subexpression-invocations
1 parent 9837162 commit be6400b

4 files changed

Lines changed: 473 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,7 @@ rules in templates can be disabled with eslint directives with mustache or html
207207
| [template-no-capital-arguments](docs/rules/template-no-capital-arguments.md) | disallow capital arguments (use lowercase @arg instead of @Arg) | | | |
208208
| [template-no-chained-this](docs/rules/template-no-chained-this.md) | disallow redundant `this.this` in templates | | 🔧 | |
209209
| [template-no-debugger](docs/rules/template-no-debugger.md) | disallow {{debugger}} in templates | | | |
210+
| [template-no-dynamic-subexpression-invocations](docs/rules/template-no-dynamic-subexpression-invocations.md) | disallow dynamic subexpression invocations | | | |
210211
| [template-no-element-event-actions](docs/rules/template-no-element-event-actions.md) | disallow element event actions (use {{on}} modifier instead) | | | |
211212
| [template-no-implicit-this](docs/rules/template-no-implicit-this.md) | require explicit `this` in property access | | | |
212213
| [template-no-inline-event-handlers](docs/rules/template-no-inline-event-handlers.md) | disallow DOM event handler attributes | | | |
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
# ember/template-no-dynamic-subexpression-invocations
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallow dynamic helper invocations.
6+
7+
Dynamic helper invocations (where the helper name comes from a property or argument) make code harder to understand and can have performance implications. Use explicit helper names instead.
8+
9+
## Rule Details
10+
11+
This rule disallows invoking helpers dynamically using `this` or `@` properties.
12+
13+
## Examples
14+
15+
### Incorrect ❌
16+
17+
```gjs
18+
<template>
19+
{{(this.helper "arg")}}
20+
</template>
21+
```
22+
23+
```gjs
24+
<template>
25+
{{(@helperName "value")}}
26+
</template>
27+
```
28+
29+
```gjs
30+
<template>
31+
{{this.formatter this.data}}
32+
</template>
33+
```
34+
35+
### Correct ✅
36+
37+
```gjs
38+
<template>
39+
{{format-date this.date}}
40+
</template>
41+
```
42+
43+
```gjs
44+
<template>
45+
{{(upper-case this.name)}}
46+
</template>
47+
```
48+
49+
```gjs
50+
<template>
51+
{{this.formattedData}}
52+
</template>
53+
```
54+
55+
## Related Rules
56+
57+
- [template-no-implicit-this](./template-no-implicit-this.md)
58+
59+
## References
60+
61+
- [Ember Guides - Template Helpers](https://guides.emberjs.com/release/components/helper-functions/)
62+
- [eslint-plugin-ember template-no-dynamic-subexpression-invocations](https://github.com/ember-cli/eslint-plugin-ember/blob/master/docs/rules/template-no-dynamic-subexpression-invocations.md)
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
function isInAttrPosition(node) {
2+
let p = node.parent;
3+
while (p) {
4+
if (p.type === 'GlimmerAttrNode') {
5+
return true;
6+
}
7+
if (p.type === 'GlimmerConcatStatement') {
8+
p = p.parent;
9+
continue;
10+
}
11+
if (
12+
p.type === 'GlimmerElementNode' ||
13+
p.type === 'GlimmerTemplate' ||
14+
p.type === 'GlimmerBlockStatement' ||
15+
p.type === 'GlimmerBlock'
16+
) {
17+
return false;
18+
}
19+
p = p.parent;
20+
}
21+
return false;
22+
}
23+
24+
/** @type {import('eslint').Rule.RuleModule} */
25+
module.exports = {
26+
meta: {
27+
type: 'problem',
28+
docs: {
29+
description: 'disallow dynamic subexpression invocations',
30+
category: 'Best Practices',
31+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-dynamic-subexpression-invocations.md',
32+
templateMode: 'both',
33+
},
34+
fixable: null,
35+
schema: [],
36+
messages: {
37+
noDynamicSubexpressionInvocations:
38+
'Do not use dynamic helper invocations. Use explicit helper names instead.',
39+
},
40+
originallyFrom: {
41+
name: 'ember-template-lint',
42+
rule: 'lib/rules/no-dynamic-subexpression-invocations.js',
43+
docs: 'docs/rule/no-dynamic-subexpression-invocations.md',
44+
tests: 'test/unit/rules/no-dynamic-subexpression-invocations-test.js',
45+
},
46+
},
47+
48+
create(context) {
49+
const localScopes = [];
50+
51+
function pushLocals(params) {
52+
localScopes.push(new Set(params || []));
53+
}
54+
55+
function popLocals() {
56+
localScopes.pop();
57+
}
58+
59+
function isLocal(name) {
60+
for (const scope of localScopes) {
61+
if (scope.has(name)) {
62+
return true;
63+
}
64+
}
65+
return false;
66+
}
67+
68+
function isDynamicPath(path) {
69+
if (!path || path.type !== 'GlimmerPathExpression') {
70+
return false;
71+
}
72+
if (path.head?.type === 'AtHead') {
73+
return true;
74+
}
75+
if (path.head?.type === 'ThisHead') {
76+
return true;
77+
}
78+
if (path.original && path.original.includes('.')) {
79+
return true;
80+
}
81+
if (path.original && isLocal(path.original)) {
82+
return true;
83+
}
84+
return false;
85+
}
86+
87+
return {
88+
GlimmerBlockStatement(node) {
89+
if (node.program && node.program.blockParams) {
90+
pushLocals(node.program.blockParams);
91+
}
92+
},
93+
'GlimmerBlockStatement:exit'(node) {
94+
if (node.program && node.program.blockParams) {
95+
popLocals();
96+
}
97+
},
98+
99+
GlimmerElementNode(node) {
100+
if (node.blockParams && node.blockParams.length > 0) {
101+
pushLocals(node.blockParams);
102+
}
103+
},
104+
'GlimmerElementNode:exit'(node) {
105+
if (node.blockParams && node.blockParams.length > 0) {
106+
popLocals();
107+
}
108+
},
109+
110+
GlimmerSubExpression(node) {
111+
if (node.path && node.path.type === 'GlimmerPathExpression' && isDynamicPath(node.path)) {
112+
context.report({
113+
node,
114+
messageId: 'noDynamicSubexpressionInvocations',
115+
});
116+
}
117+
},
118+
119+
GlimmerElementModifierStatement(node) {
120+
if (node.path && node.path.type === 'GlimmerPathExpression' && isDynamicPath(node.path)) {
121+
context.report({
122+
node,
123+
messageId: 'noDynamicSubexpressionInvocations',
124+
});
125+
}
126+
},
127+
128+
GlimmerMustacheStatement(node) {
129+
if (node.path && node.path.type === 'GlimmerPathExpression') {
130+
const inAttr = isInAttrPosition(node);
131+
const hasArgs =
132+
(node.params && node.params.length > 0) ||
133+
(node.hash && node.hash.pairs && node.hash.pairs.length > 0);
134+
135+
if (inAttr && isDynamicPath(node.path) && hasArgs) {
136+
// In attribute context, flag dynamic paths with arguments
137+
context.report({
138+
node,
139+
messageId: 'noDynamicSubexpressionInvocations',
140+
});
141+
return;
142+
}
143+
144+
if (!inAttr && hasArgs) {
145+
// In body context, only flag this.* paths (not @args)
146+
if (node.path.head?.type === 'ThisHead') {
147+
context.report({
148+
node,
149+
messageId: 'noDynamicSubexpressionInvocations',
150+
});
151+
}
152+
}
153+
}
154+
},
155+
};
156+
},
157+
};

0 commit comments

Comments
 (0)