Skip to content

Commit b430b92

Browse files
Merge pull request #2575 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-this-in-template-only-components
Extract rule: template-no-this-in-template-only-components
2 parents 9ba83ed + 8f4b7b4 commit b430b92

4 files changed

Lines changed: 214 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,7 @@ rules in templates can be disabled with eslint directives with mustache or html
262262
| [template-no-positional-data-test-selectors](docs/rules/template-no-positional-data-test-selectors.md) | disallow positional data-test-* params in curly invocations | | | |
263263
| [template-no-potential-path-strings](docs/rules/template-no-potential-path-strings.md) | disallow potential path strings in attribute values | | | |
264264
| [template-no-splattributes-with-class](docs/rules/template-no-splattributes-with-class.md) | disallow splattributes with class attribute | | | |
265+
| [template-no-this-in-template-only-components](docs/rules/template-no-this-in-template-only-components.md) | disallow this in template-only components (gjs/gts) | | 🔧 | |
265266
| [template-no-trailing-spaces](docs/rules/template-no-trailing-spaces.md) | disallow trailing whitespace at the end of lines in templates | | 🔧 | |
266267
| [template-no-unavailable-this](docs/rules/template-no-unavailable-this.md) | disallow `this` in templates that are not inside a class or function | | | |
267268
| [template-no-unnecessary-component-helper](docs/rules/template-no-unnecessary-component-helper.md) | disallow unnecessary component helper | | 🔧 | |
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# ember/template-no-this-in-template-only-components
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+
There is no `this` context in template-only components.
8+
9+
## Examples
10+
11+
This rule **forbids** `this` in template-only components:
12+
13+
```hbs
14+
<h1>Hello {{this.name}}!</h1>
15+
```
16+
17+
The `--fix` option will convert to named arguments:
18+
19+
```hbs
20+
<h1>Hello {{@name}}!</h1>
21+
```
22+
23+
## Migration
24+
25+
- use [ember-no-implicit-this-codemod](https://github.com/ember-codemods/ember-no-implicit-this-codemod)
26+
- [upgrade to Glimmer components](https://guides.emberjs.com/release/upgrading/current-edition/glimmer-components/), which don't allow ambiguous access
27+
- classic components have [auto-reflection](https://github.com/emberjs/rfcs/blob/master/text/0276-named-args.md#motivation), and can use `this.myArgName` or `this.args.myArgNme` or `@myArgName` interchangeably
28+
29+
## References
30+
31+
- [Glimmer components](https://guides.emberjs.com/release/upgrading/current-edition/glimmer-components/)
32+
- [rfcs/named args](https://github.com/emberjs/rfcs/blob/master/text/0276-named-args.md#motivation)
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
meta: {
4+
type: 'suggestion',
5+
docs: {
6+
description: 'disallow this in template-only components (gjs/gts)',
7+
category: 'Best Practices',
8+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-this-in-template-only-components.md',
9+
templateMode: 'both',
10+
},
11+
fixable: 'code',
12+
schema: [],
13+
messages: {
14+
noThis:
15+
"Usage of 'this' in path '{{original}}' is not allowed in a template-only component. Use '{{fixed}}' if it is a named argument.",
16+
},
17+
originallyFrom: {
18+
name: 'ember-template-lint',
19+
rule: 'lib/rules/no-this-in-template-only-components.js',
20+
docs: 'docs/rule/no-this-in-template-only-components.md',
21+
tests: 'test/unit/rules/no-this-in-template-only-components-test.js',
22+
},
23+
},
24+
create(context) {
25+
// Properties that should not be auto-fixed (built-in component properties)
26+
const BUILTIN_PROPERTIES = new Set([
27+
'action',
28+
'element',
29+
'parentView',
30+
'attrs',
31+
'elementId',
32+
'tagName',
33+
'ariaRole',
34+
'class',
35+
'classNames',
36+
'classNameBindings',
37+
'attributeBindings',
38+
'isVisible',
39+
'isDestroying',
40+
'isDestroyed',
41+
]);
42+
43+
return {
44+
GlimmerPathExpression(node) {
45+
// Only flag template-only components, not class components.
46+
// Walk ancestors to check if the <template> is inside a class body.
47+
const sourceCode = context.sourceCode ?? context.getSourceCode();
48+
const ancestors = sourceCode.getAncestors
49+
? sourceCode.getAncestors(node)
50+
: context.getAncestors();
51+
const isInsideClass = ancestors.some(
52+
(ancestor) => ancestor.type === 'ClassBody' || ancestor.type === 'ClassDeclaration'
53+
);
54+
if (isInsideClass) {
55+
return;
56+
}
57+
58+
// In gjs/gts files with <template> tags, check for this.* usage
59+
if (node.head?.type === 'ThisHead' && node.tail?.length > 0) {
60+
const original = node.original;
61+
const firstPart = node.tail[0];
62+
const fixed = `@${node.tail.join('.')}`;
63+
const canFix = !BUILTIN_PROPERTIES.has(firstPart);
64+
65+
context.report({
66+
node,
67+
messageId: 'noThis',
68+
data: { original, fixed },
69+
fix: canFix ? (fixer) => fixer.replaceText(node, fixed) : undefined,
70+
});
71+
}
72+
},
73+
};
74+
},
75+
};
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
const rule = require('../../../lib/rules/template-no-this-in-template-only-components');
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+
ruleTester.run('template-no-this-in-template-only-components', rule, {
9+
valid: [
10+
'<template>{{@foo}}</template>',
11+
'<template>{{welcome-page}}</template>',
12+
'<template><WelcomePage /></template>',
13+
'<template><MyComponent @prop={{can "edit" @model}} /></template>',
14+
'<template>{{my-component model=model}}</template>',
15+
// Class components should not be flagged (not template-only)
16+
'class MyComponent extends Component { <template>{{this.foo}}</template> }',
17+
'class MyComponent extends Component { <template>{{this.bar}} {{this.baz}}</template> }',
18+
],
19+
invalid: [
20+
{
21+
code: '<template>{{this.foo}}</template>',
22+
output: '<template>{{@foo}}</template>',
23+
errors: [{ messageId: 'noThis' }],
24+
},
25+
26+
{
27+
code: '<template>{{my-component model=this.model}}</template>',
28+
output: '<template>{{my-component model=@model}}</template>',
29+
errors: [{ messageId: 'noThis' }],
30+
},
31+
{
32+
code: '<template>{{my-component action=(action this.myAction)}}</template>',
33+
output: '<template>{{my-component action=(action @myAction)}}</template>',
34+
errors: [{ messageId: 'noThis' }],
35+
},
36+
{
37+
code: '<template><MyComponent @prop={{can "edit" this.model}} /></template>',
38+
output: '<template><MyComponent @prop={{can "edit" @model}} /></template>',
39+
errors: [{ messageId: 'noThis' }],
40+
},
41+
{
42+
code: '<template>{{input id=(concat this.elementId "-username")}}</template>',
43+
output: null,
44+
errors: [{ messageId: 'noThis' }],
45+
},
46+
],
47+
});
48+
49+
const hbsRuleTester = new RuleTester({
50+
parser: require.resolve('ember-eslint-parser/hbs'),
51+
parserOptions: {
52+
ecmaVersion: 2022,
53+
sourceType: 'module',
54+
},
55+
});
56+
57+
hbsRuleTester.run('template-no-this-in-template-only-components', rule, {
58+
valid: [
59+
'{{welcome-page}}',
60+
'<WelcomePage />',
61+
'<MyComponent @prop={{can "edit" @model}} />',
62+
'{{my-component model=model}}',
63+
],
64+
invalid: [
65+
{
66+
code: '{{my-component model=this.model}}',
67+
output: '{{my-component model=@model}}',
68+
errors: [
69+
{
70+
message:
71+
"Usage of 'this' in path 'this.model' is not allowed in a template-only component. Use '@model' if it is a named argument.",
72+
},
73+
],
74+
},
75+
{
76+
code: '{{my-component action=(action this.myAction)}}',
77+
output: '{{my-component action=(action @myAction)}}',
78+
errors: [
79+
{
80+
message:
81+
"Usage of 'this' in path 'this.myAction' is not allowed in a template-only component. Use '@myAction' if it is a named argument.",
82+
},
83+
],
84+
},
85+
{
86+
code: '<MyComponent @prop={{can "edit" this.model}} />',
87+
output: '<MyComponent @prop={{can "edit" @model}} />',
88+
errors: [
89+
{
90+
message:
91+
"Usage of 'this' in path 'this.model' is not allowed in a template-only component. Use '@model' if it is a named argument.",
92+
},
93+
],
94+
},
95+
{
96+
code: '{{input id=(concat this.elementId "-username")}}',
97+
output: null,
98+
errors: [
99+
{
100+
message:
101+
"Usage of 'this' in path 'this.elementId' is not allowed in a template-only component. Use '@elementId' if it is a named argument.",
102+
},
103+
],
104+
},
105+
],
106+
});

0 commit comments

Comments
 (0)