Skip to content

Commit 29fe7f6

Browse files
committed
Account for imports and aliasing
1 parent 82c9577 commit 29fe7f6

2 files changed

Lines changed: 162 additions & 48 deletions

File tree

lib/rules/template-no-builtin-form-components.js

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,57 @@ module.exports = {
3030
Textarea: 'noTextarea',
3131
};
3232

33+
const filename = context.filename ?? context.getFilename();
34+
const isStrictMode = filename.endsWith('.gjs') || filename.endsWith('.gts');
35+
36+
// local name → original name ('Input' | 'Textarea')
37+
// Only populated in GJS/GTS files via ImportDeclaration
38+
const importedComponents = new Map();
39+
3340
return {
41+
ImportDeclaration(node) {
42+
if (node.source.value === '@ember/component') {
43+
for (const specifier of node.specifiers) {
44+
if (specifier.type === 'ImportSpecifier') {
45+
const original = specifier.imported.name;
46+
if (original === 'Input' || original === 'Textarea') {
47+
importedComponents.set(specifier.local.name, original);
48+
}
49+
}
50+
}
51+
}
52+
},
53+
3454
GlimmerElementNode(node) {
35-
const messageId = MESSAGE_IDS[node.tag];
36-
if (messageId) {
37-
context.report({
38-
node,
39-
messageId,
40-
});
55+
const tag = node.tag;
56+
if (isStrictMode) {
57+
// In GJS/GTS: only flag if explicitly imported from @ember/component
58+
const original = importedComponents.get(tag);
59+
if (original) {
60+
context.report({ node, messageId: MESSAGE_IDS[original] });
61+
}
62+
} else {
63+
// In HBS: flag by canonical name (no import context available)
64+
const messageId = MESSAGE_IDS[tag];
65+
if (messageId) {
66+
context.report({ node, messageId });
67+
}
68+
}
69+
},
70+
71+
// Catch usages as a value: {{yield Input}}, (component Input), @field={{Input}}, etc.
72+
GlimmerPathExpression(node) {
73+
const name = node.original;
74+
if (isStrictMode) {
75+
const original = importedComponents.get(name);
76+
if (original) {
77+
context.report({ node, messageId: MESSAGE_IDS[original] });
78+
}
79+
} else {
80+
const messageId = MESSAGE_IDS[name];
81+
if (messageId) {
82+
context.report({ node, messageId });
83+
}
4184
}
4285
},
4386
};

tests/lib/rules/template-no-builtin-form-components.js

Lines changed: 113 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -8,48 +8,97 @@ const ruleTester = new RuleTester({
88

99
ruleTester.run('template-no-builtin-form-components', rule, {
1010
valid: [
11-
'<template><input type="text" /></template>',
12-
'<template><input type="checkbox" /></template>',
13-
'<template><input type="radio" /></template>',
14-
'<template><textarea></textarea></template>',
15-
'<template><div></div></template>',
11+
// Native HTML elements are always fine
12+
{ filename: 'test.gjs', code: '<template><input type="text" /></template>' },
13+
{ filename: 'test.gjs', code: '<template><input type="checkbox" /></template>' },
14+
{ filename: 'test.gjs', code: '<template><input type="radio" /></template>' },
15+
{ filename: 'test.gjs', code: '<template><textarea></textarea></template>' },
16+
{ filename: 'test.gjs', code: '<template><div></div></template>' },
17+
18+
// In GJS without an import from @ember/component, <Input>/<Textarea> are not the builtins
19+
{ filename: 'test.gjs', code: '<template><Input /></template>' },
20+
{ filename: 'test.gjs', code: '<template><Textarea></Textarea></template>' },
21+
22+
// Importing from a different source is fine
23+
{
24+
filename: 'test.gjs',
25+
code: "import { Input } from './my-components'; <template><Input /></template>",
26+
},
27+
{
28+
filename: 'test.gjs',
29+
code: "import { Textarea } from './my-components'; <template><Textarea></Textarea></template>",
30+
},
1631
],
1732
invalid: [
1833
{
19-
code: '<template><Input /></template>',
34+
filename: 'test.gjs',
35+
code: "import { Input } from '@ember/component'; <template><Input /></template>",
36+
output: null,
37+
errors: [{ messageId: 'noInput' }],
38+
},
39+
{
40+
filename: 'test.gjs',
41+
code: 'import { Input } from \'@ember/component\'; <template><Input type="text" /></template>',
42+
output: null,
43+
errors: [{ messageId: 'noInput' }],
44+
},
45+
{
46+
// Aliased import must still be flagged
47+
filename: 'test.gjs',
48+
code: "import { Input as EmberInput } from '@ember/component'; <template><EmberInput /></template>",
2049
output: null,
21-
errors: [
22-
{
23-
messageId: 'noInput',
24-
},
25-
],
50+
errors: [{ messageId: 'noInput' }],
2651
},
2752
{
28-
code: '<template><Input type="text" /></template>',
53+
filename: 'test.gjs',
54+
code: "import { Textarea } from '@ember/component'; <template><Textarea></Textarea></template>",
2955
output: null,
30-
errors: [
31-
{
32-
messageId: 'noInput',
33-
},
34-
],
56+
errors: [{ messageId: 'noTextarea' }],
3557
},
3658
{
37-
code: '<template><Textarea></Textarea></template>',
59+
filename: 'test.gjs',
60+
code: "import { Textarea } from '@ember/component'; <template><Textarea @value={{this.body}}></Textarea></template>",
3861
output: null,
39-
errors: [
40-
{
41-
messageId: 'noTextarea',
42-
},
43-
],
62+
errors: [{ messageId: 'noTextarea' }],
4463
},
4564
{
46-
code: '<template><Textarea @value={{this.body}}></Textarea></template>',
65+
// Aliased Textarea import must still be flagged
66+
filename: 'test.gjs',
67+
code: "import { Textarea as EmberTextarea } from '@ember/component'; <template><EmberTextarea></EmberTextarea></template>",
4768
output: null,
48-
errors: [
49-
{
50-
messageId: 'noTextarea',
51-
},
52-
],
69+
errors: [{ messageId: 'noTextarea' }],
70+
},
71+
// Yielded as a value
72+
{
73+
filename: 'test.gjs',
74+
code: "import { Input } from '@ember/component'; <template>{{yield Input}}</template>",
75+
output: null,
76+
errors: [{ messageId: 'noInput' }],
77+
},
78+
{
79+
filename: 'test.gjs',
80+
code: "import { Input as EmberInput } from '@ember/component'; <template>{{yield EmberInput}}</template>",
81+
output: null,
82+
errors: [{ messageId: 'noInput' }],
83+
},
84+
{
85+
filename: 'test.gjs',
86+
code: "import { Textarea } from '@ember/component'; <template>{{yield Textarea}}</template>",
87+
output: null,
88+
errors: [{ messageId: 'noTextarea' }],
89+
},
90+
// Used in helpers / passed as argument
91+
{
92+
filename: 'test.gjs',
93+
code: "import { Input } from '@ember/component'; <template><MyForm @field={{Input}} /></template>",
94+
output: null,
95+
errors: [{ messageId: 'noInput' }],
96+
},
97+
{
98+
filename: 'test.gjs',
99+
code: "import { Input } from '@ember/component'; <template>{{component Input}}</template>",
100+
output: null,
101+
errors: [{ messageId: 'noInput' }],
53102
},
54103
],
55104
});
@@ -62,7 +111,7 @@ const hbsRuleTester = new RuleTester({
62111
},
63112
});
64113

65-
hbsRuleTester.run('template-no-builtin-form-components', rule, {
114+
hbsRuleTester.run('template-no-builtin-form-components (hbs)', rule, {
66115
valid: [
67116
'<input type="text" />',
68117
'<input type="checkbox" />',
@@ -73,22 +122,44 @@ hbsRuleTester.run('template-no-builtin-form-components', rule, {
73122
{
74123
code: '<Input />',
75124
output: null,
76-
errors: [
77-
{
78-
message:
79-
'Do not use the `Input` component. Built-in form components use two-way binding to mutate values. Instead, refactor to use a native HTML element.',
80-
},
81-
],
125+
errors: [{ messageId: 'noInput' }],
126+
},
127+
{
128+
code: '<Input type="text" />',
129+
output: null,
130+
errors: [{ messageId: 'noInput' }],
82131
},
83132
{
84133
code: '<Textarea></Textarea>',
85134
output: null,
86-
errors: [
87-
{
88-
message:
89-
'Do not use the `Textarea` component. Built-in form components use two-way binding to mutate values. Instead, refactor to use a native HTML element.',
90-
},
91-
],
135+
errors: [{ messageId: 'noTextarea' }],
136+
},
137+
{
138+
code: '<Textarea @value={{this.body}}></Textarea>',
139+
output: null,
140+
errors: [{ messageId: 'noTextarea' }],
141+
},
142+
// Yielded as a value
143+
{
144+
code: '{{yield Input}}',
145+
output: null,
146+
errors: [{ messageId: 'noInput' }],
147+
},
148+
{
149+
code: '{{yield Textarea}}',
150+
output: null,
151+
errors: [{ messageId: 'noTextarea' }],
152+
},
153+
// Used in helpers / passed as argument
154+
{
155+
code: '<MyForm @field={{Input}} />',
156+
output: null,
157+
errors: [{ messageId: 'noInput' }],
158+
},
159+
{
160+
code: '{{component Input}}',
161+
output: null,
162+
errors: [{ messageId: 'noInput' }],
92163
},
93164
],
94165
});

0 commit comments

Comments
 (0)