Skip to content

Commit d007394

Browse files
committed
Extract rule: template-no-this-in-template-only-components
1 parent 9ba83ed commit d007394

4 files changed

Lines changed: 192 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: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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-tag (`<template>`) components that don't extend a class.
8+
9+
## Rule Details
10+
11+
In gjs/gts files with `<template>` tags, this rule flags all `this.*` path expressions and suggests converting them to named arguments (`@*`). The auto-fix replaces `this.foo` with `@foo` (except for built-in component properties like `elementId`, `tagName`, `ariaRole`, `class`, `classNames`, `classNameBindings`, `attributeBindings`, and `isVisible`, which are reported but not auto-fixed).
12+
13+
## Examples
14+
15+
This rule **forbids** the following:
16+
17+
```gjs
18+
<template><h1>Hello {{this.name}}!</h1></template>
19+
```
20+
21+
This rule **allows** the following:
22+
23+
```gjs
24+
<template><h1>Hello {{@name}}!</h1></template>
25+
```
26+
27+
The `--fix` option will convert to named arguments:
28+
29+
```gjs
30+
<template><h1>Hello {{@name}}!</h1></template>
31+
```
32+
33+
## Migration
34+
35+
- use [ember-no-implicit-this-codemod](https://github.com/ember-codemods/ember-no-implicit-this-codemod)
36+
- [upgrade to Glimmer components](https://guides.emberjs.com/release/upgrading/current-edition/glimmer-components/), which don't allow ambiguous access
37+
- 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
38+
39+
## References
40+
41+
- [Glimmer components](https://guides.emberjs.com/release/upgrading/current-edition/glimmer-components/)
42+
- [rfcs/named args](https://github.com/emberjs/rfcs/blob/master/text/0276-named-args.md#motivation)
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
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+
'elementId',
28+
'tagName',
29+
'ariaRole',
30+
'class',
31+
'classNames',
32+
'classNameBindings',
33+
'attributeBindings',
34+
'isVisible',
35+
]);
36+
37+
return {
38+
GlimmerPathExpression(node) {
39+
// In gjs/gts files with <template> tags, check for this.* usage
40+
if (node.head?.type === 'ThisHead' && node.tail?.length > 0) {
41+
const original = node.original;
42+
const firstPart = node.tail[0];
43+
const fixed = `@${node.tail.join('.')}`;
44+
const canFix = !BUILTIN_PROPERTIES.has(firstPart);
45+
46+
context.report({
47+
node,
48+
messageId: 'noThis',
49+
data: { original, fixed },
50+
fix: canFix ? (fixer) => fixer.replaceText(node, fixed) : undefined,
51+
});
52+
}
53+
},
54+
};
55+
},
56+
};
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
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+
],
16+
invalid: [
17+
{
18+
code: '<template>{{this.foo}}</template>',
19+
output: '<template>{{@foo}}</template>',
20+
errors: [{ messageId: 'noThis' }],
21+
},
22+
23+
{
24+
code: '<template>{{my-component model=this.model}}</template>',
25+
output: '<template>{{my-component model=@model}}</template>',
26+
errors: [{ messageId: 'noThis' }],
27+
},
28+
{
29+
code: '<template>{{my-component action=(action this.myAction)}}</template>',
30+
output: '<template>{{my-component action=(action @myAction)}}</template>',
31+
errors: [{ messageId: 'noThis' }],
32+
},
33+
{
34+
code: '<template><MyComponent @prop={{can "edit" this.model}} /></template>',
35+
output: '<template><MyComponent @prop={{can "edit" @model}} /></template>',
36+
errors: [{ messageId: 'noThis' }],
37+
},
38+
{
39+
code: '<template>{{input id=(concat this.elementId "-username")}}</template>',
40+
output: null,
41+
errors: [{ messageId: 'noThis' }],
42+
},
43+
],
44+
});
45+
46+
const hbsRuleTester = new RuleTester({
47+
parser: require.resolve('ember-eslint-parser/hbs'),
48+
parserOptions: {
49+
ecmaVersion: 2022,
50+
sourceType: 'module',
51+
},
52+
});
53+
54+
hbsRuleTester.run('template-no-this-in-template-only-components', rule, {
55+
valid: [
56+
'{{welcome-page}}',
57+
'<WelcomePage />',
58+
'<MyComponent @prop={{can "edit" @model}} />',
59+
'{{my-component model=model}}',
60+
],
61+
invalid: [
62+
{
63+
code: '{{my-component action=(action this.myAction)}}',
64+
output: '{{my-component action=(action @myAction)}}',
65+
errors: [
66+
{
67+
message:
68+
"Usage of 'this' in path 'this.myAction' is not allowed in a template-only component. Use '@myAction' if it is a named argument.",
69+
},
70+
],
71+
},
72+
{
73+
code: '<MyComponent @prop={{can "edit" this.model}} />',
74+
output: '<MyComponent @prop={{can "edit" @model}} />',
75+
errors: [
76+
{
77+
message:
78+
"Usage of 'this' in path 'this.model' is not allowed in a template-only component. Use '@model' if it is a named argument.",
79+
},
80+
],
81+
},
82+
{
83+
code: '{{input id=(concat this.elementId "-username")}}',
84+
output: null,
85+
errors: [
86+
{
87+
message:
88+
"Usage of 'this' in path 'this.elementId' is not allowed in a template-only component. Use '@elementId' if it is a named argument.",
89+
},
90+
],
91+
},
92+
],
93+
});

0 commit comments

Comments
 (0)