Skip to content

Commit 3fcdcf0

Browse files
committed
Do not walk to the end of a PHPDoc for every generic
TypeParser::isHtml() looked for the end tag of every "<name>" by walking the rest of the tokens, so a PHPDoc with many generics like list<string> 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
1 parent 4e50147 commit 3fcdcf0

2 files changed

Lines changed: 38 additions & 0 deletions

File tree

‎src/Parser/TokenIterator.php‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@
99
use function assert;
1010
use function count;
1111
use function in_array;
12+
use function preg_match_all;
1213
use function strlen;
14+
use function strpos;
1315
use function substr;
1416

1517
class TokenIterator
@@ -34,6 +36,9 @@ class TokenIterator
3436
/** @var list<int>|null */
3537
private ?array $offsets = null;
3638

39+
/** @var array<string, int>|null the last index of a token containing "/name>", by the name */
40+
private ?array $endTagIndexes = null;
41+
3742
/**
3843
* @param list<array{string, int, int}> $tokens
3944
*/
@@ -390,6 +395,34 @@ public function hasTokenImmediatelyAfter(int $pos, int $expectedTokenType): bool
390395
return false;
391396
}
392397

398+
/**
399+
* Whether a token at the current index or after it contains "/$name>",
400+
* which every end tag of an HTML tag of that name does.
401+
*
402+
* The tokens are searched once, so that asking about every "<" of a
403+
* PHPDoc does not walk to its end every time.
404+
*
405+
* @internal
406+
*/
407+
public function hasEndTagCandidateAhead(string $name): bool
408+
{
409+
if ($this->endTagIndexes === null) {
410+
$this->endTagIndexes = [];
411+
foreach ($this->tokens as $index => $token) {
412+
$value = $token[Lexer::VALUE_OFFSET];
413+
if (strpos($value, '/') === false || preg_match_all('~/([^/>]++)>~', $value, $matches) === 0) {
414+
continue;
415+
}
416+
417+
foreach ($matches[1] as $candidate) {
418+
$this->endTagIndexes[$candidate] = $index;
419+
}
420+
}
421+
}
422+
423+
return ($this->endTagIndexes[$name] ?? -1) >= $this->index;
424+
}
425+
393426
public function getDetectedNewline(): ?string
394427
{
395428
return $this->newline;

‎src/Parser/TypeParser.php‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -419,6 +419,11 @@ public function isHtml(TokenIterator $tokens): bool
419419
return false;
420420
}
421421

422+
// every end tag the walk below looks for contains this
423+
if (!$tokens->hasEndTagCandidateAhead($htmlTagName)) {
424+
return false;
425+
}
426+
422427
$endTag = '</' . $htmlTagName . '>';
423428
$endTagSearchOffset = - strlen($endTag);
424429

0 commit comments

Comments
 (0)