Skip to content

Commit 842d8f7

Browse files
committed
Extract rule: template-no-index-component-invocation
1 parent 0149ef1 commit 842d8f7

4 files changed

Lines changed: 249 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,7 @@ rules in templates can be disabled with eslint directives with mustache or html
199199
| [template-no-capital-arguments](docs/rules/template-no-capital-arguments.md) | disallow capital arguments (use lowercase @arg instead of @Arg) | | | |
200200
| [template-no-debugger](docs/rules/template-no-debugger.md) | disallow {{debugger}} in templates | | | |
201201
| [template-no-element-event-actions](docs/rules/template-no-element-event-actions.md) | disallow element event actions (use {{on}} modifier instead) | | | |
202+
| [template-no-index-component-invocation](docs/rules/template-no-index-component-invocation.md) | disallow index component invocations | | | |
202203
| [template-no-log](docs/rules/template-no-log.md) | disallow {{log}} in templates | | | |
203204

204205
### Components
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
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+
```
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
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: true,
10+
strictGjs: true,
11+
strictGts: true,
12+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-index-component-invocation.md',
13+
},
14+
fixable: null,
15+
schema: [],
16+
messages: {},
17+
},
18+
19+
create(context) {
20+
function lintIndexUsage(node) {
21+
// Handle angle bracket components: <Foo::Index />
22+
if (node.type === 'GlimmerElementNode') {
23+
if (node.tag && node.tag.endsWith('::Index')) {
24+
const invocation = `<${node.tag}`;
25+
const replacement = `<${node.tag.replace('::Index', '')}`;
26+
27+
context.report({
28+
node,
29+
message: `Replace \`${invocation} ...\` to \`${replacement} ...\``,
30+
});
31+
}
32+
return;
33+
}
34+
35+
// Handle mustache and block statements: {{foo/index}} or {{#foo/index}}
36+
if (node.type === 'GlimmerMustacheStatement' || node.type === 'GlimmerBlockStatement') {
37+
if (
38+
node.path &&
39+
node.path.type === 'GlimmerPathExpression' &&
40+
node.path.original &&
41+
node.path.original.endsWith('/index')
42+
) {
43+
const invocationPrefix = node.type === 'GlimmerBlockStatement' ? '{{#' : '{{';
44+
const invocation = `${invocationPrefix}${node.path.original}`;
45+
const replacement = `${invocationPrefix}${node.path.original.replace('/index', '')}`;
46+
47+
context.report({
48+
node: node.path,
49+
message: `Replace \`${invocation} ...\` to \`${replacement} ...\``,
50+
});
51+
return;
52+
}
53+
}
54+
55+
// Handle component helper: {{component "foo/index"}} or (component "foo/index")
56+
if (
57+
node.type === 'GlimmerMustacheStatement' ||
58+
node.type === 'GlimmerBlockStatement' ||
59+
node.type === 'GlimmerSubExpression'
60+
) {
61+
const prefix =
62+
node.type === 'GlimmerMustacheStatement'
63+
? '{{'
64+
: node.type === 'GlimmerBlockStatement'
65+
? '{{#'
66+
: '(';
67+
68+
if (
69+
node.path &&
70+
node.path.type === 'GlimmerPathExpression' &&
71+
node.path.original === 'component' &&
72+
node.params &&
73+
node.params.length > 0 &&
74+
node.params[0].type === 'GlimmerStringLiteral'
75+
) {
76+
const componentName = node.params[0].value;
77+
78+
if (componentName.endsWith('/index')) {
79+
const invocation = `${prefix}component "${componentName}"`;
80+
const replacement = `${prefix}component "${componentName.replace('/index', '')}"`;
81+
82+
context.report({
83+
node: node.params[0],
84+
message: `Replace \`${invocation} ...\` to \`${replacement} ...\``,
85+
});
86+
}
87+
}
88+
}
89+
}
90+
91+
return {
92+
GlimmerElementNode: lintIndexUsage,
93+
GlimmerMustacheStatement: lintIndexUsage,
94+
GlimmerBlockStatement: lintIndexUsage,
95+
GlimmerSubExpression: lintIndexUsage,
96+
};
97+
},
98+
};
99+
/* eslint-enable complexity, eslint-plugin/prefer-placeholders, unicorn/explicit-length-check */
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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+
// Test cases ported from ember-template-lint
81+
{
82+
code: '<template>{{foo/bar (component "foo/index")}}</template>',
83+
output: null,
84+
errors: [{ message: 'Replace `(component "foo/index" ...` to `(component "foo" ...`' }],
85+
},
86+
{
87+
code: '<template>{{foo/bar name=(component "foo/index")}}</template>',
88+
output: null,
89+
errors: [{ message: 'Replace `(component "foo/index" ...` to `(component "foo" ...`' }],
90+
},
91+
{
92+
code: '<template><Foo::Bar::Index /></template>',
93+
output: null,
94+
errors: [{ message: 'Replace `<Foo::Bar::Index ...` to `<Foo::Bar ...`' }],
95+
},
96+
],
97+
});

0 commit comments

Comments
 (0)