Skip to content

Commit b3991f2

Browse files
committed
Extract rule: template-modifier-name-case
1 parent 292c12d commit b3991f2

4 files changed

Lines changed: 336 additions & 0 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ rules in templates can be disabled with eslint directives with mustache or html
403403
| [template-block-indentation](docs/rules/template-block-indentation.md) | enforce consistent indentation for block statements and their children | | | |
404404
| [template-eol-last](docs/rules/template-eol-last.md) | require or disallow newline at the end of template files | | 🔧 | |
405405
| [template-linebreak-style](docs/rules/template-linebreak-style.md) | enforce consistent linebreaks in templates | | 🔧 | |
406+
| [template-modifier-name-case](docs/rules/template-modifier-name-case.md) | require dasherized names for modifiers | | 🔧 | |
406407
| [template-no-only-default-slot](docs/rules/template-no-only-default-slot.md) | disallow using only the default slot | | 🔧 | |
407408

408409
### Testing
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
# ember/template-modifier-name-case
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+
Requires dasherized names for modifiers.
8+
9+
Modifiers should use dasherized names when being invoked, not camelCase. This is a stylistic rule that will prevent you from using camelCase modifiers, requiring you to use dasherized modifier names instead.
10+
11+
## Examples
12+
13+
This rule **forbids** the following:
14+
15+
```gjs
16+
<template><div {{didInsert}}></div></template>
17+
```
18+
19+
```gjs
20+
<template><div {{onFocus}}></div></template>
21+
```
22+
23+
This rule **allows** the following:
24+
25+
```gjs
26+
<template><div {{did-insert}}></div></template>
27+
```
28+
29+
```gjs
30+
<template><div {{on-focus}}></div></template>
31+
```
32+
33+
## See Also
34+
35+
- [named-functions-in-promises](named-functions-in-promises.md)
36+
37+
## References
38+
39+
- [Template syntax guide - Modifiers](https://guides.emberjs.com/release/components/template-syntax/#toc_modifiers)
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
/* eslint-disable unicorn/consistent-function-scoping, eslint-plugin/no-unused-message-ids */
2+
3+
function dasherize(str) {
4+
return str.replaceAll(/([A-Z])/g, '-$1').toLowerCase();
5+
}
6+
7+
/** @type {import('eslint').Rule.RuleModule} */
8+
module.exports = {
9+
meta: {
10+
type: 'suggestion',
11+
docs: {
12+
description: 'require dasherized names for modifiers',
13+
category: 'Stylistic Issues',
14+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-modifier-name-case.md',
15+
templateMode: 'both',
16+
},
17+
fixable: 'code',
18+
schema: [],
19+
messages: {
20+
dasherized:
21+
'Use dasherized names for modifier invocation. Please replace `{{dasherizeModifierName}}` with `{{dasherizeModifierName}}`.',
22+
},
23+
originallyFrom: {
24+
name: 'ember-template-lint',
25+
rule: 'lib/rules/modifier-name-case.js',
26+
docs: 'docs/rule/modifier-name-case.md',
27+
tests: 'test/unit/rules/modifier-name-case-test.js',
28+
},
29+
},
30+
31+
create(context) {
32+
function generateErrorMessage(modifierName) {
33+
const dasherizedName = dasherize(modifierName);
34+
return `Use dasherized names for modifier invocation. Please replace \`${modifierName}\` with \`${dasherizedName}\`.`;
35+
}
36+
37+
function isModifierHelper(node) {
38+
return (
39+
node.path && node.path.type === 'GlimmerPathExpression' && node.path.original === 'modifier'
40+
);
41+
}
42+
43+
return {
44+
GlimmerElementModifierStatement(node) {
45+
const modifierName = node.path?.original;
46+
47+
if (typeof modifierName === 'string' && modifierName !== dasherize(modifierName)) {
48+
context.report({
49+
node,
50+
message: generateErrorMessage(modifierName),
51+
fix(fixer) {
52+
const dasherizedName = dasherize(modifierName);
53+
return fixer.replaceTextRange(node.path.range, dasherizedName);
54+
},
55+
});
56+
}
57+
},
58+
59+
GlimmerSubExpression(node) {
60+
if (!isModifierHelper(node)) {
61+
return;
62+
}
63+
64+
const nameParam = node.params?.[0];
65+
66+
if (nameParam && nameParam.type === 'GlimmerStringLiteral') {
67+
const modifierName = nameParam.value;
68+
69+
if (typeof modifierName === 'string' && modifierName !== dasherize(modifierName)) {
70+
context.report({
71+
node: nameParam,
72+
message: generateErrorMessage(modifierName),
73+
fix(fixer) {
74+
const dasherizedName = dasherize(modifierName);
75+
return fixer.replaceTextRange(nameParam.range, `"${dasherizedName}"`);
76+
},
77+
});
78+
}
79+
}
80+
},
81+
82+
GlimmerMustacheStatement(node) {
83+
if (!isModifierHelper(node)) {
84+
return;
85+
}
86+
87+
const nameParam = node.params?.[0];
88+
89+
if (nameParam && nameParam.type === 'GlimmerStringLiteral') {
90+
const modifierName = nameParam.value;
91+
92+
if (typeof modifierName === 'string' && modifierName !== dasherize(modifierName)) {
93+
context.report({
94+
node: nameParam,
95+
message: generateErrorMessage(modifierName),
96+
fix(fixer) {
97+
const dasherizedName = dasherize(modifierName);
98+
return fixer.replaceTextRange(nameParam.range, `"${dasherizedName}"`);
99+
},
100+
});
101+
}
102+
}
103+
},
104+
};
105+
},
106+
};
107+
/* eslint-enable unicorn/consistent-function-scoping, eslint-plugin/no-unused-message-ids */
Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,189 @@
1+
const rule = require('../../../lib/rules/template-modifier-name-case');
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-modifier-name-case', rule, {
10+
valid: [
11+
'<template><div {{did-insert}}></div></template>',
12+
'<template><div {{did-update}}></div></template>',
13+
'<template><div {{on-click}}></div></template>',
14+
'<template><div {{(modifier "did-insert")}}></div></template>',
15+
'<template><div {{(modifier "on-click")}}></div></template>',
16+
17+
'<template><div {{did-insert "something"}}></div></template>',
18+
'<template><div {{did-insert action=something}}></div></template>',
19+
'<template><button {{on "click" somethingAmazing}}></button></template>',
20+
'<template><button onclick={{do-a-thing "foo"}}></button></template>',
21+
'<template><button onclick={{doAThing "foo"}}></button></template>',
22+
'<template><a href="#" onclick={{amazingActionThing "foo"}} {{did-insert}}></a></template>',
23+
'<template><div didInsert></div></template>',
24+
'<template><div {{(modifier "foo-bar")}}></div></template>',
25+
'<template><div {{(if this.foo (modifier "foo-bar"))}}></div></template>',
26+
'<template><div {{(modifier this.fooBar)}}></div></template>',
27+
],
28+
invalid: [
29+
{
30+
code: '<template><div {{didInsert}}></div></template>',
31+
output: '<template><div {{did-insert}}></div></template>',
32+
errors: [
33+
{
34+
message:
35+
'Use dasherized names for modifier invocation. Please replace `didInsert` with `did-insert`.',
36+
},
37+
],
38+
},
39+
{
40+
code: '<template><div {{doSomething}}></div></template>',
41+
output: '<template><div {{do-something}}></div></template>',
42+
errors: [
43+
{
44+
message:
45+
'Use dasherized names for modifier invocation. Please replace `doSomething` with `do-something`.',
46+
},
47+
],
48+
},
49+
{
50+
code: '<template><div {{fooBar}}></div></template>',
51+
output: '<template><div {{foo-bar}}></div></template>',
52+
errors: [
53+
{
54+
message:
55+
'Use dasherized names for modifier invocation. Please replace `fooBar` with `foo-bar`.',
56+
},
57+
],
58+
},
59+
{
60+
code: '<template><div {{(modifier "didInsert")}}></div></template>',
61+
output: '<template><div {{(modifier "did-insert")}}></div></template>',
62+
errors: [
63+
{
64+
message:
65+
'Use dasherized names for modifier invocation. Please replace `didInsert` with `did-insert`.',
66+
},
67+
],
68+
},
69+
70+
{
71+
code: '<template><div class="monkey" {{didInsert "something" with="somethingElse"}}></div></template>',
72+
output:
73+
'<template><div class="monkey" {{did-insert "something" with="somethingElse"}}></div></template>',
74+
errors: [
75+
{
76+
message:
77+
'Use dasherized names for modifier invocation. Please replace `didInsert` with `did-insert`.',
78+
},
79+
],
80+
},
81+
{
82+
code: '<template><a href="#" onclick={{amazingActionThing "foo"}} {{doSomething}}></a></template>',
83+
output:
84+
'<template><a href="#" onclick={{amazingActionThing "foo"}} {{do-something}}></a></template>',
85+
errors: [
86+
{
87+
message:
88+
'Use dasherized names for modifier invocation. Please replace `doSomething` with `do-something`.',
89+
},
90+
],
91+
},
92+
{
93+
code: '<template><div {{(modifier "fooBar")}}></div></template>',
94+
output: '<template><div {{(modifier "foo-bar")}}></div></template>',
95+
errors: [
96+
{
97+
message:
98+
'Use dasherized names for modifier invocation. Please replace `fooBar` with `foo-bar`.',
99+
},
100+
],
101+
},
102+
{
103+
code: '<template><div {{(if this.foo (modifier "fooBar"))}}></div></template>',
104+
output: '<template><div {{(if this.foo (modifier "foo-bar"))}}></div></template>',
105+
errors: [
106+
{
107+
message:
108+
'Use dasherized names for modifier invocation. Please replace `fooBar` with `foo-bar`.',
109+
},
110+
],
111+
},
112+
],
113+
});
114+
115+
const hbsRuleTester = new RuleTester({
116+
parser: require.resolve('ember-eslint-parser/hbs'),
117+
parserOptions: {
118+
ecmaVersion: 2022,
119+
sourceType: 'module',
120+
},
121+
});
122+
123+
hbsRuleTester.run('template-modifier-name-case', rule, {
124+
valid: [
125+
'<div {{did-insert}}></div>',
126+
'<div {{did-insert "something"}}></div>',
127+
'<div {{did-insert action=something}}></div>',
128+
'<button {{on "click" somethingAmazing}}></button>',
129+
'<button onclick={{do-a-thing "foo"}}></button>',
130+
'<button onclick={{doAThing "foo"}}></button>',
131+
'<a href="#" onclick={{amazingActionThing "foo"}} {{did-insert}}></a>',
132+
'<div didInsert></div>',
133+
'<div {{(modifier "foo-bar")}}></div>',
134+
'<div {{(if this.foo (modifier "foo-bar"))}}></div>',
135+
'<div {{(modifier this.fooBar)}}></div>',
136+
],
137+
invalid: [
138+
{
139+
code: '<div {{didInsert}}></div>',
140+
output: '<div {{did-insert}}></div>',
141+
errors: [
142+
{
143+
message:
144+
'Use dasherized names for modifier invocation. Please replace `didInsert` with `did-insert`.',
145+
},
146+
],
147+
},
148+
{
149+
code: '<div class="monkey" {{didInsert "something" with="somethingElse"}}></div>',
150+
output: '<div class="monkey" {{did-insert "something" with="somethingElse"}}></div>',
151+
errors: [
152+
{
153+
message:
154+
'Use dasherized names for modifier invocation. Please replace `didInsert` with `did-insert`.',
155+
},
156+
],
157+
},
158+
{
159+
code: '<a href="#" onclick={{amazingActionThing "foo"}} {{doSomething}}></a>',
160+
output: '<a href="#" onclick={{amazingActionThing "foo"}} {{do-something}}></a>',
161+
errors: [
162+
{
163+
message:
164+
'Use dasherized names for modifier invocation. Please replace `doSomething` with `do-something`.',
165+
},
166+
],
167+
},
168+
{
169+
code: '<div {{(modifier "fooBar")}}></div>',
170+
output: '<div {{(modifier "foo-bar")}}></div>',
171+
errors: [
172+
{
173+
message:
174+
'Use dasherized names for modifier invocation. Please replace `fooBar` with `foo-bar`.',
175+
},
176+
],
177+
},
178+
{
179+
code: '<div {{(if this.foo (modifier "fooBar"))}}></div>',
180+
output: '<div {{(if this.foo (modifier "foo-bar"))}}></div>',
181+
errors: [
182+
{
183+
message:
184+
'Use dasherized names for modifier invocation. Please replace `fooBar` with `foo-bar`.',
185+
},
186+
],
187+
},
188+
],
189+
});

0 commit comments

Comments
 (0)