Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -262,6 +262,7 @@ rules in templates can be disabled with eslint directives with mustache or html
| [template-no-positional-data-test-selectors](docs/rules/template-no-positional-data-test-selectors.md) | disallow positional data-test-* params in curly invocations | | | |
| [template-no-potential-path-strings](docs/rules/template-no-potential-path-strings.md) | disallow potential path strings in attribute values | | | |
| [template-no-splattributes-with-class](docs/rules/template-no-splattributes-with-class.md) | disallow splattributes with class attribute | | | |
| [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) | | 🔧 | |
| [template-no-trailing-spaces](docs/rules/template-no-trailing-spaces.md) | disallow trailing whitespace at the end of lines in templates | | 🔧 | |
| [template-no-unavailable-this](docs/rules/template-no-unavailable-this.md) | disallow `this` in templates that are not inside a class or function | | | |
| [template-no-unnecessary-component-helper](docs/rules/template-no-unnecessary-component-helper.md) | disallow unnecessary component helper | | 🔧 | |
Expand Down
32 changes: 32 additions & 0 deletions docs/rules/template-no-this-in-template-only-components.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# ember/template-no-this-in-template-only-components

🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).

<!-- end auto-generated rule header -->

There is no `this` context in template-only components.

## Examples

This rule **forbids** `this` in template-only components:

```hbs
<h1>Hello {{this.name}}!</h1>
```

The `--fix` option will convert to named arguments:

```hbs
<h1>Hello {{@name}}!</h1>
```

## Migration

- use [ember-no-implicit-this-codemod](https://github.com/ember-codemods/ember-no-implicit-this-codemod)
- [upgrade to Glimmer components](https://guides.emberjs.com/release/upgrading/current-edition/glimmer-components/), which don't allow ambiguous access
- 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

## References

- [Glimmer components](https://guides.emberjs.com/release/upgrading/current-edition/glimmer-components/)
- [rfcs/named args](https://github.com/emberjs/rfcs/blob/master/text/0276-named-args.md#motivation)
75 changes: 75 additions & 0 deletions lib/rules/template-no-this-in-template-only-components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/** @type {import('eslint').Rule.RuleModule} */
module.exports = {
meta: {
type: 'suggestion',
docs: {
description: 'disallow this in template-only components (gjs/gts)',
category: 'Best Practices',
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-this-in-template-only-components.md',
templateMode: 'both',
},
fixable: 'code',
schema: [],
messages: {
noThis:
"Usage of 'this' in path '{{original}}' is not allowed in a template-only component. Use '{{fixed}}' if it is a named argument.",
},
originallyFrom: {
name: 'ember-template-lint',
rule: 'lib/rules/no-this-in-template-only-components.js',
docs: 'docs/rule/no-this-in-template-only-components.md',
tests: 'test/unit/rules/no-this-in-template-only-components-test.js',
},
},
create(context) {
// Properties that should not be auto-fixed (built-in component properties)
const BUILTIN_PROPERTIES = new Set([
'action',
'element',
'parentView',
'attrs',
'elementId',
'tagName',
'ariaRole',
'class',
'classNames',
'classNameBindings',
'attributeBindings',
'isVisible',
'isDestroying',
'isDestroyed',
]);

return {
GlimmerPathExpression(node) {
// Only flag template-only components, not class components.
// Walk ancestors to check if the <template> is inside a class body.
const sourceCode = context.sourceCode ?? context.getSourceCode();
const ancestors = sourceCode.getAncestors
? sourceCode.getAncestors(node)
: context.getAncestors();
const isInsideClass = ancestors.some(
(ancestor) => ancestor.type === 'ClassBody' || ancestor.type === 'ClassDeclaration'
);
if (isInsideClass) {
return;
}

// In gjs/gts files with <template> tags, check for this.* usage
if (node.head?.type === 'ThisHead' && node.tail?.length > 0) {
const original = node.original;
const firstPart = node.tail[0];
const fixed = `@${node.tail.join('.')}`;
const canFix = !BUILTIN_PROPERTIES.has(firstPart);

context.report({
node,
messageId: 'noThis',
data: { original, fixed },
fix: canFix ? (fixer) => fixer.replaceText(node, fixed) : undefined,
});
}
},
};
},
};
106 changes: 106 additions & 0 deletions tests/lib/rules/template-no-this-in-template-only-components.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const rule = require('../../../lib/rules/template-no-this-in-template-only-components');
const RuleTester = require('eslint').RuleTester;

const ruleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser'),
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
});
ruleTester.run('template-no-this-in-template-only-components', rule, {
valid: [
'<template>{{@foo}}</template>',
'<template>{{welcome-page}}</template>',
'<template><WelcomePage /></template>',
'<template><MyComponent @prop={{can "edit" @model}} /></template>',
'<template>{{my-component model=model}}</template>',
// Class components should not be flagged (not template-only)
'class MyComponent extends Component { <template>{{this.foo}}</template> }',
'class MyComponent extends Component { <template>{{this.bar}} {{this.baz}}</template> }',
],
invalid: [
{
code: '<template>{{this.foo}}</template>',
output: '<template>{{@foo}}</template>',
errors: [{ messageId: 'noThis' }],
},

{
code: '<template>{{my-component model=this.model}}</template>',
output: '<template>{{my-component model=@model}}</template>',
errors: [{ messageId: 'noThis' }],
},
{
code: '<template>{{my-component action=(action this.myAction)}}</template>',
output: '<template>{{my-component action=(action @myAction)}}</template>',
errors: [{ messageId: 'noThis' }],
},
{
code: '<template><MyComponent @prop={{can "edit" this.model}} /></template>',
output: '<template><MyComponent @prop={{can "edit" @model}} /></template>',
errors: [{ messageId: 'noThis' }],
},
{
code: '<template>{{input id=(concat this.elementId "-username")}}</template>',
output: null,
errors: [{ messageId: 'noThis' }],
},
],
});

const hbsRuleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser/hbs'),
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
});

hbsRuleTester.run('template-no-this-in-template-only-components', rule, {
valid: [
'{{welcome-page}}',
'<WelcomePage />',
'<MyComponent @prop={{can "edit" @model}} />',
'{{my-component model=model}}',
],
invalid: [
{
code: '{{my-component model=this.model}}',
output: '{{my-component model=@model}}',
errors: [
{
message:
"Usage of 'this' in path 'this.model' is not allowed in a template-only component. Use '@model' if it is a named argument.",
},
],
},
{
code: '{{my-component action=(action this.myAction)}}',
output: '{{my-component action=(action @myAction)}}',
errors: [
{
message:
"Usage of 'this' in path 'this.myAction' is not allowed in a template-only component. Use '@myAction' if it is a named argument.",
},
],
},
{
code: '<MyComponent @prop={{can "edit" this.model}} />',
output: '<MyComponent @prop={{can "edit" @model}} />',
errors: [
{
message:
"Usage of 'this' in path 'this.model' is not allowed in a template-only component. Use '@model' if it is a named argument.",
},
],
},
{
code: '{{input id=(concat this.elementId "-username")}}',
output: null,
errors: [
{
message:
"Usage of 'this' in path 'this.elementId' is not allowed in a template-only component. Use '@elementId' if it is a named argument.",
},
],
},
],
});
Loading