Skip to content

Add template-valid-input-attributes: flag input attributes incompatible with declared type#46

Closed
johanrd wants to merge 1 commit into
masterfrom
html-validate/template-valid-input-attributes
Closed

Add template-valid-input-attributes: flag input attributes incompatible with declared type#46
johanrd wants to merge 1 commit into
masterfrom
html-validate/template-valid-input-attributes

Conversation

@johanrd

@johanrd johanrd commented Apr 22, 2026

Copy link
Copy Markdown
Owner

Note

This is part of a series where Claude has audited eslint-plugin-ember against jsx-a11y, vuejs-accessibility, angular-eslint, lit-a11y and html-validate, ember-template-lint, and the HTML and WCAG specs.

Summary

Add template-valid-input-attributes: flag <input> attributes whose applicability depends on the type when the declared type doesn't permit them. For example, pattern on type="number" is silently ignored by the browser.

  • Premise 1: HTML spec §4.10.5.1 — States of the type attribute pins most attributes to specific type states. pattern is only defined for Text / Search / URL / Telephone / E-mail / Password. accept is only for File Upload. min/max/step apply only to Date / Month / Week / Time / Local Date and Time / Number / Range. The spec's per-state tables (e.g. §4.10.5.1.2 Text state and Search state) enumerate exactly which attributes each state accepts.
  • Premise 2: When an author writes <input type="number" pattern="\d+">, the pattern attribute is ignored by the browser — it doesn't cause a validation error, but the author's expectation isn't met. This is the "silent failure" class of bug that html-validate's input-attributes rule catches.
  • Conclusion: For each <input> with a static type, look up the attribute/type compatibility table (ported verbatim from html-validate) and flag attributes that aren't defined on that type. Skip inputs without a static type (the author's type={{this.t}} can be anything).

Fix

  • New rule lib/rules/template-valid-input-attributes.js; tests in tests/lib/rules/template-valid-input-attributes.js (38 cases).
  • RESTRICTED table (template-valid-input-attributes.js:3-90) maps each restricted attribute (accept, alt, capture, checked, dirname, height, list, max, maxlength, min, minlength, multiple, pattern, placeholder, readonly, required, size, src, step, width) to the set of type states that accept it.
  • Visitor iterates the element's attributes, checks the table, reports incompatible pairs.

Prior art

Plugin Equivalent Verified behavior
jsx-a11y No equivalent rule.
vuejs-accessibility No equivalent rule.
lit-a11y No equivalent rule.
@angular-eslint/template No equivalent rule.
html-validate input-attributesspec Direct port. Same attribute/type table, same skip-on-dynamic-type behavior.

Peer absence verified by grep -rli "input-attributes" on 2026-04-22.

Flags

<input type='number' pattern='\d+' />         {{! pattern only on text-like types }}
<input type='text' accept='image/*' />        {{! accept only on file }}
<input type='radio' maxlength='10' />         {{! maxlength only on text-like types }}
<input type='checkbox' placeholder='label' /> {{! placeholder only on text-like types }}

Allows

<input type='text' pattern='\d+' />
<input type='file' accept='image/*' multiple />
<input type='number' min='0' max='100' step='1' />
<input type='image' alt='submit' src='/submit.png' />

{{! Dynamic type — skipped }}
<input type={{this.t}} pattern='\d+' />

