{"record":{"id":"4c84262df3972d58","repo":"symfony/css-selector","slug":"got-too-deeply-nested-has","errorCode":null,"errorMessage":"Got too deeply nested :has().","messagePattern":"Got too deeply nested :has\\(\\)\\.","errorType":"exception","errorClass":"SyntaxErrorException","httpStatus":null,"severity":"error","filePath":"Parser/Parser.php","lineNumber":162,"sourceCode":"            }\n\n            [$nextSelector, $pseudoElement] = $this->parseSimpleSelector($stream, false, $isArgument, $insideRelativeSelector);\n            $result = new Node\\CombinedSelectorNode($result, $combinator, $nextSelector);\n        }\n\n        return new Node\\SelectorNode($result, $pseudoElement);\n    }\n\n    /**\n     * @return list<array{0: string, 1: Node\\SelectorNode}>\n     *\n     * @throws SyntaxErrorException\n     * @throws InternalErrorException\n     */\n    private function parseRelativeSelector(TokenStream $stream): array\n    {\n        if ($this->hasNestingDepth >= self::HAS_NESTING_LIMIT) {\n            throw SyntaxErrorException::nestedHas();\n        }\n\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","sourceCodeStart":144,"sourceCodeEnd":180,"githubUrl":"https://github.com/symfony/css-selector/blob/08e2905152a39cf3fd1745d83f8c483e258887d9/Parser/Parser.php#L144-L180","documentation":"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(...)))'.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\n$parser->parse(str_repeat('div:has(', 20) . 'p' . str_repeat(')', 20));\n\n// after\nif (substr_count($selector, ':has(') > 16) {\n    throw new \\InvalidArgumentException('Selector exceeds :has() nesting limit');\n}\n$parser->parse($selector);","handlingStrategy":"validation","validationCode":"if (substr_count($selector, ':has(') > 16) {\n    throw new \\InvalidArgumentException('Selector exceeds the :has() nesting limit of 16');\n}","typeGuard":null,"tryCatchPattern":"try {\n    $nodes = (new Parser())->parse($selector);\n} catch (SyntaxErrorException $e) {\n    if (str_contains($e->getMessage(), 'nested')) { /* too complex */ }\n}","preventionTips":["Cap generated :has() depth in selector builders","Sanitize/fuzz-limit untrusted selector input","Prefer flat descendant/child combinators over stacked :has()"],"tags":["css-selector","parser","nesting-limit","recursion","php"],"backgroundTag":"value-out-of-range","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"}