Description
I got some strange results selecting DOM elements recently, caused by how querySelectorAll handles comma separated selectors.
The following analysis is mostly via AI (Claude Opus 5.5), but I have checked it over and it looks like it has captured the issue well. I'm less sure of the PHP src fix at the end as I'm not a C developer and have never contributed to internals.
The following code:
<?php
$doc = Dom\HTMLDocument::createFromString(
'<!DOCTYPE html><p class="a b">one</p><p class="b">two</p>'
);
foreach (['.a, .b', '.a, .b, .a.b', ':is(.a, .b)'] as $selector) {
$texts = [];
foreach ($doc->querySelectorAll($selector) as $element) {
$texts[] = $element->textContent;
}
echo $selector, ' => ', implode(' ', $texts), "\n";
}
Resulted in this output (3v4l shows same output on 8.4.1 through 8.4.26 and 8.5.0 through 8.5.11)
.a, .b => one one two
.a, .b, .a.b => one one one two
:is(.a, .b) => one two
But I expected this output instead:
.a, .b => one two
.a, .b, .a.b => one two
:is(.a, .b) => one two
Each element should appear once: "A comma-separated list of selectors represents the union of all elements selected by each of the individual selectors in the selector list." (Selectors 4, §4.1)
The same code in JavaScript prints the expected output (click Run).
AI (Claude Opus 5.5) source analysis: cause and tested fix
querySelectorAll() passes LXB_SELECTORS_OPT_DEFAULT to Lexbor in ext/dom/parentnode/css_selectors.c, so a node is reported once per matching selector. querySelector(), matches() and closest() pass LXB_SELECTORS_OPT_MATCH_FIRST, which Lexbor documents as removing these duplicates. Using it in querySelectorAll() fixes this on an 8.5.8 build, and the ext/dom tests still pass.
--- a/ext/dom/parentnode/css_selectors.c
+++ b/ext/dom/parentnode/css_selectors.c
@@ -241,7 +241,7 @@
selectors_str,
dom_query_selector_find_array_callback,
&ctx,
- LXB_SELECTORS_OPT_DEFAULT
+ LXB_SELECTORS_OPT_MATCH_FIRST
) != LXB_STATUS_OK) {
zend_array_destroy(list);
RETURN_THROWS();
PHP Version
PHP 8.5.8 (cli) (built: Jul 7 2026 05:21:52) (NTS gcc 14.2.0)
Copyright (c) The PHP Group
Built by static-php-cli 2.8.6
Zend Engine v4.5.8, Copyright (c) Zend Technologies
with Zend OPcache v8.5.8, Copyright (c), by Zend Technologies
Operating System
Ubuntu 24.04 (x86_64)
Description
I got some strange results selecting DOM elements recently, caused by how querySelectorAll handles comma separated selectors.
The following analysis is mostly via AI (Claude Opus 5.5), but I have checked it over and it looks like it has captured the issue well. I'm less sure of the PHP src fix at the end as I'm not a C developer and have never contributed to internals.
The following code:
Resulted in this output (3v4l shows same output on 8.4.1 through 8.4.26 and 8.5.0 through 8.5.11)
But I expected this output instead:
Each element should appear once: "A comma-separated list of selectors represents the union of all elements selected by each of the individual selectors in the selector list." (Selectors 4, §4.1)
The same code in JavaScript prints the expected output (click Run).
AI (Claude Opus 5.5) source analysis: cause and tested fix
PHP Version
Operating System
Ubuntu 24.04 (x86_64)