Skip to content

Commit 27f8466

Browse files
Merge pull request #2669 from johanrd/night_fix/template-no-input-tagname
Post-merge-review: Fix `template-no-input-tagname` false positive in GJS/GTS
2 parents 1aedbdd + fd4491a commit 27f8466

File tree

2 files changed

+126
-4
lines changed

2 files changed

+126
-4
lines changed

lib/rules/template-no-input-tagname.js

Lines changed: 43 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,18 @@ module.exports = {
77
category: 'Best Practices',
88

99
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-no-input-tagname.md',
10+
templateMode: 'both',
1011
},
1112
schema: [],
1213
messages: { unexpected: 'Unexpected tagName usage on input helper.' },
1314
},
1415
create(context) {
15-
function check(node) {
16+
const isStrictMode = context.filename.endsWith('.gjs') || context.filename.endsWith('.gts');
17+
18+
// local name → 'Input'. Only populated in GJS/GTS via ImportDeclaration.
19+
const importedComponents = new Map();
20+
21+
function checkCurly(node) {
1622
if (!node.path) {
1723
return;
1824
}
@@ -29,9 +35,42 @@ module.exports = {
2935
context.report({ node, messageId: 'unexpected' });
3036
}
3137
}
32-
return {
33-
GlimmerMustacheStatement: check,
34-
GlimmerSubExpression: check,
38+
39+
const visitors = {
40+
GlimmerElementNode(node) {
41+
const hasTagName = node.attributes?.some((a) => a.name === '@tagName');
42+
if (!hasTagName) {
43+
return;
44+
}
45+
if (isStrictMode) {
46+
// In GJS/GTS: only flag if explicitly imported from @ember/component
47+
if (importedComponents.has(node.tag)) {
48+
context.report({ node, messageId: 'unexpected' });
49+
}
50+
} else {
51+
// In HBS: <Input ...> always resolves to the framework Input
52+
if (node.tag === 'Input') {
53+
context.report({ node, messageId: 'unexpected' });
54+
}
55+
}
56+
},
3557
};
58+
59+
if (isStrictMode) {
60+
visitors.ImportDeclaration = function (node) {
61+
if (node.source.value === '@ember/component') {
62+
for (const specifier of node.specifiers) {
63+
if (specifier.type === 'ImportSpecifier' && specifier.imported.name === 'Input') {
64+
importedComponents.set(specifier.local.name, 'Input');
65+
}
66+
}
67+
}
68+
};
69+
} else {
70+
visitors.GlimmerMustacheStatement = checkCurly;
71+
visitors.GlimmerSubExpression = checkCurly;
72+
}
73+
74+
return visitors;
3675
},
3776
};

tests/lib/rules/template-no-input-tagname.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,37 @@ const ruleTester = new RuleTester({
55
parser: require.resolve('ember-eslint-parser'),
66
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
77
});
8+
89
ruleTester.run('template-no-input-tagname', rule, {
910
valid: [
1011
'<template>{{input value=this.foo}}</template>',
1112
// Test cases ported from ember-template-lint
1213
'<template>{{input type="text"}}</template>',
1314
'<template>{{component "input" type="text"}}</template>',
1415
'<template>{{yield (component "input" type="text")}}</template>',
16+
// Rule is disabled in GJS/GTS: `input` is a user-imported binding, not the classic helper
17+
{ filename: 'test.gjs', code: '<template>{{input tagName="span"}}</template>' },
18+
{ filename: 'test.gts', code: '<template>{{input tagName="foo"}}</template>' },
19+
// GJS/GTS angle-bracket: without an import from @ember/component, <Input> is a user binding
20+
{ filename: 'test.gjs', code: '<template><Input @tagName="button" /></template>' },
21+
{
22+
filename: 'test.gjs',
23+
code: 'const Input = <template>hi</template>;\n<template><Input @tagName="button" /></template>',
24+
},
1525
],
1626
invalid: [
27+
{
28+
filename: 'test.gjs',
29+
code: 'import { Input } from \'@ember/component\';\n<template><Input @tagName="button" /></template>',
30+
output: null,
31+
errors: [{ messageId: 'unexpected' }],
32+
},
33+
{
34+
filename: 'test.gts',
35+
code: 'import { Input as Field } from \'@ember/component\';\n<template><Field @tagName="span" /></template>',
36+
output: null,
37+
errors: [{ messageId: 'unexpected' }],
38+
},
1739
{
1840
code: '<template>{{input tagName="span"}}</template>',
1941
output: null,
@@ -53,3 +75,64 @@ ruleTester.run('template-no-input-tagname', rule, {
5375
},
5476
],
5577
});
78+
79+
const hbsRuleTester = new RuleTester({
80+
parser: require.resolve('ember-eslint-parser/hbs'),
81+
parserOptions: {
82+
ecmaVersion: 2022,
83+
sourceType: 'module',
84+
},
85+
});
86+
87+
hbsRuleTester.run('template-no-input-tagname', rule, {
88+
valid: [
89+
'{{input value=foo}}',
90+
'{{input type="text"}}',
91+
'{{component "input" type="text"}}',
92+
'{{yield (component "input" type="text")}}',
93+
'<Input />',
94+
'<Input @value={{this.foo}} />',
95+
],
96+
invalid: [
97+
{
98+
code: '<Input @tagName="button" />',
99+
output: null,
100+
errors: [{ messageId: 'unexpected' }],
101+
},
102+
{
103+
code: '{{input tagName="span"}}',
104+
output: null,
105+
errors: [{ messageId: 'unexpected' }],
106+
},
107+
{
108+
code: '{{input tagName="foo"}}',
109+
output: null,
110+
errors: [{ messageId: 'unexpected' }],
111+
},
112+
{
113+
code: '{{input tagName=bar}}',
114+
output: null,
115+
errors: [{ messageId: 'unexpected' }],
116+
},
117+
{
118+
code: '{{component "input" tagName="foo"}}',
119+
output: null,
120+
errors: [{ messageId: 'unexpected' }],
121+
},
122+
{
123+
code: '{{component "input" tagName=bar}}',
124+
output: null,
125+
errors: [{ messageId: 'unexpected' }],
126+
},
127+
{
128+
code: '{{yield (component "input" tagName="foo")}}',
129+
output: null,
130+
errors: [{ messageId: 'unexpected' }],
131+
},
132+
{
133+
code: '{{yield (component "input" tagName=bar)}}',
134+
output: null,
135+
errors: [{ messageId: 'unexpected' }],
136+
},
137+
],
138+
});

0 commit comments

Comments
 (0)