symfony/css-selector · error · SyntaxErrorException

Expected , but found.

Error message

Expected %s, but %s found.

What it means

SyntaxErrorException thrown by Parser::parseRelativeSelector() when a :has() argument begins with a string or number token instead of a selector. Relative selectors inside :has() must be element/compound selectors (optionally preceded by >, +, ~), so tokens like "'text'" or '42' are rejected as unexpected tokens where 'an argument' was expected.

Solutions

  1. Replace string/number arguments with a proper selector: 'div:has(p)' to match divs containing a <p>.
  2. For text matching, use a different mechanism (e.g. XPath contains(text(),...) or filter results after conversion).
  3. Escape or strip user input so stray strings/numbers never reach :has() arguments.
  4. Catch SyntaxErrorException and show the offending token to the user.

Example fix

// before
$parser->parse('div:has("hello")');

// after
$parser->parse('div:has(span)'); // or filter text content post-conversion
Defensive patterns

Strategy: validation

Validate before calling

if (preg_match('/:has\(\s*("|\'|[0-9])/', $selector)) {
    throw new \InvalidArgumentException(':has() arguments must be selectors, not strings or numbers');
}

Try / catch

try {
    $nodes = (new Parser())->parse($selector);
} catch (SyntaxErrorException $e) {
    // surface 'unexpected token' details to caller
}

Prevention

When it happens

Trigger: Parsing selectors like 'div:has("text")', 'div:has(42)', 'div:has(> "foo")'. The check at Parser/Parser.php:181-183 fires when the peeked token after the optional combinator is a string or number token.

Common situations: Confusing :has() with functions that take strings (e.g. :contains-style selectors from other engines), copy-pasting jQuery-style selectors, passing raw text matches inside :has() expecting XPath-like text() semantics.

Understand the failure class

Background: "Must be a positive integer", "Invalid value", "Unsupported": the invalid-argument-value error family, when a library rejects the value you pass — this error's family across 35 libraries.

Related errors


AI-assisted analysis of symfony/css-selector@08e2905152 (2026-09-14). Data as JSON: /api/errors/f93f06dce9ae086a. Report an issue: GitHub.

Appendix: source

Thrown at Parser/Parser.php:182

        ++$this->hasNestingDepth;

        try {
            $arguments = [];
            while (true) {
                $stream->skipWhitespace();
                $peek = $stream->getPeek();

                if ($peek->isDelimiter(['+', '>', '~'])) {
                    $combinator = $stream->getNext()->getValue();
                    $stream->skipWhitespace();
                    $peek = $stream->getPeek();
                } else {
                    $combinator = ' ';
                }

                if ($peek->isString() || $peek->isNumber()) {
                    throw SyntaxErrorException::unexpectedToken('an argument', $stream->getNext());
                }

                $selector = $this->parserSelectorNode($stream, true, true);

                if (null !== $pseudoElement = $selector->getPseudoElement()) {
                    throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'inside :has()');
                }

                $arguments[] = [$combinator, $selector];

                if ($stream->getPeek()->isDelimiter([','])) {
                    $stream->getNext();
                    continue;
                }

                break;
            }

View on GitHub (pinned to 08e2905152)