Skip to content

Commit 3875eb8

Browse files
committed
Extract rule: template-modifier-name-case
1 parent a15aa2a commit 3875eb8

4 files changed

Lines changed: 350 additions & 7 deletions

File tree

README.md

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -352,13 +352,14 @@ rules in templates can be disabled with eslint directives with mustache or html
352352

353353
### Stylistic Issues
354354

355-
| Name                     | Description | 💼 | 🔧 | 💡 |
356-
| :----------------------------------------------------------------- | :------------------------------------------------------------- | :- | :- | :- |
357-
| [order-in-components](docs/rules/order-in-components.md) | enforce proper order of properties in components | | 🔧 | |
358-
| [order-in-controllers](docs/rules/order-in-controllers.md) | enforce proper order of properties in controllers | | 🔧 | |
359-
| [order-in-models](docs/rules/order-in-models.md) | enforce proper order of properties in models | | 🔧 | |
360-
| [order-in-routes](docs/rules/order-in-routes.md) | enforce proper order of properties in routes | | 🔧 | |
361-
| [template-attribute-order](docs/rules/template-attribute-order.md) | enforce consistent ordering of attributes in template elements | | | |
355+
| Name                        | Description | 💼 | 🔧 | 💡 |
356+
| :----------------------------------------------------------------------- | :------------------------------------------------------------- | :- | :- | :- |
357+
| [order-in-components](docs/rules/order-in-components.md) | enforce proper order of properties in components | | 🔧 | |
358+
| [order-in-controllers](docs/rules/order-in-controllers.md) | enforce proper order of properties in controllers | | 🔧 | |
359+
| [order-in-models](docs/rules/order-in-models.md) | enforce proper order of properties in models | | 🔧 | |
360+
| [order-in-routes](docs/rules/order-in-routes.md) | enforce proper order of properties in routes | | 🔧 | |
361+
| [template-attribute-order](docs/rules/template-attribute-order.md) | enforce consistent ordering of attributes in template elements | | | |
362+
| [template-modifier-name-case](docs/rules/template-modifier-name-case.md) | require dasherized names for modifiers | | 🔧 | |
362363

363364
### Testing
364365

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
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+
```gjs
24+
<template><div {{modifier 'didInsert'}}></div></template>
25+
```
26+
27+
This rule **allows** the following:
28+
29+
```gjs
30+
<template><div {{did-insert}}></div></template>
31+
```
32+
33+
```gjs
34+
<template><div {{on-focus}}></div></template>
35+
```
36+
37+
```gjs
38+
<template><div {{modifier 'did-insert'}}></div></template>
39+
```
40+
41+
## See Also
42+
43+
- [named-functions-in-promises](named-functions-in-promises.md)
44+
45+
## References
46+
47+
- [Template syntax guide - Modifiers](https://guides.emberjs.com/release/components/template-syntax/#toc_modifiers)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
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+
},
16+
fixable: 'code',
17+
schema: [],
18+
messages: {
19+
dasherized:
20+
'Use dasherized names for modifier invocation. Please replace `{{dasherizeModifierName}}` with `{{dasherizeModifierName}}`.',
21+
},
22+
originallyFrom: {
23+
name: 'ember-template-lint',
24+
rule: 'lib/rules/modifier-name-case.js',
25+
docs: 'docs/rule/modifier-name-case.md',
26+
tests: 'test/unit/rules/modifier-name-case-test.js',
27+
},
28+
},
29+
30+
create(context) {
31+
function generateErrorMessage(modifierName) {
32+
const dasherizedName = dasherize(modifierName);
33+
return `Use dasherized names for modifier invocation. Please replace \`${modifierName}\` with \`${dasherizedName}\`.`;
34+
}
35+
36+
function isModifierHelper(node) {
37+
return (
38+
node.path && node.path.type === 'GlimmerPathExpression' && node.path.original === 'modifier'
39+
);
40+
}
41+
42+
return {
43+
GlimmerElementModifierStatement(node) {
44+
const modifierName = node.path?.original;
45+
46+
if (typeof modifierName === 'string' && modifierName !== dasherize(modifierName)) {
47+
context.report({
48+
node,
49+
message: generateErrorMessage(modifierName),
50+
fix(fixer) {
51+
const dasherizedName = dasherize(modifierName);
52+
return fixer.replaceTextRange(node.path.range, dasherizedName);
53+
},
54+
});
55+
}
56+
},
57+
58+
GlimmerSubExpression(node) {
59+
if (!isModifierHelper(node)) {
60+
return;
61+
}
62+
63+
const nameParam = node.params?.[0];
64+
65+
if (nameParam && nameParam.type === 'GlimmerStringLiteral') {
66+
const modifierName = nameParam.value;
67+
68+
if (typeof modifierName === 'string' && modifierName !== dasherize(modifierName)) {
69+
context.report({
70+
node: nameParam,
71+
message: generateErrorMessage(modifierName),
72+
fix(fixer) {
73+
const dasherizedName = dasherize(modifierName);
74+
return fixer.replaceTextRange(nameParam.range, `"${dasherizedName}"`);
75+
},
76+
});
77+
}
78+
}
79+
},
80+
81+
GlimmerMustacheStatement(node) {
82+
if (!isModifierHelper(node)) {
83+
return;
84+
}
85+
86+
const nameParam = node.params?.[0];
87+
88+
if (nameParam && nameParam.type === 'GlimmerStringLiteral') {
89+
const modifierName = nameParam.value;
90+
91+
if (typeof modifierName === 'string' && modifierName !== dasherize(modifierName)) {
92+
context.report({
93+
node: nameParam,
94+
message: generateErrorMessage(modifierName),
95+
fix(fixer) {
96+
const dasherizedName = dasherize(modifierName);
97+
return fixer.replaceTextRange(nameParam.range, `"${dasherizedName}"`);
98+
},
99+
});
100+
}
101+
}
102+
},
103+
};
104+
},
105+
};
106+
/* 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)