{"record":{"id":"f93f06dce9ae086a","repo":"symfony/css-selector","slug":"expected-s-but-s-found","errorCode":null,"errorMessage":"Expected %s, but %s found.","messagePattern":"Expected (.+?), but (.+?) found\\.","errorType":"exception","errorClass":"SyntaxErrorException","httpStatus":null,"severity":"error","filePath":"Parser/Parser.php","lineNumber":182,"sourceCode":"\n        ++$this->hasNestingDepth;\n\n        try {\n            $arguments = [];\n            while (true) {\n                $stream->skipWhitespace();\n                $peek = $stream->getPeek();\n\n                if ($peek->isDelimiter(['+', '>', '~'])) {\n                    $combinator = $stream->getNext()->getValue();\n                    $stream->skipWhitespace();\n                    $peek = $stream->getPeek();\n                } else {\n                    $combinator = ' ';\n                }\n\n                if ($peek->isString() || $peek->isNumber()) {\n                    throw SyntaxErrorException::unexpectedToken('an argument', $stream->getNext());\n                }\n\n                $selector = $this->parserSelectorNode($stream, true, true);\n\n                if (null !== $pseudoElement = $selector->getPseudoElement()) {\n                    throw SyntaxErrorException::pseudoElementFound($pseudoElement, 'inside :has()');\n                }\n\n                $arguments[] = [$combinator, $selector];\n\n                if ($stream->getPeek()->isDelimiter([','])) {\n                    $stream->getNext();\n                    continue;\n                }\n\n                break;\n            }\n","sourceCodeStart":164,"sourceCodeEnd":200,"githubUrl":"https://github.com/symfony/css-selector/blob/08e2905152a39cf3fd1745d83f8c483e258887d9/Parser/Parser.php#L164-L200","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Replace string/number arguments with a proper selector: 'div:has(p)' to match divs containing a <p>.","For text matching, use a different mechanism (e.g. XPath contains(text(),...) or filter results after conversion).","Escape or strip user input so stray strings/numbers never reach :has() arguments.","Catch SyntaxErrorException and show the offending token to the user."],"exampleFix":"// before\n$parser->parse('div:has(\"hello\")');\n\n// after\n$parser->parse('div:has(span)'); // or filter text content post-conversion","handlingStrategy":"validation","validationCode":"if (preg_match('/:has\\(\\s*(\"|\\'|[0-9])/', $selector)) {\n    throw new \\InvalidArgumentException(':has() arguments must be selectors, not strings or numbers');\n}","typeGuard":null,"tryCatchPattern":"try {\n    $nodes = (new Parser())->parse($selector);\n} catch (SyntaxErrorException $e) {\n    // surface 'unexpected token' details to caller\n}","preventionTips":["Only pass element/class/attribute selectors into :has()","Use XPath or post-filtering for text matching instead of :has(\"text\")","Escape user input that might inject quotes or digits into :has()"],"tags":["css-selector","parser","unexpected-token","has-selector","php"],"backgroundTag":"invalid-argument-value","analyzedSha":"08e2905152a39cf3fd1745d83f8c483e258887d9","analyzedAt":"2026-09-14T11:41:28.509Z","contentChangedAt":"2026-09-14T11:41:28.509Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}