diff --git a/UPGRADING.md b/UPGRADING.md index db8e1da6017..139777f1cbd 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,3 +1,23 @@ +# Upgrading from Psalm 6 to Psalm 7 + +## Changed + +- [BC] The `startScanningFiles`, `startAnalyzingFiles`, `startAlteringFiles` of `Psalm\Progress\Progress` and subclasses were removed and replaced with a new `startPhase` method, taking a `Psalm\Progress\Phase` enum case. + +- [BC] The `start` method was removed, use `expand`, instead; the progress is reset to 0 when changing the current phase. + +- [BC] Method `doesTerminalSupportUtf8` of class `Psalm\Progress\Progress` became final + +- [BC] Method debug() of class Psalm\Progress\Progress changed from concrete to abstract + +- [BC] Method alterFileDone() of class Psalm\Progress\Progress changed from concrete to abstract + +- [BC] Method expand() of class Psalm\Progress\Progress changed from concrete to abstract + +- [BC] Method taskDone() of class Psalm\Progress\Progress changed from concrete to abstract + +- [BC] Method finish() of class Psalm\Progress\Progress changed from concrete to abstract + # Upgrading from Psalm 5 to Psalm 6 ## Changed diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index d3c0de3d907..25f2106f59f 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -47,6 +47,7 @@ use Psalm\Issue\UnusedProperty; use Psalm\Issue\UnusedVariable; use Psalm\Plugin\EventHandler\Event\AfterCodebasePopulatedEvent; +use Psalm\Progress\Phase; use Psalm\Progress\Progress; use Psalm\Progress\VoidProgress; use Psalm\Report; @@ -461,7 +462,7 @@ public function check(string $base_dir, bool $is_diff = false): void } $this->progress->write($this->generatePHPVersionMessage()); - $this->progress->startScanningFiles(); + $this->progress->startPhase(Phase::SCAN); $diff_no_files = false; @@ -519,7 +520,7 @@ public function check(string $base_dir, bool $is_diff = false): void $this->config->eventDispatcher->dispatchAfterCodebasePopulated($event); } - $this->progress->startAnalyzingFiles(); + $this->progress->startPhase(Phase::ANALYSIS); $this->codebase->analyzer->analyzeFiles( $this, @@ -875,7 +876,7 @@ public function checkDir(string $dir_name): void $this->checkDirWithConfig($dir_name, $this->config, true); $this->progress->write($this->generatePHPVersionMessage()); - $this->progress->startScanningFiles(); + $this->progress->startPhase(Phase::SCAN); $this->config->initializePlugins($this); @@ -883,7 +884,7 @@ public function checkDir(string $dir_name): void $this->config->visitStubFiles($this->codebase, $this->progress); - $this->progress->startAnalyzingFiles(); + $this->progress->startPhase(Phase::ANALYSIS); $this->codebase->analyzer->analyzeFiles( $this, @@ -975,7 +976,7 @@ public function checkFile(string $file_path): void $this->file_reference_provider->loadReferenceCache(); $this->progress->write($this->generatePHPVersionMessage()); - $this->progress->startScanningFiles(); + $this->progress->startPhase(Phase::SCAN); $this->config->initializePlugins($this); @@ -983,7 +984,7 @@ public function checkFile(string $file_path): void $this->config->visitStubFiles($this->codebase, $this->progress); - $this->progress->startAnalyzingFiles(); + $this->progress->startPhase(Phase::ANALYSIS); $this->codebase->analyzer->analyzeFiles( $this, @@ -999,7 +1000,7 @@ public function checkFile(string $file_path): void public function checkPaths(array $paths_to_check): void { $this->progress->write($this->generatePHPVersionMessage()); - $this->progress->startScanningFiles(); + $this->progress->startPhase(Phase::SCAN); $this->config->visitPreloadedStubFiles($this->codebase, $this->progress); $this->visitAutoloadFiles(); @@ -1031,7 +1032,7 @@ public function checkPaths(array $paths_to_check): void $this->config->eventDispatcher->dispatchAfterCodebasePopulated($event); - $this->progress->startAnalyzingFiles(); + $this->progress->startPhase(Phase::ANALYSIS); $this->codebase->analyzer->analyzeFiles( $this, diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index 3482a4497d2..5d99a386d9b 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -13,6 +13,7 @@ use Psalm\Exception\ConfigException; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\CliUtils; +use Psalm\Internal\Codebase\InternalCallMapHandler; use Psalm\Internal\Codebase\ReferenceMapGenerator; use Psalm\Internal\Composer; use Psalm\Internal\ErrorHandler; @@ -389,6 +390,9 @@ public static function run(array $argv): void $config->addPluginPath($plugin_path); } + // Prime cache + InternalCallMapHandler::getCallMap(); + if ($paths_to_check === null) { $project_analyzer->check($current_dir, $is_diff); } elseif ($paths_to_check) { diff --git a/src/Psalm/Internal/Codebase/Analyzer.php b/src/Psalm/Internal/Codebase/Analyzer.php index ec9969c87c9..ee664870864 100644 --- a/src/Psalm/Internal/Codebase/Analyzer.php +++ b/src/Psalm/Internal/Codebase/Analyzer.php @@ -25,6 +25,7 @@ use Psalm\Internal\Provider\FileProvider; use Psalm\Internal\Provider\FileStorageProvider; use Psalm\IssueBuffer; +use Psalm\Progress\Phase; use Psalm\Progress\Progress; use Psalm\Type; use Psalm\Type\Union; @@ -247,7 +248,7 @@ public function analyzeFiles( $scanned_files = $codebase->scanner->getScannedFiles(); if ($codebase->taint_flow_graph) { - $codebase->taint_flow_graph->connectSinksAndSources(); + $codebase->taint_flow_graph->connectSinksAndSources($codebase->progress); } $this->progress->finish(); @@ -280,7 +281,7 @@ public function analyzeFiles( } if ($alter_code) { - $this->progress->startAlteringFiles(); + $this->progress->startPhase(Phase::ALTERING); $project_analyzer->prepareMigration(); @@ -296,7 +297,7 @@ public function analyzeFiles( private function doAnalysis(ProjectAnalyzer $project_analyzer, int $pool_size): void { - $this->progress->start(count($this->files_to_analyze)); + $this->progress->expand(count($this->files_to_analyze)); ksort($this->files_to_analyze); diff --git a/src/Psalm/Internal/Codebase/TaintFlowGraph.php b/src/Psalm/Internal/Codebase/TaintFlowGraph.php index 64bc7ac67a0..1405581ac70 100644 --- a/src/Psalm/Internal/Codebase/TaintFlowGraph.php +++ b/src/Psalm/Internal/Codebase/TaintFlowGraph.php @@ -31,6 +31,8 @@ use Psalm\Issue\TaintedUserSecret; use Psalm\Issue\TaintedXpath; use Psalm\IssueBuffer; +use Psalm\Progress\Phase; +use Psalm\Progress\Progress; use Psalm\Type\TaintKind; use function array_diff; @@ -202,8 +204,10 @@ public function getIssueTrace(DataFlowNode $source): array return [$node]; } - public function connectSinksAndSources(): void + public function connectSinksAndSources(Progress $progress): void { + $progress->startPhase(Phase::TAINT_GRAPH_RESOLUTION); + $visited_source_ids = []; $sources = $this->sources; @@ -217,6 +221,7 @@ public function connectSinksAndSources(): void $new_sources = []; ksort($sources); + $progress->expand(count($sources)); foreach ($sources as $source) { $source_taints = $source->taints; @@ -234,6 +239,8 @@ public function connectSinksAndSources(): void $visited_source_ids, )]; } + + $progress->taskDone(0); } $sources = $new_sources; diff --git a/src/Psalm/Internal/LanguageServer/Progress.php b/src/Psalm/Internal/LanguageServer/Progress.php index cc21674b37b..3541b98dfbf 100644 --- a/src/Psalm/Internal/LanguageServer/Progress.php +++ b/src/Psalm/Internal/LanguageServer/Progress.php @@ -5,6 +5,7 @@ namespace Psalm\Internal\LanguageServer; use Override; +use Psalm\Progress\Phase; use Psalm\Progress\Progress as Base; use function str_replace; @@ -30,6 +31,31 @@ public function debug(string $message): void } } + #[Override] + public function startPhase(Phase $phase): void + { + } + + #[Override] + public function alterFileDone(string $file_name): void + { + } + + #[Override] + public function expand(int $number_of_tasks): void + { + } + + #[Override] + public function taskDone(int $level): void + { + } + + #[Override] + public function finish(): void + { + } + #[Override] public function write(string $message): void { diff --git a/src/Psalm/Internal/Preloader.php b/src/Psalm/Internal/Preloader.php index 5e744625d59..35ab324812b 100644 --- a/src/Psalm/Internal/Preloader.php +++ b/src/Psalm/Internal/Preloader.php @@ -4,11 +4,12 @@ namespace Psalm\Internal; +use Psalm\Progress\Phase; use Psalm\Progress\Progress; use function class_exists; -use const PHP_EOL; +use function count; /** @internal */ final class Preloader @@ -21,13 +22,15 @@ public static function preload(?Progress $progress = null, bool $hasJit = false) } if ($hasJit) { - $progress?->write("JIT compilation in progress... "); + $progress?->startPhase(Phase::JIT_COMPILATION); + $progress?->expand(count(PreloaderList::CLASSES)+1); } foreach (PreloaderList::CLASSES as $class) { + $progress?->taskDone(0); class_exists($class); } if ($hasJit) { - $progress?->write("Done.".PHP_EOL.PHP_EOL); + $progress?->finish(); } self::$preloaded = true; } diff --git a/src/Psalm/Progress/DebugProgress.php b/src/Psalm/Progress/DebugProgress.php index f20c26681ac..9c2d9587113 100644 --- a/src/Psalm/Progress/DebugProgress.php +++ b/src/Psalm/Progress/DebugProgress.php @@ -25,21 +25,30 @@ public function debug(string $message): void } #[Override] - public function startScanningFiles(): void + public function startPhase(Phase $phase): void + { + $this->write(match ($phase) { + Phase::SCAN => "\nScanning files...\n\n", + Phase::ANALYSIS => "\nAnalyzing files...\n", + Phase::ALTERING => "\nUpdating files...\n", + Phase::TAINT_GRAPH_RESOLUTION => "\nResolving taint graph...\n", + Phase::JIT_COMPILATION => "\nJIT compilation in progress...\n", + }); + } + + #[Override] + public function expand(int $number_of_tasks): void { - $this->write("\n" . 'Scanning files...' . "\n\n"); } #[Override] - public function startAnalyzingFiles(): void + public function taskDone(int $level): void { - $this->write("\n" . 'Analyzing files...' . "\n"); } #[Override] - public function startAlteringFiles(): void + public function finish(): void { - $this->write('Updating files...' . "\n"); } #[Override] diff --git a/src/Psalm/Progress/DefaultProgress.php b/src/Psalm/Progress/DefaultProgress.php index 4f08807d990..46d28e266d2 100644 --- a/src/Psalm/Progress/DefaultProgress.php +++ b/src/Psalm/Progress/DefaultProgress.php @@ -6,8 +6,8 @@ use Override; +use function hrtime; use function max; -use function microtime; use function str_repeat; use function strlen; @@ -17,10 +17,13 @@ class DefaultProgress extends LongProgress // Update the progress bar at most once per 0.1 seconds. // This reduces flickering and reduces the amount of time spent writing to STDERR and updating the terminal. - private const PROGRESS_BAR_SAMPLE_INTERVAL = 0.1; + private const PROGRESS_BAR_SAMPLE_INTERVAL_NANOSECONDS = 100_000_000; - /** @var float the last time when the progress bar UI was updated */ - private float $previous_update_time = 0.0; + /** the last time when the progress bar UI was updated (seconds) */ + private int $previous_update_seconds = 0; + + /** the last time when the progress bar UI was updated (nanoseconds) */ + private int $previous_update_nseconds = 0; #[Override] public function taskDone(int $level): void @@ -28,21 +31,21 @@ public function taskDone(int $level): void if ($this->fixed_size && $this->number_of_tasks > self::TOO_MANY_FILES) { ++$this->progress; - // Source for rate limiting: - // https://github.com/phan/phan/blob/9a788581ee1a4e1c35bebf89c435fd8a238c1d17/src/Phan/CLI.php - $time = microtime(true); + [$seconds, $nseconds] = hrtime(); // If not enough time has elapsed, then don't update the progress bar. // Making the update frequency based on time (instead of the number of files) // prevents the terminal from rapidly flickering while processing small/empty files, // and reduces the time spent writing to stderr. - if ($time - $this->previous_update_time < self::PROGRESS_BAR_SAMPLE_INTERVAL) { - // Make sure to output the section for 100% completion regardless of limits, to avoid confusion. - if ($this->progress !== $this->number_of_tasks) { - return; - } + // Make sure to output the section for 100% completion regardless of limits, to avoid confusion. + if ($seconds === $this->previous_update_seconds + && ($nseconds - $this->previous_update_nseconds < self::PROGRESS_BAR_SAMPLE_INTERVAL_NANOSECONDS) + && $this->progress !== $this->number_of_tasks + ) { + return; } - $this->previous_update_time = $time; + $this->previous_update_seconds = $seconds; + $this->previous_update_nseconds = $nseconds; $inner_progress = self::renderInnerProgressBar( self::NUMBER_OF_COLUMNS, @@ -103,8 +106,7 @@ public function finish(): void { if ($this->number_of_tasks > self::TOO_MANY_FILES) { $this->write(str_repeat(' ', self::NUMBER_OF_COLUMNS + strlen($this->getOverview()) + 1) . "\r"); - } else { - parent::finish(); } + parent::finish(); } } diff --git a/src/Psalm/Progress/LongProgress.php b/src/Psalm/Progress/LongProgress.php index 03dd630fd0a..f55de1f01f2 100644 --- a/src/Psalm/Progress/LongProgress.php +++ b/src/Psalm/Progress/LongProgress.php @@ -8,6 +8,8 @@ use Override; use function floor; +use function microtime; +use function round; use function sprintf; use function str_repeat; use function strlen; @@ -24,6 +26,9 @@ class LongProgress extends Progress protected bool $fixed_size = false; + protected ?Phase $prevPhase = null; + protected float $started = 0.0; + public function __construct( protected bool $print_errors = true, protected bool $print_infos = true, @@ -32,24 +37,45 @@ public function __construct( } #[Override] - public function startScanningFiles(): void + public function debug(string $message): void { - $this->fixed_size = false; - $this->write("\n" . 'Scanning files...' . ($this->in_ci ? '' : "\n\n")); } #[Override] - public function startAnalyzingFiles(): void + public function startPhase(Phase $phase): void { - $this->fixed_size = true; - $this->write("\n\n" . 'Analyzing files...' . "\n\n"); + $this->reportPhaseDuration($phase); + $this->write(match ($phase) { + Phase::SCAN => "\nScanning files...\n\n", + Phase::ANALYSIS => "\nAnalyzing files...\n\n", + Phase::ALTERING => "\nUpdating files...\n", + Phase::TAINT_GRAPH_RESOLUTION => "\n\nResolving taint graph...\n\n", + Phase::JIT_COMPILATION => "JIT compilation in progress...\n\n", + }); + $this->fixed_size = $phase === Phase::ANALYSIS + || $phase === Phase::ALTERING + || $phase === Phase::JIT_COMPILATION; } - #[Override] - public function startAlteringFiles(): void + protected function reportPhaseDuration(?Phase $newPhase = null): void { - $this->fixed_size = true; - $this->write('Altering files...' . "\n"); + if ($this->prevPhase === $newPhase) { + return; + } + $this->progress = 0; + $this->number_of_tasks = 0; + if ($this->prevPhase !== null) { + $took = round(microtime(true) - $this->started, 1); + $this->write(match ($this->prevPhase) { + Phase::SCAN => "\n\nScan took $took seconds.\n", + Phase::ANALYSIS => "\n\nAnalysis took $took seconds.\n", + Phase::ALTERING => "\n\nUpdating files took $took seconds.\n", + Phase::TAINT_GRAPH_RESOLUTION => "\n\nTaint graph resolution took $took seconds.\n", + Phase::JIT_COMPILATION => "JIT compilation took $took seconds.\n\n", + }); + } + $this->started = microtime(true); + $this->prevPhase = $newPhase; } #[Override] @@ -58,13 +84,6 @@ public function alterFileDone(string $file_name): void $this->write('Altered ' . $file_name . "\n"); } - #[Override] - public function start(int $number_of_tasks): void - { - $this->number_of_tasks = $number_of_tasks; - $this->progress = 0; - } - #[Override] public function expand(int $number_of_tasks): void { @@ -114,7 +133,7 @@ public function taskDone(int $level): void #[Override] public function finish(): void { - $this->write(PHP_EOL); + $this->reportPhaseDuration(null); } protected function getOverview(): string diff --git a/src/Psalm/Progress/Phase.php b/src/Psalm/Progress/Phase.php new file mode 100644 index 00000000000..701d0827048 --- /dev/null +++ b/src/Psalm/Progress/Phase.php @@ -0,0 +1,18 @@ +write('Warning: ' . $message . PHP_EOL); } - protected static function doesTerminalSupportUtf8(): bool + final protected static function doesTerminalSupportUtf8(): bool { if (stripos(PHP_OS, 'WIN') === 0) { if (!function_exists('sapi_windows_cp_is_utf8') || !sapi_windows_cp_is_utf8()) { diff --git a/src/Psalm/Progress/VoidProgress.php b/src/Psalm/Progress/VoidProgress.php index 0758204900f..e339eb4ae40 100644 --- a/src/Psalm/Progress/VoidProgress.php +++ b/src/Psalm/Progress/VoidProgress.php @@ -8,6 +8,36 @@ final class VoidProgress extends Progress { + #[Override] + public function debug(string $message): void + { + } + + #[Override] + public function startPhase(Phase $phase): void + { + } + + #[Override] + public function alterFileDone(string $file_name): void + { + } + + #[Override] + public function expand(int $number_of_tasks): void + { + } + + #[Override] + public function taskDone(int $level): void + { + } + + #[Override] + public function finish(): void + { + } + #[Override] public function write(string $message): void { diff --git a/tests/AsyncTestCase.php b/tests/AsyncTestCase.php index fc50ee67f0e..7cb1948f754 100644 --- a/tests/AsyncTestCase.php +++ b/tests/AsyncTestCase.php @@ -129,7 +129,7 @@ public function analyzeFile(string $file_path, Context $context, bool $track_unu $file_analyzer->analyze($context); if ($codebase->taint_flow_graph) { - $codebase->taint_flow_graph->connectSinksAndSources(); + $codebase->taint_flow_graph->connectSinksAndSources($codebase->progress); } if ($track_unused_suppressions) { diff --git a/tests/TestCase.php b/tests/TestCase.php index e4638711c40..d69aa9a92d0 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -151,7 +151,7 @@ public function analyzeFile(string $file_path, Context $context, bool $track_unu $file_analyzer->analyze($context); if ($codebase->taint_flow_graph) { - $codebase->taint_flow_graph->connectSinksAndSources(); + $codebase->taint_flow_graph->connectSinksAndSources($codebase->progress); } if ($track_unused_suppressions) {