Skip to content

Commit 863b040

Browse files
committed
Extract rule: template-no-curly-component-invocation
1 parent a15aa2a commit 863b040

4 files changed

Lines changed: 582 additions & 16 deletions

File tree

README.md

Lines changed: 17 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -192,22 +192,23 @@ rules in templates can be disabled with eslint directives with mustache or html
192192

193193
### Best Practices
194194

195-
| Name                                       | Description | 💼 | 🔧 | 💡 |
196-
| :----------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------- | :- | :- | :- |
197-
| [template-builtin-component-arguments](docs/rules/template-builtin-component-arguments.md) | disallow setting certain attributes on builtin components | | | |
198-
| [template-no-action-modifiers](docs/rules/template-no-action-modifiers.md) | disallow usage of {{action}} modifiers | | | |
199-
| [template-no-arguments-for-html-elements](docs/rules/template-no-arguments-for-html-elements.md) | disallow @arguments on HTML elements | | | |
200-
| [template-no-array-prototype-extensions](docs/rules/template-no-array-prototype-extensions.md) | disallow usage of Ember Array prototype extensions | | | |
201-
| [template-no-block-params-for-html-elements](docs/rules/template-no-block-params-for-html-elements.md) | disallow block params on HTML elements | | | |
202-
| [template-no-capital-arguments](docs/rules/template-no-capital-arguments.md) | disallow capital arguments (use lowercase @arg instead of @Arg) | | | |
203-
| [template-no-chained-this](docs/rules/template-no-chained-this.md) | disallow redundant `this.this` in templates | | 🔧 | |
204-
| [template-no-debugger](docs/rules/template-no-debugger.md) | disallow {{debugger}} in templates | | | |
205-
| [template-no-element-event-actions](docs/rules/template-no-element-event-actions.md) | disallow element event actions (use {{on}} modifier instead) | | | |
206-
| [template-no-inline-event-handlers](docs/rules/template-no-inline-event-handlers.md) | disallow DOM event handler attributes | | | |
207-
| [template-no-inline-styles](docs/rules/template-no-inline-styles.md) | disallow inline styles | | | |
208-
| [template-no-input-placeholder](docs/rules/template-no-input-placeholder.md) | disallow placeholder attribute on input elements | | | |
209-
| [template-no-input-tagname](docs/rules/template-no-input-tagname.md) | disallow tagName attribute on {{input}} helper | | | |
210-
| [template-no-log](docs/rules/template-no-log.md) | disallow {{log}} in templates | | | |
195+
| Name                                       | Description | 💼 | 🔧 | 💡 |
196+
| :----------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------- | :- | :- | :- |
197+
| [template-builtin-component-arguments](docs/rules/template-builtin-component-arguments.md) | disallow setting certain attributes on builtin components | | | |
198+
| [template-no-action-modifiers](docs/rules/template-no-action-modifiers.md) | disallow usage of {{action}} modifiers | | | |
199+
| [template-no-arguments-for-html-elements](docs/rules/template-no-arguments-for-html-elements.md) | disallow @arguments on HTML elements | | | |
200+
| [template-no-array-prototype-extensions](docs/rules/template-no-array-prototype-extensions.md) | disallow usage of Ember Array prototype extensions | | | |
201+
| [template-no-block-params-for-html-elements](docs/rules/template-no-block-params-for-html-elements.md) | disallow block params on HTML elements | | | |
202+
| [template-no-capital-arguments](docs/rules/template-no-capital-arguments.md) | disallow capital arguments (use lowercase @arg instead of @Arg) | | | |
203+
| [template-no-chained-this](docs/rules/template-no-chained-this.md) | disallow redundant `this.this` in templates | | 🔧 | |
204+
| [template-no-curly-component-invocation](docs/rules/template-no-curly-component-invocation.md) | disallow curly component invocation, use angle bracket syntax instead | | | |
205+
| [template-no-debugger](docs/rules/template-no-debugger.md) | disallow {{debugger}} in templates | | | |
206+
| [template-no-element-event-actions](docs/rules/template-no-element-event-actions.md) | disallow element event actions (use {{on}} modifier instead) | | | |
207+
| [template-no-inline-event-handlers](docs/rules/template-no-inline-event-handlers.md) | disallow DOM event handler attributes | | | |
208+
| [template-no-inline-styles](docs/rules/template-no-inline-styles.md) | disallow inline styles | | | |
209+
| [template-no-input-placeholder](docs/rules/template-no-input-placeholder.md) | disallow placeholder attribute on input elements | | | |
210+
| [template-no-input-tagname](docs/rules/template-no-input-tagname.md) | disallow tagName attribute on {{input}} helper | | | |
211+
| [template-no-log](docs/rules/template-no-log.md) | disallow {{log}} in templates | | | |
211212

