From dfb38131b2b7765e88545d4da304d37f390b8fb5 Mon Sep 17 00:00:00 2001 From: "ondrej@mirtes.cz" Date: Mon, 26 Oct 2026 17:23:11 +0000 Subject: [PATCH] Do not walk to the end of a PHPDoc for every generic TypeParser::isHtml() looked for the end tag of every "" by walking the rest of the tokens, so a PHPDoc with many generics like list took quadratic time. The iterator now remembers once where each "/name>" is written, and the walk only runs when one is written ahead. Amp-Thread-ID: https://ampcode.com/threads/T-01a17820-94d1-75c8-9206-2e802d9c0556 --- src/Parser/TokenIterator.php | 33 +++++++++++++++++++++++++++++++++ src/Parser/TypeParser.php | 5 +++++ 2 files changed, 38 insertions(+) diff --git a/src/Parser/TokenIterator.php b/src/Parser/TokenIterator.php index 043639f1..259a6bdd 100644 --- a/src/Parser/TokenIterator.php +++ b/src/Parser/TokenIterator.php @@ -9,7 +9,9 @@ use function assert; use function count; use function in_array; +use function preg_match_all; use function strlen; +use function strpos; use function substr; class TokenIterator @@ -34,6 +36,9 @@ class TokenIterator /** @var list|null */ private ?array $offsets = null; + /** @var array|null the last index of a token containing "/name>", by the name */ + private ?array $endTagIndexes = null; + /** * @param list $tokens */ @@ -390,6 +395,34 @@ public function hasTokenImmediatelyAfter(int $pos, int $expectedTokenType): bool return false; } + /** + * Whether a token at the current index or after it contains "/$name>", + * which every end tag of an HTML tag of that name does. + * + * The tokens are searched once, so that asking about every "<" of a + * PHPDoc does not walk to its end every time. + * + * @internal + */ + public function hasEndTagCandidateAhead(string $name): bool + { + if ($this->endTagIndexes === null) { + $this->endTagIndexes = []; + foreach ($this->tokens as $index => $token) { + $value = $token[Lexer::VALUE_OFFSET]; + if (strpos($value, '/') === false || preg_match_all('~/([^/>]++)>~', $value, $matches) === 0) { + continue; + } + + foreach ($matches[1] as $candidate) { + $this->endTagIndexes[$candidate] = $index; + } + } + } + + return ($this->endTagIndexes[$name] ?? -1) >= $this->index; + } + public function getDetectedNewline(): ?string { return $this->newline; diff --git a/src/Parser/TypeParser.php b/src/Parser/TypeParser.php index a2db4fce..348c56d1 100644 --- a/src/Parser/TypeParser.php +++ b/src/Parser/TypeParser.php @@ -419,6 +419,11 @@ public function isHtml(TokenIterator $tokens): bool return false; } + // every end tag the walk below looks for contains this + if (!$tokens->hasEndTagCandidateAhead($htmlTagName)) { + return false; + } + $endTag = ''; $endTagSearchOffset = - strlen($endTag);