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
46 changes: 40 additions & 6 deletions lib/rules/template-require-input-label.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,10 @@ module.exports = {
const isStrictMode = filename.endsWith('.gjs') || filename.endsWith('.gts');
const elementStack = [];

// local name → original name ('Input' | 'Textarea')
// Only populated in GJS/GTS files via ImportDeclaration
const importedFormComponents = new Map();

function hasValidLabelParent() {
for (let i = elementStack.length - 1; i >= 0; i--) {
const entry = elementStack[i];
Expand All @@ -104,15 +108,38 @@ module.exports = {
}

return {
GlimmerElementNode(node) {
elementStack.push({ tag: node.tag, node });

if (isStrictMode && (node.tag === 'Input' || node.tag === 'Textarea')) {
ImportDeclaration(node) {
if (!isStrictMode) {
return;
}
if (node.source.value === '@ember/component') {
for (const specifier of node.specifiers) {
if (specifier.type === 'ImportSpecifier') {
const original = specifier.imported.name;
if (original === 'Input' || original === 'Textarea') {
importedFormComponents.set(specifier.local.name, original);
}
}
}
}
},

const tagName = node.tag?.toLowerCase();
if (tagName !== 'input' && tagName !== 'textarea' && tagName !== 'select') {
GlimmerElementNode(node) {
elementStack.push({ tag: node.tag, node });

const tag = node.tag;
// Is this tag one we should check?
// - Native <input>/<textarea>/<select> always.
// - <Input>/<Textarea> built-ins:
// - In strict mode (.gjs/.gts): only if the tag resolves to a tracked
// import from '@ember/component' (supports renames).
// - In HBS: match by bare tag name.
const isNativeFormElement = tag === 'input' || tag === 'textarea' || tag === 'select';
const isBuiltinFormComponent = isStrictMode
? importedFormComponents.has(tag)
: tag === 'Input' || tag === 'Textarea';

if (!isNativeFormElement && !isBuiltinFormComponent) {
return;
}

Expand Down Expand Up @@ -164,6 +191,13 @@ module.exports = {
},

GlimmerMustacheStatement(node) {
// Classic {{input}}/{{textarea}} curly helpers only exist in HBS.
// In GJS/GTS, these identifiers are user-imported JS bindings with
// no relation to the classic helpers, so skip.
if (isStrictMode) {
return;
}

const name = node.path?.original;
if (name !== 'input' && name !== 'textarea') {
return;
Expand Down
25 changes: 25 additions & 0 deletions tests/lib/rules/template-require-input-label.js
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,19 @@ ruleTester.run('template-require-input-label', rule, {
'<template><input type="hidden" /></template>',
'<template><Input type="hidden" /></template>',
'<template>{{input type="hidden"}}</template>',
// In GJS/GTS with no @ember/component import, <Input>/<Textarea> are
// user-authored components — do not treat them as the built-in.
{ filename: 'layout.gjs', code: '<template><Input /></template>' },
{ filename: 'layout.gts', code: '<template><Textarea /></template>' },
// In GJS/GTS, {{input}} / {{textarea}} are user-imported bindings, not
// the classic Ember helpers — skip the mustache-form check.
{ filename: 'layout.gjs', code: '<template>{{input}}</template>' },
{ filename: 'layout.gts', code: '<template>{{textarea}}</template>' },
// Built-in <Input> imported from @ember/component, wrapped in a label.
{
filename: 'layout.gjs',
code: "import { Input } from '@ember/component';\n<template><label>Name <Input /></label></template>",
},
{
code: '<template><CustomLabel><input /></CustomLabel></template>',
options: [{ labelTags: ['CustomLabel'] }],
Expand Down Expand Up @@ -125,6 +136,20 @@ ruleTester.run('template-require-input-label', rule, {
output: null,
errors: [{ message: MULTIPLE_LABELS }],
},
// Built-in <Input> imported from @ember/component in GJS → flagged.
{
filename: 'layout.gjs',
code: "import { Input } from '@ember/component';\n<template><Input /></template>",
output: null,
errors: [{ message: NO_LABEL }],
},
// Renamed import of <Textarea> from @ember/component in GTS → flagged.
{
filename: 'layout.gts',
code: "import { Textarea as TA } from '@ember/component';\n<template><TA /></template>",
output: null,
errors: [{ message: NO_LABEL }],
},
],
});

Expand Down
Loading