symfony/css-selector · error · SyntaxErrorException
Got too deeply nested :has().
Error message
Got too deeply nested :has().
What it means
SyntaxErrorException thrown by Parser::parseRelativeSelector() when :has() selectors are nested deeper than HAS_NESTING_LIMIT (16). The parser tracks hasNestingDepth and aborts to prevent unbounded recursion / DoS on pathological inputs like 'a:has(b:has(c:has(...)))'.
Solutions
- Flatten the selector: reduce :has() nesting to 16 levels or fewer by restructuring the query.
- Add a pre-parse depth check counting ':has(' occurrences in the input string and reject inputs >16 before calling parse().
- Catch SyntaxErrorException and report 'selector too complex' to the user instead of crashing.
- If the deep nesting is generated by your code, add an explicit depth counter in the generator.
Example fix
// before
$parser->parse(str_repeat('div:has(', 20) . 'p' . str_repeat(')', 20));
// after
if (substr_count($selector, ':has(') > 16) {
throw new \InvalidArgumentException('Selector exceeds :has() nesting limit');
}
$parser->parse($selector); Defensive patterns
Strategy: validation
Validate before calling
if (substr_count($selector, ':has(') > 16) {
throw new \InvalidArgumentException('Selector exceeds the :has() nesting limit of 16');
} Try / catch
try {
$nodes = (new Parser())->parse($selector);
} catch (SyntaxErrorException $e) {
if (str_contains($e->getMessage(), 'nested')) { /* too complex */ }
} Prevention
- Cap generated :has() depth in selector builders
- Sanitize/fuzz-limit untrusted selector input
- Prefer flat descendant/child combinators over stacked :has()
When it happens
Trigger: Parsing a selector with more than 16 levels of nested :has(), e.g. building ':has(' repeated >16 times such as 'div:has(> div:has(> div:has(...)))'. Triggered whenever parseRelativeSelector is entered while hasNestingDepth >= 16.
Common situations: Programmatic/generative selector construction (template loops appending :has()), malicious or fuzzed user input to an HTML-to-XPath converter, recursive data-driven selector builders without depth limiting.
Understand the failure class
Background: "value must be between 0 and 1" / "out of range" / "must not be negative" errors: fixing range-validation failures across open-source libraries — this error's family across 42 libraries.
Related errors
- Unexpected pseudo-element
- Expected , but found.
- Got immediate child pseudo-element
- Got nested :not().
- Got too deeply nested
AI-assisted analysis of symfony/css-selector@08e2905152 (2026-09-14).
Data as JSON: /api/errors/4c84262df3972d58.
Report an issue: GitHub.
Appendix: source
Thrown at Parser/Parser.php:162
}
[$nextSelector, $pseudoElement] = $this->parseSimpleSelector($stream, false, $isArgument, $insideRelativeSelector);
$result = new Node\CombinedSelectorNode($result, $combinator, $nextSelector);
}
return new Node\SelectorNode($result, $pseudoElement);
}
/**
* @return list<array{0: string, 1: Node\SelectorNode}>
*
* @throws SyntaxErrorException
* @throws InternalErrorException
*/
private function parseRelativeSelector(TokenStream $stream): array
{
if ($this->hasNestingDepth >= self::HAS_NESTING_LIMIT) {
throw SyntaxErrorException::nestedHas();
}
++$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 = ' ';
}
View on GitHub (pinned to 08e2905152)