Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 43 additions & 13 deletions lib/rules/template-no-unnecessary-component-helper.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,14 @@
function toPascalCase(name) {
return name
.split(/[/-]/)
.map((part) => part.charAt(0).toUpperCase() + part.slice(1))
.join('');
}

function isValidIdentifier(name) {
return /^[$A-Z_a-z][\w$]*$/.test(name);
}

function isComponentWithStringLiteral(node) {
return (
node.path &&
Expand Down Expand Up @@ -48,6 +59,10 @@ module.exports = {
schema: [],
messages: {
noUnnecessaryComponent: 'Invoke component directly instead of using `component` helper',
noUnnecessaryComponentKebab:
'In GJS/GTS, "{{name}}" must be imported as a JS binding (e.g. `import {{pascal}} from "..."`). ' +
'Invoke it directly as `<{{pascal}}>` instead of via the `component` helper. ' +
'The ember-codemods angle-brackets-codemod can automate this migration.',
},
originallyFrom: {
name: 'ember-template-lint',
Expand All @@ -59,8 +74,29 @@ module.exports = {

create(context) {
const sourceCode = context.sourceCode;
const filename = context.filename;
const isStrictMode = filename.endsWith('.gjs') || filename.endsWith('.gts');
let inAttribute = 0;

// In strict mode, a kebab-case / slash component name cannot become a bare
// mustache invocation — the result would not be a valid JS binding and would
// require an import. Ecosystem tooling (ember-codemods/angle-brackets-codemod)
// handles this migration end-to-end including adding the import.
function buildReport(node, componentName, fix) {
if (isStrictMode && !isValidIdentifier(componentName)) {
return {
node,
messageId: 'noUnnecessaryComponentKebab',
data: { name: componentName, pascal: toPascalCase(componentName) },
};
}
const report = { node, messageId: 'noUnnecessaryComponent' };
if (!isStrictMode || isValidIdentifier(componentName)) {
report.fix = fix;
}
return report;
}

return {
GlimmerAttrNode() {
inAttribute++;
Expand All @@ -79,13 +115,9 @@ module.exports = {

const componentName = node.params[0].value || node.params[0].original;
const invocation = getComponentInvocationText(sourceCode, node, componentName);
context.report({
node,
messageId: 'noUnnecessaryComponent',
fix(fixer) {
return fixer.replaceText(node, `{{${invocation}}}`);
},
});
context.report(
buildReport(node, componentName, (fixer) => fixer.replaceText(node, `{{${invocation}}}`))
);
},

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

context.report({
node,
messageId: 'noUnnecessaryComponent',
fix(fixer) {
context.report(
buildReport(node, componentName, (fixer) => {
const openInvocationEnd = getOpenInvocationEnd(node);
const closingPathEnd = node.range[1] - 2;
const closingPathStart = closingPathEnd - node.path.original.length;
Expand All @@ -111,8 +141,8 @@ module.exports = {
fixer.replaceTextRange([node.path.range[0], openInvocationEnd], invocation),
fixer.replaceTextRange([closingPathStart, closingPathEnd], componentName),
];
},
});
})
);
},
};
},
Expand Down
27 changes: 26 additions & 1 deletion tests/lib/rules/template-no-unnecessary-component-helper.js
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,32 @@ const gjsRuleTester = new RuleTester({

gjsRuleTester.run('template-no-unnecessary-component-helper', rule, {
valid: validGjs,
invalid: invalidHbs.map(wrapTemplate),
invalid: [
...invalidHbs.map(wrapTemplate),
// GJS/GTS: kebab-case names can't be valid JS identifiers — report with
// a dedicated message suggesting the PascalCase form and import.
// Full migration (including adding the import) is best handled by
// ember-codemods/angle-brackets-codemod.
{
filename: 'test.gjs',
code: '<template>{{component "my-component-name" foo=123}}</template>',
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can autofix this, I think

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You mean like the angle-brackets-codemod?

Autofix inside here would produce undefined identifier without the import i think — Added noUnnecessaryComponentKebab messageId that tells the user the PascalCase equivalent (e.g. my-component → MyComponent) and points to the angle-brackets-codemod.

output: null,
errors: [{ messageId: 'noUnnecessaryComponentKebab' }],
},
{
filename: 'test.gts',
code: '<template>{{#component "my-component-name"}}content{{/component}}</template>',
output: null,
errors: [{ messageId: 'noUnnecessaryComponentKebab' }],
},
// GJS/GTS: valid JS identifier → autofix still applies
{
filename: 'test.gjs',
code: '<template>{{component "myComponent" foo=123}}</template>',
output: '<template>{{myComponent foo=123}}</template>',
errors: [{ messageId: 'noUnnecessaryComponent' }],
},
],
});

const hbsRuleTester = new RuleTester({
Expand Down
Loading