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
47 changes: 43 additions & 4 deletions lib/rules/template-no-input-tagname.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,18 @@ module.exports = {
category: 'Best Practices',

url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-input-tagname.md',
templateMode: 'both',
},
schema: [],
messages: { unexpected: 'Unexpected tagName usage on input helper.' },
},
create(context) {
function check(node) {
const isStrictMode = context.filename.endsWith('.gjs') || context.filename.endsWith('.gts');
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.

should this be extracted to a shared helper?

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.

yes, i was thinking to add that as a separate 'clean' cleanup PR after the 'Post-merge-reviews'

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.

sounds good


// local name → 'Input'. Only populated in GJS/GTS via ImportDeclaration.
const importedComponents = new Map();

function checkCurly(node) {
if (!node.path) {
return;
}
Expand All @@ -29,9 +35,42 @@ module.exports = {
context.report({ node, messageId: 'unexpected' });
}
}
return {
GlimmerMustacheStatement: check,
GlimmerSubExpression: check,

const visitors = {
GlimmerElementNode(node) {
const hasTagName = node.attributes?.some((a) => a.name === '@tagName');
if (!hasTagName) {
return;
}
if (isStrictMode) {
// In GJS/GTS: only flag if explicitly imported from @ember/component
if (importedComponents.has(node.tag)) {
context.report({ node, messageId: 'unexpected' });
}
} else {
// In HBS: <Input ...> always resolves to the framework Input
if (node.tag === 'Input') {
context.report({ node, messageId: 'unexpected' });
}
}
},
};

if (isStrictMode) {
visitors.ImportDeclaration = function (node) {
if (node.source.value === '@ember/component') {
for (const specifier of node.specifiers) {
if (specifier.type === 'ImportSpecifier' && specifier.imported.name === 'Input') {
importedComponents.set(specifier.local.name, 'Input');
}
}
}
};
} else {
visitors.GlimmerMustacheStatement = checkCurly;
visitors.GlimmerSubExpression = checkCurly;
}

return visitors;
},
};
83 changes: 83 additions & 0 deletions tests/lib/rules/template-no-input-tagname.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,37 @@ const ruleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser'),
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
});

ruleTester.run('template-no-input-tagname', rule, {
valid: [
'<template>{{input value=this.foo}}</template>',
// Test cases ported from ember-template-lint
'<template>{{input type="text"}}</template>',
'<template>{{component "input" type="text"}}</template>',
'<template>{{yield (component "input" type="text")}}</template>',
// Rule is disabled in GJS/GTS: `input` is a user-imported binding, not the classic helper
{ filename: 'test.gjs', code: '<template>{{input tagName="span"}}</template>' },
{ filename: 'test.gts', code: '<template>{{input tagName="foo"}}</template>' },
// GJS/GTS angle-bracket: without an import from @ember/component, <Input> is a user binding
{ filename: 'test.gjs', code: '<template><Input @tagName="button" /></template>' },
{
filename: 'test.gjs',
code: 'const Input = <template>hi</template>;\n<template><Input @tagName="button" /></template>',
},
],
invalid: [
{
filename: 'test.gjs',
code: 'import { Input } from \'@ember/component\';\n<template><Input @tagName="button" /></template>',
output: null,
errors: [{ messageId: 'unexpected' }],
},
{
filename: 'test.gts',
code: 'import { Input as Field } from \'@ember/component\';\n<template><Field @tagName="span" /></template>',
output: null,
errors: [{ messageId: 'unexpected' }],
},
{
code: '<template>{{input tagName="span"}}</template>',
output: null,
Expand Down Expand Up @@ -53,3 +75,64 @@ ruleTester.run('template-no-input-tagname', rule, {
},
],
});

const hbsRuleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser/hbs'),
parserOptions: {
ecmaVersion: 2022,
sourceType: 'module',
},
});

hbsRuleTester.run('template-no-input-tagname', rule, {
valid: [
'{{input value=foo}}',
'{{input type="text"}}',
'{{component "input" type="text"}}',
'{{yield (component "input" type="text")}}',
'<Input />',
'<Input @value={{this.foo}} />',
],
invalid: [
{
code: '<Input @tagName="button" />',
output: null,
errors: [{ messageId: 'unexpected' }],
},
{
code: '{{input tagName="span"}}',
output: null,
errors: [{ messageId: 'unexpected' }],
},
{
code: '{{input tagName="foo"}}',
output: null,
errors: [{ messageId: 'unexpected' }],
},
{
code: '{{input tagName=bar}}',
output: null,
errors: [{ messageId: 'unexpected' }],
},
{
code: '{{component "input" tagName="foo"}}',
output: null,
errors: [{ messageId: 'unexpected' }],
},
{
code: '{{component "input" tagName=bar}}',
output: null,
errors: [{ messageId: 'unexpected' }],
},
{
code: '{{yield (component "input" tagName="foo")}}',
output: null,
errors: [{ messageId: 'unexpected' }],
},
{
code: '{{yield (component "input" tagName=bar)}}',
output: null,
errors: [{ messageId: 'unexpected' }],
},
],
});
Loading