Skip to content

Commit a072f87

Browse files
Merge pull request #2434 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-chained-this
Extract rule: template-no-chained-this
2 parents b88efee + 4f0db93 commit a072f87

File tree

4 files changed

+223
-0
lines changed

4 files changed

+223
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ rules in templates can be disabled with eslint directives with mustache or html
198198
| [template-no-array-prototype-extensions](docs/rules/template-no-array-prototype-extensions.md) | disallow usage of Ember Array prototype extensions | | | |
199199
| [template-no-block-params-for-html-elements](docs/rules/template-no-block-params-for-html-elements.md) | disallow block params on HTML elements | | | |
200200
| [template-no-capital-arguments](docs/rules/template-no-capital-arguments.md) | disallow capital arguments (use lowercase @arg instead of @Arg) | | | |
201+
| [template-no-chained-this](docs/rules/template-no-chained-this.md) | disallow redundant `this.this` in templates | | 🔧 | |
201202
| [template-no-debugger](docs/rules/template-no-debugger.md) | disallow {{debugger}} in templates | | | |
202203
| [template-no-element-event-actions](docs/rules/template-no-element-event-actions.md) | disallow element event actions (use {{on}} modifier instead) | | | |
203204
| [template-no-log](docs/rules/template-no-log.md) | disallow {{log}} in templates | | | |
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# ember/template-no-chained-this
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
Disallow redundant `this.this` in templates.
8+
9+
Using `this.this.*` in templates is almost always a typo or copy/paste mistake. These patterns are misleading and result in unnecessary ambiguity about scope and component context.
10+
11+
## Rule Details
12+
13+
This rule disallows `this.this.*` patterns in templates (e.g., `{{this.this.foo}}` or `<this.this.Bar />`).
14+
15+
## Examples
16+
17+
### Incorrect ❌
18+
19+
```gjs
20+
<template>
21+
{{this.this.value}}
22+
</template>
23+
```
24+
25+
```gjs
26+
<template>
27+
{{#this.this.foo}}
28+
some text
29+
{{/this.this.foo}}
30+
</template>
31+
```
32+
33+
```gjs
34+
<template>
35+
{{helper value=this.this.foo}}
36+
</template>
37+
```
38+
39+
```gjs
40+
<template>
41+
<this.this.Component />
42+
</template>
43+
```
44+
45+
```gjs
46+
<template>
47+
{{component this.this.dynamicComponent}}
48+
</template>
49+
```
50+
51+
### Correct ✅
52+
53+
```gjs
54+
<template>
55+
{{this.value}}
56+
</template>
57+
```
58+
59+
```gjs
60+
<template>
61+
<this.Component />
62+
</template>
63+
```
64+
65+
```gjs
66+
<template>
67+
{{component this.dynamicComponent}}
68+
</template>
69+
```
70+
71+
```gjs
72+
<template>
73+
{{@argName}}
74+
</template>
75+
```
76+
77+
## Migration
78+
79+
Remove the extra `this`:
80+
81+
Before:
82+
83+
```gjs
84+
<template>
85+
{{this.this.foo}}
86+
<this.this.bar />
87+
</template>
88+
```
89+
90+
After:
91+
92+
```gjs
93+
<template>
94+
{{this.foo}}
95+
<this.bar />
96+
</template>
97+
```
98+
99+
## References
100+
101+
- [Ember Guides - Glimmer Component Templates](https://guides.emberjs.com/release/upgrading/current-edition/glimmer-components/)
102+
- [Handlebars Strict Mode](https://github.com/emberjs/rfcs/blob/master/text/0496-handlebars-strict-mode.md)
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'problem',
5+
docs: {
6+
description: 'disallow redundant `this.this` in templates',
7+
category: 'Best Practices',
8+
strictGjs: true,
9+
strictGts: true,
10+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-chained-this.md',
11+
},
12+
fixable: 'code',
13+
schema: [],
14+
messages: {
15+
noChainedThis:
16+
'this.this.* is not allowed in templates. This is likely a mistake — remove the redundant this.',
17+
},
18+
},
19+
20+
create(context) {
21+
const sourceCode = context.sourceCode;
22+
23+
return {
24+
GlimmerPathExpression(node) {
25+
const text = sourceCode.getText(node);
26+
if (!text.startsWith('this.this.')) {
27+
return;
28+
}
29+
30+
const fixed = text.replace('this.this.', 'this.');
31+
32+
context.report({
33+
node,
34+
messageId: 'noChainedThis',
35+
fix(fixer) {
36+
const fixes = [fixer.replaceText(node, fixed)];
37+
38+
// Block statements have a closing tag path that must match
39+
const parent = node.parent;
40+
if (parent?.type === 'GlimmerBlockStatement' && parent.path === node) {
41+
const closingPathEnd = parent.range[1] - 2; // before '}}'
42+
const closingPathStart = closingPathEnd - text.length;
43+
fixes.push(fixer.replaceTextRange([closingPathStart, closingPathEnd], fixed));
44+
}
45+
46+
return fixes;
47+
},
48+
});
49+
},
50+
GlimmerElementNode(node) {
51+
if (!node.tag?.startsWith('this.this.')) {
52+
return;
53+
}
54+
55+
const fixedTag = node.tag.replace('this.this.', 'this.');
56+
57+
context.report({
58+
node,
59+
messageId: 'noChainedThis',
60+
fix(fixer) {
61+
// Replace the tag name after '<'
62+
const openStart = node.range[0] + 1;
63+
return fixer.replaceTextRange([openStart, openStart + node.tag.length], fixedTag);
64+
},
65+
});
66+
},
67+
};
68+
},
69+
};
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
const rule = require('../../../lib/rules/template-no-chained-this');
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-chained-this', rule, {
10+
valid: [
11+
'<template>{{this.value}}</template>',
12+
'<template>{{this.thisvalue}}</template>',
13+
'<template>{{@argName}}</template>',
14+
'<template>{{this.user.name}}</template>',
15+
'<template><this.Component /></template>',
16+
'<template>{{component this.dynamicComponent}}</template>',
17+
],
18+
19+
invalid: [
20+
{
21+
code: '<template>{{this.this.value}}</template>',
22+
output: '<template>{{this.value}}</template>',
23+
errors: [{ messageId: 'noChainedThis' }],
24+
},
25+
{
26+
code: '<template>{{helper value=this.this.foo}}</template>',
27+
output: '<template>{{helper value=this.foo}}</template>',
28+
errors: [{ messageId: 'noChainedThis' }],
29+
},
30+
{
31+
code: '<template>{{#if this.this.condition}}true{{/if}}</template>',
32+
output: '<template>{{#if this.condition}}true{{/if}}</template>',
33+
errors: [{ messageId: 'noChainedThis' }],
34+
},
35+
{
36+
code: '<template><this.this.Component /></template>',
37+
output: '<template><this.Component /></template>',
38+
errors: [{ messageId: 'noChainedThis' }],
39+
},
40+
{
41+
code: '<template>{{#this.this.value}}woo{{/this.this.value}}</template>',
42+
output: '<template>{{#this.value}}woo{{/this.value}}</template>',
43+
errors: [{ messageId: 'noChainedThis' }],
44+
},
45+
{
46+
code: '<template>{{component this.this.dynamicComponent}}</template>',
47+
output: '<template>{{component this.dynamicComponent}}</template>',
48+
errors: [{ messageId: 'noChainedThis' }],
49+
},
50+
],
51+
});

0 commit comments

Comments
 (0)