[php-src] Issue #24260: querySelectorAll() returns duplicate elements for selector lists
| From: | fivefilters | Date: | Sun, 11 Oct 2026 11:42:42 +0000 |
| Subject: | [php-src] Issue #24260: querySelectorAll() returns duplicate elements for selector lists | ||
| Groups: | php.bugs | ||
| Request: | Send a blank email to php-bugs+get-252973@lists.php.net to get a copy of this message | ||
Issue: https://github.com/php/php-src/issues/24260
Author: fivefilters
### Description
I got some strange results while working on some PHP code 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 change as I'm not a C
developer and have never contributed to internals.
The following code:
```php
<?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](https://3v4l.org/11SH2) 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](https://drafts.csswg.org/selectors-4/#grouping))
The [same code in
JavaScript](https://www.typescriptlang.org/play/?filetype=js#code/MYewdgzgLgBAJiYMC8MwFMDuMAiB5AWQAUBDAJwnTIAoBKAOgAdzKAxMkAWwGUoyBLMAHNqAKBgwA5AB4AhPgDCAFQCaRAKIwAFlE4AbAHzTGMYHpIQIyAEQkYAI2sHw6aQHpGRk2YtXrjgyhMEHdPSQAaKSh0AA8oNx19SVFaAG5RUQAzEDIYalBIWEo9dGAoHJgQTJgAbUl6Ekj6ewipBqb7JpJm1skALn4IanaYZtpJAF1aGABvcVNwaBhouIgUGABBMjISAE96TI5OagRgegBHAFcqXe50ErKcjb09amLS8rJaSPQUAxh0PQVlAFOBomAoGl5gUICASvQ9CARO9HmRIpJkAZWsCIPQAFYgQTUSRSWhQgC+oiAA)
prints the expected output (click Run).
> AI (Claude Opus 5.5) 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.
>
> ```diff
> --- 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
```plain
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)