diff --git a/bench/Source/NestedTryCatchSource.php b/bench/Source/NestedTryCatchSource.php new file mode 100644 index 00000000000..d1a6a9b905c --- /dev/null +++ b/bench/Source/NestedTryCatchSource.php @@ -0,0 +1,87 @@ +process($item); + } catch (InvalidArgumentException $invalidArgumentException) { + try { + $count += $this->retry($item, $invalidArgumentException->getMessage()); + } catch (RuntimeException $runtimeException) { + try { + $count += $this->fallback($item, $runtimeException->getCode()); + } catch (Throwable $throwable) { + try { + $this->log($throwable->getMessage() . ' ' . $item); + } catch (Throwable) { + $count -= 1; + } + } + } + } + } + + return $count; + } + + public function sequential(string $value): string + { + try { + $value = $this->normalize($value); + } catch (InvalidArgumentException $invalidArgumentException) { + $value = $invalidArgumentException->getMessage(); + } + + try { + $value = strtoupper($this->normalize($value)); + } catch (RuntimeException $runtimeException) { + try { + $value = $this->normalize($runtimeException->getMessage()); + } catch (Throwable) { + $value = ''; + } + } finally { + $value .= '!'; + } + + return $value; + } + + private function process(mixed $item): int + { + return is_int($item) ? $item : throw new InvalidArgumentException('not int'); + } + + private function retry(mixed $item, string $reason): int + { + return is_numeric($item) ? (int) $item : throw new RuntimeException($reason); + } + + private function fallback(mixed $item, int $code): int + { + return $code + (is_string($item) ? strlen($item) : 0); + } + + private function log(string $message): void + { + error_log($message); + } + + private function normalize(string $value): string + { + return trim($value) !== '' ? trim($value) : throw new InvalidArgumentException('empty'); + } +} diff --git a/bench/php_stan_node_scope_resolver_catch_bench.php b/bench/php_stan_node_scope_resolver_catch_bench.php new file mode 100644 index 00000000000..23f31f44952 --- /dev/null +++ b/bench/php_stan_node_scope_resolver_catch_bench.php @@ -0,0 +1,56 @@ +create(); +$rectorConfig->boot(); + +/** @var TestingParser $testingParser */ +$testingParser = $rectorConfig->make(TestingParser::class); + +$sourceFilePath = __DIR__ . '/Source/NestedTryCatchSource.php'; + +$cpuSeconds = static function (): float { + $usage = getrusage(); + return $usage['ru_utime.tv_sec'] + $usage['ru_utime.tv_usec'] / 1_000_000 + + $usage['ru_stime.tv_sec'] + $usage['ru_stime.tv_usec'] / 1_000_000; +}; + +// warm up autoloading / reflection once, excluded from timing +$testingParser->parseFileToDecoratedNodes($sourceFilePath); + +$cpuStart = $cpuSeconds(); +$wallStart = hrtime(true); + +for ($repeat = 0; $repeat < REPEAT; ++$repeat) { + $testingParser->parseFileToDecoratedNodes($sourceFilePath); +} + +printf('PHPStanNodeScopeResolver::processNodes() nested try/catch benchmark%s', PHP_EOL); +printf(' repeat=%d%s', REPEAT, PHP_EOL); +printf('%s', PHP_EOL); +printf(' cpu=%.4fs wall=%.4fs%s', $cpuSeconds() - $cpuStart, (hrtime(true) - $wallStart) / 1_000_000_000, PHP_EOL); diff --git a/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php b/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php index ef8994833c7..21f75b182b1 100644 --- a/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php +++ b/src/NodeTypeResolver/PHPStan/Scope/PHPStanNodeScopeResolver.php @@ -147,9 +147,26 @@ public function processNodes( $scope = $formerMutatingScope ?? $this->scopeFactory->createFromFile($filePath); + $this->fillScope($stmts, $filePath, $scope); + + // use after scope filling so DecoratingNodeVisitorInterface instance can fetch the scope of target node + // @see https://github.com/rectorphp/rector-src/pull/7721#discussion_r2595932460 + $this->nodeTraverser->traverse($stmts); + + return $stmts; + } + + /** + * @param Stmt[] $stmts + */ + private function fillScope(array $stmts, string $filePath, MutatingScope $scope): void + { + /** @var array $catches */ + $catches = []; + $nodeCallback = function (Node $node, MutatingScope $mutatingScope) use ( &$nodeCallback, - $filePath, + &$catches, ): void { if ($mutatingScope instanceof NodeCallbackScope) { $mutatingScope = $mutatingScope->toWalkScope(); @@ -312,7 +329,13 @@ public function processNodes( } if ($node instanceof Catch_) { - $this->processCatch($node, $filePath, $mutatingScope); + // PHPStan fills the catch stmts itself right after this callback, unless no throw point matches; + // reset the first stmt scope, so the skipped ones can be detected after processing + if ($node->stmts !== []) { + $node->stmts[0]->setAttribute(AttributeKey::SCOPE, null); + $catches[] = [$node, $mutatingScope]; + } + return; } @@ -398,11 +421,9 @@ public function processNodes( RectorNodeScopeResolver::processNodes($stmts, $scope); } - // use after scope filling so DecoratingNodeVisitorInterface instance can fetch the scope of target node - // @see https://github.com/rectorphp/rector-src/pull/7721#discussion_r2595932460 - $this->nodeTraverser->traverse($stmts); - - return $stmts; + foreach ($catches as [$catch, $catchScope]) { + $this->processCatch($catch, $filePath, $catchScope); + } } private function processYield(Yield_ $yield, MutatingScope $mutatingScope): void @@ -532,6 +553,10 @@ private function processSwitch(Switch_ $switch, MutatingScope $mutatingScope): v private function processCatch(Catch_ $catch, string $filePath, MutatingScope $mutatingScope): void { + if ($catch->stmts[0]->getAttribute(AttributeKey::SCOPE) instanceof MutatingScope) { + return; + } + $varName = $catch->var instanceof Variable ? $this->nodeNameResolver->getName($catch->var) : null; @@ -541,7 +566,7 @@ private function processCatch(Catch_ $catch, string $filePath, MutatingScope $mu ); $catchMutatingScope = $mutatingScope->enterCatchType($type, $varName); - $this->processNodes($catch->stmts, $filePath, $catchMutatingScope); + $this->fillScope($catch->stmts, $filePath, $catchMutatingScope); } private function processTryCatch(TryCatch $tryCatch, MutatingScope $mutatingScope): void