Skip to content

Commit 1b384a7

Browse files
committed
docs(glimmer-attribute-behavior): add "Reading attribute values in rules" guide
Adds a practical-implementation section between the reference table and the reproduction template. It maps each AST shape (GlimmerTextNode / GlimmerMustacheStatement with each path type / GlimmerConcatStatement) to a verdict, citing the row IDs from the reference table so rule authors can implement classification correctly without re-deriving the model. Includes: - AST-shape verdict table — direct mapping rule authors can copy from - Six common mistakes section, each tied to specific row IDs - Pointer to the (forthcoming) lib/utils/glimmer-attr-presence.js utility that will encode the verdict table once and let rules consume the resolved kind + value rather than re-walking the AST The audit of master rules and the open feature PRs found 18 REAL_BUG findings (12 in PRs, 6 in master) — all classifiable into the bullet-1 through bullet-4 footguns this guide enumerates.
1 parent e0a8d17 commit 1b384a7

1 file changed

Lines changed: 42 additions & 0 deletions

File tree

docs/glimmer-attribute-behavior.md

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,48 @@ A regular string attribute. Glimmer's bare-mustache **does not** apply falsy-coe
119119
- **Concat-mustache forks by attribute kind.** For HTML boolean attrs (`muted`, `disabled`), any concat — including `"{{false}}"`, `"{{'false'}}"`, `"x{{false}}"` — sets the IDL property to `true`, regardless of the literal value inside. For ARIA / string attrs (`aria-hidden`, `autocomplete`), concat renders the stringified value as the attribute value (no boolean coercion); `aria-hidden="{{false}}"` becomes `aria-hidden="false"` (visible).
120120
- **Concat is never falsy.** Across all attribute kinds tested, no concat form produces an absent attribute. Rules treating `attr="{{false}}"` as "off" are wrong for boolean attrs (it's IDL-true) and wrong for string attrs (the rendered value is `"false"`, attribute present).
121121

122+
## Reading attribute values in rules
123+
124+
Rule authors who classify attribute values must consume the reference table above through one of these AST shapes — each maps to a known rendering verdict:
125+
126+
| AST shape | Source examples | Verdict |
127+
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
128+
| `attr.value === null` (no value) | `<input disabled />` | Attribute is **present** with empty value (rendered as `attr=""`) — see d1, h1 |
129+
| `attr.value.type === 'GlimmerTextNode'` | `attr="literal text"` | Attribute is **present** with the literal `chars` string — see m1–m4, h2–h4, d1, t-static, i1 |
130+
| `attr.value.type === 'GlimmerMustacheStatement'` with `path.type === 'GlimmerBooleanLiteral'` and `path.value === true` | `attr={{true}}` | **Reflecting boolean attrs**: present (e.g., `disabled=""`). **Non-reflecting boolean attrs** (`muted`, `autoplay`, etc.): IDL property set true, HTML attribute omitted. **ARIA string attrs**: present as `attr=""`. **Numeric attrs**: untested. — see m5, d2, h5 |
131+
| `attr.value.type === 'GlimmerMustacheStatement'` with `path.type === 'GlimmerBooleanLiteral'` and `path.value === false` (or `GlimmerNullLiteral` / `GlimmerUndefinedLiteral`) | `attr={{false}}` / `{{null}}` / `{{undefined}}` | Attribute is **omitted** at runtime — see m6, m9, m10, d3, d6, h6, h9, h10, t6, t7. **Exception:** plain string attrs (e.g., `autocomplete`) do _not_ falsy-coerce; bare `{{false}}` renders as `attr="false"` (i4). |
132+
| `attr.value.type === 'GlimmerMustacheStatement'` with `path.type === 'GlimmerStringLiteral'` | `attr={{"value"}}` | Attribute is **present** with the literal `path.value` string — see m7, m8, h7, h8, d4, d5, i2 |
133+
| `attr.value.type === 'GlimmerMustacheStatement'` with `path.type === 'GlimmerNumberLiteral'` (verified for `tabindex` only) | `attr={{0}}` | **Numeric attrs**: present with stringified number (t1, t2, t3). **`muted={{0}}` is in the falsy-omit set** (m12); not yet tested for other kinds. |
134+
| `attr.value.type === 'GlimmerMustacheStatement'` with dynamic path | `attr={{this.x}}` | **Unknown** at lint time. |
135+
| `attr.value.type === 'GlimmerConcatStatement'` with all-literal parts | `attr="{{X}}"` (single literal-mustache) / `attr="text{{X}}"` | **Boolean HTML attrs** (`muted`, `disabled`, etc.): IDL true regardless of inner literal — m13–m19, d7–d10. **ARIA / string attrs** (`aria-hidden`, `autocomplete`): renders the stringified value literally — h12–h15, i3, i5. |
136+
| `attr.value.type === 'GlimmerConcatStatement'` with any dynamic part | `attr="{{this.x}}"` / `attr="x{{this.y}}"` | **Concat is never falsy** — present at runtime, but the value is generally **unknown** at lint time. |
137+
138+
### Common mistakes to avoid
139+
140+
1. **Don't use `findAttr(node, 'foo')` (AST-presence) as a proxy for "attribute set at runtime."** It's wrong for bare `{{false}}` / `{{null}}` / `{{undefined}}` on boolean-coerced attrs (m6/m9/m10/d3/d6/h6/h9/h10/t6/t7) — those forms still create an `AttrNode` in the AST but are omitted at runtime.
141+
2. **Don't lump `BooleanLiteral(false)` with `StringLiteral("false")`.** Bare `{{"false"}}` is a JS-truthy string; it renders the literal `"false"` value (i4, h8, d4, m8). Treating them together as "off" is the most common audit footgun in this codebase.
142+
3. **Don't treat single-mustache concat as the inner literal.** `attr="{{X}}"` is **never** falsy. For boolean HTML attrs the IDL property is set true regardless of `X`'s literal value (m14 verified: `<video muted="{{false}}">``videoEl.muted === true`). For ARIA/string attrs the rendered HTML is the stringified value (h13: `aria-hidden="{{false}}"``aria-hidden="false"`, visible per ARIA spec).
143+
4. **Don't apply boolean-coercion to plain string attrs.** `autocomplete`, `name`, `id`, `for`, `href`, `role`, `type`, `method`, `lang`, `title`, `alt` etc. do **not** falsy-coerce. Bare `{{false}}` on these renders the literal `"false"`. Plain-string attrs are documented under `i1``i5`; the falsy-coercion list (cross-attribute observations) covers HTML boolean attrs, ARIA attrs, and numeric attrs only.
144+
5. **`role` is plain string, not ARIA-coerced.** Despite living in the ARIA family conceptually, `role` is a plain string DOM attribute — bare `role={{false}}` renders `role="false"` (analogous to i4), not omitted.
145+
6. **`{{true}}` for `aria-hidden` (h5) renders `aria-hidden=""` — contested per ARIA spec, _not_ `aria-hidden="true"`.** Rules deciding "is this hidden?" should be explicit about which interpretation they take. Don't conflate h5 with h7 (`{{"true"}}` → renders the string `"true"`, hidden).
146+
147+
### Recommended pattern
148+
149+
A shared utility — `lib/utils/glimmer-attr-presence.js` (forthcoming) — encodes this table once. Rule authors should consume it rather than re-implementing the AST walk:
150+
151+
```js
152+
// Sketch of the API surface — actual implementation tracked separately.
153+
const { classifyAttribute } = require('../utils/glimmer-attr-presence');
154+
155+
const result = classifyAttribute(attr, {
156+
kind: 'boolean-coerced' /* or 'plain-string' / 'numeric' */,
157+
});
158+
// result.kind: 'absent' | 'omitted-bare-falsy' | 'static' | 'present-unknown'
159+
// result.value: string | null (only present when kind === 'static')
160+
```
161+
162+
Until the utility lands, follow the AST-shape table above directly and cite the specific row IDs in code comments where the classification logic lives.
163+
122164
## To reproduce the reference table
123165

124166
Every cell above was populated by rendering the template below in an Ember dev app and running the bundled JS console snippet to print each test case's `outerHTML` and IDL state. To re-verify (or extend with new attributes):

0 commit comments

Comments
 (0)