From 28a4072aedb4e1450e0ab6a12f653ce4aa38a268 Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 16:58:43 +1100 Subject: [PATCH 1/2] Return no siblings for an element with no parent, without warnings siblings() read the parent's childNodes without checking for a parent, so an element outside any tree raised two warnings. Fixes #139. Co-Authored-By: Claude Opus 5.5 --- src/Helpers/QueryFilters.php | 6 ++++-- tests/Issues/Issue139Test.php | 40 +++++++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 2 deletions(-) create mode 100644 tests/Issues/Issue139Test.php diff --git a/src/Helpers/QueryFilters.php b/src/Helpers/QueryFilters.php index 8b73c7b..f3114e4 100644 --- a/src/Helpers/QueryFilters.php +++ b/src/Helpers/QueryFilters.php @@ -940,8 +940,10 @@ public function siblings($selector = null): Query { $found = new SplObjectStorage(); foreach ($this->matches as $m) { - $parent = $m->parentNode; - foreach ($parent->childNodes as $n) { + if ($m->parentNode === null) { + continue; + } + foreach ($m->parentNode->childNodes as $n) { if ($n->nodeType === XML_ELEMENT_NODE && $n !== $m) { $found->offsetSet($n); } diff --git a/tests/Issues/Issue139Test.php b/tests/Issues/Issue139Test.php new file mode 100644 index 0000000..2c64b3d --- /dev/null +++ b/tests/Issues/Issue139Test.php @@ -0,0 +1,40 @@ +')->get(0)->ownerDocument; + $lone = $doc->createElement('lone'); + + $this->assertSame(0, qp($lone)->siblings()->count()); + $this->assertSame(0, qp($lone)->siblings('*')->count()); + } + + public function testSiblingsSkipsAParentlessElementInASet() + { + $qp = qp('', '#b'); + $lone = $qp->get(0)->ownerDocument->createElement('lone'); + + $this->assertSame('a,c', $this->idList($qp->add($lone)->siblings())); + } + + public function testSiblingsInADetachedSubtree() + { + $doc = qp('')->get(0)->ownerDocument; + $div = $doc->createElement('div'); + $a = $div->appendChild($doc->createElement('a')); + $b = $div->appendChild($doc->createElement('b')); + + $this->assertSame([$b], qp($a)->siblings()->get()); + } +} From 2d184944a8948d8bf4ae053746ec556a3430e9de Mon Sep 17 00:00:00 2001 From: gravitypdf-bot Date: Mon, 26 Oct 2026 17:17:21 +1100 Subject: [PATCH 2/2] Add a CHANGELOG entry Co-Authored-By: Claude Opus 5.5 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 527a87d..7fbcf81 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -58,6 +58,7 @@ QueryPath Changelog - Consolidate the sibling and ancestor walks behind `next()`, `prev()`, `nextAll()`, `prevAll()`, `nextUntil()`, `prevUntil()`, `parent()`, `parents()`, `parentsUntil()` and `closest()` into one helper, so a fix to one no longer has to be repeated in its mirror (issue #115). As a side effect, `parentsUntil()` no longer loops forever on an element that is not attached to the document, and `nextUntil('')` and `prevUntil('')` treat an empty selector as no selector, as the other methods already did, instead of throwing `QueryPath\CSS\ParseException`. `closest('')` returns an empty set, as in jQuery. `nextUntil()`, `prevUntil()` and `parentsUntil()` also stop at an element already collected from an earlier starting element, as `nextAll()` and `prevAll()` do, so `nextUntil()` on each of 2000 siblings falls from about 100 ms to 1 ms - **Behaviour change.** `prevAll()`, `prevUntil()`, `nextAll()`, `nextUntil()`, `siblings()`, `parent()` and `closest()` now return the results for a multi-element set in jQuery's order, instead of grouped by starting element (issue #117). `prevAll()` and `prevUntil()` return reverse document order, as `parents()` and `parentsUntil()` already did, and the others return document order. `qp($xml, 'c')->add('d')->prevAll()` now returns `c, b, a` rather than `b, a, c`. `next()` and `prev()` keep the order of the starting elements, as in jQuery, and the results for a single starting element are unchanged - **Behaviour change.** A positional pseudo-class (`:first`, `:eq()`, `:last`, ...) in the stop selector of `nextUntil()`, `prevUntil()` and `parentsUntil()` now counts across the whole document, as jQuery's does, instead of seeing each sibling or ancestor as a one-element set (issue #118). `nextUntil(':first')` used to stop at once and now walks to the end, and `nextUntil(':eq(3)')` stops at the fourth element in the document. `closest()` no longer matches a positional selector at all, as in jQuery, where `closest(':first')` used to return the element itself. `next()`, `prev()` and `parent()` still test each candidate on its own, which agrees with jQuery for a single starting element +- Fix `siblings()` raising an "Attempt to read property "childNodes" on null" warning and a `foreach()` warning when the match set contains an element with no parent, such as one made with `createElement()` and not yet inserted (issue #139). Such an element has no siblings, so it now adds nothing to the result, as in jQuery # 4.2.0