Skip to content

Commit 96131f4

Browse files
Merge pull request #2675 from johanrd/night_fix/template-no-unnecessary-component-helper
Post-merge-review: Fix `template-no-unnecessary-component-helper`: skip autofix for invalid GJS/GTS identifiers
2 parents 753ca46 + 575e867 commit 96131f4

2 files changed

Lines changed: 69 additions & 14 deletions

File tree

lib/rules/template-no-unnecessary-component-helper.js

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,14 @@
1+
function toPascalCase(name) {
2+
return name
3+
.split(/[/-]/)
4+
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
5+
.join('');
6+
}
7+
8+
function isValidIdentifier(name) {
9+
return /^[$A-Z_a-z][\w$]*$/.test(name);
10+
}
11+
112
function isComponentWithStringLiteral(node) {
213
return (
314
node.path &&
@@ -48,6 +59,10 @@ module.exports = {
4859
schema: [],
4960
messages: {
5061
noUnnecessaryComponent: 'Invoke component directly instead of using `component` helper',
62+
noUnnecessaryComponentKebab:
63+
'In GJS/GTS, "{{name}}" must be imported as a JS binding (e.g. `import {{pascal}} from "..."`). ' +
64+
'Invoke it directly as `<{{pascal}}>` instead of via the `component` helper. ' +
65+
'The ember-codemods angle-brackets-codemod can automate this migration.',
5166
},
5267
originallyFrom: {
5368
name: 'ember-template-lint',
@@ -59,8 +74,29 @@ module.exports = {
5974

6075
create(context) {
6176
const sourceCode = context.sourceCode;
77+
const filename = context.filename;
78+
const isStrictMode = filename.endsWith('.gjs') || filename.endsWith('.gts');
6279
let inAttribute = 0;
6380

81+
// In strict mode, a kebab-case / slash component name cannot become a bare
82+
// mustache invocation — the result would not be a valid JS binding and would
83+
// require an import. Ecosystem tooling (ember-codemods/angle-brackets-codemod)
84+
// handles this migration end-to-end including adding the import.
85+
function buildReport(node, componentName, fix) {
86+
if (isStrictMode && !isValidIdentifier(componentName)) {
87+
return {
88+
node,
89+
messageId: 'noUnnecessaryComponentKebab',
90+
data: { name: componentName, pascal: toPascalCase(componentName) },
91+
};
92+
}
93+
const report = { node, messageId: 'noUnnecessaryComponent' };
94+
if (!isStrictMode || isValidIdentifier(componentName)) {
95+
report.fix = fix;
96+
}
97+
return report;
98+
}
99+
64100
return {
65101
GlimmerAttrNode() {
66102
inAttribute++;
@@ -79,13 +115,9 @@ module.exports = {
79115

80116
const componentName = node.params[0].value || node.params[0].original;
81117
const invocation = getComponentInvocationText(sourceCode, node, componentName);
82-
context.report({
83-
node,
84-
messageId: 'noUnnecessaryComponent',
85-
fix(fixer) {
86-
return fixer.replaceText(node, `{{${invocation}}}`);
87-
},
88-
});
118+
context.report(
119+
buildReport(node, componentName, (fixer) => fixer.replaceText(node, `{{${invocation}}}`))
120+
);
89121
},
90122

91123
GlimmerBlockStatement(node) {
@@ -99,10 +131,8 @@ module.exports = {
99131
const componentName = node.params[0].value || node.params[0].original;
100132
const invocation = getComponentInvocationText(sourceCode, node, componentName);
101133

102-
context.report({
103-
node,
104-
messageId: 'noUnnecessaryComponent',
105-
fix(fixer) {
134+
context.report(
135+
buildReport(node, componentName, (fixer) => {
106136
const openInvocationEnd = getOpenInvocationEnd(node);
107137
const closingPathEnd = node.range[1] - 2;
108138
const closingPathStart = closingPathEnd - node.path.original.length;
@@ -111,8 +141,8 @@ module.exports = {
111141
fixer.replaceTextRange([node.path.range[0], openInvocationEnd], invocation),
112142
fixer.replaceTextRange([closingPathStart, closingPathEnd], componentName),
113143
];
114-
},
115-
});
144+
})
145+
);
116146
},
117147
};
118148
},

tests/lib/rules/template-no-unnecessary-component-helper.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,32 @@ const gjsRuleTester = new RuleTester({
9191

9292
gjsRuleTester.run('template-no-unnecessary-component-helper', rule, {
9393
valid: validGjs,
94-
invalid: invalidHbs.map(wrapTemplate),
94+
invalid: [
95+
...invalidHbs.map(wrapTemplate),
96+
// GJS/GTS: kebab-case names can't be valid JS identifiers — report with
97+
// a dedicated message suggesting the PascalCase form and import.
98+
// Full migration (including adding the import) is best handled by
99+
// ember-codemods/angle-brackets-codemod.
100+
{
101+
filename: 'test.gjs',
102+
code: '<template>{{component "my-component-name" foo=123}}</template>',
103+
output: null,
104+
errors: [{ messageId: 'noUnnecessaryComponentKebab' }],
105+
},
106+
{
107+
filename: 'test.gts',
108+
code: '<template>{{#component "my-component-name"}}content{{/component}}</template>',
109+
output: null,
110+
errors: [{ messageId: 'noUnnecessaryComponentKebab' }],
111+
},
112+
// GJS/GTS: valid JS identifier → autofix still applies
113+
{
114+
filename: 'test.gjs',
115+
code: '<template>{{component "myComponent" foo=123}}</template>',
116+
output: '<template>{{myComponent foo=123}}</template>',
117+
errors: [{ messageId: 'noUnnecessaryComponent' }],
118+
},
119+
],
95120
});
96121

97122
const hbsRuleTester = new RuleTester({

0 commit comments

Comments
 (0)