Add template-no-duplicate-form-names: flag duplicate form-control name within a form#43
Add template-no-duplicate-form-names: flag duplicate form-control name within a form#43johanrd wants to merge 4 commits into
template-no-duplicate-form-names: flag duplicate form-control name within a form#43Conversation
🏎️ Benchmark Comparison
Full mitata output |
There was a problem hiding this comment.
Pull request overview
Adds a new Ember template lint rule to detect duplicate static name attributes across submittable form controls within the same <form> scope (or template root when no enclosing form), with explicit allowances for radio groups and repeated submit controls.
Changes:
- Added
template-no-duplicate-form-namesrule implementation with per-form/root name tracking and control-type exceptions. - Added comprehensive rule tests for both GJS (
<template>) and HBS parsing modes. - Added rule documentation and listed the rule in the README rules table.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.
| File | Description |
|---|---|
lib/rules/template-no-duplicate-form-names.js |
Implements the new duplicate-form-control-name detection rule with form scoping and type-based exceptions. |
tests/lib/rules/template-no-duplicate-form-names.js |
Adds valid/invalid test cases for the rule in both GJS and HBS modes. |
docs/rules/template-no-duplicate-form-names.md |
Documents rule behavior, rationale, and limitations. |
README.md |
Adds the rule to the rules index/table. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 4 out of 4 changed files in this pull request and generated 5 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 8 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…tton type fallback - lib/utils/control-flow.js: header comment & createConditionalScope example updated to reflect post-PR API (isBranchingBlockStatement / isConditionalBlockBranch / each-with-else support) - tests/lib/utils/control-flow-test.js: drop unused imports (isConditional BlockBranch, setParent) - lib/rules/template-no-duplicate-form-names.js: header comment now reflects the full 'share category' taxonomy (radio group + submit-like mix); getControlType treats invalid/unknown button[type] as 'submit' per HTML §4.10.9 default, and invalid/unknown input[type] as 'text' per HTML §4.10.5 - docs/rules/template-no-duplicate-form-names.md: mention input[type=image] and bare <button> explicitly; rewrite the share-category explanation
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 7 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…tton type fallback - lib/utils/control-flow.js: header comment & createConditionalScope example updated to reflect post-PR API (isBranchingBlockStatement / isConditionalBlockBranch / each-with-else support) - tests/lib/utils/control-flow-test.js: drop unused imports (isConditional BlockBranch, setParent) - lib/rules/template-no-duplicate-form-names.js: header comment now reflects the full 'share category' taxonomy (radio group + submit-like mix); getControlType treats invalid/unknown button[type] as 'submit' per HTML §4.10.9 default, and invalid/unknown input[type] as 'text' per HTML §4.10.5 - docs/rules/template-no-duplicate-form-names.md: mention input[type=image] and bare <button> explicitly; rewrite the share-category explanation
b24e352 to
152f800
Compare
152f800 to
31c618c
Compare
…rol name within a form
b8c7c40 to
0c19b04
Compare
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 8 out of 8 changed files in this pull request and generated 2 comments.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (t.kind === 'absent') { | ||
| return 'submit'; | ||
| } | ||
| // Dynamic or empty type (mustache / concat / valueless) — the runtime | ||
| // value is unknown. Return a sentinel so the caller can skip duplicate- | ||
| // name checks for this node rather than baking in a wrong default. |
There was a problem hiding this comment.
getControlType() treats a valueless <button type> (represented as an attribute with no value, i.e. t.kind === 'empty') as unknown and skips duplicate-name checks for that button. In HTML, an empty/invalid type value falls back to the default (submit for <button>), so this creates a false negative. Consider treating t.kind === 'empty' the same as absent (default to submit) for <button> type parsing, and reserve unknown for truly dynamic values (mustache/concat).
| if (t.kind === 'absent') { | |
| return 'submit'; | |
| } | |
| // Dynamic or empty type (mustache / concat / valueless) — the runtime | |
| // value is unknown. Return a sentinel so the caller can skip duplicate- | |
| // name checks for this node rather than baking in a wrong default. | |
| if (t.kind === 'absent' || t.kind === 'empty') { | |
| return 'submit'; | |
| } | |
| // Dynamic type (mustache / concat) — the runtime value is unknown. | |
| // Return a sentinel so the caller can skip duplicate-name checks for | |
| // this node rather than baking in a wrong default. |
There was a problem hiding this comment.
DISAGREE — getControlType handles kind:'empty' for input → 'text'. No false negative here.
| conditionalStack.at(-1).add(key); | ||
| } | ||
| } else { | ||
| seenStack.push(keys); |
There was a problem hiding this comment.
createConditionalScope.exitConditional() promotes keys from a finished conditional by seenStack.push(keys), which grows seenStack by one Set per conditional in the template. Since has() linearly scans seenStack, this can degrade to O(#conditionals) per lookup in large templates. You can avoid this unbounded stack growth by merging keys into the current outer set (e.g. seenStack.at(-1)) when leaving the outermost conditional instead of pushing a new set.
| seenStack.push(keys); | |
| for (const key of keys) { | |
| seenStack.at(-1).add(key); | |
| } |
Note
This is part of a series where Claude has audited
eslint-plugin-emberagainst jsx-a11y, vuejs-accessibility, angular-eslint, lit-a11y and html-validate,ember-template-lint, and the HTML and WCAG specs.Summary
Add
template-no-duplicate-form-names: flag two form controls sharing anameattribute within the same<form>(or template root if no enclosing form). Radio groups and repeated submit/reset/button types are explicitly allowed to share a name.nameboth contribute, and server-side frameworks that expect a single value will silently see one — usually not the one the author intended.<input type="button"|"reset">,<button type="button"|"reset">. These do not contribute to the form-data entry list at all, so theirnamecan't collide with anything.<input type="radio">,<input type="submit">,<button type="submit">(and<button>with default type). Sharing a name with same-type siblings is the legitimate pattern: a radio group, or multiple submits distinguished byvalue.nameis a real collision.namewithin the same<form>scope. Skip dynamic names (name={{this.field}}) anddisabledcontrols.What the rule does not try to detect
The rule does not try to answer "does this form actually submit data?" That question is not statically decidable from HBS alone:
<form method="get|post">defaults to native submission, butevent.preventDefault()in a{{on "submit"}}handler cancels it.<form method="dialog">defaults to dialog-close without constructing an entry list — but a{{on "submit"}}handler can still callnew FormData(form)and observe duplicate names there. The FormData construction algorithm doesn't check the form'smethod.form.submit()called from JS never fires a submit event, so form attributes are irrelevant to that path.So we restrict ourselves to a control-level invariant (the skip condition above), which holds regardless of how — or whether — the form actually submits. A duplicate name on
<input type="button">can never collide with anything in any construction path. Duplicate regular-control names can, in any path that reads FormData, regardless of form method.Fix
lib/rules/template-no-duplicate-form-names.js; tests intests/lib/rules/template-no-duplicate-form-names.js(44 cases).Map<name, {type}>populated as we visitGlimmerElementNodes.findEnclosingFormOrRootwalks parents for the scoping form; absent form, the template root is the scope.NON_SUBMITTING_TYPES = {button, reset}—<input>/<button>of these types are skipped before the map is even consulted.SHARED_NAME_TYPES = {radio, submit}— activation-only groups: same type in the same scope is allowed; mixing with a different shared type, or with a regular control, is flagged.Prior art
no-duplicate-attributesnameacross controls.form-dup-name— specname[]array handling, hidden-input-default-value pattern, and a complete form-associated-element registry. Our port is a subset.Peer absence verified by
grep -rli "form-dup\|dup-name\|duplicate.*name"on 2026-04-22.Flags
Allows
Edge case — not flagged (not endorsed either)
Why not flagged:
<input type="button">is spec-guaranteed to contribute nothing to FormData construction per HTML §4.10.22.4 step 5 — the algorithm skips it regardless of who invokes it (native submit,{{on "submit"}}handler,new FormData(form)called directly from JS, etc.). So there is no entry-list collision — thetextinput is the only contributor of namex.This is not an endorsement of the pattern. Putting the same
nameon a button and a real field is weird and probably a mistake. The rule doesn't flag it only because the no-collision claim is airtight at the spec level. If you find yourself writing this, consider whether the button really needs anameattribute — usually it doesn't.Notes
name[]array syntax; the<input type="hidden">+<input type="checkbox">default-value pattern. Both are rarely seen in Ember apps; can be added on demand.