Repository navigation
lint: flag instance ids built by appending to a generated id - #44
Merged
Merged
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #44 +/- ##
=======================================
Coverage 95.72% 95.72%
=======================================
Files 20 20
Lines 1870 1870
=======================================
Hits 1790 1790
Misses 48 48
Partials 32 32 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
A component rendered per item used to get its instance id by hand:
id=f`@{Page.Row |> id}-@{row.ID}`, or rowID + "-" + row.ID with rowID
from structpages.ID. That id is no component's, so the htmx target
selectors can't match a request targeting it to Page.Row, and Props
can't read the key back. Key and KeyTarget generate the instance's id.
The new keyed check reads every id and hx-target value on an HTML
element in .gsx and .templ, part by part: a gsx f`…` template's text
and interpolations (gsxscan.Values), an expression value split at
string concatenation. Each expression joins exactly to its generated
code, through gsx's /*line*/ directives or templ's source map, as for
region. A part is a generated id when it is a call of ID, IDTarget, Key
or KeyTarget (filter, hand-written or *StructPages), or a local
variable assigned once, from one; a local assigned once from another
expression stands for it.
What follows decides it. A compile-time constant (-document, a named
constant) is the same in every render: a sub-element's id, not
flagged. Anything else is a runtime value, so the id is an instance's.
In an id the generated id must start the value. In an hx-target the
runtime value must sit inside the #id's token, after literal identifier
text extending it, so another compound of the selector doesn't count;
ID and Key count there only after a literal "#". A Key id extended at
runtime is flagged too: KeyOf reads the extra text as part of the key.
Component tags' attributes are props and aren't checked, as for region.
On eos the check reports 8 ids; the 17 other hand-built ones are props
of design-system components (<input.Input id=…>).
jackielii
force-pushed
the
feat/lint-keyed-ids
branch
from
October 29, 2026 20:42
bf49dcf to
d3d6dd2
Compare
This was referenced Oct 29, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Follow-up to #41, so it is based on
feat/keyed-targets. Retarget it tomainonce #41 merges.What
A new
structpages-lintcategory,keyed. It flags an elementidorhx-targetthat appends a runtime value to a generated id, which is how instance ids were built by hand beforeKey/KeyTarget:No component has such an id, so the htmx target selectors can't match a request targeting it to
Page.Row, and Props can't read the key back. The finding suggestsKey/KeyTarget(gsx|> key(k)/|> keytarget(k)), whose key Props reads withKeyOf.How it matches (no heuristics)
idandhx-targetvalue (any htmx spelling) on an HTML element in.gsxand.templ, part by part. A gsxf`…`template splits into its text and interpolations (newgsxscan.Values). An expression value splits at string+. Each expression joins exactly to its generated code through gsx/*line*/directives or templ's source map, asregiondoes (token-compared, so a failed join drops the attribute).ID/IDTarget/Key/KeyTarget(filter, hand-written, or*StructPagesmethod), or a local variable assigned once, from such a call. A local assigned once from another expression stands for that expression. "Once" means it's declared with a value and never assigned again, ranged over with=, or address-taken (&v, pointer-receiver method).-document, or a namedconst) is the same in every render, so the id names a sub-element and isn't flagged. Anything else (variable, field, call) varies per render, so it's an instance id and is flagged. This uses the type checker's constant-ness, not the suffix's spelling. One case it treats as an instance key: a role passed in at runtime (e.g. arole stringparam).id, the whole value is the id: the generated id must start it, and any runtime part after it counts.hx-target, the value is a selector. Only a runtime value inside the#idtoken counts: after literal CSS identifier characters that extend it (escapes handled), before any character that ends it. Sof`@{X |> target} [data-id="@{k}"]`isn't flagged.@{X |> target}@{sel}) might start another compound, so it isn't flagged.ID/Keycount inhx-targetonly after a literal#.f`@{X |> key(k)}-@{j}`is flagged:KeyOfwould read-jas part of the key.f`@{X |> key(k)}-title`isn't.region).fmt.Sprintf.Real-world run (eos @ 40aaf5a91)
8 findings, all genuine instance ids (
groups/index.gsx×4,users/person.gsx,datalinks/page.gsx,files/derivatives.gsx,files/viewer.gsx). The other 17 hand-built ids in eos are all props of design-system components (<input.Input id=…>,<button.Button hx-target=…>,<messageview.Scroller id=…>,<pickbox.Root id=…>), which this check skips, likeregion. Covering them precisely means following the prop into the component and checking that it reaches an element'sid. That's a separate change, if you want it.Tests
TestKeyed_Integration(testdata/keyed): gsxapp/and templtapp/fixtures with 24 flagged lines, plus quiet cases (constant roles, a role on a keyed id, another selector compound, other attributes, a reassigned variable, a component prop, suppression).TestRuntimeInToken/TestEndsWithHashunit-test the selector token scan, including CSS escapes.cd tools/lint && go test ./...: everything passes exceptTestRegion_Integration, which fails as onmainand the base branch (the same 8.templfindings missing; this PR doesn't change them). The new templ fixture was generated with templ v0.3.1020 and is checked.golangci-lint run ./...intools/lint: the 7 pre-existing issues, none new.Docs: a
keyedrow and a "Howkeyeddecides" section indocs/lint.md, a row inskills/structpages/reference.md§Lint, and a pointer from the keyed-components section ofdocs/htmx.md. No release.