Skip to content

not() keeps the excluded nodes instead of removing them when given an SplObjectStorage #75

Description

@jakejackson1

Guidelines

Description of the bug

not() is documented as removing the matching elements. When the argument is an SplObjectStorage it does the exact opposite: it keeps only the nodes in the storage and discards everything else, turning the negation into an intersection.

The other three accepted argument types (CSS selector, DOMElement, array of DOMNodes) all behave correctly, so the bug is confined to one branch.

Root cause

src/Helpers/QueryFilters.php:719-723 — the condition is missing its negation:

} elseif ($selector instanceof SplObjectStorage) {
	foreach ($this->matches as $m) {
		if ($selector->offsetExists($m)) {   // <-- should be ! $selector->offsetExists($m)
			$found->offsetSet($m);
		}
	}
}

Compare the array branch directly above it, which has the negation the storage branch lacks:

} elseif (is_array($selector)) {
	foreach ($this->matches as $m) {
		if (! in_array($m, $selector, true)) {
			$found->offsetSet($m);
		}
	}
}

Note when reproducing

Node identity is per-document, so both sides must come from the same DOMDocument or nothing matches either way and the bug is masked. The snippet below uses branch() to guarantee that.

Workaround

Pass $other->get() (array) rather than $other->get(null, true) (SplObjectStorage).

QueryPath version

4.2.0 (also reproduces on main at bed5d2c)

PHP Version and environment (server type, cli provider etc., enclosing libraries and their respective versions)

PHP 8.3.16 CLI (macOS, Homebrew). Not PHP-8 specific — the cause is present on every supported version.

Minimal reproducible PHP+HTML snippet to replicate bug

<?php
require __DIR__ . '/vendor/autoload.php';

$xml = '<?xml version="1.0"?><root><a/><b/><c/></root>';

$all     = qp($xml, 'a, b, c');
$exclude = $all->branch()->top()->find('a');   // same document, so node identity holds

$names = function ($q) {
    $o = [];
    foreach ($q->get() as $n) { $o[] = $n->nodeName; }
    return implode(',', $o) ?: '(empty)';
};

echo $names($all->not($exclude->get())), "\n";            // b,c  - correct
echo $names($all->not($exclude->get(null, true))), "\n";  // a    - wrong, expected b,c

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions