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: [ '', @@ -12,8 +13,29 @@ ruleTester.run('template-no-input-tagname', rule, { '', '', '', + // Rule is disabled in GJS/GTS: `input` is a user-imported binding, not the classic helper + { filename: 'test.gjs', code: '' }, + { filename: 'test.gts', code: '' }, + // GJS/GTS angle-bracket: without an import from @ember/component, is a user binding + { filename: 'test.gjs', code: '' }, + { + filename: 'test.gjs', + code: 'const Input = ;\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: '', 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' }], + }, + ], +});