From 9e8e45ea51325da24e9172919872d31cb55ece63 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 27 Apr 2026 19:16:29 +0000 Subject: [PATCH 01/15] Prepare Release v13.1.4 using 'release-plan' --- .release-plan.json | 6 +++--- CHANGELOG.md | 17 +++++++++++++++++ package.json | 2 +- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/.release-plan.json b/.release-plan.json index 83a94f63a8..2549a7d96b 100644 --- a/.release-plan.json +++ b/.release-plan.json @@ -2,8 +2,8 @@ "solution": { "eslint-plugin-ember": { "impact": "patch", - "oldVersion": "13.1.2", - "newVersion": "13.1.3", + "oldVersion": "13.1.3", + "newVersion": "13.1.4", "tagName": "latest", "constraints": [ { @@ -18,5 +18,5 @@ "pkgJSONPath": "./package.json" } }, - "description": "## Release (2026-04-25)\n\n* eslint-plugin-ember 13.1.3 (patch)\n\n#### :bug: Bug Fix\n* `eslint-plugin-ember`\n * [#2730](https://github.com/ember-cli/eslint-plugin-ember/pull/2730) BUGFIX: template-require-valid-alt-text — reject empty-string aria-label/labelledby/alt on , , ([@johanrd](https://github.com/johanrd))\n * [#2729](https://github.com/ember-cli/eslint-plugin-ember/pull/2729) BUGFIX: template-no-invalid-role — support DPUB/Graphics-ARIA and role-fallback lists ([@johanrd](https://github.com/johanrd))\n * [#2726](https://github.com/ember-cli/eslint-plugin-ember/pull/2726) BUGFIX: template-no-unsupported-role-attributes — honor aria-query attribute constraints ([@johanrd](https://github.com/johanrd))\n * [#2727](https://github.com/ember-cli/eslint-plugin-ember/pull/2727) BUGFIX: template-no-redundant-role — case-insensitive match + ` values that are not one of the input +types defined by the HTML spec, and (optionally) requires every `` to +declare a `type` attribute. + +An invalid value like `` silently falls back to the Text +state — the browser reports no error, but the author's intent (validation, +inputmode hint, platform keyboard) is lost. That's a genuine silent-failure +class, which this rule always flags and auto-fixes to `type="text"`. + +A missing `type` attribute (``) is _spec-compliant_ — the +missing-value default is the Text state — so flagging it is a style / +consistency choice, not a correctness one. Opt in with `requireExplicit: true` +if your team wants parity with `template-require-button-type`. + +## Examples + +This rule **forbids** the following (always): + +```hbs + + + +``` + +With `requireExplicit: true` the rule **also forbids**: + +```hbs + + +``` + +This rule **allows** the following: + +```hbs + + + + +``` + +Dynamic values such as `type={{this.inputType}}` are not flagged at lint time. + +## Configuration + +- `requireExplicit` (`boolean`, default `false`): when true, also flag + `` elements that have no `type` attribute. Auto-fix inserts + `type="text"`. + +```js +module.exports = { + rules: { + 'ember/template-require-input-type': ['error', { requireExplicit: true }], + }, +}; +``` + +## References + +- [HTML spec — the input element](https://html.spec.whatwg.org/multipage/input.html#the-input-element) +- Adapted from [`html-validate`'s `no-implicit-input-type`](https://html-validate.org/rules/no-implicit-input-type.html) (MIT). diff --git a/lib/rules/template-require-input-type.js b/lib/rules/template-require-input-type.js new file mode 100644 index 0000000000..270d5433fe --- /dev/null +++ b/lib/rules/template-require-input-type.js @@ -0,0 +1,145 @@ +'use strict'; + +// See html-validate (https://html-validate.org) for the peer rule concept. + +const { isNativeElement } = require('../utils/is-native-element'); + +const VALID_TYPES = new Set([ + 'button', + 'checkbox', + 'color', + 'date', + 'datetime-local', + 'email', + 'file', + 'hidden', + 'image', + 'month', + 'number', + 'password', + 'radio', + 'range', + 'reset', + 'search', + 'submit', + 'tel', + 'text', + 'time', + 'url', + 'week', +]); + +/** @type {import('eslint').Rule.RuleModule} */ +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'require input elements to have a valid type attribute', + category: 'Best Practices', + url: 'https://github.com/ember-cli/eslint-plugin-ember/tree/master/docs/rules/template-require-input-type.md', + templateMode: 'both', + }, + fixable: 'code', + schema: [ + { + type: 'object', + properties: { + requireExplicit: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + messages: { + missing: 'All `` elements should have a `type` attribute', + invalid: '`` is not a valid input type', + }, + }, + + create(context) { + // Flagging a missing `type` is a style/consistency check, not a correctness + // one: `` without `type` is spec-compliant (defaults to the Text + // state). Opt-in so teams that want parity with template-require-button- + // type can enable it without imposing it on others. + const requireExplicit = Boolean(context.options[0]?.requireExplicit); + const sourceCode = context.sourceCode || context.getSourceCode(); + + return { + GlimmerElementNode(node) { + if (node.tag !== 'input') { + return; + } + // In strict GJS, a lowercase local binding can shadow the native + // `` element. `isNativeElement` consults html/svg/mathml tag + // lists and checks bindings in the scope chain to filter out + // scope-shadowed cases. + if (!isNativeElement(node, sourceCode)) { + return; + } + + const typeAttr = node.attributes?.find((attr) => attr.name === 'type'); + + if (!typeAttr) { + if (!requireExplicit) { + return; + } + context.report({ + node, + messageId: 'missing', + fix(fixer) { + // Insert right after ``) — per HTML spec, a + // present-but-empty type attribute resolves to the missing-value + // default ("Text state"). That's the same runtime result as + // `type=""`, which we already flag. Treat them consistently: + // flag as invalid('') and autofix to `type="text"`. + if (!value) { + context.report({ + node: typeAttr, + messageId: 'invalid', + data: { value: '' }, + fix(fixer) { + return fixer.replaceText(typeAttr, 'type="text"'); + }, + }); + return; + } + + if (value.type === 'GlimmerTextNode') { + const typeValue = value.chars.toLowerCase(); + if (typeValue === '') { + context.report({ + node: typeAttr, + messageId: 'invalid', + data: { value: '' }, + fix(fixer) { + return fixer.replaceText(typeAttr, 'type="text"'); + }, + }); + } else if (!VALID_TYPES.has(typeValue)) { + context.report({ + node: typeAttr, + messageId: 'invalid', + data: { value: value.chars }, + fix(fixer) { + return fixer.replaceText(typeAttr, 'type="text"'); + }, + }); + } + } + }, + }; + }, +}; diff --git a/lib/utils/is-native-element.js b/lib/utils/is-native-element.js index 190374d9bb..ebdad3b9c4 100644 --- a/lib/utils/is-native-element.js +++ b/lib/utils/is-native-element.js @@ -21,19 +21,17 @@ const ELEMENT_TAGS = new Set([...htmlTags, ...svgTags, ...mathmlTagNames]); * MathML spec registries, reached via the `html-tags` / `svg-tags` / * `mathml-tag-names` packages). It is NOT the same as: * - * - "native accessibility" / "widget-ness" — see `interactive-roles.js` - * (aria-query widget taxonomy; an ARIA-tree-semantics question) - * - "native interactive content" / "focus behavior" — see - * `html-interactive-content.js` (HTML §3.2.5.2.7; an HTML-content-model - * question about which tags can be nested inside what) + * - "native accessibility" / "widget-ness" — an ARIA-tree-semantics + * question (for example, whether something maps to a widget role) + * - "native interactive content" / "focus behavior" — an HTML content-model + * question about which elements are considered interactive in the spec * - "natively focusable" / sequential-focus — see HTML §6.6.3 * * This util answers only: "is this tag a first-class built-in element of one * of the three markup-language standards, rather than a component invocation - * or a shadowed local binding?" Callers compose it with the other utils - * above when they need a more specific question (see e.g. `template-no- - * noninteractive-tabindex`, which consults both this and - * `html-interactive-content`). + * or a shadowed local binding?" Callers should combine it with whatever + * accessibility, interactivity, or focusability checks they need for more + * specific questions. * * Returns false for: * - components (PascalCase, dotted, @-prefixed, this.-prefixed, ::-namespaced — diff --git a/tests/lib/rules/template-require-input-type.js b/tests/lib/rules/template-require-input-type.js new file mode 100644 index 0000000000..5f8fa3e3e5 --- /dev/null +++ b/tests/lib/rules/template-require-input-type.js @@ -0,0 +1,129 @@ +const rule = require('../../../lib/rules/template-require-input-type'); +const RuleTester = require('eslint').RuleTester; + +const ERROR_MISSING = 'All `` elements should have a `type` attribute'; +const errInvalid = (value) => `\`\` is not a valid input type`; + +const validHbs = [ + '', + '', + '', + '', + '', + '', + '', + '
', + '
', + '', + // Default (requireExplicit=false): missing `type` is allowed. + '', + '', +]; + +const invalidHbs = [ + { + code: '', + output: '', + errors: [{ message: errInvalid('') }], + }, + { + code: '', + output: '', + errors: [{ message: errInvalid('foo') }], + }, + { + code: '', + output: '', + errors: [{ message: errInvalid('TEXTY') }], + }, + // Valueless type attribute — per HTML spec resolves to the missing-value + // default (Text state), same runtime result as `type=""`. Flag and autofix + // to `type="text"`. (Output loses the pre-slash space because the + // valueless attr range ends at `type`; prettier will re-insert if needed.) + { + code: '', + output: '', + errors: [{ message: errInvalid('') }], + }, +]; + +const requireExplicitInvalid = [ + { + code: '', + options: [{ requireExplicit: true }], + output: '', + errors: [{ message: ERROR_MISSING }], + }, + { + code: '', + options: [{ requireExplicit: true }], + output: '', + errors: [{ message: ERROR_MISSING }], + }, + { + code: '', + options: [{ requireExplicit: true }], + output: '', + errors: [{ message: ERROR_MISSING }], + }, +]; + +const requireExplicitValid = [ + // With requireExplicit: an explicit known type satisfies the rule. + { code: '', options: [{ requireExplicit: true }] }, + // Dynamic type also satisfies — we can't know the runtime value. + { code: '', options: [{ requireExplicit: true }] }, +]; + +const gjsValid = validHbs.map((code) => ``); +const gjsInvalid = invalidHbs.map(({ code, output, errors }) => ({ + code: ``, + output: ``, + errors, +})); + +const gjsRuleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser'), + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, +}); + +gjsRuleTester.run('template-require-input-type', rule, { + valid: [ + ...gjsValid, + ...requireExplicitValid.map(({ code, options }) => ({ + code: ``, + options, + })), + // Scope-shadowed `input` — the template's `` refers to the local + // const binding (a component), not the native HTML element. The rule + // skips it via `isNativeElement`'s scope check. + `const input = 'foo'; +`, + `const input = 'foo'; +`, + // Block-param shadowing — `` binds `input` inside the + // yield block. The inner `` should resolve to the block-param, + // not the native tag. + `import Foo from 'whatever'; +`, + ], + invalid: [ + ...gjsInvalid, + ...requireExplicitInvalid.map(({ code, options, output, errors }) => ({ + code: ``, + options, + output: ``, + errors, + })), + ], +}); + +const hbsRuleTester = new RuleTester({ + parser: require.resolve('ember-eslint-parser/hbs'), + parserOptions: { ecmaVersion: 2022, sourceType: 'module' }, +}); + +hbsRuleTester.run('template-require-input-type', rule, { + valid: [...validHbs, ...requireExplicitValid], + invalid: [...invalidHbs, ...requireExplicitInvalid], +}); From 493f15404ee8c0658a8349b9c2ad443627cb9417 Mon Sep 17 00:00:00 2001 From: NullVoxPopuli <199018+NullVoxPopuli@users.noreply.github.com> Date: Mon, 27 Apr 2026 23:08:30 -0400 Subject: [PATCH 03/15] Update parser --- package.json | 2 +- pnpm-lock.yaml | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package.json b/package.json index 44b48adb2a..9cefac377d 100644 --- a/package.json +++ b/package.json @@ -67,7 +67,7 @@ "axobject-query": "^4.1.0", "css-tree": "^3.0.1", "editorconfig": "^3.0.2", - "ember-eslint-parser": "^0.11.2", + "ember-eslint-parser": "^0.11.3", "ember-rfc176-data": "^0.3.18", "eslint-utils": "^3.0.0", "estraverse": "^5.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3df7119c27..fc56891f9f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -24,8 +24,8 @@ importers: specifier: ^3.0.2 version: 3.0.2 ember-eslint-parser: - specifier: ^0.11.2 - version: 0.11.2(@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@8.57.1))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(typescript@5.9.3) + specifier: ^0.11.3 + version: 0.11.3(@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@8.57.1))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(typescript@5.9.3) ember-rfc176-data: specifier: ^0.3.18 version: 0.3.18 @@ -1755,8 +1755,8 @@ packages: electron-to-chromium@1.5.344: resolution: {integrity: sha512-4MxfbmNDm+KPh066EZy+eUnkcDPcZ35wNmOWzFuh/ijvHsve6kbLTLURy88uCNK5FbpN+yk2nQY6BYh1GEt+wg==} - ember-eslint-parser@0.11.2: - resolution: {integrity: sha512-q38xuVA6OAYJU9zEyUazW9snG7igRWp62KfvJYoF191DHwzLnYnZzVd8u4iFS86s5RHPH1AAhVHJ+oqVTM3oOQ==} + ember-eslint-parser@0.11.3: + resolution: {integrity: sha512-tGLDVyemseglpXyt3MMnggWL2ROYglRGoL0lKVy+GySFcrD4YQbt5iyvrY9hcEnN62gmkFLIFRinIq5niqYKXg==} engines: {node: '>=16.0.0'} peerDependencies: '@babel/eslint-parser': ^7.28.6 @@ -1767,8 +1767,8 @@ packages: '@typescript-eslint/parser': optional: true - ember-estree@0.6.2: - resolution: {integrity: sha512-CeKa6FZ95jp6de+vjhmv2XmOGBB2V95iiH7RqI2av1nU0m1XhFZEG8npoEciklQqPEzFO5OXS41b0mGvXBsJCA==} + ember-estree@0.6.3: + resolution: {integrity: sha512-76NgApjUCvGHd6eC5BueOSwDdXtyO/+zSrZuKaWdRm3EyIbtE9ysvCOsm1cft8zl6dxdqeJpNpTJ5a1ljVIFMg==} ember-rfc176-data@0.3.18: resolution: {integrity: sha512-JtuLoYGSjay1W3MQAxt3eINWXNYYQliK90tLwtb8aeCuQK8zKGCRbBodVIrkcTqshULMnRuTOS6t1P7oQk3g6Q==} @@ -5707,12 +5707,12 @@ snapshots: electron-to-chromium@1.5.344: {} - ember-eslint-parser@0.11.2(@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@8.57.1))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(typescript@5.9.3): + ember-eslint-parser@0.11.3(@babel/eslint-parser@7.28.6(@babel/core@7.29.0)(eslint@8.57.1))(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0)(@typescript-eslint/parser@8.59.0(eslint@8.57.1)(typescript@5.9.3))(typescript@5.9.3): dependencies: '@glimmer/syntax': 0.95.0 '@typescript-eslint/tsconfig-utils': 8.59.1(typescript@5.9.3) content-tag: 4.1.1 - ember-estree: 0.6.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) + ember-estree: 0.6.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0) eslint-scope: 9.1.2 html-tags: 5.1.0 mathml-tag-names: 4.0.0 @@ -5725,7 +5725,7 @@ snapshots: - '@emnapi/runtime' - typescript - ember-estree@0.6.2(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): + ember-estree@0.6.3(@emnapi/core@1.10.0)(@emnapi/runtime@1.10.0): dependencies: '@glimmer/env': 0.1.7 '@glimmer/syntax': 0.95.0 From e5baa77385193f9eb8589e8988ed1861fc9d2117 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 28 Apr 2026 03:14:00 +0000 Subject: [PATCH 04/15] Prepare Release v13.2.0 using 'release-plan' --- .release-plan.json | 14 +++++++------- CHANGELOG.md | 16 ++++++++++++++++ package.json | 2 +- 3 files changed, 24 insertions(+), 8 deletions(-) diff --git a/.release-plan.json b/.release-plan.json index 2549a7d96b..a24cc968e3 100644 --- a/.release-plan.json +++ b/.release-plan.json @@ -1,22 +1,22 @@ { "solution": { "eslint-plugin-ember": { - "impact": "patch", - "oldVersion": "13.1.3", - "newVersion": "13.1.4", + "impact": "minor", + "oldVersion": "13.1.4", + "newVersion": "13.2.0", "tagName": "latest", "constraints": [ { - "impact": "patch", - "reason": "Appears in changelog section :bug: Bug Fix" + "impact": "minor", + "reason": "Appears in changelog section :rocket: Enhancement" }, { "impact": "patch", - "reason": "Appears in changelog section :house: Internal" + "reason": "Appears in changelog section :bug: Bug Fix" } ], "pkgJSONPath": "./package.json" } }, - "description": "## Release (2026-04-27)\n\n* eslint-plugin-ember 13.1.4 (patch)\n\n#### :bug: Bug Fix\n* `eslint-plugin-ember`\n * [#2752](https://github.com/ember-cli/eslint-plugin-ember/pull/2752) Update ember-eslint-parser to 0.11.2 ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n * [#2728](https://github.com/ember-cli/eslint-plugin-ember/pull/2728) BUGFIX: template-require-mandatory-role-attributes — lowercase role + split whitespace role lists ([@johanrd](https://github.com/johanrd))\n\n#### :house: Internal\n* `eslint-plugin-ember`\n * [#2748](https://github.com/ember-cli/eslint-plugin-ember/pull/2748) refactor: extract `html-interactive-content` util (HTML §3.2.5.2.7 authority) ([@johanrd](https://github.com/johanrd))\n\n#### Committers: 2\n- [@NullVoxPopuli](https://github.com/NullVoxPopuli)\n- [@johanrd](https://github.com/johanrd)\n" + "description": "## Release (2026-04-28)\n\n* eslint-plugin-ember 13.2.0 (minor)\n\n#### :rocket: Enhancement\n* `eslint-plugin-ember`\n * [#2763](https://github.com/ember-cli/eslint-plugin-ember/pull/2763) feat: add template-require-input-type ([@johanrd](https://github.com/johanrd))\n\n#### :bug: Bug Fix\n* `eslint-plugin-ember`\n * [#2766](https://github.com/ember-cli/eslint-plugin-ember/pull/2766) Update parser ([@NullVoxPopuli](https://github.com/NullVoxPopuli))\n\n#### Committers: 2\n- [@NullVoxPopuli](https://github.com/NullVoxPopuli)\n- [@johanrd](https://github.com/johanrd)\n" } diff --git a/CHANGELOG.md b/CHANGELOG.md index d1fc0ddc58..7ff7def2ca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## Release (2026-04-28) + +* eslint-plugin-ember 13.2.0 (minor) + +#### :rocket: Enhancement +* `eslint-plugin-ember` + * [#2763](https://github.com/ember-cli/eslint-plugin-ember/pull/2763) feat: add template-require-input-type ([@johanrd](https://github.com/johanrd)) + +#### :bug: Bug Fix +* `eslint-plugin-ember` + * [#2766](https://github.com/ember-cli/eslint-plugin-ember/pull/2766) Update parser ([@NullVoxPopuli](https://github.com/NullVoxPopuli)) + +#### Committers: 2 +- [@NullVoxPopuli](https://github.com/NullVoxPopuli) +- [@johanrd](https://github.com/johanrd) + ## Release (2026-04-27) * eslint-plugin-ember 13.1.4 (patch) diff --git a/package.json b/package.json index 9cefac377d..af6257b79c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "eslint-plugin-ember", - "version": "13.1.4", + "version": "13.2.0", "description": "ESLint plugin for Ember.js apps", "keywords": [ "eslint", From 6a8d7b73de41967b75d6f8896302f69e0a47835e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johan=20R=C3=B8ed?= Date: Tue, 28 Apr 2026 10:33:07 +0200 Subject: [PATCH 05/15] docs: add glimmer-attribute-behavior reference MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures empirically-verified Glimmer rendering behavior for HTML attributes with mustache values, so rule authors classifying GlimmerBooleanLiteral / GlimmerStringLiteral / GlimmerConcatStatement have a ground-truth reference instead of intuition. Notable findings the doc pins down: - attr={{"false"}} (bare string "false") renders as attr="false" — TRUTHY, not falsy as the literal suggests. - attr="{{false}}" (concat) sets the IDL property to true regardless of the literal value inside, even when HTML serialization shows nothing. Verified against