Skip to content

Commit 71ca1a4

Browse files
Merge pull request #2439 from NullVoxPopuli/nvp/template-lint-extract-rule-template-no-index-component-invocation
Extract rule: template-no-index-component-invocation
2 parents a81d6f8 + c7c0177 commit 71ca1a4

4 files changed

Lines changed: 334 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,7 @@ rules in templates can be disabled with eslint directives with mustache or html
211211
| [template-no-dynamic-subexpression-invocations](docs/rules/template-no-dynamic-subexpression-invocations.md) | disallow dynamic subexpression invocations | | | |
212212
| [template-no-element-event-actions](docs/rules/template-no-element-event-actions.md) | disallow element event actions (use {{on}} modifier instead) | | | |
213213
| [template-no-implicit-this](docs/rules/template-no-implicit-this.md) | require explicit `this` in property access | | | |
214+
| [template-no-index-component-invocation](docs/rules/template-no-index-component-invocation.md) | disallow index component invocations | | | |
214215
| [template-no-inline-event-handlers](docs/rules/template-no-inline-event-handlers.md) | disallow DOM event handler attributes | | | |
215216
| [template-no-inline-styles](docs/rules/template-no-inline-styles.md) | disallow inline styles | | | |
216217
| [template-no-input-block](docs/rules/template-no-input-block.md) | disallow block usage of {{input}} helper | | | |
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# ember/template-no-index-component-invocation
2+
3+
<!-- end auto-generated rule header -->
4+
5+
Disallows invoking components using an explicit `/index` or `::Index` suffix.
6+
7+
Components and Component Templates can be structured as `app/components/foo-bar/index.js` and
8+
`app/components/foo-bar/index.hbs`. This allows additional files related to the
9+
component (such as a `README.md` file) to be co-located on the filesystem.
10+
11+
For template-only components, they can be either `app/components/foo-bar.hbs`
12+
or `app/components/foo-bar/index.hbs` without a corresponding JavaScript file.
13+
14+
Similarly, for addons, templates can be placed inside `addon/components` with
15+
the same rules laid out above.
16+
17+
In all of these case, if a template file is present in `app/components` or
18+
`addon/components`, it will take precedence over any corresponding template
19+
files in `app/templates`, the `layout` property on classic components, or a
20+
template with the same name that is made available with the resolver API.
21+
Instead of being resolved at runtime, a template in `app/components` will be
22+
associated with the component's JavaScript class at build time.
23+
24+
## Examples
25+
26+
This rule **forbids** the following:
27+
28+
```gjs
29+
<template><Foo::Index /></template>
30+
```
31+
32+
```gjs
33+
<template>{{component 'foo/index'}}</template>
34+
```
35+
36+
```gjs
37+
<template>{{foo/index}}</template>
38+
```
39+
40+
This rule **allows** the following:
41+
42+
```gjs
43+
<template><Foo /></template>
44+
```
45+
46+
```gjs
47+
<template>{{component 'foo'}}</template>
48+
```
49+
50+
```gjs
51+
<template>{{foo}}</template>
52+
```
53+
54+
## Migration
55+
56+
- replace all `::Index>` to `>`
57+
- replace all `/index}}` to `}}`
58+
59+
## References
60+
61+
- [RFC #481](https://github.com/emberjs/rfcs/blob/master/text/0481-component-templates-co-location.md#high-level-design)
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
/* eslint-disable complexity, eslint-plugin/prefer-placeholders, unicorn/explicit-length-check */
2+
/** @type {import('eslint').Rule.RuleModule} */
3+
module.exports = {
4+
meta: {
5+
type: 'suggestion',
6+
docs: {
7+
description: 'disallow index component invocations',
8+
category: 'Best Practices',
9+
recommended: false,
10+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-index-component-invocation.md',
11+
templateMode: 'both',
12+
},
13+
fixable: null,
14+
schema: [],
15+
messages: {},
16+
originallyFrom: {
17+
name: 'ember-template-lint',
18+
rule: 'lib/rules/no-index-component-invocation.js',
19+
docs: 'docs/rule/no-index-component-invocation.md',
20+
tests: 'test/unit/rules/no-index-component-invocation-test.js',
21+
},
22+
},
23+
24+
create(context) {
25+
function lintIndexUsage(node) {
26+
// Handle angle bracket components: <Foo::Index />
27+
if (node.type === 'GlimmerElementNode') {
28+
if (node.tag && node.tag.endsWith('::Index')) {
29+
const invocation = `<${node.tag}`;
30+
const replacement = `<${node.tag.replace('::Index', '')}`;
31+
32+
context.report({
33+
node,
34+
message: `Replace \`${invocation} ...\` to \`${replacement} ...\``,
35+
});
36+
}
37+
return;
38+
}
39+
40+
// Handle mustache and block statements: {{foo/index}} or {{#foo/index}}
41+
if (node.type === 'GlimmerMustacheStatement' || node.type === 'GlimmerBlockStatement') {
42+
if (
43+
node.path &&
44+
node.path.type === 'GlimmerPathExpression' &&
45+
node.path.original &&
46+
node.path.original.endsWith('/index')
47+
) {
48+
const invocationPrefix = node.type === 'GlimmerBlockStatement' ? '{{#' : '{{';
49+
const invocation = `${invocationPrefix}${node.path.original}`;
50+
const replacement = `${invocationPrefix}${node.path.original.replace('/index', '')}`;
51+
52+
context.report({
53+
node: node.path,
54+
message: `Replace \`${invocation} ...\` to \`${replacement} ...\``,
55+
});
56+
return;
57+
}
58+
}
59+
60+
// Handle component helper: {{component "foo/index"}} or (component "foo/index")
61+
if (
62+
node.type === 'GlimmerMustacheStatement' ||
63+
node.type === 'GlimmerBlockStatement' ||
64+
node.type === 'GlimmerSubExpression'
65+
) {
66+
const prefix =
67+
node.type === 'GlimmerMustacheStatement'
68+
? '{{'
69+
: node.type === 'GlimmerBlockStatement'
70+
? '{{#'
71+
: '(';
72+
73+
if (
74+
node.path &&
75+
node.path.type === 'GlimmerPathExpression' &&
76+
node.path.original === 'component' &&
77+
node.params &&
78+
node.params.length > 0 &&
79+
node.params[0].type === 'GlimmerStringLiteral'
80+
) {
81+
const componentName = node.params[0].value;
82+
83+
if (componentName.endsWith('/index')) {
84+
const invocation = `${prefix}component "${componentName}"`;
85+
const replacement = `${prefix}component "${componentName.replace('/index', '')}"`;
86+
87+
context.report({
88+
node: node.params[0],
89+
message: `Replace \`${invocation} ...\` to \`${replacement} ...\``,
90+
});
91+
}
92+
}
93+
}
94+
}
95+
96+
return {
97+
GlimmerElementNode: lintIndexUsage,
98+
GlimmerMustacheStatement: lintIndexUsage,
99+
GlimmerBlockStatement: lintIndexUsage,
100+
GlimmerSubExpression: lintIndexUsage,
101+
};
102+
},
103+
};
104+
/* eslint-enable complexity, eslint-plugin/prefer-placeholders, unicorn/explicit-length-check */
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
const rule = require('../../../lib/rules/template-no-index-component-invocation');
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-index-component-invocation', rule, {
10+
valid: [
11+
'<template><Foo::Bar /></template>',
12+
'<template><Foo::IndexItem /></template>',
13+
'<template><Foo::MyIndex /></template>',
14+
'<template><Foo::MyIndex></Foo::MyIndex></template>',
15+
'<template>{{foo/index-item}}</template>',
16+
'<template>{{foo/my-index}}</template>',
17+
'<template>{{foo/bar}}</template>',
18+
'<template>{{#foo/bar}}{{/foo/bar}}</template>',
19+
'<template>{{component "foo/bar"}}</template>',
20+
'<template>{{component "foo/my-index"}}</template>',
21+
'<template>{{component "foo/index-item"}}</template>',
22+
'<template>{{#component "foo/index-item"}}{{/component}}</template>',
23+
],
24+
invalid: [
25+
{
26+
code: '<template>{{foo/index}}</template>',
27+
output: null,
28+
errors: [
29+
{
30+
message: 'Replace `{{foo/index ...` to `{{foo ...`',
31+
},
32+
],
33+
},
34+
{
35+
code: '<template>{{component "foo/index"}}</template>',
36+
output: null,
37+
errors: [
38+
{
39+
message: 'Replace `{{component "foo/index" ...` to `{{component "foo" ...`',
40+
},
41+
],
42+
},
43+
{
44+
code: '<template>{{#foo/index}}{{/foo/index}}</template>',
45+
output: null,
46+
errors: [
47+
{
48+
message: 'Replace `{{#foo/index ...` to `{{#foo ...`',
49+
},
50+
],
51+
},
52+
{
53+
code: '<template>{{#component "foo/index"}}{{/component}}</template>',
54+
output: null,
55+
errors: [
56+
{
57+
message: 'Replace `{{#component "foo/index" ...` to `{{#component "foo" ...`',
58+
},
59+
],
60+
},
61+
{
62+
code: '<template><Foo::Index /></template>',
63+
output: null,
64+
errors: [
65+
{
66+
message: 'Replace `<Foo::Index ...` to `<Foo ...`',
67+
},
68+
],
69+
},
70+
{
71+
code: '<template><Foo::Index></Foo::Index></template>',
72+
output: null,
73+
errors: [
74+
{
75+
message: 'Replace `<Foo::Index ...` to `<Foo ...`',
76+
},
77+
],
78+
},
79+
80+
{
81+
code: '<template>{{foo/bar (component "foo/index")}}</template>',
82+
output: null,
83+
errors: [{ message: 'Replace `(component "foo/index" ...` to `(component "foo" ...`' }],
84+
},
85+
{
86+
code: '<template>{{foo/bar name=(component "foo/index")}}</template>',
87+
output: null,
88+
errors: [{ message: 'Replace `(component "foo/index" ...` to `(component "foo" ...`' }],
89+
},
90+
{
91+
code: '<template><Foo::Bar::Index /></template>',
92+
output: null,
93+
errors: [{ message: 'Replace `<Foo::Bar::Index ...` to `<Foo::Bar ...`' }],
94+
},
95+
],
96+
});
97+
98+
const hbsRuleTester = new RuleTester({
99+
parser: require.resolve('ember-eslint-parser/hbs'),
100+
parserOptions: {
101+
ecmaVersion: 2022,
102+
sourceType: 'module',
103+
},
104+
});
105+
106+
hbsRuleTester.run('template-no-index-component-invocation', rule, {
107+
valid: [
108+
'<Foo::Bar />',
109+
'<Foo::IndexItem />',
110+
'<Foo::MyIndex />',
111+
'<Foo::MyIndex></Foo::MyIndex>',
112+
'{{foo/index-item}}',
113+
'{{foo/my-index}}',
114+
'{{foo/bar}}',
115+
'{{#foo/bar}}{{/foo/bar}}',
116+
'{{component "foo/bar"}}',
117+
'{{component "foo/my-index"}}',
118+
'{{component "foo/index-item"}}',
119+
'{{#component "foo/index-item"}}{{/component}}',
120+
],
121+
invalid: [
122+
{
123+
code: '{{foo/index}}',
124+
output: null,
125+
errors: [{ message: 'Replace `{{foo/index ...` to `{{foo ...`' }],
126+
},
127+
{
128+
code: '{{component "foo/index"}}',
129+
output: null,
130+
errors: [{ message: 'Replace `{{component "foo/index" ...` to `{{component "foo" ...`' }],
131+
},
132+
{
133+
code: '{{#foo/index}}{{/foo/index}}',
134+
output: null,
135+
errors: [{ message: 'Replace `{{#foo/index ...` to `{{#foo ...`' }],
136+
},
137+
{
138+
code: '{{#component "foo/index"}}{{/component}}',
139+
output: null,
140+
errors: [{ message: 'Replace `{{#component "foo/index" ...` to `{{#component "foo" ...`' }],
141+
},
142+
{
143+
code: '{{foo/bar (component "foo/index")}}',
144+
output: null,
145+
errors: [{ message: 'Replace `(component "foo/index" ...` to `(component "foo" ...`' }],
146+
},
147+
{
148+
code: '{{foo/bar name=(component "foo/index")}}',
149+
output: null,
150+
errors: [{ message: 'Replace `(component "foo/index" ...` to `(component "foo" ...`' }],
151+
},
152+
{
153+
code: '<Foo::Index />',
154+
output: null,
155+
errors: [{ message: 'Replace `<Foo::Index ...` to `<Foo ...`' }],
156+
},
157+
{
158+
code: '<Foo::Bar::Index />',
159+
output: null,
160+
errors: [{ message: 'Replace `<Foo::Bar::Index ...` to `<Foo::Bar ...`' }],
161+
},
162+
{
163+
code: '<Foo::Index></Foo::Index>',
164+
output: null,
165+
errors: [{ message: 'Replace `<Foo::Index ...` to `<Foo ...`' }],
166+
},
167+
],
168+
});

0 commit comments

Comments
 (0)