{{! No explicit type — skipped (can't verify what "default text" means for author intent) }}
<input pattern='\d+' />

Notes

  • Opt-in: not added to any preset config.
  • Attribute/type table is ported verbatim from html-validate. Spot-verified against HTML §4.10.5.1.X per-state tables on 2026-04-22 for: pattern (text-like only), accept (file only), min/max/step (date/month/week/time/datetime-local/number/range), readonly (not applicable to checkbox/radio/file/hidden/submit/image/reset/button/color/range), required (not applicable to hidden/range/color/submit/image/reset/button). No divergence found in the sampled rows. If WHATWG adds a new input state, both tables will need updating.

@github-actions

github-actions Bot commented Apr 22, 2026

Copy link
Copy Markdown

🏎️ Benchmark Comparison

Benchmark Control (p50) Experiment (p50) Δ
🟢 js small 13.70 ms 12.40 ms -9.5%
js medium 6.03 ms 6.01 ms -0.3%
🟠 js large 2.25 ms 2.32 ms +3.4%
gjs small 1.17 ms 1.17 ms +0.2%
gjs medium 589.78 µs 586.02 µs -0.6%
gjs large 232.08 µs 230.88 µs -0.5%
gts small 1.17 ms 1.17 ms -0.6%
gts medium 583.25 µs 583.53 µs +0.0%
gts large 233.19 µs 232.03 µs -0.5%

🟢 faster · 🔴 slower · 🟠 slightly slower · ⚪ within 2%

Full mitata output
clk: ~3.35 GHz
cpu: Intel(R) Xeon(R) Platinum 8370C CPU @ 2.80GHz
runtime: node 24.15.0 (x64-linux)

benchmark                   avg (min … max) p75 / p99    (min … top 1%)
------------------------------------------- -------------------------------
js small (control)            15.34 ms/iter  16.44 ms █                    
                      (10.70 ms … 28.93 ms)  28.22 ms █ ▇  ▅               
                    (  5.69 mb …  10.56 mb)   7.22 mb ███▃██▃▆▃▁▁▃▁▆▃▁▃▁▁▃▃

js small (experiment)         12.93 ms/iter  13.79 ms    █                 
                      (10.95 ms … 17.60 ms)  17.22 ms ▆▆▆█▆▆▃▃     ▃       
                    (  6.75 mb …   7.85 mb)   6.83 mb █████████▄█▄██▁▄▁▁▁▁█

                             ┌                                            ┐
                             ╷┌──────────┬──┐                             ╷
          js small (control) ├┤          │  ├─────────────────────────────┤
                             ╵└──────────┴──┘                             ╵
                              ╷ ┌──┬─┐        ╷
       js small (experiment)  ├─┤  │ ├────────┤
                              ╵ └──┴─┘        ╵
                             └                                            ┘
                             10.70 ms           19.46 ms           28.22 ms

summary
  js small (experiment)
   1.19x faster than js small (control)

------------------------------------------- -------------------------------
js medium (control)            6.78 ms/iter   6.98 ms  █                   
                       (5.53 ms … 13.51 ms)  13.40 ms  █                   
                    (  2.48 mb …   4.65 mb)   3.56 mb ██▅▄▃▄▃▂▃▁▂▁▁▁▂▁▁▂▁▂▂

js medium (experiment)         6.68 ms/iter   6.93 ms  █                   
                       (5.67 ms … 12.22 ms)  11.87 ms ▃█                   
                    (  2.25 mb …   4.81 mb)   3.54 mb ███▃▄▄▃▂▃▂▃▂▁▁▁▂▁▁▁▂▂

                             ┌                                            ┐
                             ╷ ┌────┬┐                                    ╷
         js medium (control) ├─┤    │├────────────────────────────────────┤
                             ╵ └────┴┘                                    ╵
                              ╷┌────┬┐                           ╷
      js medium (experiment)  ├┤    │├───────────────────────────┤
                              ╵└────┴┘                           ╵
                             └                                            ┘
                             5.53 ms            9.46 ms            13.40 ms

summary
  js medium (experiment)
   1.02x faster than js medium (control)

------------------------------------------- -------------------------------
js large (control)             2.73 ms/iter   2.56 ms █▆                   
                        (2.09 ms … 9.74 ms)   6.88 ms ██                   
                    ( 49.80 kb …   3.15 mb)   1.43 mb ██▆▃▃▃▂▃▂▂▁▂▁▁▂▂▁▂▁▁▂

js large (experiment)          2.57 ms/iter   2.40 ms █▄                   
                        (2.23 ms … 6.97 ms)   5.75 ms ██                   
                    (383.85 kb …   2.49 mb)   1.42 mb ██▃▂▂▂▂▂▂▂▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷┌────┬                                      ╷
          js large (control) ├┤    │──────────────────────────────────────┤
                             ╵└────┴                                      ╵
                              ╷┌──┬                            ╷
       js large (experiment)  ├┤  │────────────────────────────┤
                              ╵└──┴                            ╵
                             └                                            ┘
                             2.09 ms            4.48 ms             6.88 ms

summary
  js large (experiment)
   1.06x faster than js large (control)

------------------------------------------- -------------------------------
gjs small (control)            1.30 ms/iter   1.22 ms █                    
                        (1.14 ms … 6.03 ms)   5.58 ms █                    
                    (324.09 kb …   1.81 mb)   1.06 mb █▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs small (experiment)         1.30 ms/iter   1.21 ms █                    
                        (1.14 ms … 7.01 ms)   5.82 ms █                    
                    (222.86 kb …   1.91 mb)   1.06 mb █▃▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                         ╷
         gjs small (control) ││─────────────────────────────────────────┤
                             └┴                                         ╵
                             ┌─┬                                          ╷
      gjs small (experiment) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             └                                            ┘
                             1.14 ms            3.48 ms             5.82 ms

summary
  gjs small (control)
   1x faster than gjs small (experiment)

------------------------------------------- -------------------------------
gjs medium (control)         643.23 µs/iter 602.91 µs █                    
                      (561.23 µs … 5.31 ms)   3.04 ms █                    
                    ( 72.05 kb …   1.26 mb) 541.53 kb █▄▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gjs medium (experiment)      635.34 µs/iter 597.55 µs █                    
                      (559.00 µs … 5.63 ms)   2.35 ms █                    
                    ( 98.55 kb …   1.04 mb) 540.04 kb █▆▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌─┬                                          ╷
        gjs medium (control) │ │──────────────────────────────────────────┤
                             └─┴                                          ╵
                             ┌┬                               ╷
     gjs medium (experiment) ││───────────────────────────────┤
                             └┴                               ╵
                             └                                            ┘
                             559.00 µs           1.80 ms            3.04 ms

summary
  gjs medium (experiment)
   1.01x faster than gjs medium (control)

------------------------------------------- -------------------------------
gjs large (control)          255.48 µs/iter 244.71 µs  █▂                  
                      (222.21 µs … 5.38 ms) 298.40 µs  ██▅                 
                    (170.93 kb … 990.13 kb) 217.34 kb ▄███▅▆██▅▂▂▂▂▂▁▁▁▁▁▁▁

gjs large (experiment)       255.57 µs/iter 244.08 µs  █▂                  
                      (222.00 µs … 5.26 ms) 297.01 µs  ██   ▂              
                    (143.56 kb … 901.09 kb) 216.53 kb ▅██▇▄▇█▇▄▂▃▂▂▂▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷  ┌────────────────┬                        ╷
         gjs large (control) ├──┤                │────────────────────────┤
                             ╵  └────────────────┴                        ╵
                             ╷  ┌────────────────┬                       ╷
      gjs large (experiment) ├──┤                │───────────────────────┤
                             ╵  └────────────────┴                       ╵
                             └                                            ┘
                             222.00 µs         260.20 µs          298.40 µs

summary
  gjs large (control)
   1x faster than gjs large (experiment)

------------------------------------------- -------------------------------
gts small (control)            1.28 ms/iter   1.22 ms █                    
                        (1.14 ms … 6.15 ms)   5.60 ms █                    
                    (280.71 kb …   1.85 mb)   1.06 mb █▅▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts small (experiment)         1.27 ms/iter   1.20 ms █                    
                        (1.14 ms … 6.01 ms)   5.28 ms █                    
                    (  2.85 kb …   2.12 mb)   1.05 mb █▅▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                           ╷
         gts small (control) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             ┌┬                                        ╷
      gts small (experiment) ││────────────────────────────────────────┤
                             └┴                                        ╵
                             └                                            ┘
                             1.14 ms            3.37 ms             5.60 ms

summary
  gts small (experiment)
   1.01x faster than gts small (control)

------------------------------------------- -------------------------------
gts medium (control)         639.75 µs/iter 593.80 µs █                    
                      (558.96 µs … 6.41 ms)   3.19 ms █                    
                    (143.26 kb …   1.56 mb) 541.86 kb █▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

gts medium (experiment)      635.96 µs/iter 599.67 µs █                    
                      (558.38 µs … 5.72 ms)   1.82 ms █▆                   
                    (106.50 kb …   1.04 mb) 539.95 kb ██▂▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁▁

                             ┌                                            ┐
                             ┌┬                                           ╷
        gts medium (control) ││───────────────────────────────────────────┤
                             └┴                                           ╵
                             ┌┬                    ╷
     gts medium (experiment) ││────────────────────┤
                             └┴                    ╵
                             └                                            ┘
                             558.38 µs           1.88 ms            3.19 ms

summary
  gts medium (experiment)
   1.01x faster than gts medium (control)

------------------------------------------- -------------------------------
gts large (control)          255.31 µs/iter 245.41 µs  █                   
                      (222.67 µs … 5.11 ms) 295.97 µs  ██▆  ▄              
                    (175.99 kb … 693.12 kb) 216.91 kb ▄███▅▇██▆▃▂▃▂▂▁▁▁▁▁▁▁

gts large (experiment)       256.90 µs/iter 247.09 µs  █▂                  
                      (223.63 µs … 5.30 ms) 292.51 µs  ██    ▂             
                    (170.17 kb …   1.27 mb) 216.72 kb ▄██▆▄▄▇█▇▃▂▂▂▂▂▁▁▁▁▁▁

                             ┌                                            ┐
                             ╷  ┌────────────────┬                        ╷
         gts large (control) ├──┤                │────────────────────────┤
                             ╵  └────────────────┴                        ╵
                              ╷ ┌─────────────────┬                     ╷
      gts large (experiment)  ├─┤                 │─────────────────────┤
                              ╵ └─────────────────┴                     ╵
                             └                                            ┘
                             222.67 µs         259.32 µs          295.97 µs

summary
  gts large (control)
   1.01x faster than gts large (experiment)

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new opt-in template rule to detect <input> attributes that are incompatible with a statically-declared type, preventing “silently ignored by the browser” authoring mistakes in Ember templates.

Changes:

  • Added template-valid-input-attributes rule implementing an attribute/type compatibility check for static type values.
  • Added comprehensive rule tests for both .gjs and .hbs parsing modes.
  • Added rule documentation and listed the new rule in the README.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.

File Description
lib/rules/template-valid-input-attributes.js Implements the rule logic and the restricted attribute/type table.
tests/lib/rules/template-valid-input-attributes.js Adds test coverage for allowed/forbidden combinations in both template modes.
docs/rules/template-valid-input-attributes.md Documents the rule’s intent, examples, and references.
README.md Adds the rule to the “Possible Errors” rules table.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-valid-input-attributes.js Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-valid-input-attributes.js
Comment thread lib/rules/template-valid-input-attributes.js
Comment thread lib/rules/template-valid-input-attributes.js
Comment thread tests/lib/rules/template-valid-input-attributes.js

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread docs/rules/template-valid-input-attributes.md Outdated
Comment thread tests/lib/rules/template-valid-input-attributes.js
Comment thread lib/rules/template-valid-input-attributes.js
Comment thread lib/rules/template-valid-input-attributes.js Outdated
johanrd added a commit that referenced this pull request Apr 24, 2026
@johanrd
johanrd requested a review from Copilot April 24, 2026 13:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-valid-input-attributes.js

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@johanrd
johanrd requested a review from Copilot April 24, 2026 18:57

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

@johanrd
johanrd requested a review from Copilot April 26, 2026 08:42

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot encountered an error and was unable to review this pull request. You can try again by re-requesting a review.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread lib/rules/template-valid-input-attributes.js
johanrd added a commit that referenced this pull request Apr 27, 2026
@johanrd
johanrd force-pushed the html-validate/template-valid-input-attributes branch from 1304651 to c152804 Compare April 27, 2026 14:01
@johanrd
johanrd force-pushed the html-validate/template-valid-input-attributes branch from d6d34d5 to a48260c Compare April 27, 2026 19:32
@johanrd
johanrd requested a review from Copilot April 27, 2026 19:34

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated 4 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +153 to +166
for (const attr of node.attributes || []) {
const validTypes = RESTRICTED.get(attr.name);
if (!validTypes) {
continue;
}
if (validTypes.has(type)) {
continue;
}
context.report({
node: attr,
messageId: 'incompatible',
data: { attr: attr.name, type },
});
}

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTML attribute names are ASCII case-insensitive, but the lookup uses attr.name as-is. This can miss invalid attributes written in uppercase/mixed case (e.g. ACCEPT, MaxLength). Normalize attribute names (e.g. const attrName = attr.name.toLowerCase()) before RESTRICTED.get(...), and consider reporting the normalized name for consistent messaging.

Copilot uses AI. Check for mistakes.
Comment on lines +132 to +135
// Per HTML §4.10.5, an <input> with missing, valueless, empty, or
// unknown `type` falls back to the Text state. Only a DYNAMIC type
// (mustache/concat) is opaque to static analysis — skip those.
const typeAttr = node.attributes?.find((a) => a.name === 'type');

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR description says inputs with no explicit type are skipped, but the implementation does not skip them—it treats missing/valueless/empty/unknown type as the Text state and validates accordingly (as these comments describe). Please align the PR description with the implemented behavior so reviewers/users don’t get conflicting expectations.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DISAGREE — the current implementation treats missing type as 'text' (per spec). The PR description was updated to match. See disagree.md entry 9.

Comment on lines +136 to +151
let normalized;
if (!typeAttr || !typeAttr.value) {
// Missing attribute OR valueless `<input type />` — Text state.
normalized = '';
} else if (typeAttr.value.type === 'GlimmerTextNode') {
normalized = typeAttr.value.chars.trim().toLowerCase();
} else if (
typeAttr.value.type === 'GlimmerMustacheStatement' &&
typeAttr.value.path?.type === 'GlimmerStringLiteral'
) {
normalized = typeAttr.value.path.value.trim().toLowerCase();
} else {
// Dynamic value — can't statically determine; skip.
return;
}
const type = KNOWN_INPUT_TYPES.has(normalized) ? normalized : 'text';

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

normalized = '' is being used as a sentinel for “missing/valueless type”, but it’s not itself a meaningful type value. Setting normalized directly to 'text' in that branch (or using a more explicitly named variable like rawTypetype) would make the control flow easier to read and reduce reliance on magic values.

Copilot uses AI. Check for mistakes.
code: '<input type="text" accept="image/*" />',
errors: [{ message: err('accept', 'text') }],
},
{

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given the rule should follow HTML’s case-insensitive attribute semantics, add a test case that uses an uppercase/mixed-case restricted attribute name (e.g. <input type=\"text\" ACCEPT=\"image/*\" />) and assert it is flagged. This will prevent regressions if attr.name casing varies by parser or template authoring style.

Suggested change
{
{
code: '<input type="text" ACCEPT="image/*" />',
errors: [{ message: err('accept', 'text') }],
},
{

Copilot uses AI. Check for mistakes.
@johanrd johanrd closed this Apr 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants