fix: avoid reusing descendant results across :has anchors

AS
6d ago
+103-1

A compiled selector such as :has(:is(.c .b) :disabled) can return different matches depending on the order in which elements are tested. Its descendant cache reuses an ancestor's answer after :has() changes the anchor that answer depends on.

Disable descendant result caching when compiling against the mutable :has() placeholder. Descendant caches with a fixed query context and the separate text/subtree caches retain their existing behavior.

Add regressions for forward/reverse evaluation, repeated queries, functional aliases, negation, child combinators, and combinations with :contains() / :icontains(). A control verifies that fixed-context descendant caching still avoids repeated work.

Validation: npm test passes (230 tests, one existing skip, including ESLint, TypeScript and Biome); npm run build passes on Node 24.22.1. Nine new tests fail on the unchanged base. A deterministic 8,050-check matrix agrees with caching disabled, including after installing the packed npm archive.

Fixes #1929.

A separate Codex reviewer checked 6,048 generated results against an independent DOM relationship oracle for both the candidate and an installed package; all pass, while the original produces 4,719 mismatches. Another 26 checks confirm stable-context and independent text caches keep working.

OpenAI Codex implemented and tested this change; a separate Codex agent reviewed it. No human review is claimed.

Summary by CodeRabbit

  • Bug Fixes
    • Corrected result caching for selectors using nested pseudos inside :has, improving match consistency across query orders and whole-document searches.
  • Tests
    • Added coverage for cached and uncached results, reversed evaluation order, and stable query contexts.