Skip to content

fix(compiler): drop a namespace-qualified attribute selector - #449

Open
YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:fix/attribute-selector-namespaces
Open

fix(compiler): drop a namespace-qualified attribute selector#449
YevheniiKotyrlo wants to merge 1 commit into
nativewind:mainfrom
YevheniiKotyrlo:fix/attribute-selector-namespaces

Conversation

@YevheniiKotyrlo

@YevheniiKotyrlo YevheniiKotyrlo commented Sep 10, 2026

Copy link
Copy Markdown
Contributor

Summary

component.namespace is discarded, so a namespace-qualified attribute selector matches the unqualified set instead of nothing — including an undeclared prefix, which Selectors L3 §6.3.3 makes an invalid selector.

Problem

Measured — all five forms compile to the identical query ["d", "x", "=", "a"]:

selector Selectors §6 before
[data-x='a'] attributes in no namespace matches — correct
[|data-x='a'] the same set, explicitly matches — correct
[*|data-x='a'] attributes in any namespace matches — correct here, see below
[ns|data-x='a'] with @namespace ns … only attributes in ns matches
[undeclared|data-x='a'] §6.3.3 — invalid, so the selector is dropped matches

The last row is the worse half: a rule the author's own stylesheet says cannot apply was applying. lightningcss reports the prefix rather than rejecting it, so dropping it is this compiler's job.

Solution

A specific namespace can never match, because no React Native prop carries one — so the selector is dropped, at both attribute build sites.

The first three rows are untouched and stay correct for a reason worth stating: with no namespaced props in the tree, "no namespace" and "any namespace" denote the same set, so [*|att] needs no special handling.

function isNamespacedAttribute(component): boolean {
  return component.namespace?.type === "specific";
}

Test plan

Five cases in src/__tests__/native/attributes.test.tsx — the three that must keep matching, plus a declared and an undeclared prefix. Mutation-proved: making the predicate false turns exactly the last two red.

yarn typecheck and yarn lint clean; yarn test 1053 passed, 3 failed, 21 skipped. The three failures are two babel suites that fail identically on an untouched main worktree (Windows-only module-specifier rewrites) — this touches no babel file.

Note

Independent of #447 and #448, though all three touch the same two build sites — whichever lands last needs a trivial rebase.

Selectors §6 — `[ns|att]` represents only attributes in `ns`. React Native
has no namespaced props, so nothing can match, and `component.namespace`
was discarded: every one of these compiled to the same query as the
unqualified form and matched the unqualified set instead.

An UNDECLARED prefix reaches the same path. lightningcss passes it through
rather than rejecting it, and Selectors L3 §6.3.3 makes such a selector
invalid, so a rule the author's own stylesheet says cannot apply was
applying.

`[att]`, `[|att]` and `[*|att]` are untouched — measured, the first two
report no namespace and the third reports `any`, and with no namespaced
props those three denote the same set.
@YevheniiKotyrlo

Copy link
Copy Markdown
Contributor Author

Device evidence — before / after

UNFIXED — Both attributed bars are red. The namespace qualifier is discarded, so a selector Selectors L3 §6.3.3 makes INVALID matches the unqualified set instead of nothing.

FIXED — Only the unqualified bar is red. A prefixed attribute selector represents nothing, because no React Native prop carries a namespace.

before — stock 3.0.7 after — with this PR

This is the over-match direction, so the UNFIXED frame is the one with MORE paint on it. Worth saying before you read the pair: a dropped declaration is visible the first time anyone looks at a screen, and this one paints — on elements the author's own stylesheet says it cannot reach.

Two rules over one attribute. The first bar is unqualified and must keep matching on both builds — it is the control, and without it two blue bars on the right would be indistinguishable from a build that dropped both rules. The second is the same selector behind an undeclared prefix over an element that does carry the attribute. The third carries the qualified rule's class and no attribute at all, so the withheld case has something to be compared to.

The undeclared arm rather than a declared one, because @namespace is valid only before any style rule and this probe's stylesheet is imported beneath others — and because it is the worse half: a declared prefix merely over-matches, while an undeclared one applies a selector the stylesheet has already invalidated. lightningcss reports the prefix rather than rejecting it, which is what leaves the drop to this compiler.

Both frames come from the same device in the same run (Android 36 emulator, 1140×2400 @ 480dpi), but before is stock 3.0.7 rather than "this build minus this PR" — so one unrelated difference is visible and worth naming: the compiler's inlineRem defaults to 14 and our build sets it to 16, so every rem-derived length in the before frame renders at 87.5% of the after one. That is a different fix, not this one.

The build-probe width=<dp> line is that same rem fold used as a build stamp — a 3rem box, so 42 unfixed and 48 fixed. The capture harness reads it off the device and refuses to save a frame whose probe disagrees with the variant it claims, so a before image cannot silently be a second after.

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.

1 participant