Skip to content

Correct selector matching - #679

Merged
olavoasantos merged 1 commit into
mainfrom
correct-selector-matching
Sep 16, 2026
Merged

olavoasantos merged 1 commit into
mainfrom
correct-selector-matching

Conversation

@olavoasantos

@olavoasantos olavoasantos commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Problem

The polyfill implements a deliberately limited selector surface, but several supported forms were matched incorrectly. :has() evaluated its argument against the candidate itself rather than relative descendants; a multi-part selector could restart matching from the original leaf after finding an ancestor or sibling; leading whitespace produced an empty selector part; and unquoted exact attribute equality was not parsed as an equality value.

Impact

Important. Calls to the exposed querySelector() and querySelectorAll() APIs could silently select the wrong element or no element for supported selectors. In a worker DOM, that can direct application updates at the wrong local node before Remote DOM synchronization.

Reproduction

const container = document.createElement('div');
container.innerHTML = `
  <article id="main-post">
    <aside class="sidebar"><a class="active">Active Link</a></aside>
  </article>
`;

container.querySelector('article:has(.active)');

Before this change, the final call returned null: .active was tested against the article itself, not its descendants. It now returns the article. The same regression file demonstrates correct chained-combinator state, leading whitespace, and [class=content] exact attribute matching.

Change

  • Parse supported unquoted attribute equality and trim insignificant outer whitespace.
  • Preserve the currently matched ancestor or sibling while recursively evaluating chained combinators.
  • Evaluate :has() as a scoped relative selector, including supported leading child and sibling relations, instead of matching the candidate itself.
  • Read balanced functional pseudo-class arguments and ASCII-normalize pseudo-class names without folding selector values.

The implementation remains scoped to the selector forms this polyfill exposes; it does not claim a complete CSS selector grammar.

Tests

Expands selector parsing and matching coverage with positive and negative cases for unquoted and quoted exact attributes, 3+ part child/descendant/sibling chains, descendant and relative :has(), nested :has()/:not(), balanced quoted function arguments, pseudo-class name casing, and leading/internal whitespace.

Stack

The restacked diff contains only selector implementation, selector tests, and its changeset; the unrelated text-content test fix was moved into #671.

Validation

Fresh GitHub CI on restacked head 1204f44 passes:

  • lint;
  • type-check and build;
  • unit tests with coverage;
  • bundle-size checks;
  • Playwright end-to-end tests;
  • the classified Web Platform Test suite.

@olavoasantos
olavoasantos force-pushed the correct-selector-matching branch from e728f56 to 9adce31 Compare September 3, 2026 15:29
@olavoasantos
olavoasantos force-pushed the correct-selector-matching branch from 9adce31 to 1204f44 Compare September 4, 2026 14:30
@olavoasantos
olavoasantos marked this pull request as ready for review September 14, 2026 18:41
@henrytao-me

Copy link
Copy Markdown
Member

The combinator and relative :has() fixes look right. I reviewed 1204f44 against its actual #678 base 5100d3f; two corrections remain:

1. Use CSS whitespace instead of JavaScript trim()

selectors.ts:130-137 removes characters that CSS treats as identifier characters:

const root = document.createElement('div');
root.appendChild(document.createElement('article'));
root.querySelector('\u1680article');

Native/base return null; this head returns the ordinary article. U+1680 is not CSS whitespace. If an element with the literal U+1680-prefixed name exists, native selects that element, base misses it, and this head selects the wrong ordinary article. U+FEFF reproduces the same issue.

Please use CSS whitespace (space, tab, LF, CR, FF) consistently in normalization and tokenizer character classes, rather than trim()/generic \s. Add exact wrong-node checks alongside the existing ordinary-whitespace tests. The leading-name false positives are introduced here; related trailing/functional Unicode matching gaps already existed.

A build-only probe correction fixes all eight targeted U+1680/U+FEFF cases without changing other comparison results. No production source was changed. References: CSS whitespace, identifier code points.

2. Reject :has() nested inside :has()

The balanced argument parsing now permits forbidden nesting:

const root = document.createElement('div');
const article = document.createElement('article');
const section = document.createElement('section');
const active = document.createElement('a');
active.setAttribute('class', 'active');
section.appendChild(active);
article.appendChild(section);
root.appendChild(article);

root.querySelector('article:has(:has(.active))');
root.querySelector('article:has(:not(:has(.active)))');

Each native call throws SyntaxError; the base throws a generic Error on these populated fixtures, while this head selects the article. Selectors explicitly forbids :has() within another :has(), including through :not().

Please track the inside-has context structurally during parsing, before inspecting the DOM. Preserve valid :not(:has(...)), :has(span:not(...)), independent sibling :has() functions, and quoted attribute text containing :has(...). Test both query APIs against populated and empty roots; empty-root missed errors are inherited, but the structural guard should not depend on finding a candidate element.

This is the same nested-has issue already raised on #692. That branch inherited the acceptance; #679 identifies where it starts. Implement the structural rejection once here, retaining #692's named-error/integration coverage rather than moving its whole error-contract change forward.

Validation: 285 polyfill tests and 59 focused core tests pass, as do the locked-TypeScript polyfill check and locked formatting. The updated selector file on the actual base has 32 failures / 57 passes. Head matches 259/279 standards-mode native comparison cases, including all 180 generated supported combinator chains. Remaining differences cover the above plus known selector-list/error-contract limits. Reported CI is green; full-repository and live Shell/admin validation remain separate gates. No #679 source fix or approval performed.

@olavoasantos
olavoasantos force-pushed the correct-selector-matching branch from 1204f44 to 2c920f3 Compare September 16, 2026 13:56
@olavoasantos
olavoasantos force-pushed the correct-selector-matching branch from 2c920f3 to 0fbcbae Compare September 16, 2026 14:08
@olavoasantos

Copy link
Copy Markdown
Contributor Author

Fixed both. Selector parsing now uses CSS whitespace explicitly instead of trim() / generic \s, including class token matching. Nested :has() is rejected structurally during parsing, including through :not(), without depending on a candidate node. The named SyntaxError integration stays in #692 rather than pulling that whole error contract down here.

Base automatically changed from respect-default-namespace to normalize-dom-namespaces September 16, 2026 19:56
@olavoasantos
olavoasantos force-pushed the correct-selector-matching branch from 0fbcbae to 1c1a644 Compare September 16, 2026 19:56
@olavoasantos
olavoasantos force-pushed the correct-selector-matching branch from 1c1a644 to da43e02 Compare September 16, 2026 20:13
@olavoasantos
olavoasantos removed this pull request from stack #698 September 16, 2026 20:14
@olavoasantos
olavoasantos changed the base branch from normalize-dom-namespaces to main September 16, 2026 20:14
@olavoasantos
olavoasantos added this pull request to stack #715 September 16, 2026 20:14
@olavoasantos
olavoasantos merged commit 28955a5 into main Sep 16, 2026
8 checks passed
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.

3 participants