212213
### Components
213214

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
# ember/template-no-curly-component-invocation
2+
3+
<!-- end auto-generated rule header -->
4+
5+
> **HBS Only**: This rule applies to classic `.hbs` template files only (loose mode). It is not relevant for `gjs`/`gts` files (strict mode), where these patterns cannot occur.
6+
7+
Disallows curly component invocation syntax. Use angle bracket syntax instead.
8+
9+
There are two ways to invoke a component in a template: curly component syntax
10+
(`{{my-component}}`), and angle bracket syntax (`<MyComponent />`). The
11+
difference between them is syntactical. You should favour angle bracket syntax
12+
as it improves readability of templates, i.e. disambiguates components from
13+
helpers, and is also the future direction Ember is going with the Octane
14+
Edition.
15+
16+
This rule checks all the curly braces in your app and warns about those that
17+
look like they could be component invocations.
18+
19+
## Examples
20+
21+
This rule **forbids** the following:
22+
23+
```hbs
24+
{{foo-bar}}
25+
```
26+
27+
```hbs
28+
{{nested/component}}
29+
```
30+
31+
```hbs
32+
{{#foo-bar}}content{{/foo-bar}}
33+
```
34+
35+
This rule **allows** the following:
36+
37+
```hbs
38+
{{foo bar}}
39+
```
40+
41+
```hbs
42+
<FooBar />
43+
```
44+
45+
```hbs
46+
<Nested::Component />
47+
```
48+
49+
## Migration
50+
51+
- use <https://github.com/ember-codemods/ember-angle-brackets-codemod>
52+
53+
## Configuration
54+
55+
This rule accepts an options object with the following properties:
56+
57+
- `allow` (default: `[]`) - Array of component names to allow in curly syntax
58+
- `disallow` (default: `[]`) - Array of component names to disallow in curly syntax
59+
- `requireDash` (default: `false`) - Require dashes in component names
60+
- `noImplicitThis` (default: `true`) - Don't allow implicit `this` references
61+
62+
```js
63+
// .eslintrc.js
64+
module.exports = {
65+
rules: {
66+
'ember/template-no-curly-component-invocation': [
67+
'error',
68+
{
69+
allow: ['some-helper'],
70+
disallow: [],
71+
},
72+
],
73+
},
74+
};
75+
```
76+
77+
## References
78+
79+
- [Ember Guides - Angle Bracket Syntax](https://guides.emberjs.com/release/components/template-syntax/#toc_angle-bracket-syntax)
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
/* eslint-disable eslint-plugin/prefer-placeholders */
2+
const BUILT_INS = new Set([
3+
'action',
4+
'array',
5+
'component',
6+
'concat',
7+
'debugger',
8+
'each',
9+
'each-in',
10+
'fn',
11+
'get',
12+
'hasBlock',
13+
'has-block',
14+
'has-block-params',
15+
'hash',
16+
'if',
17+
'input',
18+
'let',
19+
'link-to',
20+
'loc',
21+
'log',
22+
'mount',
23+
'mut',
24+
'on',
25+
'outlet',
26+
'partial',
27+
'query-params',
28+
'textarea',
29+
'unbound',
30+
'unique-id',
31+
'unless',
32+
'with',
33+
'-in-element',
34+
'in-element',
35+
'app-version',
36+
'rootURL',
37+
]);
38+
39+
const ALWAYS_CURLY = new Set(['yield']);
40+
41+
function transformTagName(name) {
42+
// Convert kebab-case to PascalCase for angle bracket syntax
43+
const parts = name.split('/');
44+
return parts
45+
.map((part) => {
46+
return part
47+
.split('-')
48+
.map((p) => p.charAt(0).toUpperCase() + p.slice(1))
49+
.join('');
50+
})
51+
.join('::');
52+
}
53+
54+
function parseConfig(config) {
55+
const defaults = {
56+
allow: [],
57+
disallow: [],
58+
requireDash: false,
59+
noImplicitThis: true,
60+
};
61+
62+
if (config === true) {
63+
return defaults;
64+
}
65+
66+
return { ...defaults, ...config };
67+
}
68+
69+
/** @type {import('eslint').Rule.RuleModule} */
70+
module.exports = {
71+
meta: {
72+
type: 'suggestion',
73+
docs: {
74+
description: 'disallow curly component invocation, use angle bracket syntax instead',
75+
category: 'Best Practices',
76+
recommended: false,
77+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-curly-component-invocation.md',
78+
templateMode: 'loose',
79+
},
80+
fixable: null,
81+
schema: [
82+
{
83+
type: 'object',
84+
properties: {
85+
allow: {
86+
type: 'array',
87+
items: { type: 'string' },
88+
},
89+
disallow: {
90+
type: 'array',
91+
items: { type: 'string' },
92+
},
93+
requireDash: {
94+
type: 'boolean',
95+
},
96+
noImplicitThis: {
97+
type: 'boolean',
98+
},
99+
},
100+
additionalProperties: false,
101+
},
102+
],
103+
messages: {},
104+
originallyFrom: {
105+
name: 'ember-template-lint',
106+
rule: 'lib/rules/no-curly-component-invocation.js',
107+
docs: 'docs/rule/no-curly-component-invocation.md',
108+
tests: 'test/unit/rules/no-curly-component-invocation-test.js',
109+
},
110+
},
111+
112+
create(context) {
113+
const config = parseConfig(context.options[0]);
114+
115+
function shouldCheckComponent(pathOriginal) {
116+
// Check if in allow list
117+
if (config.allow.includes(pathOriginal)) {
118+
return false;
119+
}
120+
121+
// Check if in disallow list - always report these
122+
if (config.disallow.includes(pathOriginal)) {
123+
return true;
124+
}
125+
126+
// Always curly - don't report
127+
if (ALWAYS_CURLY.has(pathOriginal)) {
128+
return false;
129+
}
130+
131+
// Built-in helpers - don't report
132+
if (BUILT_INS.has(pathOriginal)) {
133+
return false;
134+
}
135+
136+
// If it looks like a component (has dash or slash), flag it
137+
if (pathOriginal.includes('-') || pathOriginal.includes('/')) {
138+
return true;
139+
}
140+
141+
return false;
142+
}
143+
144+
return {
145+
GlimmerMustacheStatement(node) {
146+
if (!node.path || node.path.type !== 'GlimmerPathExpression') {
147+
return;
148+
}
149+
150+
const pathOriginal = node.path.original;
151+
152+
// Skip if has positional params or hash arguments (can't be converted to angle brackets)
153+
if (
154+
(node.params && node.params.length > 0) ||
155+
(node.hash && node.hash.pairs && node.hash.pairs.length > 0)
156+
) {
157+
return;
158+
}
159+
160+
if (shouldCheckComponent(pathOriginal)) {
161+
const angleBracketName = transformTagName(pathOriginal);
162+
context.report({
163+
node,
164+
message: `You are using the component {{${pathOriginal}}} with curly component syntax. You should use <${angleBracketName}> instead. If it is actually a helper you must manually add it to the 'no-curly-component-invocation' rule configuration, e.g. \`'no-curly-component-invocation': { allow: ['${pathOriginal}'] }\`.`,
165+
});
166+
}
167+
},
168+
169+
GlimmerBlockStatement(node) {
170+
if (!node.path || node.path.type !== 'GlimmerPathExpression') {
171+
return;
172+
}
173+
174+
const pathOriginal = node.path.original;
175+
176+
// Skip if has positional params or hash arguments
177+
if (
178+
(node.params && node.params.length > 0) ||
179+
(node.hash && node.hash.pairs && node.hash.pairs.length > 0)
180+
) {
181+
return;
182+
}
183+
184+
if (shouldCheckComponent(pathOriginal)) {
185+
const angleBracketName = transformTagName(pathOriginal);
186+
context.report({
187+
node,
188+
message: `You are using the component {{#${pathOriginal}}} with curly component syntax. You should use <${angleBracketName}> instead. If it is actually a helper you must manually add it to the 'no-curly-component-invocation' rule configuration, e.g. \`'no-curly-component-invocation': { allow: ['${pathOriginal}'] }\`.`,
189+
});
190+
}
191+
},
192+
};
193+
},
194+
};
195+
/* eslint-enable eslint-plugin/prefer-placeholders */

0 commit comments

Comments
 (0)