diff --git a/lib/rules/template-no-input-tagname.js b/lib/rules/template-no-input-tagname.js
index 5d114dabd2..4175832513 100644
--- a/lib/rules/template-no-input-tagname.js
+++ b/lib/rules/template-no-input-tagname.js
@@ -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');
+
+ // local name → 'Input'. Only populated in GJS/GTS via ImportDeclaration.
+ const importedComponents = new Map();
+
+ function checkCurly(node) {
if (!node.path) {
return;
}
@@ -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: 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;
},
};
diff --git a/tests/lib/rules/template-no-input-tagname.js b/tests/lib/rules/template-no-input-tagname.js
index efd4c26eb6..3c393cf959 100644
--- a/tests/lib/rules/template-no-input-tagname.js
+++ b/tests/lib/rules/template-no-input-tagname.js
@@ -5,6 +5,7 @@ const ruleTester = new RuleTester({
parser: require.resolve('ember-eslint-parser'),
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
});
+
ruleTester.run('template-no-input-tagname', rule, {
valid: [
'{{input value=this.foo}}',
@@ -12,8 +13,29 @@ ruleTester.run('template-no-input-tagname', rule, {
'{{input type="text"}}',
'{{component "input" type="text"}}',
'{{yield (component "input" type="text")}}',
+ // Rule is disabled in GJS/GTS: `input` is a user-imported binding, not the classic helper
+ { filename: 'test.gjs', code: '{{input tagName="span"}}' },
+ { filename: 'test.gts', code: '{{input tagName="foo"}}' },
+ // GJS/GTS angle-bracket: without an import from @ember/component, is a user binding
+ { filename: 'test.gjs', code: '' },
+ {
+ filename: 'test.gjs',
+ code: 'const Input = hi;\n',
+ },
],
invalid: [
+ {
+ filename: 'test.gjs',
+ code: 'import { Input } from \'@ember/component\';\n',
+ output: null,
+ errors: [{ messageId: 'unexpected' }],
+ },
+ {
+ filename: 'test.gts',
+ code: 'import { Input as Field } from \'@ember/component\';\n',
+ output: null,
+ errors: [{ messageId: 'unexpected' }],
+ },
{
code: '{{input tagName="span"}}',
output: null,
@@ -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")}}',
+ '',
+ '',
+ ],
+ invalid: [
+ {
+ code: '',
+ 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' }],
+ },
+ ],
+});