Skip to content

Commit 675427a

Browse files
Merge pull request #2763 from johanrd/html-validate/template-require-input-type
feat: add template-require-input-type
2 parents 0cf348b + 148a2a3 commit 675427a

5 files changed

Lines changed: 348 additions & 9 deletions

File tree

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -358,6 +358,7 @@ To disable a rule for an entire `.gjs`/`.gts` file, use a regular ESLint file-le
358358
| [template-require-form-method](docs/rules/template-require-form-method.md) | require form method attribute | | 🔧 | |
359359
| [template-require-has-block-helper](docs/rules/template-require-has-block-helper.md) | require (has-block) helper usage instead of hasBlock property | 📋 | 🔧 | |
360360
| [template-require-iframe-src-attribute](docs/rules/template-require-iframe-src-attribute.md) | require iframe elements to have src attribute | | 🔧 | |
361+
| [template-require-input-type](docs/rules/template-require-input-type.md) | require input elements to have a valid type attribute | | 🔧 | |
361362
| [template-require-splattributes](docs/rules/template-require-splattributes.md) | require splattributes usage in component templates | | | |
362363
| [template-require-strict-mode](docs/rules/template-require-strict-mode.md) | require templates to be in strict mode | | | |
363364
| [template-require-valid-named-block-naming-format](docs/rules/template-require-valid-named-block-naming-format.md) | require valid named block naming format | 📋 | 🔧 | |
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
# ember/template-require-input-type
2+
3+
🔧 This rule is automatically fixable by the [`--fix` CLI option](https://eslint.org/docs/latest/user-guide/command-line-interface#--fix).
4+
5+
<!-- end auto-generated rule header -->
6+
7+
This rule rejects `<input type="...">` values that are not one of the input
8+
types defined by the HTML spec, and (optionally) requires every `<input>` to
9+
declare a `type` attribute.
10+
11+
An invalid value like `<input type="foo">` silently falls back to the Text
12+
state — the browser reports no error, but the author's intent (validation,
13+
inputmode hint, platform keyboard) is lost. That's a genuine silent-failure
14+
class, which this rule always flags and auto-fixes to `type="text"`.
15+
16+
A missing `type` attribute (`<input />`) is _spec-compliant_ — the
17+
missing-value default is the Text state — so flagging it is a style /
18+
consistency choice, not a correctness one. Opt in with `requireExplicit: true`
19+
if your team wants parity with `template-require-button-type`.
20+
21+
## Examples
22+
23+
This rule **forbids** the following (always):
24+
25+
```hbs
26+
<input type='' />
27+
<input type='foo' />
28+
<input type='TEXTY' />
29+
```
30+
31+
With `requireExplicit: true` the rule **also forbids**:
32+
33+
```hbs
34+
<input />
35+
<input name='email' />
36+
```
37+
38+
This rule **allows** the following:
39+
40+
```hbs
41+
<input type='text' />
42+
<input type='email' />
43+
<input type='checkbox' />
44+
<input type={{this.inputType}} />
45+
```
46+
47+
Dynamic values such as `type={{this.inputType}}` are not flagged at lint time.
48+
49+
## Configuration
50+
51+
- `requireExplicit` (`boolean`, default `false`): when true, also flag
52+
`<input>` elements that have no `type` attribute. Auto-fix inserts
53+
`type="text"`.
54+
55+
```js
56+
module.exports = {
57+
rules: {
58+
'ember/template-require-input-type': ['error', { requireExplicit: true }],
59+
},
60+
};
61+
```
62+
63+
## References
64+
65+
- [HTML spec — the input element](https://html.spec.whatwg.org/multipage/input.html#the-input-element)
66+
- Adapted from [`html-validate`'s `no-implicit-input-type`](https://html-validate.org/rules/no-implicit-input-type.html) (MIT).
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
'use strict';
2+
3+
// See html-validate (https://html-validate.org) for the peer rule concept.
4+
5+
const { isNativeElement } = require('../utils/is-native-element');
6+
7+
const VALID_TYPES = new Set([
8+
'button',
9+
'checkbox',
10+
'color',
11+
'date',
12+
'datetime-local',
13+
'email',
14+
'file',
15+
'hidden',
16+
'image',
17+
'month',
18+
'number',
19+
'password',
20+
'radio',
21+
'range',
22+
'reset',
23+
'search',
24+
'submit',
25+
'tel',
26+
'text',
27+
'time',
28+
'url',
29+
'week',
30+
]);
31+
32+
/** @type {import('eslint').Rule.RuleModule} */
33+
module.exports = {
34+
meta: {
35+
type: 'problem',
36+
docs: {
37+
description: 'require input elements to have a valid type attribute',
38+
category: 'Best Practices',
39+
url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-require-input-type.md',
40+
templateMode: 'both',
41+
},
42+
fixable: 'code',
43+
schema: [
44+
{
45+
type: 'object',
46+
properties: {
47+
requireExplicit: {
48+
type: 'boolean',
49+
},
50+
},
51+
additionalProperties: false,
52+
},
53+
],
54+
messages: {
55+
missing: 'All `<input>` elements should have a `type` attribute',
56+
invalid: '`<input type="{{value}}">` is not a valid input type',
57+
},
58+
},
59+
60+
create(context) {
61+
// Flagging a missing `type` is a style/consistency check, not a correctness
62+
// one: `<input>` without `type` is spec-compliant (defaults to the Text
63+
// state). Opt-in so teams that want parity with template-require-button-
64+
// type can enable it without imposing it on others.
65+
const requireExplicit = Boolean(context.options[0]?.requireExplicit);
66+
const sourceCode = context.sourceCode || context.getSourceCode();
67+
68+
return {
69+
GlimmerElementNode(node) {
70+
if (node.tag !== 'input') {
71+
return;
72+
}
73+
// In strict GJS, a lowercase local binding can shadow the native
74+
// `<input>` element. `isNativeElement` consults html/svg/mathml tag
75+
// lists and checks bindings in the scope chain to filter out
76+
// scope-shadowed cases.
77+
if (!isNativeElement(node, sourceCode)) {
78+
return;
79+
}
80+
81+
const typeAttr = node.attributes?.find((attr) => attr.name === 'type');
82+
83+
if (!typeAttr) {
84+
if (!requireExplicit) {
85+
return;
86+
}
87+
context.report({
88+
node,
89+
messageId: 'missing',
90+
fix(fixer) {
91+
// Insert right after `<input` so the new attribute is the first
92+
// one — avoids the fragile "find end of open tag" regex that can
93+
// mis-place the attribute past the `/` in self-closing syntax.
94+
const insertPos = node.range[0] + '<input'.length;
95+
return fixer.insertTextBeforeRange([insertPos, insertPos], ' type="text"');
96+
},
97+
});
98+
return;
99+
}
100+
101+
const value = typeAttr.value;
102+
103+
// Valueless attribute form (`<input type />`) — per HTML spec, a
104+
// present-but-empty type attribute resolves to the missing-value
105+
// default ("Text state"). That's the same runtime result as
106+
// `type=""`, which we already flag. Treat them consistently:
107+
// flag as invalid('') and autofix to `type="text"`.
108+
if (!value) {
109+
context.report({
110+
node: typeAttr,
111+
messageId: 'invalid',
112+
data: { value: '' },
113+
fix(fixer) {
114+
return fixer.replaceText(typeAttr, 'type="text"');
115+
},
116+
});
117+
return;
118+
}
119+
120+
if (value.type === 'GlimmerTextNode') {
121+
const typeValue = value.chars.toLowerCase();
122+
if (typeValue === '') {
123+
context.report({
124+
node: typeAttr,
125+
messageId: 'invalid',
126+
data: { value: '' },
127+
fix(fixer) {
128+
return fixer.replaceText(typeAttr, 'type="text"');
129+
},
130+
});
131+
} else if (!VALID_TYPES.has(typeValue)) {
132+
context.report({
133+
node: typeAttr,
134+
messageId: 'invalid',
135+
data: { value: value.chars },
136+
fix(fixer) {
137+
return fixer.replaceText(typeAttr, 'type="text"');
138+
},
139+
});
140+
}
141+
}
142+
},
143+
};
144+
},
145+
};

lib/utils/is-native-element.js

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,19 +21,17 @@ const ELEMENT_TAGS = new Set([...htmlTags, ...svgTags, ...mathmlTagNames]);
2121
* MathML spec registries, reached via the `html-tags` / `svg-tags` /
2222
* `mathml-tag-names` packages). It is NOT the same as:
2323
*
24-
* - "native accessibility" / "widget-ness" — see `interactive-roles.js`
25-
* (aria-query widget taxonomy; an ARIA-tree-semantics question)
26-
* - "native interactive content" / "focus behavior" — see
27-
* `html-interactive-content.js` (HTML §3.2.5.2.7; an HTML-content-model
28-
* question about which tags can be nested inside what)
24+
* - "native accessibility" / "widget-ness" — an ARIA-tree-semantics
25+
* question (for example, whether something maps to a widget role)
26+
* - "native interactive content" / "focus behavior" — an HTML content-model
27+
* question about which elements are considered interactive in the spec
2928
* - "natively focusable" / sequential-focus — see HTML §6.6.3
3029
*
3130
* This util answers only: "is this tag a first-class built-in element of one
3231
* of the three markup-language standards, rather than a component invocation
33-
* or a shadowed local binding?" Callers compose it with the other utils
34-
* above when they need a more specific question (see e.g. `template-no-
35-
* noninteractive-tabindex`, which consults both this and
36-
* `html-interactive-content`).
32+
* or a shadowed local binding?" Callers should combine it with whatever
33+
* accessibility, interactivity, or focusability checks they need for more
34+
* specific questions.
3735
*
3836
* Returns false for:
3937
* - components (PascalCase, dotted, @-prefixed, this.-prefixed, ::-namespaced —
Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
const rule = require('../../../lib/rules/template-require-input-type');
2+
const RuleTester = require('eslint').RuleTester;
3+
4+
const ERROR_MISSING = 'All `<input>` elements should have a `type` attribute';
5+
const errInvalid = (value) => `\`<input type="${value}">\` is not a valid input type`;
6+
7+
const validHbs = [
8+
'<input type="text" />',
9+
'<input type="email" />',
10+
'<input type="checkbox" />',
11+
'<input type="submit" />',
12+
'<input type="datetime-local" />',
13+
'<input type="{{this.inputType}}" />',
14+
'<input type={{this.inputType}} />',
15+
'<div />',
16+
'<div type="foo" />',
17+
'<MyInput type="unknown" />',
18+
// Default (requireExplicit=false): missing `type` is allowed.
19+
'<input />',
20+
'<input name="email" />',
21+
];
22+
23+
const invalidHbs = [
24+
{
25+
code: '<input type="" />',
26+
output: '<input type="text" />',
27+
errors: [{ message: errInvalid('') }],
28+
},
29+
{
30+
code: '<input type="foo" />',
31+
output: '<input type="text" />',
32+
errors: [{ message: errInvalid('foo') }],
33+
},
34+
{
35+
code: '<input type="TEXTY" />',
36+
output: '<input type="text" />',
37+
errors: [{ message: errInvalid('TEXTY') }],
38+
},
39+
// Valueless type attribute — per HTML spec resolves to the missing-value
40+
// default (Text state), same runtime result as `type=""`. Flag and autofix
41+
// to `type="text"`. (Output loses the pre-slash space because the
42+
// valueless attr range ends at `type`; prettier will re-insert if needed.)
43+
{
44+
code: '<input type />',
45+
output: '<input type="text"/>',
46+
errors: [{ message: errInvalid('') }],
47+
},
48+
];
49+
50+
const requireExplicitInvalid = [
51+
{
52+
code: '<input />',
53+
options: [{ requireExplicit: true }],
54+
output: '<input type="text" />',
55+
errors: [{ message: ERROR_MISSING }],
56+
},
57+
{
58+
code: '<input name="email" />',
59+
options: [{ requireExplicit: true }],
60+
output: '<input type="text" name="email" />',
61+
errors: [{ message: ERROR_MISSING }],
62+
},
63+
{
64+
code: '<input name="email" />',
65+
options: [{ requireExplicit: true }],
66+
output: '<input type="text" name="email" />',
67+
errors: [{ message: ERROR_MISSING }],
68+
},
69+
];
70+
71+
const requireExplicitValid = [
72+
// With requireExplicit: an explicit known type satisfies the rule.
73+
{ code: '<input type="text" />', options: [{ requireExplicit: true }] },
74+
// Dynamic type also satisfies — we can't know the runtime value.
75+
{ code: '<input type={{this.inputType}} />', options: [{ requireExplicit: true }] },
76+
];
77+
78+
const gjsValid = validHbs.map((code) => `<template>${code}</template>`);
79+
const gjsInvalid = invalidHbs.map(({ code, output, errors }) => ({
80+
code: `<template>${code}</template>`,
81+
output: `<template>${output}</template>`,
82+
errors,
83+
}));
84+
85+
const gjsRuleTester = new RuleTester({
86+
parser: require.resolve('ember-eslint-parser'),
87+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
88+
});
89+
90+
gjsRuleTester.run('template-require-input-type', rule, {
91+
valid: [
92+
...gjsValid,
93+
...requireExplicitValid.map(({ code, options }) => ({
94+
code: `<template>${code}</template>`,
95+
options,
96+
})),
97+
// Scope-shadowed `input` — the template's `<input>` refers to the local
98+
// const binding (a component), not the native HTML element. The rule
99+
// skips it via `isNativeElement`'s scope check.
100+
`const input = 'foo';
101+
<template><input type="not-a-valid-type" /></template>`,
102+
`const input = 'foo';
103+
<template><input /></template>`,
104+
// Block-param shadowing — `<Foo as |input|>` binds `input` inside the
105+
// yield block. The inner `<input>` should resolve to the block-param,
106+
// not the native tag.
107+
`import Foo from 'whatever';
108+
<template><Foo as |input|><input type="not-a-valid-type" /></Foo></template>`,
109+
],
110+
invalid: [
111+
...gjsInvalid,
112+
...requireExplicitInvalid.map(({ code, options, output, errors }) => ({
113+
code: `<template>${code}</template>`,
114+
options,
115+
output: `<template>${output}</template>`,
116+
errors,
117+
})),
118+
],
119+
});
120+
121+
const hbsRuleTester = new RuleTester({
122+
parser: require.resolve('ember-eslint-parser/hbs'),
123+
parserOptions: { ecmaVersion: 2022, sourceType: 'module' },
124+
});
125+
126+
hbsRuleTester.run('template-require-input-type', rule, {
127+
valid: [...validHbs, ...requireExplicitValid],
128+
invalid: [...invalidHbs, ...requireExplicitInvalid],
129+
});

0 commit comments

Comments
 (0)