From c6854cf5676ce3505e4fc0875c2aee1259f83044 Mon Sep 17 00:00:00 2001 From: asrar Date: Sat, 21 May 2022 17:39:51 +0200 Subject: [PATCH 001/194] Adds support for fixing missing throws doc block --- .../Analyzer/FunctionLikeAnalyzer.php | 17 +++++++++ .../Internal/Analyzer/ProjectAnalyzer.php | 1 + .../FunctionDocblockManipulator.php | 21 +++++++++++ .../FileManipulationTestCase.php | 1 + .../ThrowsBlockAdditionTest.php | 37 +++++++++++++++++++ 5 files changed, 77 insertions(+) create mode 100644 tests/FileManipulation/ThrowsBlockAdditionTest.php diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 6e3d216c61e..2fea7dd9ce8 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -706,6 +706,10 @@ public function analyze( } } + /** + * @var list + */ + $missingThrowsDocblockErrors = []; foreach ($statements_analyzer->getUncaughtThrows($context) as $possibly_thrown_exception => $codelocations) { $is_expected = false; @@ -719,6 +723,7 @@ public function analyze( } if (!$is_expected) { + $missingThrowsDocblockErrors[] = $possibly_thrown_exception; foreach ($codelocations as $codelocation) { // issues are suppressed in ThrowAnalyzer, CallAnalyzer, etc. IssueBuffer::maybeAdd( @@ -732,6 +737,18 @@ public function analyze( } } + if ( + $codebase->alter_code + && isset($project_analyzer->getIssuesToFix()['MissingThrowsDocblock']) + ) { + $manipulator = FunctionDocblockManipulator::getForFunction( + $project_analyzer, + $this->source->getFilePath(), + $this->function + ); + $manipulator->addThrowsDocblock($missingThrowsDocblockErrors); + } + if ($codebase->taint_flow_graph && $this->function instanceof ClassMethod && $cased_method_id diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 4e99005e19e..7275069e174 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -1322,6 +1322,7 @@ public function setIssuesToFix(array $issues): void $supported_issues_to_fix[] = 'MissingImmutableAnnotation'; $supported_issues_to_fix[] = 'MissingPureAnnotation'; + $supported_issues_to_fix[] = 'MissingThrowsDocblock'; $unsupportedIssues = array_diff(array_keys($issues), $supported_issues_to_fix); diff --git a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php index 147ba4e7ccf..d2d9da9a060 100644 --- a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php @@ -96,6 +96,9 @@ class FunctionDocblockManipulator /** @var bool */ private $is_pure = false; + /** @var list */ + private $throwsExceptions = []; + /** * @param Closure|Function_|ClassMethod|ArrowFunction $stmt */ @@ -395,6 +398,16 @@ private function getDocblock(): string $modified_docblock = true; $parsed_docblock->tags['psalm-pure'] = ['']; } + if (\count($this->throwsExceptions) > 0) { + $modified_docblock = true; + $parsed_docblock->tags['throws'] = [ + \array_reduce( + $this->throwsExceptions, + fn(string $throwsClause, string $exception) => $throwsClause === '' ? $exception : $throwsClause.'|'.$exception, + '' + ) + ]; + } if ($this->new_phpdoc_return_type && $this->new_phpdoc_return_type !== $old_phpdoc_return_type) { @@ -528,6 +541,14 @@ public function makePure(): void $this->is_pure = true; } + /** + * @param list $exceptions + */ + public function addThrowsDocblock(array $exceptions): void + { + $this->throwsExceptions = $exceptions; + } + public static function clearCache(): void { self::$manipulators = []; diff --git a/tests/FileManipulation/FileManipulationTestCase.php b/tests/FileManipulation/FileManipulationTestCase.php index 61b9d7f724d..9ff5f542e83 100644 --- a/tests/FileManipulation/FileManipulationTestCase.php +++ b/tests/FileManipulation/FileManipulationTestCase.php @@ -86,6 +86,7 @@ public function testValidCode( $safe_types ); $this->project_analyzer->getCodebase()->allow_backwards_incompatible_changes = $allow_backwards_incompatible_changes; + $this->project_analyzer->getConfig()->check_for_throws_docblock = true; if (strpos(static::class, 'Unused') || strpos(static::class, 'Unnecessary')) { $this->project_analyzer->getCodebase()->reportUnusedCode(); diff --git a/tests/FileManipulation/ThrowsBlockAdditionTest.php b/tests/FileManipulation/ThrowsBlockAdditionTest.php new file mode 100644 index 00000000000..6214c7f0908 --- /dev/null +++ b/tests/FileManipulation/ThrowsBlockAdditionTest.php @@ -0,0 +1,37 @@ + + */ + public function providerValidCodeParse(): array + { + return [ + 'addThrowsAnnotationToFunction' => [ + ' Date: Sun, 22 May 2022 18:27:38 +0200 Subject: [PATCH 002/194] feat: fix ci + preserve existing throws --- .../Analyzer/FunctionLikeAnalyzer.php | 6 ++- .../FunctionDocblockManipulator.php | 23 +++++--- .../ThrowsBlockAdditionTest.php | 54 +++++++++++++++++++ 3 files changed, 73 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 2fea7dd9ce8..cc5814a25e7 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -65,6 +65,8 @@ use function array_merge; use function array_search; use function array_values; +use function assert; +use function class_exists; use function count; use function end; use function in_array; @@ -723,6 +725,7 @@ public function analyze( } if (!$is_expected) { + assert(class_exists($possibly_thrown_exception)); $missingThrowsDocblockErrors[] = $possibly_thrown_exception; foreach ($codelocations as $codelocation) { // issues are suppressed in ThrowAnalyzer, CallAnalyzer, etc. @@ -737,8 +740,7 @@ public function analyze( } } - if ( - $codebase->alter_code + if ($codebase->alter_code && isset($project_analyzer->getIssuesToFix()['MissingThrowsDocblock']) ) { $manipulator = FunctionDocblockManipulator::getForFunction( diff --git a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php index d2d9da9a060..bd2ab9ed6e7 100644 --- a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php @@ -14,7 +14,9 @@ use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\Scanner\ParsedDocblock; +use function array_key_exists; use function array_merge; +use function array_reduce; use function count; use function is_string; use function ltrim; @@ -398,15 +400,20 @@ private function getDocblock(): string $modified_docblock = true; $parsed_docblock->tags['psalm-pure'] = ['']; } - if (\count($this->throwsExceptions) > 0) { + if (count($this->throwsExceptions) > 0) { $modified_docblock = true; - $parsed_docblock->tags['throws'] = [ - \array_reduce( - $this->throwsExceptions, - fn(string $throwsClause, string $exception) => $throwsClause === '' ? $exception : $throwsClause.'|'.$exception, - '' - ) - ]; + $inferredThrowsClause = array_reduce( + $this->throwsExceptions, + function (string $throwsClause, string $exception) { + return $throwsClause === '' ? $exception : $throwsClause.'|'.$exception; + }, + '' + ); + if (array_key_exists('throws', $parsed_docblock->tags)) { + $parsed_docblock->tags['throws'][] = $inferredThrowsClause; + } else { + $parsed_docblock->tags['throws'] = [$inferredThrowsClause]; + } } diff --git a/tests/FileManipulation/ThrowsBlockAdditionTest.php b/tests/FileManipulation/ThrowsBlockAdditionTest.php index 6214c7f0908..93a15f2ee61 100644 --- a/tests/FileManipulation/ThrowsBlockAdditionTest.php +++ b/tests/FileManipulation/ThrowsBlockAdditionTest.php @@ -32,6 +32,60 @@ function foo(string $s): string { ['MissingThrowsDocblock'], true, ], + 'addMultipleThrowsAnnotationToFunction' => [ + ' [ + ' Date: Sun, 22 May 2022 18:38:18 +0200 Subject: [PATCH 003/194] chore: add another test --- .../ThrowsBlockAdditionTest.php | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/tests/FileManipulation/ThrowsBlockAdditionTest.php b/tests/FileManipulation/ThrowsBlockAdditionTest.php index 93a15f2ee61..ae9eabf415d 100644 --- a/tests/FileManipulation/ThrowsBlockAdditionTest.php +++ b/tests/FileManipulation/ThrowsBlockAdditionTest.php @@ -86,6 +86,38 @@ function foo(string $s): string { ['MissingThrowsDocblock'], true, ], + 'doesNotAddDuplicateThrows' => [ + ' Date: Mon, 23 May 2022 19:45:33 +0200 Subject: [PATCH 004/194] refactor: use list --- src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php | 4 ---- .../Internal/FileManipulation/FunctionDocblockManipulator.php | 4 ++-- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index cc5814a25e7..faf22a70627 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -708,9 +708,6 @@ public function analyze( } } - /** - * @var list - */ $missingThrowsDocblockErrors = []; foreach ($statements_analyzer->getUncaughtThrows($context) as $possibly_thrown_exception => $codelocations) { $is_expected = false; @@ -725,7 +722,6 @@ public function analyze( } if (!$is_expected) { - assert(class_exists($possibly_thrown_exception)); $missingThrowsDocblockErrors[] = $possibly_thrown_exception; foreach ($codelocations as $codelocation) { // issues are suppressed in ThrowAnalyzer, CallAnalyzer, etc. diff --git a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php index bd2ab9ed6e7..ca652333ea8 100644 --- a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php @@ -98,7 +98,7 @@ class FunctionDocblockManipulator /** @var bool */ private $is_pure = false; - /** @var list */ + /** @var list */ private $throwsExceptions = []; /** @@ -549,7 +549,7 @@ public function makePure(): void } /** - * @param list $exceptions + * @param list $exceptions */ public function addThrowsDocblock(array $exceptions): void { From ffe18296b0abab9477e858311fe459a475b45953 Mon Sep 17 00:00:00 2001 From: sergkash7 <55360924+sergkash7@users.noreply.github.com> Date: Tue, 21 Jun 2022 22:03:11 +0300 Subject: [PATCH 005/194] Update phpredis.phpstub --- stubs/phpredis.phpstub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/phpredis.phpstub b/stubs/phpredis.phpstub index 791a0cfb87c..0687320eff0 100644 --- a/stubs/phpredis.phpstub +++ b/stubs/phpredis.phpstub @@ -141,7 +141,7 @@ class Redis { public function geosearchstore(string $dst, string $src, array|string $position, array|int|float $shape, string $unit, array $options = []): array; - /** @return string|Redis */ + /** @return false|string|Redis */ public function get(string $key); public function getAuth(): mixed; From 7742d8a903d5daaadd23f278672dd102a4c0cc31 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 10 Jul 2022 09:23:13 +0200 Subject: [PATCH 006/194] use lock to fix race condition --- .../Internal/Provider/ParserCacheProvider.php | 34 +++++++++++++++++-- 1 file changed, 32 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Provider/ParserCacheProvider.php b/src/Psalm/Internal/Provider/ParserCacheProvider.php index 85c0c06c42c..755a064a807 100644 --- a/src/Psalm/Internal/Provider/ParserCacheProvider.php +++ b/src/Psalm/Internal/Provider/ParserCacheProvider.php @@ -8,9 +8,14 @@ use RuntimeException; use function error_log; +use function fclose; use function file_get_contents; use function file_put_contents; use function filemtime; +use function filesize; +use function flock; +use function fopen; +use function fread; use function gettype; use function igbinary_serialize; use function igbinary_unserialize; @@ -28,9 +33,12 @@ use function trigger_error; use function unlink; use function unserialize; +use function usleep; use const DIRECTORY_SEPARATOR; use const E_USER_ERROR; +use const LOCK_EX; +use const LOCK_SH; use const SCANDIR_SORT_NONE; /** @@ -184,7 +192,28 @@ private function getExistingFileContentHashes(): array $file_hashes_path = $root_cache_directory . DIRECTORY_SEPARATOR . self::FILE_HASHES; if ($root_cache_directory && is_readable($file_hashes_path)) { - $hashes_encoded = (string) file_get_contents($file_hashes_path); + $fp = fopen($file_hashes_path, 'r'); + $max_wait_cycles = 5; + $has_lock = false; + while ($max_wait_cycles > 0) { + if (flock($fp, LOCK_SH)) { + $has_lock = true; + break; + } + $max_wait_cycles--; + usleep(50000); + } + + if (!$has_lock) { + fclose($fp); + error_log('Could not acquire lock for content hashes file'); + $this->existing_file_content_hashes = []; + + return []; + } + + $hashes_encoded = fread($fp, filesize($file_hashes_path)); + fclose($fp); if (!$hashes_encoded) { error_log('Unexpected value when loading from file content hashes'); @@ -281,7 +310,8 @@ public function saveFileContentHashes(): void file_put_contents( $file_hashes_path, - json_encode($file_content_hashes) + json_encode($file_content_hashes), + LOCK_EX ); } From a77f6fca12df7260c123d5b11c597de7fcdaa83a Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 10 Jul 2022 12:01:45 +0200 Subject: [PATCH 007/194] use error_log --- src/Psalm/Config.php | 5 ++--- src/Psalm/Internal/Provider/ParserCacheProvider.php | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 5748f15953e..08bc9604f2e 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -54,6 +54,7 @@ use function class_exists; use function count; use function dirname; +use function error_log; use function explode; use function extension_loaded; use function file_exists; @@ -94,12 +95,10 @@ use function substr; use function substr_count; use function sys_get_temp_dir; -use function trigger_error; use function unlink; use function version_compare; use const DIRECTORY_SEPARATOR; -use const E_USER_ERROR; use const GLOB_NOSORT; use const LIBXML_ERR_ERROR; use const LIBXML_ERR_FATAL; @@ -1016,7 +1015,7 @@ private static function fromXmlAndPaths( } if (is_dir($config->cache_directory) === false && @mkdir($config->cache_directory, 0777, true) === false) { - trigger_error('Could not create cache directory: ' . $config->cache_directory, E_USER_ERROR); + error_log('Could not create cache directory: ' . $config->cache_directory); } if ($cwd) { diff --git a/src/Psalm/Internal/Provider/ParserCacheProvider.php b/src/Psalm/Internal/Provider/ParserCacheProvider.php index 755a064a807..893844e2eee 100644 --- a/src/Psalm/Internal/Provider/ParserCacheProvider.php +++ b/src/Psalm/Internal/Provider/ParserCacheProvider.php @@ -30,13 +30,11 @@ use function scandir; use function serialize; use function touch; -use function trigger_error; use function unlink; use function unserialize; use function usleep; use const DIRECTORY_SEPARATOR; -use const E_USER_ERROR; use const LOCK_EX; use const LOCK_SH; use const SCANDIR_SORT_NONE; @@ -385,7 +383,7 @@ private function createCacheDirectory(string $parser_cache_directory): void } catch (RuntimeException $e) { // Race condition (#4483) if (!is_dir($parser_cache_directory)) { - trigger_error('Could not create parser cache directory: ' . $parser_cache_directory, E_USER_ERROR); + error_log('Could not create parser cache directory: ' . $parser_cache_directory); } } } From 3b76ac85dc7f67655a855003751d25d2742cbf73 Mon Sep 17 00:00:00 2001 From: Jack Worman Date: Sun, 10 Jul 2022 17:20:45 -0500 Subject: [PATCH 008/194] Count Report Format --- .../Internal/Analyzer/ProjectAnalyzer.php | 1 + src/Psalm/IssueBuffer.php | 9 ++++- src/Psalm/Report.php | 19 +-------- src/Psalm/Report/CountReport.php | 39 +++++++++++++++++++ src/Psalm/Report/ReportOptions.php | 2 +- tests/ReportOutputTest.php | 20 ++++++++++ 6 files changed, 70 insertions(+), 20 deletions(-) create mode 100644 src/Psalm/Report/CountReport.php diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 4e99005e19e..44cec8c4246 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -397,6 +397,7 @@ public static function getFileReportOptions(array $report_file_paths, bool $show '.pylint' => Report::TYPE_PYLINT, '.console' => Report::TYPE_CONSOLE, '.sarif' => Report::TYPE_SARIF, + 'count.txt' => Report::TYPE_COUNT, ]; foreach ($report_file_paths as $report_file_path) { diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index 0fea8f035fc..47bde92fccd 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -16,11 +16,11 @@ use Psalm\Issue\TaintedInput; use Psalm\Issue\UnusedPsalmSuppress; use Psalm\Plugin\EventHandler\Event\AfterAnalysisEvent; -use Psalm\Report; use Psalm\Report\CheckstyleReport; use Psalm\Report\CodeClimateReport; use Psalm\Report\CompactReport; use Psalm\Report\ConsoleReport; +use Psalm\Report\CountReport; use Psalm\Report\EmacsReport; use Psalm\Report\GithubActionsReport; use Psalm\Report\JsonReport; @@ -908,6 +908,13 @@ public static function getOutput( case Report::TYPE_CODECLIMATE: $output = new CodeClimateReport($normalized_data, self::$fixable_issue_counts, $report_options); break; + + case Report::TYPE_COUNT: + $output = new CountReport($normalized_data, self::$fixable_issue_counts, $report_options); + break; + + default: + throw new RuntimeException('Unexpected report format: ' . $report_options->format); } return $output->create(); diff --git a/src/Psalm/Report.php b/src/Psalm/Report.php index 6ad69c849fc..97604a799dd 100644 --- a/src/Psalm/Report.php +++ b/src/Psalm/Report.php @@ -28,24 +28,7 @@ abstract class Report public const TYPE_PHP_STORM = 'phpstorm'; public const TYPE_SARIF = 'sarif'; public const TYPE_CODECLIMATE = 'codeclimate'; - - public const SUPPORTED_OUTPUT_TYPES = [ - self::TYPE_COMPACT, - self::TYPE_CONSOLE, - self::TYPE_PYLINT, - self::TYPE_JSON, - self::TYPE_JSON_SUMMARY, - self::TYPE_SONARQUBE, - self::TYPE_EMACS, - self::TYPE_XML, - self::TYPE_JUNIT, - self::TYPE_CHECKSTYLE, - self::TYPE_TEXT, - self::TYPE_GITHUB_ACTIONS, - self::TYPE_PHP_STORM, - self::TYPE_SARIF, - self::TYPE_CODECLIMATE, - ]; + public const TYPE_COUNT = 'count'; /** * @var array diff --git a/src/Psalm/Report/CountReport.php b/src/Psalm/Report/CountReport.php new file mode 100644 index 00000000000..b044d851651 --- /dev/null +++ b/src/Psalm/Report/CountReport.php @@ -0,0 +1,39 @@ +issues_data as $issue_data) { + if (array_key_exists($issue_data->type, $issue_type_counts)) { + $issue_type_counts[$issue_data->type]++; + } else { + $issue_type_counts[$issue_data->type] = 1; + } + } + uksort($issue_type_counts, function (string $a, string $b) use ($issue_type_counts): int { + $cmp_result = $issue_type_counts[$a] <=> $issue_type_counts[$b]; + if ($cmp_result === 0) { + return $a <=> $b; + } else { + return $cmp_result; + } + }); + + $output = ''; + foreach ($issue_type_counts as $issue_type => $count) { + $output .= "{$issue_type}: {$count}\n"; + } + return $output; + } +} diff --git a/src/Psalm/Report/ReportOptions.php b/src/Psalm/Report/ReportOptions.php index 3ade03bd083..3f9143536b2 100644 --- a/src/Psalm/Report/ReportOptions.php +++ b/src/Psalm/Report/ReportOptions.php @@ -22,7 +22,7 @@ class ReportOptions public $show_info = true; /** - * @var value-of + * @var Report::TYPE_* */ public $format = Report::TYPE_CONSOLE; diff --git a/tests/ReportOutputTest.php b/tests/ReportOutputTest.php index 5fd7b59bc47..895aef8271d 100644 --- a/tests/ReportOutputTest.php +++ b/tests/ReportOutputTest.php @@ -1269,6 +1269,26 @@ public function testGithubActionsOutput(): void ); } + public function testCountOutput(): void + { + $this->analyzeFileForReport(); + + $report_options = new ReportOptions(); + $report_options->format = Report::TYPE_COUNT; + $expected_output = <<<'EOF' +MixedInferredReturnType: 1 +MixedReturnStatement: 1 +PossiblyUndefinedGlobalVariable: 1 +UndefinedConstant: 1 +UndefinedVariable: 1 + +EOF; + $this->assertSame( + $expected_output, + IssueBuffer::getOutput(IssueBuffer::getIssuesData(), $report_options) + ); + } + public function testEmptyReportIfNotError(): void { $this->addFile( From bcf3c5153c10830f6b71a3103f46e61d0f26b366 Mon Sep 17 00:00:00 2001 From: Benjamin Morel Date: Mon, 4 Jul 2022 01:10:49 +0200 Subject: [PATCH 009/194] Fix GEOSGeometry stubs with default values --- dictionaries/CallMap.php | 4 ++-- dictionaries/CallMap_historical.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 10f9d7189c7..4ea825e6c59 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -3633,7 +3633,7 @@ 'GEOSGeometry::__toString' => ['string'], 'GEOSGeometry::project' => ['float', 'other'=>'GEOSGeometry', 'normalized'=>'bool'], 'GEOSGeometry::interpolate' => ['GEOSGeometry', 'dist'=>'float', 'normalized'=>'bool'], -'GEOSGeometry::buffer' => ['GEOSGeometry', 'dist'=>'float', 'styleArray'=>'array'], +'GEOSGeometry::buffer' => ['GEOSGeometry', 'dist'=>'float', 'styleArray='=>'array'], 'GEOSGeometry::offsetCurve' => ['GEOSGeometry', 'dist'=>'float', 'styleArray'=>'array'], 'GEOSGeometry::envelope' => ['GEOSGeometry'], 'GEOSGeometry::intersection' => ['GEOSGeometry', 'geom'=>'GEOSGeometry'], @@ -3646,7 +3646,7 @@ 'GEOSGeometry::centroid' => ['GEOSGeometry'], 'GEOSGeometry::relate' => ['string|bool', 'otherGeom'=>'GEOSGeometry', 'pattern'=>'string'], 'GEOSGeometry::relateBoundaryNodeRule' => ['string', 'otherGeom'=>'GEOSGeometry', 'rule'=>'int'], -'GEOSGeometry::simplify' => ['GEOSGeometry', 'tolerance'=>'float', 'preserveTopology'=>'bool'], +'GEOSGeometry::simplify' => ['GEOSGeometry', 'tolerance'=>'float', 'preserveTopology='=>'bool'], 'GEOSGeometry::normalize' => ['GEOSGeometry'], 'GEOSGeometry::extractUniquePoints' => ['GEOSGeometry'], 'GEOSGeometry::disjoint' => ['bool', 'geom'=>'GEOSGeometry'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index ffff495f1fc..3f2b43e60a1 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1781,7 +1781,7 @@ 'GEOSGeometry::__toString' => ['string'], 'GEOSGeometry::area' => ['float'], 'GEOSGeometry::boundary' => ['GEOSGeometry'], - 'GEOSGeometry::buffer' => ['GEOSGeometry', 'dist'=>'float', 'styleArray'=>'array'], + 'GEOSGeometry::buffer' => ['GEOSGeometry', 'dist'=>'float', 'styleArray='=>'array'], 'GEOSGeometry::centroid' => ['GEOSGeometry'], 'GEOSGeometry::checkValidity' => ['array{valid: bool, reason?: string, location?: GEOSGeometry}'], 'GEOSGeometry::contains' => ['bool', 'geom'=>'GEOSGeometry'], @@ -1830,7 +1830,7 @@ 'GEOSGeometry::relate' => ['string|bool', 'otherGeom'=>'GEOSGeometry', 'pattern'=>'string'], 'GEOSGeometry::relateBoundaryNodeRule' => ['string', 'otherGeom'=>'GEOSGeometry', 'rule'=>'int'], 'GEOSGeometry::setSRID' => ['void', 'srid'=>'int'], - 'GEOSGeometry::simplify' => ['GEOSGeometry', 'tolerance'=>'float', 'preserveTopology'=>'bool'], + 'GEOSGeometry::simplify' => ['GEOSGeometry', 'tolerance'=>'float', 'preserveTopology='=>'bool'], 'GEOSGeometry::snapTo' => ['GEOSGeometry', 'geom'=>'GEOSGeometry', 'tolerance'=>'float'], 'GEOSGeometry::startPoint' => ['GEOSGeometry'], 'GEOSGeometry::symDifference' => ['GEOSGeometry', 'geom'=>'GEOSGeometry'], From 470885e4f1c366d769660471a60d25f64902cd87 Mon Sep 17 00:00:00 2001 From: someniatko Date: Tue, 12 Jul 2022 13:43:53 +0300 Subject: [PATCH 010/194] #8200 - improve inferring the "final" `static` type when calling static methods inside a different class differentiate between `static` defined in a class which CALLS a given static method, and `static` defined in the method which IS CALLED. --- .../ExistingAtomicStaticCallAnalyzer.php | 34 +++++++++++ tests/Template/Issue8200Test.php | 58 +++++++++++++++++++ 2 files changed, 92 insertions(+) create mode 100644 tests/Template/Issue8200Test.php diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index 7855200e95e..d8af9168d55 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -551,6 +551,18 @@ private static function getMethodReturnType( ) { $static_type = $context->self; $context_final = $codebase->classlike_storage_provider->get($context->self)->final; + } elseif ($context->calling_method_id !== null) { + $self_method_return = $codebase->methods->getMethodReturnType( + MethodIdentifier::fromMethodIdReference($context->calling_method_id), + $context->self + ); + // differentiate between these cases: + // 1. "static" in return type comes from return type of the + // method CALLING the currently analyzed static method - use $context->self. + // 2. "static" comes from the CALLED static method - use $fq_class_name. + $static_type = $self_method_return !== null && self::hasStaticInType($self_method_return) + ? $context->self + : $fq_class_name; } else { $static_type = $fq_class_name; } @@ -613,4 +625,26 @@ private static function getMethodReturnType( return $return_type_candidate; } + + /** + * Dumb way to determine whether a type contains "static" somewhere inside. + */ + private static function hasStaticInType(Union $union_type): bool + { + foreach ($union_type->getAtomicTypes() as $atomic_type) { + if ($atomic_type instanceof Atomic\TGenericObject) { + foreach ($atomic_type->type_params as $type_param) { + if (self::hasStaticInType($type_param)) { + return true; + } + } + } elseif ($atomic_type instanceof TNamedObject) { + if ($atomic_type->value === 'static') { + return true; + } + } + } + + return false; + } } diff --git a/tests/Template/Issue8200Test.php b/tests/Template/Issue8200Test.php new file mode 100644 index 00000000000..a20b5fa7507 --- /dev/null +++ b/tests/Template/Issue8200Test.php @@ -0,0 +1,58 @@ +,error_levels?:string[]}> + */ + public function providerValidCodeParse(): iterable + { + return [ + 'return TemplatedClass' => [ + ' + * + * @psalm-pure + */ + public static function just($value): self + { + return new self($value); + } + } + + abstract class Test + { + final private function __construct() {} + + /** @return Maybe */ + final public static function create(): Maybe + { + return Maybe::just(new static()); + } + }', + ], + ]; + } +} From 3a5054018bcc3a0708e53883fa0e4935a4ff6dad Mon Sep 17 00:00:00 2001 From: someniatko Date: Tue, 12 Jul 2022 21:00:19 +0300 Subject: [PATCH 011/194] #8200 - generalize ExistingAtomicStaticCallAnalyzer::hasStaticInType() for non-object cases --- .../ExistingAtomicStaticCallAnalyzer.php | 24 ++++++++------- tests/Template/Issue8200Test.php | 29 +++++++++++++++++++ 2 files changed, 43 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index d8af9168d55..4d13f11bd32 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -35,6 +35,7 @@ use Psalm\Type\Union; use function array_map; +use function assert; use function count; use function explode; use function in_array; @@ -629,17 +630,20 @@ private static function getMethodReturnType( /** * Dumb way to determine whether a type contains "static" somewhere inside. */ - private static function hasStaticInType(Union $union_type): bool + private static function hasStaticInType(Type\TypeNode $type): bool { - foreach ($union_type->getAtomicTypes() as $atomic_type) { - if ($atomic_type instanceof Atomic\TGenericObject) { - foreach ($atomic_type->type_params as $type_param) { - if (self::hasStaticInType($type_param)) { - return true; - } - } - } elseif ($atomic_type instanceof TNamedObject) { - if ($atomic_type->value === 'static') { + assert($type instanceof Atomic || $type instanceof Union); + $union_parts = $type instanceof Union + ? $type->getAtomicTypes() + : [ $type ]; + + foreach ($union_parts as $atomic_type) { + if ($atomic_type instanceof TNamedObject && $atomic_type->value === 'static') { + return true; + } + + foreach ($atomic_type->getChildNodes() as $child_type) { + if (self::hasStaticInType($child_type)) { return true; } } diff --git a/tests/Template/Issue8200Test.php b/tests/Template/Issue8200Test.php index a20b5fa7507..2e04bc1a51b 100644 --- a/tests/Template/Issue8200Test.php +++ b/tests/Template/Issue8200Test.php @@ -53,6 +53,35 @@ final public static function create(): Maybe } }', ], + 'return list' => [ + ' + * + * @psalm-pure + */ + public static function mklist($value): array + { + return [ $value ]; + } + } + + abstract class Test + { + final private function __construct() {} + + /** @return list */ + final public static function create(): array + { + return Lister::mklist(new static()); + } + }', + ] ]; } } From b3e673d7ec3dd2e07393ae08ee4e37a57f46c0d0 Mon Sep 17 00:00:00 2001 From: someniatko Date: Tue, 12 Jul 2022 21:17:10 +0300 Subject: [PATCH 012/194] #8200 - flip logic of determining "source" of `static` type in ExistingAtomicStaticCallAnalyzer::getMethodReturnType() --- .../ExistingAtomicStaticCallAnalyzer.php | 14 +++---- tests/Template/Issue8200Test.php | 38 +++++++++++++++++++ 2 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index 4d13f11bd32..21484782e0b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -553,17 +553,13 @@ private static function getMethodReturnType( $static_type = $context->self; $context_final = $codebase->classlike_storage_provider->get($context->self)->final; } elseif ($context->calling_method_id !== null) { - $self_method_return = $codebase->methods->getMethodReturnType( - MethodIdentifier::fromMethodIdReference($context->calling_method_id), - $context->self - ); // differentiate between these cases: - // 1. "static" in return type comes from return type of the + // 1. "static" comes from the CALLED static method - use $fq_class_name. + // 2. "static" in return type comes from return type of the // method CALLING the currently analyzed static method - use $context->self. - // 2. "static" comes from the CALLED static method - use $fq_class_name. - $static_type = $self_method_return !== null && self::hasStaticInType($self_method_return) - ? $context->self - : $fq_class_name; + $static_type = self::hasStaticInType($return_type_candidate) + ? $fq_class_name + : $context->self; } else { $static_type = $fq_class_name; } diff --git a/tests/Template/Issue8200Test.php b/tests/Template/Issue8200Test.php index 2e04bc1a51b..202466b9019 100644 --- a/tests/Template/Issue8200Test.php +++ b/tests/Template/Issue8200Test.php @@ -81,6 +81,44 @@ final public static function create(): array return Lister::mklist(new static()); } }', + ], + 'use TemplatedClass as an intermediate variable inside a method' => [ + ' + * + * @psalm-pure + */ + public static function just($value): self + { + return new self($value); + } + } + + abstract class Test + { + final private function __construct() {} + + final public static function create(): static + { + $maybe = Maybe::just(new static()); + return $maybe->value; + } + }', ] ]; } From ecbceb1d58736fe582949e2d2b7b198f529bc098 Mon Sep 17 00:00:00 2001 From: someniatko Date: Tue, 12 Jul 2022 21:38:23 +0300 Subject: [PATCH 013/194] #8200 - move Issue8200Test to ClassTemplateTest --- tests/Template/ClassTemplateTest.php | 105 ++++++++++++++++++++++ tests/Template/Issue8200Test.php | 125 --------------------------- 2 files changed, 105 insertions(+), 125 deletions(-) delete mode 100644 tests/Template/Issue8200Test.php diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index 62ee25a2380..9ea95e8d35b 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -4474,6 +4474,111 @@ class B {} ', 'error_message' => 'InvalidArgument', ], + 'return TemplatedClass' => [ + ' + * + * @psalm-pure + */ + public static function just($value): self + { + return new self($value); + } + } + + abstract class Test + { + final private function __construct() {} + + /** @return Maybe */ + final public static function create(): Maybe + { + return Maybe::just(new static()); + } + }', + ], + 'return list created in a static method of another class' => [ + ' + * + * @psalm-pure + */ + public static function mklist($value): array + { + return [ $value ]; + } + } + + abstract class Test + { + final private function __construct() {} + + /** @return list */ + final public static function create(): array + { + return Lister::mklist(new static()); + } + }', + ], + 'use TemplatedClass as an intermediate variable inside a method' => [ + ' + * + * @psalm-pure + */ + public static function just($value): self + { + return new self($value); + } + } + + abstract class Test + { + final private function __construct() {} + + final public static function create(): static + { + $maybe = Maybe::just(new static()); + return $maybe->value; + } + }', + ], ]; } } diff --git a/tests/Template/Issue8200Test.php b/tests/Template/Issue8200Test.php deleted file mode 100644 index 202466b9019..00000000000 --- a/tests/Template/Issue8200Test.php +++ /dev/null @@ -1,125 +0,0 @@ -,error_levels?:string[]}> - */ - public function providerValidCodeParse(): iterable - { - return [ - 'return TemplatedClass' => [ - ' - * - * @psalm-pure - */ - public static function just($value): self - { - return new self($value); - } - } - - abstract class Test - { - final private function __construct() {} - - /** @return Maybe */ - final public static function create(): Maybe - { - return Maybe::just(new static()); - } - }', - ], - 'return list' => [ - ' - * - * @psalm-pure - */ - public static function mklist($value): array - { - return [ $value ]; - } - } - - abstract class Test - { - final private function __construct() {} - - /** @return list */ - final public static function create(): array - { - return Lister::mklist(new static()); - } - }', - ], - 'use TemplatedClass as an intermediate variable inside a method' => [ - ' - * - * @psalm-pure - */ - public static function just($value): self - { - return new self($value); - } - } - - abstract class Test - { - final private function __construct() {} - - final public static function create(): static - { - $maybe = Maybe::just(new static()); - return $maybe->value; - } - }', - ] - ]; - } -} From 931b3bb18b3fa900cae5d51130ee8360ad4be58f Mon Sep 17 00:00:00 2001 From: someniatko Date: Tue, 12 Jul 2022 21:41:06 +0300 Subject: [PATCH 014/194] #8200 - simplify ExistingAtomicStaticCallAnalyzer::hasStaticType() --- .../ExistingAtomicStaticCallAnalyzer.php | 18 +++++------------- 1 file changed, 5 insertions(+), 13 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index 21484782e0b..d7ac4ef16ff 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -35,7 +35,6 @@ use Psalm\Type\Union; use function array_map; -use function assert; use function count; use function explode; use function in_array; @@ -628,21 +627,14 @@ private static function getMethodReturnType( */ private static function hasStaticInType(Type\TypeNode $type): bool { - assert($type instanceof Atomic || $type instanceof Union); - $union_parts = $type instanceof Union - ? $type->getAtomicTypes() - : [ $type ]; + if ($type instanceof TNamedObject && $type->value === 'static') { + return true; + } - foreach ($union_parts as $atomic_type) { - if ($atomic_type instanceof TNamedObject && $atomic_type->value === 'static') { + foreach ($type->getChildNodes() as $child_type) { + if (self::hasStaticInType($child_type)) { return true; } - - foreach ($atomic_type->getChildNodes() as $child_type) { - if (self::hasStaticInType($child_type)) { - return true; - } - } } return false; From 21a6dd909669f1faf85f9be8607e252183e91bb2 Mon Sep 17 00:00:00 2001 From: someniatko Date: Tue, 12 Jul 2022 21:53:14 +0300 Subject: [PATCH 015/194] #8200 - move tests to the correct provider ("valid" instead of "invalid") --- tests/Template/ClassTemplateTest.php | 210 +++++++++++++-------------- 1 file changed, 105 insertions(+), 105 deletions(-) diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index 9ea95e8d35b..73a0fa9d811 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -3694,6 +3694,111 @@ protected function setUp(): void } }', ], + 'return TemplatedClass' => [ + ' + * + * @psalm-pure + */ + public static function just($value): self + { + return new self($value); + } + } + + abstract class Test + { + final private function __construct() {} + + /** @return Maybe */ + final public static function create(): Maybe + { + return Maybe::just(new static()); + } + }', + ], + 'return list created in a static method of another class' => [ + ' + * + * @psalm-pure + */ + public static function mklist($value): array + { + return [ $value ]; + } + } + + abstract class Test + { + final private function __construct() {} + + /** @return list */ + final public static function create(): array + { + return Lister::mklist(new static()); + } + }', + ], + 'use TemplatedClass as an intermediate variable inside a method' => [ + ' + * + * @psalm-pure + */ + public static function just($value): self + { + return new self($value); + } + } + + abstract class Test + { + final private function __construct() {} + + final public static function create(): static + { + $maybe = Maybe::just(new static()); + return $maybe->value; + } + }', + ], ]; } @@ -4474,111 +4579,6 @@ class B {} ', 'error_message' => 'InvalidArgument', ], - 'return TemplatedClass' => [ - ' - * - * @psalm-pure - */ - public static function just($value): self - { - return new self($value); - } - } - - abstract class Test - { - final private function __construct() {} - - /** @return Maybe */ - final public static function create(): Maybe - { - return Maybe::just(new static()); - } - }', - ], - 'return list created in a static method of another class' => [ - ' - * - * @psalm-pure - */ - public static function mklist($value): array - { - return [ $value ]; - } - } - - abstract class Test - { - final private function __construct() {} - - /** @return list */ - final public static function create(): array - { - return Lister::mklist(new static()); - } - }', - ], - 'use TemplatedClass as an intermediate variable inside a method' => [ - ' - * - * @psalm-pure - */ - public static function just($value): self - { - return new self($value); - } - } - - abstract class Test - { - final private function __construct() {} - - final public static function create(): static - { - $maybe = Maybe::just(new static()); - return $maybe->value; - } - }', - ], ]; } } From f28ac7377778e281c1b406251dd839f88ea4622e Mon Sep 17 00:00:00 2001 From: Corey Taylor Date: Fri, 8 Jul 2022 01:08:00 -0500 Subject: [PATCH 016/194] Fix nullable return types for CallMap functions --- dictionaries/CallMap.php | 56 +++++++++---------- dictionaries/CallMap_71_delta.php | 2 +- dictionaries/CallMap_80_delta.php | 8 +-- dictionaries/CallMap_81_delta.php | 4 +- dictionaries/CallMap_historical.php | 54 +++++++++--------- stubs/CoreGenericFunctions.phpstub | 2 +- .../Codebase/InternalCallMapHandlerTest.php | 16 ++++-- 7 files changed, 73 insertions(+), 69 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index ce0ffd4b209..c81fd16b8e4 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1066,7 +1066,7 @@ 'classObj::settext' => ['int', 'text'=>'string'], 'classObj::updateFromString' => ['int', 'snippet'=>'string'], 'clearstatcache' => ['void', 'clear_realpath_cache='=>'bool', 'filename='=>'string'], -'cli_get_process_title' => ['string'], +'cli_get_process_title' => ['?string'], 'cli_set_process_title' => ['bool', 'title'=>'string'], 'ClosedGeneratorException::__clone' => ['void'], 'ClosedGeneratorException::__toString' => ['string'], @@ -1106,7 +1106,7 @@ 'Collator::sortWithSortKeys' => ['bool', '&rw_arr'=>'array'], 'collator_asort' => ['bool', 'object'=>'collator', '&rw_array'=>'array', 'flags='=>'int'], 'collator_compare' => ['int', 'object'=>'collator', 'string1'=>'string', 'string2'=>'string'], -'collator_create' => ['Collator', 'locale'=>'string'], +'collator_create' => ['?Collator', 'locale'=>'string'], 'collator_get_attribute' => ['int|false', 'object'=>'collator', 'attribute'=>'int'], 'collator_get_error_code' => ['int', 'object'=>'collator'], 'collator_get_error_message' => ['string', 'object'=>'collator'], @@ -1678,7 +1678,7 @@ 'curl_multi_close' => ['void', 'multi_handle'=>'CurlMultiHandle'], 'curl_multi_errno' => ['int', 'multi_handle'=>'CurlMultiHandle'], 'curl_multi_exec' => ['int', 'multi_handle'=>'CurlMultiHandle', '&w_still_running'=>'int'], -'curl_multi_getcontent' => ['string', 'handle'=>'CurlHandle'], +'curl_multi_getcontent' => ['?string', 'handle'=>'CurlHandle'], 'curl_multi_info_read' => ['array|false', 'multi_handle'=>'CurlMultiHandle', '&w_queued_messages='=>'int'], 'curl_multi_init' => ['CurlMultiHandle|false'], 'curl_multi_remove_handle' => ['int', 'multi_handle'=>'CurlMultiHandle', 'handle'=>'CurlHandle'], @@ -1744,7 +1744,7 @@ 'datefmt_format' => ['string|false', 'formatter'=>'IntlDateFormatter', 'datetime'=>'DateTime|IntlCalendar|array|int'], 'datefmt_format_object' => ['string|false', 'datetime'=>'object', 'format='=>'mixed', 'locale='=>'string'], 'datefmt_get_calendar' => ['int', 'formatter'=>'IntlDateFormatter'], -'datefmt_get_calendar_object' => ['IntlCalendar', 'formatter'=>'IntlDateFormatter'], +'datefmt_get_calendar_object' => ['IntlCalendar|false|null', 'formatter'=>'IntlDateFormatter'], 'datefmt_get_datetype' => ['int', 'formatter'=>'IntlDateFormatter'], 'datefmt_get_error_code' => ['int', 'formatter'=>'IntlDateFormatter'], 'datefmt_get_error_message' => ['string', 'formatter'=>'IntlDateFormatter'], @@ -3362,7 +3362,7 @@ 'ftp_put' => ['bool', 'ftp'=>'FTP\Connection', 'remote_filename'=>'string', 'local_filename'=>'string', 'mode='=>'int', 'offset='=>'int'], 'ftp_pwd' => ['string|false', 'ftp'=>'FTP\Connection'], 'ftp_quit' => ['bool', 'ftp'=>'FTP\Connection'], -'ftp_raw' => ['array', 'ftp'=>'FTP\Connection', 'command'=>'string'], +'ftp_raw' => ['?array', 'ftp'=>'FTP\Connection', 'command'=>'string'], 'ftp_rawlist' => ['array|false', 'ftp'=>'FTP\Connection', 'directory'=>'string', 'recursive='=>'bool'], 'ftp_rename' => ['bool', 'ftp'=>'FTP\Connection', 'from'=>'string', 'to'=>'string'], 'ftp_rmdir' => ['bool', 'ftp'=>'FTP\Connection', 'directory'=>'string'], @@ -6190,7 +6190,7 @@ 'intlcal_after' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_before' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_clear' => ['bool', 'calendar'=>'IntlCalendar', 'field='=>'int'], -'intlcal_create_instance' => ['IntlCalendar', 'timezone='=>'mixed', 'locale='=>'string'], +'intlcal_create_instance' => ['?IntlCalendar', 'timezone='=>'mixed', 'locale='=>'string'], 'intlcal_equals' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_field_difference' => ['int', 'calendar'=>'IntlCalendar', 'timestamp'=>'float', 'field'=>'int'], 'intlcal_from_date_time' => ['IntlCalendar', 'datetime'=>'DateTime|string'], @@ -6503,8 +6503,8 @@ 'IntlTimeZone::useDaylightTime' => ['bool'], 'intltz_count_equivalent_ids' => ['int', 'timezoneId'=>'string'], 'intltz_create_enumeration' => ['IntlIterator', 'countryOrRawOffset'=>'mixed'], -'intltz_create_time_zone' => ['IntlTimeZone', 'timezoneId'=>'string'], -'intltz_from_date_time_zone' => ['IntlTimeZone', 'timezone'=>'DateTimeZone'], +'intltz_create_time_zone' => ['?IntlTimeZone', 'timezoneId'=>'string'], +'intltz_from_date_time_zone' => ['?IntlTimeZone', 'timezone'=>'DateTimeZone'], 'intltz_get_canonical_id' => ['string', 'timezoneId'=>'string', '&isSystemId'=>'bool'], 'intltz_get_display_name' => ['string', 'timezone'=>'IntlTimeZone', 'dst'=>'bool', 'style'=>'int', 'locale'=>'string'], 'intltz_get_dst_savings' => ['int', 'timezone'=>'IntlTimeZone'], @@ -6923,22 +6923,22 @@ 'Locale::parseLocale' => ['array', 'locale'=>'string'], 'Locale::setDefault' => ['bool', 'locale'=>'string'], 'locale_accept_from_http' => ['string|false', 'header'=>'string'], -'locale_canonicalize' => ['string', 'locale'=>'string'], +'locale_canonicalize' => ['?string', 'locale'=>'string'], 'locale_compose' => ['string|false', 'subtags'=>'array'], -'locale_filter_matches' => ['bool', 'languageTag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], -'locale_get_all_variants' => ['array', 'locale'=>'string'], +'locale_filter_matches' => ['?bool', 'languageTag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], +'locale_get_all_variants' => ['?array', 'locale'=>'string'], 'locale_get_default' => ['string'], 'locale_get_display_language' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_name' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_region' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_script' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_variant' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], -'locale_get_keywords' => ['array|false', 'locale'=>'string'], -'locale_get_primary_language' => ['string', 'locale'=>'string'], -'locale_get_region' => ['string', 'locale'=>'string'], -'locale_get_script' => ['string', 'locale'=>'string'], -'locale_lookup' => ['string', 'languageTag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'], -'locale_parse' => ['array', 'locale'=>'string'], +'locale_get_keywords' => ['array|false|null', 'locale'=>'string'], +'locale_get_primary_language' => ['?string', 'locale'=>'string'], +'locale_get_region' => ['?string', 'locale'=>'string'], +'locale_get_script' => ['?string', 'locale'=>'string'], +'locale_lookup' => ['?string', 'languageTag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'], +'locale_parse' => ['?array', 'locale'=>'string'], 'locale_set_default' => ['bool', 'locale'=>'string'], 'localeconv' => ['array'], 'localtime' => ['array', 'timestamp='=>'int', 'associative='=>'bool'], @@ -7279,7 +7279,7 @@ 'mb_ereg' => ['bool', 'pattern'=>'string', 'string'=>'string', '&w_matches='=>'array|null'], 'mb_ereg_match' => ['bool', 'pattern'=>'string', 'string'=>'string', 'options='=>'string|null'], 'mb_ereg_replace' => ['string|false|null', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'options='=>'string|null'], -'mb_ereg_replace_callback' => ['string|false', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string|null'], +'mb_ereg_replace_callback' => ['string|false|null', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string|null'], 'mb_ereg_search' => ['bool', 'pattern='=>'string|null', 'options='=>'string|null'], 'mb_ereg_search_getpos' => ['int'], 'mb_ereg_search_getregs' => ['string[]|false'], @@ -8167,7 +8167,7 @@ 'msg_send' => ['bool', 'queue'=>'resource', 'message_type'=>'int', 'message'=>'mixed', 'serialize='=>'bool', 'blocking='=>'bool', '&w_error_code='=>'int'], 'msg_set_queue' => ['bool', 'queue'=>'resource', 'data'=>'array'], 'msg_stat_queue' => ['array', 'queue'=>'resource'], -'msgfmt_create' => ['MessageFormatter', 'locale'=>'string', 'pattern'=>'string'], +'msgfmt_create' => ['?MessageFormatter', 'locale'=>'string', 'pattern'=>'string'], 'msgfmt_format' => ['string|false', 'formatter'=>'MessageFormatter', 'values'=>'array'], 'msgfmt_format_message' => ['string|false', 'locale'=>'string', 'pattern'=>'string', 'values'=>'array'], 'msgfmt_get_error_code' => ['int', 'formatter'=>'MessageFormatter'], @@ -8481,7 +8481,7 @@ 'mysqli_field_tell' => ['int', 'result'=>'mysqli_result'], 'mysqli_free_result' => ['void', 'result'=>'mysqli_result'], 'mysqli_get_cache_stats' => ['array|false'], -'mysqli_get_charset' => ['object', 'mysql'=>'mysqli'], +'mysqli_get_charset' => ['?object', 'mysql'=>'mysqli'], 'mysqli_get_client_info' => ['string', 'mysql='=>'?mysqli'], 'mysqli_get_client_stats' => ['array'], 'mysqli_get_client_version' => ['int', 'link'=>'mysqli'], @@ -11783,7 +11783,7 @@ 'SAMConnection::unsubscribe' => ['bool', 'subscriptionid'=>'string', 'targettopic='=>'string'], 'SAMMessage::body' => ['string'], 'SAMMessage::header' => ['object'], -'sapi_windows_cp_conv' => ['string', 'in_codepage'=>'int|string', 'out_codepage'=>'int|string', 'subject'=>'string'], +'sapi_windows_cp_conv' => ['?string', 'in_codepage'=>'int|string', 'out_codepage'=>'int|string', 'subject'=>'string'], 'sapi_windows_cp_get' => ['int'], 'sapi_windows_cp_is_utf8' => ['bool'], 'sapi_windows_cp_set' => ['bool', 'codepage'=>'int'], @@ -13786,7 +13786,7 @@ 'strcoll' => ['int', 'string1'=>'string', 'string2'=>'string'], 'strcspn' => ['int', 'string'=>'string', 'characters'=>'string', 'offset='=>'int', 'length='=>'int'], 'stream_bucket_append' => ['void', 'brigade'=>'resource', 'bucket'=>'object'], -'stream_bucket_make_writeable' => ['object', 'brigade'=>'resource'], +'stream_bucket_make_writeable' => ['?object', 'brigade'=>'resource'], 'stream_bucket_new' => ['object|false', 'stream'=>'resource', 'buffer'=>'string'], 'stream_bucket_prepend' => ['void', 'brigade'=>'resource', 'bucket'=>'object'], 'stream_context_create' => ['resource', 'options='=>'array', 'params='=>'array'], @@ -14590,16 +14590,16 @@ 'tidy_config_count' => ['int', 'tidy'=>'tidy'], 'tidy_diagnose' => ['bool', 'tidy'=>'tidy'], 'tidy_error_count' => ['int', 'tidy'=>'tidy'], -'tidy_get_body' => ['tidyNode', 'tidy'=>'tidy'], +'tidy_get_body' => ['?tidyNode', 'tidy'=>'tidy'], 'tidy_get_config' => ['array', 'tidy'=>'tidy'], 'tidy_get_error_buffer' => ['string', 'tidy'=>'tidy'], -'tidy_get_head' => ['tidyNode', 'tidy'=>'tidy'], -'tidy_get_html' => ['tidyNode', 'tidy'=>'tidy'], +'tidy_get_head' => ['?tidyNode', 'tidy'=>'tidy'], +'tidy_get_html' => ['?tidyNode', 'tidy'=>'tidy'], 'tidy_get_html_ver' => ['int', 'tidy'=>'tidy'], 'tidy_get_opt_doc' => ['string', 'tidy'=>'tidy', 'option'=>'string'], 'tidy_get_output' => ['string', 'tidy'=>'tidy'], 'tidy_get_release' => ['string'], -'tidy_get_root' => ['tidyNode', 'tidy'=>'tidy'], +'tidy_get_root' => ['?tidyNode', 'tidy'=>'tidy'], 'tidy_get_status' => ['int', 'tidy'=>'tidy'], 'tidy_getopt' => ['mixed', 'tidy'=>'string', 'option'=>'tidy'], 'tidy_is_xhtml' => ['bool', 'tidy'=>'tidy'], @@ -14868,7 +14868,7 @@ 'Transliterator::transliterate' => ['string|false', 'subject'=>'string', 'start='=>'int', 'end='=>'int'], 'transliterator_create' => ['?Transliterator', 'id'=>'string', 'direction='=>'int'], 'transliterator_create_from_rules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'], -'transliterator_create_inverse' => ['Transliterator', 'transliterator'=>'Transliterator'], +'transliterator_create_inverse' => ['?Transliterator', 'transliterator'=>'Transliterator'], 'transliterator_get_error_code' => ['int', 'transliterator'=>'Transliterator'], 'transliterator_get_error_message' => ['string', 'transliterator'=>'Transliterator'], 'transliterator_list_ids' => ['array'], @@ -15597,7 +15597,7 @@ 'xhprof_sample_enable' => ['void'], 'xlswriter_get_author' => ['string'], 'xlswriter_get_version' => ['string'], -'xml_error_string' => ['string', 'error_code'=>'int'], +'xml_error_string' => ['?string', 'error_code'=>'int'], 'xml_get_current_byte_index' => ['int|false', 'parser'=>'XMLParser'], 'xml_get_current_column_number' => ['int|false', 'parser'=>'XMLParser'], 'xml_get_current_line_number' => ['int|false', 'parser'=>'XMLParser'], diff --git a/dictionaries/CallMap_71_delta.php b/dictionaries/CallMap_71_delta.php index 987bf2b861a..89451449402 100644 --- a/dictionaries/CallMap_71_delta.php +++ b/dictionaries/CallMap_71_delta.php @@ -26,7 +26,7 @@ 'openssl_get_curve_names' => ['list'], 'pcntl_async_signals' => ['bool', 'enable='=>'bool'], 'pcntl_signal_get_handler' => ['int|string', 'signal'=>'int'], - 'sapi_windows_cp_conv' => ['string', 'in_codepage'=>'int|string', 'out_codepage'=>'int|string', 'subject'=>'string'], + 'sapi_windows_cp_conv' => ['?string', 'in_codepage'=>'int|string', 'out_codepage'=>'int|string', 'subject'=>'string'], 'sapi_windows_cp_get' => ['int'], 'sapi_windows_cp_is_utf8' => ['bool'], 'sapi_windows_cp_set' => ['bool', 'codepage'=>'int'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index e7e4323b5a2..d03035cb424 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -278,8 +278,8 @@ 'new' => ['int', 'multi_handle'=>'CurlMultiHandle', '&w_still_running'=>'int'], ], 'curl_multi_getcontent' => [ - 'old' => ['string', 'ch'=>'resource'], - 'new' => ['string', 'handle'=>'CurlHandle'], + 'old' => ['?string', 'ch'=>'resource'], + 'new' => ['?string', 'handle'=>'CurlHandle'], ], 'curl_multi_info_read' => [ 'old' => ['array|false', 'mh'=>'resource', '&w_msgs_in_queue='=>'int'], @@ -834,8 +834,8 @@ 'new' => ['string|false|null', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'options='=>'string|null'], ], 'mb_ereg_replace_callback' => [ - 'old' => ['string|false', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string'], - 'new' => ['string|false', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string|null'], + 'old' => ['string|false|null', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string'], + 'new' => ['string|false|null', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string|null'], ], 'mb_ereg_search' => [ 'old' => ['bool', 'pattern='=>'string', 'options='=>'string'], diff --git a/dictionaries/CallMap_81_delta.php b/dictionaries/CallMap_81_delta.php index f6299baad94..5651886c22a 100644 --- a/dictionaries/CallMap_81_delta.php +++ b/dictionaries/CallMap_81_delta.php @@ -106,8 +106,8 @@ 'new' => ['bool', 'ftp' => 'FTP\Connection', 'command' => 'string'], ], 'ftp_raw' => [ - 'old' => ['array', 'ftp' => 'resource', 'command' => 'string'], - 'new' => ['array', 'ftp' => 'FTP\Connection', 'command' => 'string'], + 'old' => ['?array', 'ftp' => 'resource', 'command' => 'string'], + 'new' => ['?array', 'ftp' => 'FTP\Connection', 'command' => 'string'], ], 'ftp_mkdir' => [ 'old' => ['string|false', 'ftp' => 'resource', 'directory' => 'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 0f5a0f2d4c0..bb7bec1c0c5 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -9909,7 +9909,7 @@ 'classkit_method_remove' => ['bool', 'classname'=>'string', 'methodname'=>'string'], 'classkit_method_rename' => ['bool', 'classname'=>'string', 'methodname'=>'string', 'newname'=>'string'], 'clearstatcache' => ['void', 'clear_realpath_cache='=>'bool', 'filename='=>'string'], - 'cli_get_process_title' => ['string'], + 'cli_get_process_title' => ['?string'], 'cli_set_process_title' => ['bool', 'title'=>'string'], 'closedir' => ['void', 'dir_handle='=>'resource'], 'closelog' => ['bool'], @@ -9920,7 +9920,7 @@ 'clusterObj::setGroup' => ['int', 'expression'=>'string'], 'collator_asort' => ['bool', 'object'=>'collator', '&rw_array'=>'array', 'flags='=>'int'], 'collator_compare' => ['int', 'object'=>'collator', 'string1'=>'string', 'string2'=>'string'], - 'collator_create' => ['Collator', 'locale'=>'string'], + 'collator_create' => ['?Collator', 'locale'=>'string'], 'collator_get_attribute' => ['int|false', 'object'=>'collator', 'attribute'=>'int'], 'collator_get_error_code' => ['int', 'object'=>'collator'], 'collator_get_error_message' => ['string', 'object'=>'collator'], @@ -10135,7 +10135,7 @@ 'curl_multi_add_handle' => ['int', 'mh'=>'resource', 'ch'=>'resource'], 'curl_multi_close' => ['void', 'mh'=>'resource'], 'curl_multi_exec' => ['int', 'mh'=>'resource', '&w_still_running'=>'int'], - 'curl_multi_getcontent' => ['string', 'ch'=>'resource'], + 'curl_multi_getcontent' => ['?string', 'ch'=>'resource'], 'curl_multi_info_read' => ['array|false', 'mh'=>'resource', '&w_msgs_in_queue='=>'int'], 'curl_multi_init' => ['resource|false'], 'curl_multi_remove_handle' => ['int', 'mh'=>'resource', 'ch'=>'resource'], @@ -10191,7 +10191,7 @@ 'datefmt_format' => ['string|false', 'formatter'=>'IntlDateFormatter', 'datetime'=>'DateTime|IntlCalendar|array|int'], 'datefmt_format_object' => ['string|false', 'datetime'=>'object', 'format='=>'mixed', 'locale='=>'string'], 'datefmt_get_calendar' => ['int', 'formatter'=>'IntlDateFormatter'], - 'datefmt_get_calendar_object' => ['IntlCalendar', 'formatter'=>'IntlDateFormatter'], + 'datefmt_get_calendar_object' => ['IntlCalendar|false|null', 'formatter'=>'IntlDateFormatter'], 'datefmt_get_datetype' => ['int', 'formatter'=>'IntlDateFormatter'], 'datefmt_get_error_code' => ['int', 'formatter'=>'IntlDateFormatter'], 'datefmt_get_error_message' => ['string', 'formatter'=>'IntlDateFormatter'], @@ -10923,7 +10923,7 @@ 'ftp_put' => ['bool', 'ftp'=>'resource', 'remote_filename'=>'string', 'local_filename'=>'string', 'mode='=>'int', 'offset='=>'int'], 'ftp_pwd' => ['string|false', 'ftp'=>'resource'], 'ftp_quit' => ['bool', 'ftp'=>'resource'], - 'ftp_raw' => ['array', 'ftp'=>'resource', 'command'=>'string'], + 'ftp_raw' => ['?array', 'ftp'=>'resource', 'command'=>'string'], 'ftp_rawlist' => ['array|false', 'ftp'=>'resource', 'directory'=>'string', 'recursive='=>'bool'], 'ftp_rename' => ['bool', 'ftp'=>'resource', 'from'=>'string', 'to'=>'string'], 'ftp_rmdir' => ['bool', 'ftp'=>'resource', 'directory'=>'string'], @@ -12272,7 +12272,7 @@ 'intlcal_after' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_before' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_clear' => ['bool', 'calendar'=>'IntlCalendar', 'field='=>'int'], - 'intlcal_create_instance' => ['IntlCalendar', 'timezone='=>'mixed', 'locale='=>'string'], + 'intlcal_create_instance' => ['?IntlCalendar', 'timezone='=>'mixed', 'locale='=>'string'], 'intlcal_equals' => ['bool', 'calendar'=>'IntlCalendar', 'other'=>'IntlCalendar'], 'intlcal_field_difference' => ['int', 'calendar'=>'IntlCalendar', 'timestamp'=>'float', 'field'=>'int'], 'intlcal_from_date_time' => ['IntlCalendar', 'datetime'=>'DateTime|string'], @@ -12317,8 +12317,8 @@ 'intlgregcal_set_gregorian_change' => ['void', 'calendar'=>'IntlGregorianCalendar', 'timestamp'=>'float'], 'intltz_count_equivalent_ids' => ['int', 'timezoneId'=>'string'], 'intltz_create_enumeration' => ['IntlIterator', 'countryOrRawOffset'=>'mixed'], - 'intltz_create_time_zone' => ['IntlTimeZone', 'timezoneId'=>'string'], - 'intltz_from_date_time_zone' => ['IntlTimeZone', 'timezone'=>'DateTimeZone'], + 'intltz_create_time_zone' => ['?IntlTimeZone', 'timezoneId'=>'string'], + 'intltz_from_date_time_zone' => ['?IntlTimeZone', 'timezone'=>'DateTimeZone'], 'intltz_getGMT' => ['IntlTimeZone'], 'intltz_get_canonical_id' => ['string', 'timezoneId'=>'string', '&isSystemId'=>'bool'], 'intltz_get_display_name' => ['string', 'timezone'=>'IntlTimeZone', 'dst'=>'bool', 'style'=>'int', 'locale'=>'string'], @@ -12558,22 +12558,22 @@ 'litespeed_request_headers' => ['array'], 'litespeed_response_headers' => ['array'], 'locale_accept_from_http' => ['string|false', 'header'=>'string'], - 'locale_canonicalize' => ['string', 'locale'=>'string'], + 'locale_canonicalize' => ['?string', 'locale'=>'string'], 'locale_compose' => ['string|false', 'subtags'=>'array'], - 'locale_filter_matches' => ['bool', 'languageTag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], - 'locale_get_all_variants' => ['array', 'locale'=>'string'], + 'locale_filter_matches' => ['?bool', 'languageTag'=>'string', 'locale'=>'string', 'canonicalize='=>'bool'], + 'locale_get_all_variants' => ['?array', 'locale'=>'string'], 'locale_get_default' => ['string'], 'locale_get_display_language' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_name' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_region' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_script' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], 'locale_get_display_variant' => ['string', 'locale'=>'string', 'displayLocale='=>'string'], - 'locale_get_keywords' => ['array|false', 'locale'=>'string'], - 'locale_get_primary_language' => ['string', 'locale'=>'string'], - 'locale_get_region' => ['string', 'locale'=>'string'], - 'locale_get_script' => ['string', 'locale'=>'string'], - 'locale_lookup' => ['string', 'languageTag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'], - 'locale_parse' => ['array', 'locale'=>'string'], + 'locale_get_keywords' => ['array|false|null', 'locale'=>'string'], + 'locale_get_primary_language' => ['?string', 'locale'=>'string'], + 'locale_get_region' => ['?string', 'locale'=>'string'], + 'locale_get_script' => ['?string', 'locale'=>'string'], + 'locale_lookup' => ['?string', 'languageTag'=>'array', 'locale'=>'string', 'canonicalize='=>'bool', 'defaultLocale='=>'string'], + 'locale_parse' => ['?array', 'locale'=>'string'], 'locale_set_default' => ['bool', 'locale'=>'string'], 'localeconv' => ['array'], 'localtime' => ['array', 'timestamp='=>'int', 'associative='=>'bool'], @@ -12893,7 +12893,7 @@ 'mb_ereg' => ['int|false', 'pattern'=>'string', 'string'=>'string', '&w_matches='=>'array|null'], 'mb_ereg_match' => ['bool', 'pattern'=>'string', 'string'=>'string', 'options='=>'string'], 'mb_ereg_replace' => ['string|false', 'pattern'=>'string', 'replacement'=>'string', 'string'=>'string', 'options='=>'string'], - 'mb_ereg_replace_callback' => ['string|false', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string'], + 'mb_ereg_replace_callback' => ['string|false|null', 'pattern'=>'string', 'callback'=>'callable', 'string'=>'string', 'options='=>'string'], 'mb_ereg_search' => ['bool', 'pattern='=>'string', 'options='=>'string'], 'mb_ereg_search_getpos' => ['int'], 'mb_ereg_search_getregs' => ['string[]|false'], @@ -13122,7 +13122,7 @@ 'msg_send' => ['bool', 'queue'=>'resource', 'message_type'=>'int', 'message'=>'mixed', 'serialize='=>'bool', 'blocking='=>'bool', '&w_error_code='=>'int'], 'msg_set_queue' => ['bool', 'queue'=>'resource', 'data'=>'array'], 'msg_stat_queue' => ['array', 'queue'=>'resource'], - 'msgfmt_create' => ['MessageFormatter', 'locale'=>'string', 'pattern'=>'string'], + 'msgfmt_create' => ['?MessageFormatter', 'locale'=>'string', 'pattern'=>'string'], 'msgfmt_format' => ['string|false', 'formatter'=>'MessageFormatter', 'values'=>'array'], 'msgfmt_format_message' => ['string|false', 'locale'=>'string', 'pattern'=>'string', 'values'=>'array'], 'msgfmt_get_error_code' => ['int', 'formatter'=>'MessageFormatter'], @@ -13418,7 +13418,7 @@ 'mysqli_field_tell' => ['int', 'result'=>'mysqli_result'], 'mysqli_free_result' => ['void', 'result'=>'mysqli_result'], 'mysqli_get_cache_stats' => ['array|false'], - 'mysqli_get_charset' => ['object', 'mysql'=>'mysqli'], + 'mysqli_get_charset' => ['?object', 'mysql'=>'mysqli'], 'mysqli_get_client_info' => ['string', 'mysql='=>'?mysqli'], 'mysqli_get_client_stats' => ['array'], 'mysqli_get_client_version' => ['int', 'link'=>'mysqli'], @@ -15221,7 +15221,7 @@ 'streamWrapper::unlink' => ['bool', 'path'=>'string'], 'streamWrapper::url_stat' => ['array', 'path'=>'string', 'flags'=>'int'], 'stream_bucket_append' => ['void', 'brigade'=>'resource', 'bucket'=>'object'], - 'stream_bucket_make_writeable' => ['object', 'brigade'=>'resource'], + 'stream_bucket_make_writeable' => ['?object', 'brigade'=>'resource'], 'stream_bucket_new' => ['object|false', 'stream'=>'resource', 'buffer'=>'string'], 'stream_bucket_prepend' => ['void', 'brigade'=>'resource', 'bucket'=>'object'], 'stream_context_create' => ['resource', 'options='=>'array', 'params='=>'array'], @@ -15739,16 +15739,16 @@ 'tidy_config_count' => ['int', 'tidy'=>'tidy'], 'tidy_diagnose' => ['bool', 'tidy'=>'tidy'], 'tidy_error_count' => ['int', 'tidy'=>'tidy'], - 'tidy_get_body' => ['tidyNode', 'tidy'=>'tidy'], + 'tidy_get_body' => ['?tidyNode', 'tidy'=>'tidy'], 'tidy_get_config' => ['array', 'tidy'=>'tidy'], 'tidy_get_error_buffer' => ['string', 'tidy'=>'tidy'], - 'tidy_get_head' => ['tidyNode', 'tidy'=>'tidy'], - 'tidy_get_html' => ['tidyNode', 'tidy'=>'tidy'], + 'tidy_get_head' => ['?tidyNode', 'tidy'=>'tidy'], + 'tidy_get_html' => ['?tidyNode', 'tidy'=>'tidy'], 'tidy_get_html_ver' => ['int', 'tidy'=>'tidy'], 'tidy_get_opt_doc' => ['string', 'tidy'=>'tidy', 'option'=>'string'], 'tidy_get_output' => ['string', 'tidy'=>'tidy'], 'tidy_get_release' => ['string'], - 'tidy_get_root' => ['tidyNode', 'tidy'=>'tidy'], + 'tidy_get_root' => ['?tidyNode', 'tidy'=>'tidy'], 'tidy_get_status' => ['int', 'tidy'=>'tidy'], 'tidy_getopt' => ['mixed', 'tidy'=>'string', 'option'=>'tidy'], 'tidy_is_xhtml' => ['bool', 'tidy'=>'tidy'], @@ -15945,7 +15945,7 @@ 'trait_exists' => ['bool', 'trait'=>'string', 'autoload='=>'bool'], 'transliterator_create' => ['?Transliterator', 'id'=>'string', 'direction='=>'int'], 'transliterator_create_from_rules' => ['?Transliterator', 'rules'=>'string', 'direction='=>'int'], - 'transliterator_create_inverse' => ['Transliterator', 'transliterator'=>'Transliterator'], + 'transliterator_create_inverse' => ['?Transliterator', 'transliterator'=>'Transliterator'], 'transliterator_get_error_code' => ['int', 'transliterator'=>'Transliterator'], 'transliterator_get_error_message' => ['string', 'transliterator'=>'Transliterator'], 'transliterator_list_ids' => ['array'], @@ -16412,7 +16412,7 @@ 'xhprof_sample_enable' => ['void'], 'xlswriter_get_author' => ['string'], 'xlswriter_get_version' => ['string'], - 'xml_error_string' => ['string', 'error_code'=>'int'], + 'xml_error_string' => ['?string', 'error_code'=>'int'], 'xml_get_current_byte_index' => ['int|false', 'parser'=>'resource'], 'xml_get_current_column_number' => ['int|false', 'parser'=>'resource'], 'xml_get_current_line_number' => ['int|false', 'parser'=>'resource'], diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index 4d33ebbd053..54bd9d8f27b 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -1275,7 +1275,7 @@ function pack(string $format, mixed ...$values) {} * @param string|int $out_codepage * @psalm-flow ($subject) -> return */ -function sapi_windows_cp_conv($in_codepage, $out_codepage, string $subject) : string {} +function sapi_windows_cp_conv($in_codepage, $out_codepage, string $subject) : ?string {} /** * @psalm-pure diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index e69385648bc..164fb4143d1 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -750,7 +750,7 @@ private function assertParameter(array $normalizedEntry, ReflectionParameter $pa $expectedType = $param->getType(); if (isset($expectedType) && !empty($normalizedEntry['type'])) { - $this->assertTypeValidity($expectedType, $normalizedEntry['type'], "Param '{$name}' has incorrect type"); + $this->assertTypeValidity($expectedType, $normalizedEntry['type'], false, "Param '{$name}' has incorrect type"); } } @@ -771,20 +771,19 @@ public function assertEntryReturnType(ReflectionFunction $function, string $entr return; } - $this->assertTypeValidity($expectedType, $entryReturnType, 'CallMap entry has incorrect return type'); + $this->assertTypeValidity($expectedType, $entryReturnType, true, 'CallMap entry has incorrect return type'); } /** * Since string equality is too strict, we do some extra checking here */ - private function assertTypeValidity(ReflectionType $reflected, string $specified, string $message): void + private function assertTypeValidity(ReflectionType $reflected, string $specified, bool $checkNullable, string $message): void { $expectedType = Reflection::getPsalmTypeFromReflectionType($reflected); - - $parsedType = Type::parseString($specified); + $callMapType = Type::parseString($specified); try { - $this->assertTrue(UnionTypeComparator::isContainedBy(self::$codebase, $parsedType, $expectedType), $message); + $this->assertTrue(UnionTypeComparator::isContainedBy(self::$codebase, $callMapType, $expectedType), $message); } catch (InvalidArgumentException $e) { if (preg_match('/^Could not get class storage for (.*)$/', $e->getMessage(), $matches) && !class_exists($matches[1]) @@ -792,5 +791,10 @@ private function assertTypeValidity(ReflectionType $reflected, string $specified $this->fail("Class used in CallMap does not exist: {$matches[1]}"); } } + + // Reflection::getPsalmTypeFromReflectionType adds |null to mixed types so skip comparison + if ($checkNullable && !$expectedType->hasMixed()) { + $this->assertSame($expectedType->isNullable(), $callMapType->isNullable(), $message); + } } } From 1e0b57226494f7aa4608bf9e7e681c5976e85fba Mon Sep 17 00:00:00 2001 From: someniatko Date: Thu, 14 Jul 2022 10:03:47 +0300 Subject: [PATCH 017/194] #8200 - bikeshedding the tests --- tests/Template/ClassTemplateTest.php | 154 +++++++++++++-------------- 1 file changed, 77 insertions(+), 77 deletions(-) diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index 73a0fa9d811..413f13add1f 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -3697,107 +3697,107 @@ protected function setUp(): void 'return TemplatedClass' => [ ' - * - * @psalm-pure + * @template-covariant A + * @psalm-immutable */ - public static function just($value): self + final class Maybe { - return new self($value); - } - } + /** + * @param null|A $value + */ + public function __construct(private $value = null) {} - abstract class Test - { - final private function __construct() {} + /** + * @template B + * @param B $value + * @return Maybe + * + * @psalm-pure + */ + public static function just($value): self + { + return new self($value); + } + } - /** @return Maybe */ - final public static function create(): Maybe + abstract class Test { - return Maybe::just(new static()); - } - }', + final private function __construct() {} + + /** @return Maybe */ + final public static function create(): Maybe + { + return Maybe::just(new static()); + } + }', ], 'return list created in a static method of another class' => [ ' - * - * @psalm-pure - */ - public static function mklist($value): array + final class Lister { - return [ $value ]; + /** + * @template B + * @param B $value + * @return list + * + * @psalm-pure + */ + public static function mklist($value): array + { + return [ $value ]; + } } - } - - abstract class Test - { - final private function __construct() {} - /** @return list */ - final public static function create(): array + abstract class Test { - return Lister::mklist(new static()); - } - }', + final private function __construct() {} + + /** @return list */ + final public static function create(): array + { + return Lister::mklist(new static()); + } + }', ], 'use TemplatedClass as an intermediate variable inside a method' => [ ' - * - * @psalm-pure + * @template-covariant A + * @psalm-immutable */ - public static function just($value): self + final class Maybe { - return new self($value); - } - } + /** + * @param A $value + */ + public function __construct(public $value) {} - abstract class Test - { - final private function __construct() {} + /** + * @template B + * @param B $value + * @return Maybe + * + * @psalm-pure + */ + public static function just($value): self + { + return new self($value); + } + } - final public static function create(): static + abstract class Test { - $maybe = Maybe::just(new static()); - return $maybe->value; - } - }', + final private function __construct() {} + + final public static function create(): static + { + $maybe = Maybe::just(new static()); + return $maybe->value; + } + }', ], ]; } From bb760a22241d31ad0ccee109fc7b6484ca067e7a Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 21 Jul 2022 11:32:24 +0200 Subject: [PATCH 018/194] fix race conditions causing notices if directory does not exist --- src/Psalm/Config.php | 28 ++++++++++++++----- .../Internal/Provider/ParserCacheProvider.php | 8 ++++++ 2 files changed, 29 insertions(+), 7 deletions(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 08bc9604f2e..28e4ecbc95c 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -52,6 +52,7 @@ use function basename; use function chdir; use function class_exists; +use function clearstatcache; use function count; use function dirname; use function error_log; @@ -2268,6 +2269,7 @@ public function getPotentialComposerFilePathForClassLike(string $class): ?string public static function removeCacheDirectory(string $dir): void { + clearstatcache(true, $dir); if (is_dir($dir)) { $objects = scandir($dir, SCANDIR_SORT_NONE); @@ -2276,17 +2278,29 @@ public static function removeCacheDirectory(string $dir): void } foreach ($objects as $object) { - if ($object !== '.' && $object !== '..') { - if (filetype($dir . '/' . $object) === 'dir') { - self::removeCacheDirectory($dir . '/' . $object); - } else { - unlink($dir . '/' . $object); - } + if ($object === '.' || $object === '..') { + continue; + } + + // if it was deleted in the meantime/race condition with other psalm process + if (!file_exists($dir . '/' . $object)) { + continue; + } + + if (filetype($dir . '/' . $object) === 'dir') { + self::removeCacheDirectory($dir . '/' . $object); + } else { + unlink($dir . '/' . $object); } } reset($objects); - rmdir($dir); + + // may have been removed in the meantime + clearstatcache(true, $dir); + if (is_dir($dir)) { + rmdir($dir); + } } } diff --git a/src/Psalm/Internal/Provider/ParserCacheProvider.php b/src/Psalm/Internal/Provider/ParserCacheProvider.php index 893844e2eee..a8f053f09b8 100644 --- a/src/Psalm/Internal/Provider/ParserCacheProvider.php +++ b/src/Psalm/Internal/Provider/ParserCacheProvider.php @@ -7,6 +7,7 @@ use Psalm\Config; use RuntimeException; +use function clearstatcache; use function error_log; use function fclose; use function file_get_contents; @@ -302,6 +303,13 @@ public function saveFileContentHashes(): void return; } + // directory was removed + // most likely due to a race condition with other psalm instances that were manually started at the same time + clearstatcache(true, $root_cache_directory); + if (!is_dir($root_cache_directory)) { + return; + } + $file_content_hashes = $this->new_file_content_hashes + $this->getExistingFileContentHashes(); $file_hashes_path = $root_cache_directory . DIRECTORY_SEPARATOR . self::FILE_HASHES; From 233863dc0ccaa8df983b1499beee5d1be23dc046 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 21 Jul 2022 11:36:55 +0200 Subject: [PATCH 019/194] circle CI error for unrelated code? --- src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index cd885a0c87b..385a954c946 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -65,8 +65,6 @@ use function array_merge; use function array_search; use function array_values; -use function assert; -use function class_exists; use function count; use function end; use function in_array; From e1b0255db80b54e14405cbf581ca6d3b9b4e2e6d Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 21 Jul 2022 12:54:47 +0200 Subject: [PATCH 020/194] fix triggerErrorExits not working Fix https://github.com/vimeo/psalm/issues/8270 --- src/Psalm/Config.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 08bc9604f2e..878d439a023 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -961,7 +961,6 @@ private static function fromXmlAndPaths( 'reportInfo' => 'report_info', 'restrictReturnTypes' => 'restrict_return_types', 'limitMethodComplexity' => 'limit_method_complexity', - 'triggerErrorExits' => 'trigger_error_exits', ]; foreach ($booleanAttributes as $xmlName => $internalName) { @@ -1096,6 +1095,13 @@ private static function fromXmlAndPaths( $config->infer_property_types_from_constructor = $attribute_text === 'true' || $attribute_text === '1'; } + if (isset($config_xml['triggerErrorExits'])) { + $attribute_text = (string) $config_xml['triggerErrorExits']; + if ($attribute_text === 'always' || $attribute_text === 'never') { + $config->trigger_error_exits = $attribute_text; + } + } + if (isset($config_xml->projectFiles)) { $config->project_files = ProjectFileFilter::loadFromXMLElement($config_xml->projectFiles, $base_dir, true); } From 8b5999470a023d985edc60c42b6f2c7df5304f15 Mon Sep 17 00:00:00 2001 From: Honca Date: Fri, 22 Jul 2022 11:09:03 +0200 Subject: [PATCH 021/194] Fixed ini_set types for arg value --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_81_delta.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 035e87b4409..ce827081f7d 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -6153,7 +6153,7 @@ 'ini_get' => ['string|false', 'option'=>'string'], 'ini_get_all' => ['array|false', 'extension='=>'?string', 'details='=>'bool'], 'ini_restore' => ['void', 'option'=>'string'], -'ini_set' => ['string|false', 'option'=>'string', 'value'=>'string'], +'ini_set' => ['string|false', 'option'=>'string', 'value'=>'string|int|float|bool|null'], 'inotify_add_watch' => ['int', 'inotify_instance'=>'resource', 'pathname'=>'string', 'mask'=>'int'], 'inotify_init' => ['resource|false'], 'inotify_queue_len' => ['int', 'inotify_instance'=>'resource'], diff --git a/dictionaries/CallMap_81_delta.php b/dictionaries/CallMap_81_delta.php index 5651886c22a..b6cae16f3d7 100644 --- a/dictionaries/CallMap_81_delta.php +++ b/dictionaries/CallMap_81_delta.php @@ -453,6 +453,10 @@ 'old' => ['bool', 'imap'=>'resource', 'mailbox'=>'string'], 'new' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], ], + 'ini_set' => [ + 'old' => ['string|false', 'option'=>'string', 'value'=>'string'], + 'new' => ['string|false', 'option'=>'string', 'value'=>'string|int|float|bool|null'], + ], 'ldap_add' => [ 'old' => ['bool', 'ldap'=>'resource', 'dn'=>'string', 'entry'=>'array', 'controls='=>'array'], 'new' => ['bool', 'ldap'=>'LDAP\Connection', 'dn'=>'string', 'entry'=>'array', 'controls='=>'?array'], From 9d3253482d78a9b6750dd9ff5eb8df858016fdc5 Mon Sep 17 00:00:00 2001 From: Semyon <7ionmail@gmail.com> Date: Fri, 22 Jul 2022 16:03:45 +0300 Subject: [PATCH 022/194] Add stub for DatePeriod --- stubs/CoreImmutableClasses.phpstub | 21 +++++++++++++++++++++ tests/CoreStubsTest.php | 30 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+) diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index f9c8d6e3e34..20acef61755 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -16,6 +16,27 @@ class DateTimeZone public function __construct(string $timezone) {} } +/** + * @psalm-immutable + * + * @template-covariant From of string|DateTimeInterface + * @implements IteratorAggregate + */ +class DatePeriod implements IteratorAggregate +{ + const EXCLUDE_START_DATE = 1; + /** + * @param From $from + * @param (From is string ? 0|self::EXCLUDE_START_DATE : DateInterval) $interval_or_options + * @param (From is string ? never : DateTimeInterface|positive-int) $end_or_recurrences + * @param (From is string ? never : 0|self::EXCLUDE_START_DATE) $options + */ + public function __construct($from, $interval_or_options = 0, $end_or_recurrences = 1, $options = 0) {} + + /** @psalm-return (From is string ? (Traversable&Iterator) : (Traversable&Iterator)) */ + public function getIterator(): Iterator {} +} + /** * @psalm-taint-specialize */ diff --git a/tests/CoreStubsTest.php b/tests/CoreStubsTest.php index 8bbe6435522..38a38672c40 100644 --- a/tests/CoreStubsTest.php +++ b/tests/CoreStubsTest.php @@ -33,5 +33,35 @@ public function providerValidCodeParse(): iterable 'error_levels' => [], 'php_version' => '8.0', ]; + yield 'Iterating over \DatePeriod (#5954)' => [ + 'format("Y-m-d"); + }', + 'assertions' => [ + '$period' => 'DatePeriod', + '$dt' => 'DateTimeImmutable|null' + ], + ]; + yield 'Iterating over \DatePeriod (#5954), ISO string' => [ + 'format("Y-m-d"); + }', + 'assertions' => [ + '$period' => 'DatePeriod', + '$dt' => 'DateTime|null' + ], + ]; } } From 462ce7138ac926e832a37ed4be47b1a8ce70e1d2 Mon Sep 17 00:00:00 2001 From: Semyon <7ionmail@gmail.com> Date: Mon, 25 Jul 2022 16:37:49 +0300 Subject: [PATCH 023/194] Make DatePeriod implement Traversable oh PHP 7, rename constructor params --- stubs/CoreImmutableClasses.phpstub | 19 ++++++------- stubs/Php80.phpstub | 21 +++++++++++++++ tests/CoreStubsTest.php | 43 +++++++++++++++++++++++++++++- 3 files changed, 71 insertions(+), 12 deletions(-) diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index 20acef61755..405f89b43cd 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -19,22 +19,19 @@ class DateTimeZone /** * @psalm-immutable * - * @template-covariant From of string|DateTimeInterface - * @implements IteratorAggregate + * @template-covariant Start of string|DateTimeInterface + * @implements Traversable */ -class DatePeriod implements IteratorAggregate +class DatePeriod implements Traversable { const EXCLUDE_START_DATE = 1; /** - * @param From $from - * @param (From is string ? 0|self::EXCLUDE_START_DATE : DateInterval) $interval_or_options - * @param (From is string ? never : DateTimeInterface|positive-int) $end_or_recurrences - * @param (From is string ? never : 0|self::EXCLUDE_START_DATE) $options + * @param Start $start + * @param (Start is string ? 0|self::EXCLUDE_START_DATE : DateInterval) $interval + * @param (Start is string ? never : DateTimeInterface|positive-int) $end + * @param (Start is string ? never : 0|self::EXCLUDE_START_DATE) $options */ - public function __construct($from, $interval_or_options = 0, $end_or_recurrences = 1, $options = 0) {} - - /** @psalm-return (From is string ? (Traversable&Iterator) : (Traversable&Iterator)) */ - public function getIterator(): Iterator {} + public function __construct($start, $interval = 0, $end = 1, $options = 0) {} } /** diff --git a/stubs/Php80.phpstub b/stubs/Php80.phpstub index 877b156f3a0..97b0621178b 100644 --- a/stubs/Php80.phpstub +++ b/stubs/Php80.phpstub @@ -82,3 +82,24 @@ class ReflectionUnionType extends ReflectionType { } class UnhandledMatchError extends Error {} + +/** + * @psalm-immutable + * + * @template-covariant Start of string|DateTimeInterface + * @implements IteratorAggregate + */ +class DatePeriod implements IteratorAggregate +{ + const EXCLUDE_START_DATE = 1; + /** + * @param Start $start + * @param (Start is string ? 0|self::EXCLUDE_START_DATE : DateInterval) $interval + * @param (Start is string ? never : DateTimeInterface|positive-int) $end + * @param (Start is string ? never : 0|self::EXCLUDE_START_DATE) $options + */ + public function __construct($start, $interval = 0, $end = 1, $options = 0) {} + + /** @psalm-return (Start is string ? (Traversable&Iterator) : (Traversable&Iterator)) */ + public function getIterator(): Iterator {} +} \ No newline at end of file diff --git a/tests/CoreStubsTest.php b/tests/CoreStubsTest.php index 38a38672c40..d6f0a85a916 100644 --- a/tests/CoreStubsTest.php +++ b/tests/CoreStubsTest.php @@ -33,7 +33,26 @@ public function providerValidCodeParse(): iterable 'error_levels' => [], 'php_version' => '8.0', ]; - yield 'Iterating over \DatePeriod (#5954)' => [ + yield 'Iterating over \DatePeriod (#5954) PHP7 Traversable' => [ + 'format("Y-m-d"); + }', + 'assertions' => [ + '$period' => 'DatePeriod', + '$dt' => 'DateTimeInterface|null' + ], + 'error_levels' => [], + 'php_version' => '7.3', + ]; + yield 'Iterating over \DatePeriod (#5954) PHP8 IteratorAggregate' => [ ' 'DatePeriod', '$dt' => 'DateTimeImmutable|null' ], + 'error_levels' => [], + 'php_version' => '8.0', ]; yield 'Iterating over \DatePeriod (#5954), ISO string' => [ ' 'DatePeriod', '$dt' => 'DateTime|null' ], + 'error_levels' => [], + 'php_version' => '8.0', + ]; + yield 'DatePeriod implements only Traversable on PHP 7' => [ + ' [], + 'error_levels' => [], + 'php_version' => '7.3', + ]; + yield 'DatePeriod implements IteratorAggregate on PHP 8' => [ + ' [], + 'error_levels' => ['RedundantCondition'], + 'php_version' => '8.0', ]; } } From b1295d6894f5c8308ad3c846a0e994886352450e Mon Sep 17 00:00:00 2001 From: Semyon <7ionmail@gmail.com> Date: Mon, 25 Jul 2022 17:15:28 +0300 Subject: [PATCH 024/194] Code style --- src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php | 2 -- stubs/Php80.phpstub | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index cd885a0c87b..385a954c946 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -65,8 +65,6 @@ use function array_merge; use function array_search; use function array_values; -use function assert; -use function class_exists; use function count; use function end; use function in_array; diff --git a/stubs/Php80.phpstub b/stubs/Php80.phpstub index 97b0621178b..001ebf17ae6 100644 --- a/stubs/Php80.phpstub +++ b/stubs/Php80.phpstub @@ -102,4 +102,4 @@ class DatePeriod implements IteratorAggregate /** @psalm-return (Start is string ? (Traversable&Iterator) : (Traversable&Iterator)) */ public function getIterator(): Iterator {} -} \ No newline at end of file +} From 0c652f72f641ca5e9acf4f993627cd5c6a0aa01f Mon Sep 17 00:00:00 2001 From: someniatko Date: Fri, 29 Jul 2022 12:31:37 +0300 Subject: [PATCH 025/194] #8330 - take into account that `static` type may have been unwrapped in ExistingAtomicStaticCallAnalyzer#hasStaticInType() --- .../ExistingAtomicStaticCallAnalyzer.php | 2 +- tests/Template/ClassTemplateTest.php | 27 +++++++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index d7ac4ef16ff..3785023e269 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -627,7 +627,7 @@ private static function getMethodReturnType( */ private static function hasStaticInType(Type\TypeNode $type): bool { - if ($type instanceof TNamedObject && $type->value === 'static') { + if ($type instanceof TNamedObject && ($type->value === 'static' || $type->was_static)) { return true; } diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index 413f13add1f..afe14f81132 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -3799,6 +3799,33 @@ final public static function create(): static } }', ], + 'static is the return type of an analyzed static method' => [ + 'acceptA(B::create()); + } + + private function acceptA(A $_a): void + { + } + }', + ], ]; } From 0abde258fa959630b38eb1de8033bf5f91fa335b Mon Sep 17 00:00:00 2001 From: someniatko Date: Fri, 29 Jul 2022 16:50:56 +0300 Subject: [PATCH 026/194] #7731 - recognize `@psalm-allow-private-mutation` in PHP 8+ constructors --- .../Reflector/FunctionLikeNodeScanner.php | 3 ++ tests/ReadonlyPropertyTest.php | 37 ++++++++++++++++++- 2 files changed, 39 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index 969633e2e54..cc101391318 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -606,6 +606,7 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal $doc_comment = $param->getDocComment(); $var_comment_type = null; $var_comment_readonly = false; + $var_comment_allow_private_mutation = false; if ($doc_comment) { $var_comments = CommentAnalyzer::getTypeFromComment( $doc_comment, @@ -620,6 +621,7 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal if ($var_comment !== null) { $var_comment_type = $var_comment->type; $var_comment_readonly = $var_comment->readonly; + $var_comment_allow_private_mutation = $var_comment->allow_private_mutation; } } @@ -652,6 +654,7 @@ public function start(PhpParser\Node\FunctionLike $stmt, bool $fake_method = fal $property_storage->has_default = (bool)$param->default; $param_type_readonly = (bool)($param->flags & PhpParser\Node\Stmt\Class_::MODIFIER_READONLY); $property_storage->readonly = $param_type_readonly ?: $var_comment_readonly; + $property_storage->allow_private_mutation = $var_comment_allow_private_mutation; $param_storage->promoted_property = true; $property_storage->is_promoted = true; diff --git a/tests/ReadonlyPropertyTest.php b/tests/ReadonlyPropertyTest.php index 4cf7c4728c8..ec69276c81b 100644 --- a/tests/ReadonlyPropertyTest.php +++ b/tests/ReadonlyPropertyTest.php @@ -64,7 +64,7 @@ public function setBar(string $s) : void { echo (new A)->bar;' ], - 'readonlyPublicPropertySetInAnotherMEthod' => [ + 'readonlyPublicPropertySetInAnotherMethod' => [ 'bar;' ], + 'docblockReadonlyWithPrivateMutationsAllowedConstructorPropertySetInAnotherMethod' => [ + 'bar = $s; + } + } + + echo (new A)->bar;' + ], + 'readonlyPublicConstructorPropertySetInAnotherMethod' => [ + 'bar = $s; + } + } + + echo (new A)->bar;' + ], 'readonlyPropertySetChildClass' => [ ' Date: Fri, 29 Jul 2022 22:09:39 +0200 Subject: [PATCH 027/194] ReflectionProperty::getValue $object is nullable since php 8.0 https://www.php.net/manual/en/reflectionproperty.getvalue.php --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_80_delta.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index ce827081f7d..f7faaa4b875 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -11608,7 +11608,7 @@ 'ReflectionProperty::getModifiers' => ['int'], 'ReflectionProperty::getName' => ['string'], 'ReflectionProperty::getType' => ['?ReflectionType'], -'ReflectionProperty::getValue' => ['mixed', 'object='=>'object'], +'ReflectionProperty::getValue' => ['mixed', 'object='=>'null|object'], 'ReflectionProperty::hasType' => ['bool'], 'ReflectionProperty::isDefault' => ['bool'], 'ReflectionProperty::isPrivate' => ['bool'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index d03035cb424..afeb1bc4133 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -113,6 +113,10 @@ 'old' => ['object', 'args='=>'list'], 'new' => ['object', 'args='=>'array'], ], + 'ReflectionProperty::getValue' => [ + 'old' => ['mixed', 'object='=>'object'], + 'new' => ['mixed', 'object='=>'null|object'], + ], 'XMLWriter::flush' => [ 'old' => ['string|int|false', 'empty='=>'bool'], 'new' => ['string|int', 'empty='=>'bool'], From 90ac39d89f5257d0d6e8566a0ae3f80b02d21d0d Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Sat, 30 Jul 2022 23:31:21 +0200 Subject: [PATCH 028/194] Fix formatCurrency return type --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_historical.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index f7faaa4b875..219e6372641 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -9028,7 +9028,7 @@ 'NumberFormatter::__construct' => ['void', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'NumberFormatter::create' => ['NumberFormatter|false', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'NumberFormatter::format' => ['string|false', 'num'=>'', 'type='=>'int'], -'NumberFormatter::formatCurrency' => ['string', 'num'=>'float', 'currency'=>'string'], +'NumberFormatter::formatCurrency' => ['string|false', 'num'=>'float', 'currency'=>'string'], 'NumberFormatter::getAttribute' => ['int|false', 'attr'=>'int'], 'NumberFormatter::getErrorCode' => ['int'], 'NumberFormatter::getErrorMessage' => ['string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 2c60a2ca437..a5c4875aa71 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -4420,7 +4420,7 @@ 'NumberFormatter::__construct' => ['void', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'NumberFormatter::create' => ['NumberFormatter|false', 'locale'=>'string', 'style'=>'int', 'pattern='=>'string'], 'NumberFormatter::format' => ['string|false', 'num'=>'', 'type='=>'int'], - 'NumberFormatter::formatCurrency' => ['string', 'num'=>'float', 'currency'=>'string'], + 'NumberFormatter::formatCurrency' => ['string|false', 'num'=>'float', 'currency'=>'string'], 'NumberFormatter::getAttribute' => ['int|false', 'attr'=>'int'], 'NumberFormatter::getErrorCode' => ['int'], 'NumberFormatter::getErrorMessage' => ['string'], From b4b2bc66c7478e39f427114223ef7394f7b11250 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Sun, 31 Jul 2022 18:02:30 +0200 Subject: [PATCH 029/194] Added better stubs for `DateTimeImmutable`, highlighting how the constructor is **NOT** immutable `DateTimeImmutable` is **almost** immutable: `DateTimeImmutable::__construct()` is in fact not a pure method, since `new DateTimeImmutable('now')` produces a different value at each instantiation (by design). This change makes sure that `DateTimeImmutable` loses its `@psalm-immutable` class-level marker, preventing downstream misuse of the constructor inside otherwise referentially transparent code. Note: only pure methods are stubbed here: all other methods declared by `DateTimeImmutable` (parent interface) are NOT present here, and are either inferred from runtime reflection, or `CallMap*.php` definitions. Methods are sorted in the order defined by reflection on PHP 8.1.8, at the time of writing this ( https://3v4l.org/3TGg8 ). Following simplistic snippet was used to infer the current signature: ```php getName() . '(' . implode(',', array_map(function ($p) { return $p->getType() . ' $' . $p->getName() . ($p->isOptional() ? ' = ' . var_export($p->getDefaultValue(), true) : ''); }, $m->getParameters())) . ')' . ($m->getReturnType() ? (': ' . $m->getReturnType()) : ''); }, $c->getMethods()); $properties = array_map(function ($m) { return $m->getName(); }, $c->getProperties()); var_dump($methods, $properties); ``` --- stubs/CoreImmutableClasses.phpstub | 101 ++++++++++++++++++++++++++++- 1 file changed, 98 insertions(+), 3 deletions(-) diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index 405f89b43cd..325a1c2a2ce 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -1,11 +1,106 @@ Date: Sun, 31 Jul 2022 18:08:01 +0200 Subject: [PATCH 030/194] Removed `@psalm-immutable` marked from `MyDate` extending `DateTimeImmutable` `DateTimeImmutable` is not really immutable, therefore this marker was wrong upfront --- tests/MethodCallTest.php | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 843cce11941..956536988a1 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -264,7 +264,6 @@ public static function main(): void { ], 'dateTimeImmutableStatic' => [ ' Date: Sun, 31 Jul 2022 18:12:07 +0200 Subject: [PATCH 031/194] `DateTimeImmutable#sub()` always returns another `static` instance, never `false` --- tests/MethodCallTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 956536988a1..76c033f56f5 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -271,7 +271,7 @@ final class MyDate extends DateTimeImmutable {} $b = (new DateTimeImmutable())->modify("+3 hours");', 'assertions' => [ - '$yesterday' => 'MyDate|false', + '$yesterday' => 'MyDate', '$b' => 'DateTimeImmutable', ], ], From 68978b9e19c70036e6e5077a6234af513afdc77f Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Mon, 1 Aug 2022 10:08:35 +0200 Subject: [PATCH 032/194] s/psalm-pure/psalm-mutation-free, since psalm-mutation-free is safer to use Ref: https://github.com/vimeo/psalm/pull/8350/files/c205d652d1e9afd9510db59e72c3fd0a4a093b3d#r934032422 The idea is that `@psalm-pure` disallows `$this` usage in child classes, which is not wanted, while `@psalm-mutation-free` allows it. By using `@psalm-mutation-free`, we don't completely destroy inheritance use-cases based on internal (immutable) state. --- stubs/CoreImmutableClasses.phpstub | 32 +++++++++++++++--------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index 325a1c2a2ce..2d6555c3df2 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -5,99 +5,99 @@ class DateTimeImmutable implements DateTimeInterface public function __construct(string $datetime = "now", DateTimeZone $timezone = null) {} /** - * @psalm-pure + * @psalm-mutation-free * @return static|false */ public static function createFromFormat(string $format, string $datetime, ?DateTimeZone $timezone = null) {} /** - * @psalm-pure + * @psalm-mutation-free * @param string $format * @return string */ public function format($format) {} /** - * @psalm-pure + * @psalm-mutation-free * @return DateTimeZone */ public function getTimezone() {} /** - * @psalm-pure + * @psalm-mutation-free * @return int */ public function getOffset() {} /** - * @psalm-pure + * @psalm-mutation-free * @return int */ public function getTimestamp() {} /** - * @psalm-pure + * @psalm-mutation-free * @param bool $absolute * @return DateInterval */ public function diff(DateTimeInterface $targetObject, $absolute = false) {} /** - * @psalm-pure + * @psalm-mutation-free * @return static */ public function modify(string $modifier) {} /** - * @psalm-pure + * @psalm-mutation-free * @return static */ public function add(DateInterval $interval) {} /** - * @psalm-pure + * @psalm-mutation-free * @return static */ public function sub(DateInterval $interval) {} /** - * @psalm-pure + * @psalm-mutation-free * @return static|false */ public function setTimezone(DateTimeZone $timezone) {} /** - * @psalm-pure + * @psalm-mutation-free * @return static|false */ public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0) {} /** - * @psalm-pure + * @psalm-mutation-free * @return static|false */ public function setDate(int $year, int $month, int $day) {} /** - * @psalm-pure + * @psalm-mutation-free * @return static|false */ public function setISODate(int $year, int $week, int $dayOfWeek = 1) {} /** - * @psalm-pure + * @psalm-mutation-free * @return static|false */ public function setTimestamp(int $unixtimestamp) {} /** - * @psalm-pure + * @psalm-mutation-free * @return static */ public static function createFromMutable(DateTime $object) {} /** - * @psalm-pure + * @psalm-mutation-free * @return self */ public static function createFromInterface(DateTimeInterface $object) {} From 0d32203f9a94fa523bf58bb3192b20c7288aa3e3 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 3 Aug 2022 19:09:56 +0200 Subject: [PATCH 033/194] add ", but" for InvalidArgument error message where a type is provided --- .../Expression/Call/ArgumentAnalyzer.php | 14 +++++++------- .../Call/ArrayFunctionArgumentsAnalyzer.php | 12 +++++++----- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 84b777c7e20..32d6b4c7e3e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -181,7 +181,7 @@ public static function checkArgumentMatches( IssueBuffer::maybeAdd( new InvalidLiteralArgument( 'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id - . ' expects a non-literal value, ' . $arg_value_type->getId() . ' provided', + . ' expects a non-literal value, but ' . $arg_value_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $arg->value), $cased_method_id ), @@ -976,7 +976,7 @@ public static function verifyType( IssueBuffer::maybeAdd( new MixedArgumentTypeCoercion( 'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() . - ', parent type ' . $input_type->getId() . ' provided', + ', but parent type ' . $input_type->getId() . ' provided', $arg_location, $cased_method_id, $origin_location @@ -987,7 +987,7 @@ public static function verifyType( IssueBuffer::maybeAdd( new ArgumentTypeCoercion( 'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() . - ', parent type ' . $input_type->getId() . ' provided', + ', but parent type ' . $input_type->getId() . ' provided', $arg_location, $cased_method_id ), @@ -1000,7 +1000,7 @@ public static function verifyType( IssueBuffer::maybeAdd( new ImplicitToStringCast( 'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . - $param_type->getId() . ', ' . $input_type->getId() . ' provided with a __toString method', + $param_type->getId() . ', but ' . $input_type->getId() . ' provided with a __toString method', $arg_location ), $statements_analyzer->getSuppressedIssues() @@ -1022,7 +1022,7 @@ public static function verifyType( IssueBuffer::maybeAdd( new InvalidScalarArgument( 'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . - $param_type->getId() . ', ' . $type . ' provided', + $param_type->getId() . ', but ' . $type . ' provided', $arg_location, $cased_method_id ), @@ -1033,7 +1033,7 @@ public static function verifyType( IssueBuffer::maybeAdd( new PossiblyInvalidArgument( 'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() . - ', possibly different type ' . $type . ' provided', + ', but possibly different type ' . $type . ' provided', $arg_location, $cased_method_id ), @@ -1043,7 +1043,7 @@ public static function verifyType( IssueBuffer::maybeAdd( new InvalidArgument( 'Argument ' . ($argument_offset + 1) . $method_identifier . ' expects ' . $param_type->getId() . - ', ' . $type . ' provided', + ', but ' . $type . ' provided', $arg_location, $cased_method_id ), diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index 654e9bd829b..05c5aa91951 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -893,7 +893,8 @@ private static function checkClosureTypeArgs( IssueBuffer::maybeAdd( new MixedArgumentTypeCoercion( 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' . - $closure_param_type->getId() . ', parent type ' . $input_type->getId() . ' provided', + $closure_param_type->getId() . + ', but parent type ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), $method_id ), @@ -903,7 +904,8 @@ private static function checkClosureTypeArgs( IssueBuffer::maybeAdd( new ArgumentTypeCoercion( 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' . - $closure_param_type->getId() . ', parent type ' . $input_type->getId() . ' provided', + $closure_param_type->getId() . + ', but parent type ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), $method_id ), @@ -923,7 +925,7 @@ private static function checkClosureTypeArgs( IssueBuffer::maybeAdd( new InvalidScalarArgument( 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' . - $closure_param_type->getId() . ', ' . $input_type->getId() . ' provided', + $closure_param_type->getId() . ', but ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), $method_id ), @@ -933,7 +935,7 @@ private static function checkClosureTypeArgs( IssueBuffer::maybeAdd( new PossiblyInvalidArgument( 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' - . $closure_param_type->getId() . ', possibly different type ' + . $closure_param_type->getId() . ', but possibly different type ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), $method_id @@ -943,7 +945,7 @@ private static function checkClosureTypeArgs( } elseif (IssueBuffer::accepts( new InvalidArgument( 'Parameter ' . ($i + 1) . ' of closure passed to function ' . $method_id . ' expects ' . - $closure_param_type->getId() . ', ' . $input_type->getId() . ' provided', + $closure_param_type->getId() . ', but ' . $input_type->getId() . ' provided', new CodeLocation($statements_analyzer->getSource(), $closure_arg), $method_id ), From d2be169ce50938809ccd7203eb99c9d8d4f56690 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 3 Aug 2022 19:56:38 +0200 Subject: [PATCH 034/194] update tests --- tests/ClosureTest.php | 2 +- tests/ReturnTypeTest.php | 2 +- tests/Template/ClassTemplateTest.php | 4 ++-- tests/TypeReconciliation/TypeTest.php | 4 ++-- tests/UnusedVariableTest.php | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index def99f9955c..cea843a3725 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -1149,7 +1149,7 @@ function takesB(B $_b) : void {} takesA($getAButReallyB()); takesB($getAButReallyB());', - 'error_message' => 'ArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:28 - Argument 1 of takesB expects B, parent type A provided', + 'error_message' => 'ArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:28 - Argument 1 of takesB expects B, but parent type A provided', ], 'closureByRefUseToMixed' => [ ' 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:54 - Argument 1 expects T:fn-map as mixed, int provided', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:54 - Argument 1 expects T:fn-map as mixed, but int provided', ], 'cannotInferReturnClosureWithDifferentReturnTypes' => [ ' 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:20:34 - Argument 1 of type expects string, callable(State):(T:AlmostFooMap as mixed)&Foo provided', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:20:34 - Argument 1 of type expects string, but callable(State):(T:AlmostFooMap as mixed)&Foo provided', ], 'templateWithNoReturn' => [ ' 5, "name" => "Mario", "height" => 3.5]); $mario->ame = "Luigi";', - 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:47:29 - Argument 1 of CharacterRow::__set expects "height"|"id"|"name", "ame" provided', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:47:29 - Argument 1 of CharacterRow::__set expects "height"|"id"|"name", but "ame" provided', ], 'specialiseTypeBeforeReturning' => [ ' 'ArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:14:32 - Argument 1 of takesB expects B,' - . ' parent type A&static provided', + . ' but parent type A&static provided', ], 'intersectionTypeInterfaceCheckAfterInstanceof' => [ ' 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:12:32 - Argument 1 of takesI expects I, A&static provided', + 'error_message' => 'InvalidArgument - src' . DIRECTORY_SEPARATOR . 'somefile.php:12:32 - Argument 1 of takesI expects I, but A&static provided', ], ]; } diff --git a/tests/UnusedVariableTest.php b/tests/UnusedVariableTest.php index 048025186ca..618f871d651 100644 --- a/tests/UnusedVariableTest.php +++ b/tests/UnusedVariableTest.php @@ -3451,7 +3451,7 @@ function takesArray($a) : void { $arr = [$a]; takesArrayOfString($arr); }', - 'error_message' => 'MixedArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:12:44 - Argument 1 of takesArrayOfString expects array, parent type array{mixed} provided. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:10:41' + 'error_message' => 'MixedArgumentTypeCoercion - src' . DIRECTORY_SEPARATOR . 'somefile.php:12:44 - Argument 1 of takesArrayOfString expects array, but parent type array{mixed} provided. Consider improving the type at src' . DIRECTORY_SEPARATOR . 'somefile.php:10:41' ], 'warnAboutUnusedVariableInTryReassignedInCatch' => [ ' Date: Thu, 4 Aug 2022 16:50:38 +0300 Subject: [PATCH 035/194] #8363 - ensure we recognize inherited static methods for the first-class callables --- .../StaticMethod/AtomicStaticCallAnalyzer.php | 67 +++++++++++-------- tests/ClosureTest.php | 21 ++++++ 2 files changed, 61 insertions(+), 27 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index 673dc56c505..a147c4aa37a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -56,6 +56,7 @@ use function array_filter; use function array_map; +use function array_values; use function assert; use function count; use function in_array; @@ -369,39 +370,15 @@ private static function handleNamedCall( if (!$naive_method_exists && $codebase->methods->existence_provider->has($fq_class_name) ) { - $method_exists = $codebase->methods->existence_provider->doesMethodExist( + $fake_method_exists = $codebase->methods->existence_provider->doesMethodExist( $fq_class_name, $method_id->method_name, $statements_analyzer, null - ); - - if ($method_exists) { - $fake_method_exists = true; - } - } - - if ($stmt->isFirstClassCallable()) { - $method_storage = ($class_storage->methods[$method_name_lc] ?? - ($class_storage->pseudo_static_methods[$method_name_lc] ?? null)); - - if ($method_storage) { - $return_type_candidate = new Union([new TClosure( - 'Closure', - $method_storage->params, - $method_storage->return_type, - $method_storage->pure - )]); - } else { - $return_type_candidate = Type::getClosure(); - } - - $statements_analyzer->node_data->setType($stmt, $return_type_candidate); - - return true; + ) ?? false; } - $args = $stmt->getArgs(); + $args = $stmt->isFirstClassCallable() ? [] : $stmt->getArgs(); if (!$naive_method_exists && $class_storage->mixin_declaring_fqcln @@ -504,6 +481,42 @@ private static function handleNamedCall( $method_name_lc ); + if ($stmt->isFirstClassCallable()) { + if ($found_method_and_class_storage) { + [ $method_storage ] = $found_method_and_class_storage; + + $return_type_candidate = new Union([new TClosure( + 'Closure', + $method_storage->params, + $method_storage->return_type, + $method_storage->pure + )]); + } else { + $method_exists = $naive_method_exists + || $fake_method_exists + || isset($class_storage->methods[$method_name_lc]) + || isset($class_storage->pseudo_static_methods[$method_name_lc]); + + if ($method_exists) { + $declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id) ?? $method_id; + + $return_type_candidate = new Union([new TClosure( + 'Closure', + array_values($codebase->getMethodParams($method_id)), + $codebase->getMethodReturnType($method_id, $fq_class_name), + $codebase->methods->getStorage($declaring_method_id)->pure + )]); + } else { + // FIXME: perhaps Psalm should complain about nonexisting method here, or throw a logic exception? + $return_type_candidate = Type::getClosure(); + } + } + + $statements_analyzer->node_data->setType($stmt, $return_type_candidate); + + return true; + } + if (!$naive_method_exists || !MethodAnalyzer::isMethodVisible( $method_id, diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index cea843a3725..e6e5699600e 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -751,6 +751,27 @@ public static function __callStatic(string $name, array $args): mixed { [], '8.1' ], + 'FirstClassCallable:OverriddenStaticMethod' => [ + ' [], + [], + '8.1', + ], 'FirstClassCallable:WithArrayMap' => [ ' Date: Thu, 4 Aug 2022 17:16:06 +0300 Subject: [PATCH 036/194] #8363 - support `static` as a type parameter in return types of the first-class callables --- .../StaticMethod/AtomicStaticCallAnalyzer.php | 13 +++++++- tests/ClosureTest.php | 30 ++++++++++++++++++- 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index a147c4aa37a..a4c33c24c08 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -512,7 +512,18 @@ private static function handleNamedCall( } } - $statements_analyzer->node_data->setType($stmt, $return_type_candidate); + $expanded_return_type = TypeExpander::expandUnion( + $codebase, + $return_type_candidate, + $context->self, + $class_storage->name, + $context->parent, + true, + false, + true + ); + + $statements_analyzer->node_data->setType($stmt, $expanded_return_type); return true; } diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index e6e5699600e..53e5cc3f9b2 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -751,7 +751,7 @@ public static function __callStatic(string $name, array $args): mixed { [], '8.1' ], - 'FirstClassCallable:OverriddenStaticMethod' => [ + 'FirstClassCallable:InheritedStaticMethod' => [ ' [ + ' */ + public static function create(int $i): Holder + { + return new Holder(new static($i)); + } + } + + class C extends A {} + + /** @param \Closure(int):Holder $_ */ + function takesIntToHolder(\Closure $_): void {} + + takesIntToHolder(C::create(...));' + ], 'FirstClassCallable:WithArrayMap' => [ ' Date: Fri, 5 Aug 2022 12:22:27 +0200 Subject: [PATCH 037/194] Removed `DateTimeImmutable::__construct` from the CallMap: fully covered by stub --- dictionaries/CallMap.php | 2 -- dictionaries/CallMap_historical.php | 2 -- 2 files changed, 4 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 219e6372641..b34c6c59cb1 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1793,8 +1793,6 @@ 'DateTime::setTimestamp' => ['static', 'unixtimestamp'=>'int'], 'DateTime::setTimezone' => ['static', 'timezone'=>'DateTimeZone'], 'DateTime::sub' => ['static', 'interval'=>'DateInterval'], -'DateTimeImmutable::__construct' => ['void', 'time='=>'string'], -'DateTimeImmutable::__construct\'1' => ['void', 'time'=>'?string', 'timezone'=>'?DateTimeZone'], 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], 'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index a5c4875aa71..ebce06c0070 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1055,8 +1055,6 @@ 'DateTime::setTimestamp' => ['static', 'unixtimestamp'=>'int'], 'DateTime::setTimezone' => ['static', 'timezone'=>'DateTimeZone'], 'DateTime::sub' => ['static', 'interval'=>'DateInterval'], - 'DateTimeImmutable::__construct' => ['void', 'time='=>'string'], - 'DateTimeImmutable::__construct\'1' => ['void', 'time'=>'?string', 'timezone'=>'?DateTimeZone'], 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], 'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], From 267d76088d7bbdc19c31bbc7615fc3e5bebed1bc Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:23:00 +0200 Subject: [PATCH 038/194] Removed `DateTimeImmutable::sub()` from the CallMap: fully covered by stub --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_historical.php | 1 - stubs/CoreImmutableClasses.phpstub | 5 ++++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index b34c6c59cb1..7870727ba9f 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1811,7 +1811,6 @@ 'DateTimeImmutable::setTime' => ['static|false', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], 'DateTimeImmutable::setTimezone' => ['static|false', 'timezone'=>'DateTimeZone'], -'DateTimeImmutable::sub' => ['static|false', 'interval'=>'DateInterval'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], 'DateTimeInterface::getOffset' => ['int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index ebce06c0070..d387582cb51 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1072,7 +1072,6 @@ 'DateTimeImmutable::setTime' => ['static|false', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], 'DateTimeImmutable::setTimezone' => ['static|false', 'timezone'=>'DateTimeZone'], - 'DateTimeImmutable::sub' => ['static|false', 'interval'=>'DateInterval'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], 'DateTimeInterface::getOffset' => ['int'], diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index 2d6555c3df2..e5db68d978f 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -56,7 +56,10 @@ class DateTimeImmutable implements DateTimeInterface /** * @psalm-mutation-free - * @return static + * @return static|false this method can fail in case an {@see DateInterval} with relative + * week days is passed in. + * + * @see https://github.com/php/php-src/blob/534127d3b22b193ffb9511c4447584f0d2bd4e24/ext/date/php_date.c#L3157-L3160 */ public function sub(DateInterval $interval) {} From 58ca4e0b73eb52c6b171ef5b59f776e70cc4288b Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:23:43 +0200 Subject: [PATCH 039/194] Removed `DateTimeImmutable::createFromFormat()` from the CallMap: fully covered by stub --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_historical.php | 1 - 2 files changed, 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 7870727ba9f..20f68f13e05 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1796,7 +1796,6 @@ 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], 'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], -'DateTimeImmutable::createFromFormat' => ['static|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], 'DateTimeImmutable::createFromInterface' => ['self', 'object' => 'DateTimeInterface'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index d387582cb51..d0fc4bc2b02 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1058,7 +1058,6 @@ 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], 'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], - 'DateTimeImmutable::createFromFormat' => ['static|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeImmutable::format' => ['string|false', 'format'=>'string'], From 7ee12c74935dabe2b481a322acc02ba87217b049 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:24:35 +0200 Subject: [PATCH 040/194] Removed `DateTimeImmutable::format()` from the CallMap: fully covered by stub Note: some conditional return type magic was required here. See: https://github.com/vimeo/psalm/pull/8350#discussion_r937089212 --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_80_delta.php | 4 ---- dictionaries/CallMap_historical.php | 1 - stubs/CoreImmutableClasses.phpstub | 4 +++- 4 files changed, 3 insertions(+), 7 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 20f68f13e05..40633b8e18c 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1799,7 +1799,6 @@ 'DateTimeImmutable::createFromInterface' => ['self', 'object' => 'DateTimeInterface'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], -'DateTimeImmutable::format' => ['string', 'format'=>'string'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], 'DateTimeImmutable::getOffset' => ['int'], 'DateTimeImmutable::getTimestamp' => ['int|false'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index afeb1bc4133..4200608e06a 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -41,10 +41,6 @@ 'old' => ['string|false', 'format'=>'string'], 'new' => ['string', 'format'=>'string'], ], - 'DateTimeImmutable::format' => [ - 'old' => ['string|false', 'format'=>'string'], - 'new' => ['string', 'format'=>'string'], - ], 'DateTimeZone::listIdentifiers' => [ 'old' => ['list|false', 'timezoneGroup='=>'int', 'countryCode='=>'string|null'], 'new' => ['list', 'timezoneGroup='=>'int', 'countryCode='=>'string|null'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index d0fc4bc2b02..013d88e0071 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1060,7 +1060,6 @@ 'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], - 'DateTimeImmutable::format' => ['string|false', 'format'=>'string'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], 'DateTimeImmutable::getOffset' => ['int'], 'DateTimeImmutable::getTimestamp' => ['int|false'], diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index e5db68d978f..c241cb4f17a 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -12,8 +12,10 @@ class DateTimeImmutable implements DateTimeInterface /** * @psalm-mutation-free + * * @param string $format - * @return string + * + * @return (\PHP_MAJOR_VERSION is int<0, 7> ? string|false : string) */ public function format($format) {} From 2b6fddf88d5408da418f808a75bb497b6886e186 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:26:59 +0200 Subject: [PATCH 041/194] Removed `DateTimeImmutable::getTimezone()` from the CallMap: fully covered by stub Note: also verified that a `DateTimeImmutable#getTimezone()` always returns a default timezone (initialized internally), and therefore restricted the type a bit. --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_historical.php | 1 - 2 files changed, 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 40633b8e18c..52f26dba263 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1802,7 +1802,6 @@ 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], 'DateTimeImmutable::getOffset' => ['int'], 'DateTimeImmutable::getTimestamp' => ['int|false'], -'DateTimeImmutable::getTimezone' => ['DateTimeZone|false'], 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 013d88e0071..89193d9804e 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1063,7 +1063,6 @@ 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], 'DateTimeImmutable::getOffset' => ['int'], 'DateTimeImmutable::getTimestamp' => ['int|false'], - 'DateTimeImmutable::getTimezone' => ['DateTimeZone|false'], 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], From 1be04e0988286950cf34fb02ce60caad89c02be6 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:27:29 +0200 Subject: [PATCH 042/194] Removed `DateTimeImmutable::getOffset()` from the CallMap: fully covered by stub --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_historical.php | 1 - 2 files changed, 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 52f26dba263..e1c4f97da8d 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1800,7 +1800,6 @@ 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], -'DateTimeImmutable::getOffset' => ['int'], 'DateTimeImmutable::getTimestamp' => ['int|false'], 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 89193d9804e..52c0d8c2dac 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1061,7 +1061,6 @@ 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], - 'DateTimeImmutable::getOffset' => ['int'], 'DateTimeImmutable::getTimestamp' => ['int|false'], 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], From 002585b57ec014a40cf81f14cedc58936c1ddfd8 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:32:26 +0200 Subject: [PATCH 043/194] Removed `DateTimeImmutable::getTimestamp()` from the CallMap: fully covered by stub This also simplifies the return type from `int|false` to always `int`, since a timestamp can always be produced. Ref: https://github.com/php/php-src/blob/eff9aed1592f59cddb12d36a55dec0ccc3bbbfd6/ext/date/php_date.stub.php#L496-L500 --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_historical.php | 1 - 2 files changed, 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index e1c4f97da8d..651fdc08f85 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1800,7 +1800,6 @@ 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], -'DateTimeImmutable::getTimestamp' => ['int|false'], 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 52c0d8c2dac..98c7d172b0b 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1061,7 +1061,6 @@ 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], - 'DateTimeImmutable::getTimestamp' => ['int|false'], 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], From 18557b8c7005a85ab7f6ffefbf0b6683a11ea4d2 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:34:31 +0200 Subject: [PATCH 044/194] Removed `DateTimeImmutable::diff()` from the CallMap: fully covered by stub --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_historical.php | 1 - 2 files changed, 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 651fdc08f85..dc136691866 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1798,7 +1798,6 @@ 'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], 'DateTimeImmutable::createFromInterface' => ['self', 'object' => 'DateTimeInterface'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], -'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 98c7d172b0b..9e96e514e0e 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1059,7 +1059,6 @@ 'DateTimeImmutable::__wakeup' => ['void'], 'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], - 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], From 7cd3d49dc4de625b7b96a1c99c7c7f269acf6725 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:37:24 +0200 Subject: [PATCH 045/194] Removed `DateTimeImmutable::modify()` from the CallMap: fully covered by stub Also expanded the return type from `static` to `static|false`, since the operation can fail (with a warning too), such as in following example: https://3v4l.org/Xrjlc ```php modify('potato') ); ``` Produces ``` Warning: DateTimeImmutable::modify(): Failed to parse time string (potato) at position 0 (p): The timezone could not be found in the database in /in/Xrjlc on line 6 bool(false) ``` Ref: https://github.com/php/php-src/blob/534127d3b22b193ffb9511c4447584f0d2bd4e24/ext/date/php_date.stub.php#L508-L509 --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_historical.php | 1 - stubs/CoreImmutableClasses.phpstub | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index dc136691866..b6b9fe20581 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1799,7 +1799,6 @@ 'DateTimeImmutable::createFromInterface' => ['self', 'object' => 'DateTimeInterface'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], -'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], 'DateTimeImmutable::setTime' => ['static|false', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 9e96e514e0e..cda731c378e 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1060,7 +1060,6 @@ 'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], - 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], 'DateTimeImmutable::setTime' => ['static|false', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index c241cb4f17a..4e9cf0df3e9 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -46,7 +46,7 @@ class DateTimeImmutable implements DateTimeInterface /** * @psalm-mutation-free - * @return static + * @return static|false */ public function modify(string $modifier) {} From cb9939cbd37c9e7285d243716d666004af98023b Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:39:16 +0200 Subject: [PATCH 046/194] Removed `DateTimeImmutable::add()` from the CallMap: fully covered by stub Ref: https://github.com/php/php-src/blob/534127d3b22b193ffb9511c4447584f0d2bd4e24/ext/date/php_date.stub.php#L511-L512 Ref: https://github.com/php/php-src/blob/534127d3b22b193ffb9511c4447584f0d2bd4e24/ext/date/php_date.c#L3129-L3144 --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_historical.php | 1 - 2 files changed, 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index b6b9fe20581..809204d57f2 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1795,7 +1795,6 @@ 'DateTime::sub' => ['static', 'interval'=>'DateInterval'], 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], -'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], 'DateTimeImmutable::createFromInterface' => ['self', 'object' => 'DateTimeInterface'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index cda731c378e..801026e0d18 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1057,7 +1057,6 @@ 'DateTime::sub' => ['static', 'interval'=>'DateInterval'], 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], - 'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], From 4fe554d6d227e49fcdf41d2bbd8b64b3a058aa63 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:42:45 +0200 Subject: [PATCH 047/194] Removed `DateTimeImmutable::setTimezone()` from the CallMap: fully covered by stub Also simplified the return type from `static|false` to `static`, since the method throws at all times, on failure. On PHP 7.x, it could only fail if an invalid type was passed in, which is not really valid anyway, from a type perspective. Ref (PHP 8.2.x): https://github.com/php/php-src/blob/534127d3b22b193ffb9511c4447584f0d2bd4e24/ext/date/php_date.c#L3291-L3307 Ref (PHP 8.2.x): https://github.com/php/php-src/blob/534127d3b22b193ffb9511c4447584f0d2bd4e24/ext/date/php_date.stub.php#L517-L518 Ref (PHP 7.0.33): https://github.com/php/php-src/blob/bf574c2b67a1f786e36cf679f41b758b973a82c4/ext/date/php_date.c#L3363-L3379 --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_historical.php | 1 - stubs/CoreImmutableClasses.phpstub | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 809204d57f2..0da0f76ecb9 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1802,7 +1802,6 @@ 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], 'DateTimeImmutable::setTime' => ['static|false', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], -'DateTimeImmutable::setTimezone' => ['static|false', 'timezone'=>'DateTimeZone'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], 'DateTimeInterface::getOffset' => ['int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 801026e0d18..8af7c4a8165 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1063,7 +1063,6 @@ 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], 'DateTimeImmutable::setTime' => ['static|false', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], - 'DateTimeImmutable::setTimezone' => ['static|false', 'timezone'=>'DateTimeZone'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], 'DateTimeInterface::getOffset' => ['int'], diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index 4e9cf0df3e9..21efbef48e3 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -67,7 +67,7 @@ class DateTimeImmutable implements DateTimeInterface /** * @psalm-mutation-free - * @return static|false + * @return static */ public function setTimezone(DateTimeZone $timezone) {} From e61c593a2c3cadbd656ff6768e302ee94e287da2 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:47:04 +0200 Subject: [PATCH 048/194] Removed `DateTimeImmutable::setTime()` from the CallMap: fully covered by stub Also simplified the return type from `static|false` to `static`, since the method throws at all times, on failure. On PHP 7.x, it could only fail if an invalid type was passed in, which is not really valid anyway, from a type perspective. Ref (PHP 8.1.x): https://github.com/php/php-src/blob/32d55f74229e7913db0d59ef874a401744479b6a/ext/date/php_date.c#L3212-L3228 Ref (PHP 7.0.33): https://github.com/php/php-src/blob/bf574c2b67a1f786e36cf679f41b758b973a82c4/ext/date/php_date.c#L3447-L3463 --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_historical.php | 1 - stubs/CoreImmutableClasses.phpstub | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 0da0f76ecb9..86da052b3b6 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1800,7 +1800,6 @@ 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], -'DateTimeImmutable::setTime' => ['static|false', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 8af7c4a8165..176ffce3868 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1061,7 +1061,6 @@ 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], - 'DateTimeImmutable::setTime' => ['static|false', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], 'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index 21efbef48e3..aab2e880641 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -73,7 +73,7 @@ class DateTimeImmutable implements DateTimeInterface /** * @psalm-mutation-free - * @return static|false + * @return static */ public function setTime(int $hour, int $minute, int $second = 0, int $microsecond = 0) {} From 0a6c9d01776a99ab759062aa1b03c445550633d4 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:48:29 +0200 Subject: [PATCH 049/194] Removed `DateTimeImmutable::setDate()` from the CallMap: fully covered by stub Also simplified the return type from `static|false` to `static`, since the method throws at all times, on failure. On PHP 7.x, it could only fail if an invalid type was passed in, which is not really valid anyway, from a type perspective. Ref (PHP 8.1.x): https://github.com/php/php-src/blob/32d55f74229e7913db0d59ef874a401744479b6a/ext/date/php_date.c#L3258-L3274 Ref (PHP 7.0.33): https://github.com/php/php-src/blob/bf574c2b67a1f786e36cf679f41b758b973a82c4/ext/date/php_date.c#L3496-L3512 --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_historical.php | 1 - stubs/CoreImmutableClasses.phpstub | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 86da052b3b6..630770d728d 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1798,7 +1798,6 @@ 'DateTimeImmutable::createFromInterface' => ['self', 'object' => 'DateTimeInterface'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], -'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], 'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 176ffce3868..9fd8252b8a0 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1059,7 +1059,6 @@ 'DateTimeImmutable::__wakeup' => ['void'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], - 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], 'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index aab2e880641..fcdae2705d3 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -79,7 +79,7 @@ class DateTimeImmutable implements DateTimeInterface /** * @psalm-mutation-free - * @return static|false + * @return static */ public function setDate(int $year, int $month, int $day) {} From 964f64a500b987c82bd5274d07d3b67e1d0b381b Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:49:45 +0200 Subject: [PATCH 050/194] Removed `DateTimeImmutable::setISODate()` from the CallMap: fully covered by stub Also simplified the return type from `static|false` to `static`, since the method throws at all times, on failure. On PHP 7.x, it could only fail if an invalid type was passed in, which is not really valid anyway, from a type perspective. Ref (PHP 8.1.x): https://github.com/php/php-src/blob/32d55f74229e7913db0d59ef874a401744479b6a/ext/date/php_date.c#L3308-L3324 Ref (PHP 7.0.33): https://github.com/php/php-src/blob/bf574c2b67a1f786e36cf679f41b758b973a82c4/ext/date/php_date.c#L3549-L3565 --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_historical.php | 1 - stubs/CoreImmutableClasses.phpstub | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 630770d728d..28171d694a4 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1798,7 +1798,6 @@ 'DateTimeImmutable::createFromInterface' => ['self', 'object' => 'DateTimeInterface'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], -'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], 'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 9fd8252b8a0..c15978b7e50 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1059,7 +1059,6 @@ 'DateTimeImmutable::__wakeup' => ['void'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], - 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], 'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index fcdae2705d3..1829ad4e891 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -85,7 +85,7 @@ class DateTimeImmutable implements DateTimeInterface /** * @psalm-mutation-free - * @return static|false + * @return static */ public function setISODate(int $year, int $week, int $dayOfWeek = 1) {} From aaac9ccb90f32739dd36cdea7bcc16ca9e30e71a Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:50:51 +0200 Subject: [PATCH 051/194] Removed `DateTimeImmutable::setTimestamp()` from the CallMap: fully covered by stub Also simplified the return type from `static|false` to `static`, since the method throws at all times, on failure. On PHP 7.x, it could only fail if an invalid type was passed in, which is not really valid anyway, from a type perspective. Ref (PHP 8.1.x): https://github.com/php/php-src/blob/32d55f74229e7913db0d59ef874a401744479b6a/ext/date/php_date.c#L3353-L3369 Ref (PHP 7.0.33): https://github.com/php/php-src/blob/bf574c2b67a1f786e36cf679f41b758b973a82c4/ext/date/php_date.c#L3596-L3612 --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_historical.php | 1 - stubs/CoreImmutableClasses.phpstub | 2 +- 3 files changed, 1 insertion(+), 3 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 28171d694a4..caf3cfaf5f6 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1798,7 +1798,6 @@ 'DateTimeImmutable::createFromInterface' => ['self', 'object' => 'DateTimeInterface'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], -'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], 'DateTimeInterface::getOffset' => ['int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index c15978b7e50..4fa58db7dc2 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1059,7 +1059,6 @@ 'DateTimeImmutable::__wakeup' => ['void'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], - 'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], 'DateTimeInterface::getOffset' => ['int'], diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index 1829ad4e891..acf7fc0bf39 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -91,7 +91,7 @@ class DateTimeImmutable implements DateTimeInterface /** * @psalm-mutation-free - * @return static|false + * @return static */ public function setTimestamp(int $unixtimestamp) {} From a1ed84f1ed7fb8a030fea09aa2e6b4780903b3d7 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 12:52:32 +0200 Subject: [PATCH 052/194] Removed `DateTimeImmutable::createFromMutable()` from the CallMap: fully covered by stub --- dictionaries/CallMap.php | 1 - dictionaries/CallMap_historical.php | 1 - 2 files changed, 2 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index caf3cfaf5f6..329decd3234 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1796,7 +1796,6 @@ 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], 'DateTimeImmutable::createFromInterface' => ['self', 'object' => 'DateTimeInterface'], -'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 4fa58db7dc2..6df5d473acf 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1057,7 +1057,6 @@ 'DateTime::sub' => ['static', 'interval'=>'DateInterval'], 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], - 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], From 68ffae097e32d64397179da1b7353f7e4f0cdea7 Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 13:21:28 +0200 Subject: [PATCH 053/194] Simplified `DateTimeImmutable::format()`: always returns a `string` Also: * a non-empty format string will always lead to `non-empty-string` * it seems that you can throw **everything** at `DateTimeInterface#format()`, even null bytes, and it will always produce a `string` --- stubs/CoreImmutableClasses.phpstub | 2 +- tests/MethodCallTest.php | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index acf7fc0bf39..3a537ba4575 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -15,7 +15,7 @@ class DateTimeImmutable implements DateTimeInterface * * @param string $format * - * @return (\PHP_MAJOR_VERSION is int<0, 7> ? string|false : string) + * @return ($format is non-empty-string ? non-empty-string : string) */ public function format($format) {} diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 76c033f56f5..62860e8f209 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -271,8 +271,8 @@ final class MyDate extends DateTimeImmutable {} $b = (new DateTimeImmutable())->modify("+3 hours");', 'assertions' => [ - '$yesterday' => 'MyDate', - '$b' => 'DateTimeImmutable', + '$yesterday' => 'MyDate|false', + '$b' => 'DateTimeImmutable|false', ], ], 'magicCall' => [ From 13828771c7c544aa61a3db1ef49b7aae9e0996fd Mon Sep 17 00:00:00 2001 From: Marco Pivetta Date: Fri, 5 Aug 2022 13:28:53 +0200 Subject: [PATCH 054/194] Removed `DateTimeImmutable::createFromInterface()` from stubs While there is value in declaring `DateTimeImmutable::createFromInterface()` as mutation-free in a stub, this method was introduced in PHP 8.0, so we cannot really declare it in a stub. For now, we drop it, as the value of its stub declaration is much lower than the problems it introduces through its conditional existence. --- stubs/CoreImmutableClasses.phpstub | 6 ------ 1 file changed, 6 deletions(-) diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index 3a537ba4575..23eb77bdb77 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -100,12 +100,6 @@ class DateTimeImmutable implements DateTimeInterface * @return static */ public static function createFromMutable(DateTime $object) {} - - /** - * @psalm-mutation-free - * @return self - */ - public static function createFromInterface(DateTimeInterface $object) {} } /** From fefd4861d6641e2104972a3d3fc22670b2eed41d Mon Sep 17 00:00:00 2001 From: Teemu Koskinen Date: Sat, 6 Aug 2022 02:27:01 +0300 Subject: [PATCH 055/194] Use $codebase->classlike_storage_provider only if it has the required data. Fixes #8373 --- src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 7d6dbf6cee3..c09b16e7089 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -1138,7 +1138,7 @@ public static function getMappedGenericTypeParams( ): array { if ($input_type_part instanceof TGenericObject || $input_type_part instanceof TIterable) { $input_type_params = $input_type_part->type_params; - } else { + } elseif ($codebase->classlike_storage_provider->has($input_type_part->value)) { $class_storage = $codebase->classlike_storage_provider->get($input_type_part->value); $container_class = $container_type_part->value; @@ -1150,6 +1150,8 @@ public static function getMappedGenericTypeParams( } else { $input_type_params = array_fill(0, count($class_storage->template_types ?? []), Type::getMixed()); } + } else { + $input_type_params = []; } try { From 89b7b3234bf8b4fabf154b0594726b6b1c137602 Mon Sep 17 00:00:00 2001 From: Teemu Koskinen Date: Mon, 8 Aug 2022 23:47:11 +0300 Subject: [PATCH 056/194] Add test for #8373 Undefined classes in function dockblocks should not crash psalm. Test provided by @AndrolGenhald --- tests/Template/ClassTemplateTest.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index b76c93c3e05..ee84dbce884 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -3826,6 +3826,22 @@ private function acceptA(A $_a): void } }', ], + 'undefined class in function dockblock' => [ + ' $baz + */ + function foobar(DoesNotExist $baz): void {} + + /** + * @psalm-suppress UndefinedDocblockClass, UndefinedClass + * @var DoesNotExist + */ + $baz = new DoesNotExist(); + foobar($baz);', + ], ]; } From 8ca594a34d1fc307f55bbf0549d7b8912573cd90 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Fri, 5 Aug 2022 11:05:08 +0200 Subject: [PATCH 057/194] always use lock when writing/reading cache data to/from file --- .../ClassLikeStorageCacheProvider.php | 11 +- .../Provider/FileReferenceCacheProvider.php | 142 +++++++++--------- .../Provider/FileStorageCacheProvider.php | 11 +- .../Internal/Provider/ParserCacheProvider.php | 49 ++---- src/Psalm/Internal/Provider/Providers.php | 45 ++++++ 5 files changed, 139 insertions(+), 119 deletions(-) diff --git a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php index dae227f5520..c7fa19acd0c 100644 --- a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php @@ -3,13 +3,13 @@ namespace Psalm\Internal\Provider; use Psalm\Config; +use Psalm\Internal\Provider\Providers; use Psalm\Storage\ClassLikeStorage; use UnexpectedValueException; use function array_merge; use function dirname; use function file_exists; -use function file_get_contents; use function file_put_contents; use function filemtime; use function get_class; @@ -25,6 +25,7 @@ use function unserialize; use const DIRECTORY_SEPARATOR; +use const LOCK_EX; use const PHP_VERSION_ID; /** @@ -86,9 +87,9 @@ public function writeToCache(ClassLikeStorage $storage, string $file_path, strin $cache_location = $this->getCacheLocationForClass($fq_classlike_name_lc, $file_path, true); if ($this->config->use_igbinary) { - file_put_contents($cache_location, igbinary_serialize($storage)); + file_put_contents($cache_location, igbinary_serialize($storage), LOCK_EX); } else { - file_put_contents($cache_location, serialize($storage)); + file_put_contents($cache_location, serialize($storage), LOCK_EX); } } @@ -132,7 +133,7 @@ private function loadFromCache(string $fq_classlike_name_lc, ?string $file_path) if (file_exists($cache_location)) { if ($this->config->use_igbinary) { - $storage = igbinary_unserialize((string)file_get_contents($cache_location)); + $storage = igbinary_unserialize(Providers::safeFileGetContents($cache_location)); if ($storage instanceof ClassLikeStorage) { return $storage; @@ -141,7 +142,7 @@ private function loadFromCache(string $fq_classlike_name_lc, ?string $file_path) return null; } - $storage = unserialize((string)file_get_contents($cache_location)); + $storage = unserialize(Providers::safeFileGetContents($cache_location)); if ($storage instanceof ClassLikeStorage) { return $storage; diff --git a/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php b/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php index 616a1c2c4e0..e512aacf29b 100644 --- a/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php @@ -4,10 +4,10 @@ use Psalm\Config; use Psalm\Internal\Codebase\Analyzer; +use Psalm\Internal\Provider\Providers; use UnexpectedValueException; use function file_exists; -use function file_get_contents; use function file_put_contents; use function igbinary_serialize; use function igbinary_unserialize; @@ -18,6 +18,7 @@ use function unserialize; use const DIRECTORY_SEPARATOR; +use const LOCK_EX; /** * @psalm-import-type FileMapType from Analyzer @@ -84,9 +85,9 @@ public function getCachedFileReferences(): ?array } if ($this->config->use_igbinary) { - $reference_cache = igbinary_unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = igbinary_unserialize(Providers::safeFileGetContents($reference_cache_location)); } else { - $reference_cache = unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = unserialize(Providers::safeFileGetContents($reference_cache_location)); } if (!is_array($reference_cache)) { @@ -114,9 +115,9 @@ public function getCachedClassLikeFiles(): ?array } if ($this->config->use_igbinary) { - $reference_cache = igbinary_unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = igbinary_unserialize(Providers::safeFileGetContents($reference_cache_location)); } else { - $reference_cache = unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = unserialize(Providers::safeFileGetContents($reference_cache_location)); } if (!is_array($reference_cache)) { @@ -144,9 +145,9 @@ public function getCachedNonMethodClassReferences(): ?array } if ($this->config->use_igbinary) { - $reference_cache = igbinary_unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = igbinary_unserialize(Providers::safeFileGetContents($reference_cache_location)); } else { - $reference_cache = unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = unserialize(Providers::safeFileGetContents($reference_cache_location)); } if (!is_array($reference_cache)) { @@ -174,9 +175,9 @@ public function getCachedMethodClassReferences(): ?array } if ($this->config->use_igbinary) { - $reference_cache = igbinary_unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = igbinary_unserialize(Providers::safeFileGetContents($reference_cache_location)); } else { - $reference_cache = unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = unserialize(Providers::safeFileGetContents($reference_cache_location)); } if (!is_array($reference_cache)) { @@ -203,7 +204,7 @@ public function getCachedMethodMemberReferences(): ?array return null; } - $class_member_reference_cache = (string) file_get_contents($class_member_cache_location); + $class_member_reference_cache = Providers::safeFileGetContents($class_member_cache_location); if ($this->config->use_igbinary) { $class_member_reference_cache = igbinary_unserialize($class_member_reference_cache); } else { @@ -235,7 +236,7 @@ public function getCachedMethodDependencies(): ?array return null; } - $method_dependencies_cache = (string) file_get_contents($method_dependencies_cache_location); + $method_dependencies_cache = Providers::safeFileGetContents($method_dependencies_cache_location); if ($this->config->use_igbinary) { $method_dependencies_cache = igbinary_unserialize($method_dependencies_cache); } else { @@ -266,7 +267,7 @@ public function getCachedMethodPropertyReferences(): ?array return null; } - $class_member_reference_cache = (string) file_get_contents($class_member_cache_location); + $class_member_reference_cache = Providers::safeFileGetContents($class_member_cache_location); if ($this->config->use_igbinary) { $class_member_reference_cache = igbinary_unserialize($class_member_reference_cache); } else { @@ -297,7 +298,7 @@ public function getCachedMethodMethodReturnReferences(): ?array return null; } - $class_member_reference_cache = (string) file_get_contents($class_member_cache_location); + $class_member_reference_cache = Providers::safeFileGetContents($class_member_cache_location); if ($this->config->use_igbinary) { $class_member_reference_cache = igbinary_unserialize($class_member_reference_cache); } else { @@ -328,7 +329,7 @@ public function getCachedMethodMissingMemberReferences(): ?array return null; } - $class_member_reference_cache = (string) file_get_contents($class_member_cache_location); + $class_member_reference_cache = Providers::safeFileGetContents($class_member_cache_location); if ($this->config->use_igbinary) { $class_member_reference_cache = igbinary_unserialize($class_member_reference_cache); } else { @@ -359,7 +360,7 @@ public function getCachedFileMemberReferences(): ?array return null; } - $file_class_member_reference_cache = (string) file_get_contents($file_class_member_cache_location); + $file_class_member_reference_cache = Providers::safeFileGetContents($file_class_member_cache_location); if ($this->config->use_igbinary) { $file_class_member_reference_cache = igbinary_unserialize($file_class_member_reference_cache); } else { @@ -392,7 +393,7 @@ public function getCachedFilePropertyReferences(): ?array return null; } - $file_class_member_reference_cache = (string) file_get_contents($file_class_member_cache_location); + $file_class_member_reference_cache = Providers::safeFileGetContents($file_class_member_cache_location); if ($this->config->use_igbinary) { $file_class_member_reference_cache = igbinary_unserialize($file_class_member_reference_cache); } else { @@ -425,7 +426,7 @@ public function getCachedFileMethodReturnReferences(): ?array return null; } - $file_class_member_reference_cache = (string) file_get_contents($file_class_member_cache_location); + $file_class_member_reference_cache = Providers::safeFileGetContents($file_class_member_cache_location); if ($this->config->use_igbinary) { $file_class_member_reference_cache = igbinary_unserialize($file_class_member_reference_cache); } else { @@ -457,7 +458,7 @@ public function getCachedFileMissingMemberReferences(): ?array return null; } - $file_class_member_reference_cache = (string) file_get_contents($file_class_member_cache_location); + $file_class_member_reference_cache = Providers::safeFileGetContents($file_class_member_cache_location); if ($this->config->use_igbinary) { $file_class_member_reference_cache = igbinary_unserialize($file_class_member_reference_cache); } else { @@ -489,9 +490,9 @@ public function getCachedMixedMemberNameReferences(): ?array } if ($this->config->use_igbinary) { - $reference_cache = igbinary_unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = igbinary_unserialize(Providers::safeFileGetContents($reference_cache_location)); } else { - $reference_cache = unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = unserialize(Providers::safeFileGetContents($reference_cache_location)); } if (!is_array($reference_cache)) { @@ -519,9 +520,9 @@ public function getCachedMethodParamUses(): ?array } if ($this->config->use_igbinary) { - $reference_cache = igbinary_unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = igbinary_unserialize(Providers::safeFileGetContents($reference_cache_location)); } else { - $reference_cache = unserialize((string) file_get_contents($reference_cache_location)); + $reference_cache = unserialize(Providers::safeFileGetContents($reference_cache_location)); } if (!is_array($reference_cache)) { @@ -549,9 +550,9 @@ public function getCachedIssues(): ?array } if ($this->config->use_igbinary) { - $issues_cache = igbinary_unserialize((string) file_get_contents($issues_cache_location)); + $issues_cache = igbinary_unserialize(Providers::safeFileGetContents($issues_cache_location)); } else { - $issues_cache = unserialize((string) file_get_contents($issues_cache_location)); + $issues_cache = unserialize(Providers::safeFileGetContents($issues_cache_location)); } if (!is_array($issues_cache)) { @@ -572,9 +573,9 @@ public function setCachedFileReferences(array $file_references): void $reference_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::REFERENCE_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($reference_cache_location, igbinary_serialize($file_references)); + file_put_contents($reference_cache_location, igbinary_serialize($file_references), LOCK_EX); } else { - file_put_contents($reference_cache_location, serialize($file_references)); + file_put_contents($reference_cache_location, serialize($file_references), LOCK_EX); } } @@ -589,9 +590,9 @@ public function setCachedClassLikeFiles(array $file_references): void $reference_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::CLASSLIKE_FILE_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($reference_cache_location, igbinary_serialize($file_references)); + file_put_contents($reference_cache_location, igbinary_serialize($file_references), LOCK_EX); } else { - file_put_contents($reference_cache_location, serialize($file_references)); + file_put_contents($reference_cache_location, serialize($file_references), LOCK_EX); } } @@ -606,9 +607,9 @@ public function setCachedNonMethodClassReferences(array $file_class_references): $reference_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::NONMETHOD_CLASS_REFERENCE_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($reference_cache_location, igbinary_serialize($file_class_references)); + file_put_contents($reference_cache_location, igbinary_serialize($file_class_references), LOCK_EX); } else { - file_put_contents($reference_cache_location, serialize($file_class_references)); + file_put_contents($reference_cache_location, serialize($file_class_references), LOCK_EX); } } @@ -623,9 +624,9 @@ public function setCachedMethodClassReferences(array $method_class_references): $reference_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::METHOD_CLASS_REFERENCE_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($reference_cache_location, igbinary_serialize($method_class_references)); + file_put_contents($reference_cache_location, igbinary_serialize($method_class_references), LOCK_EX); } else { - file_put_contents($reference_cache_location, serialize($method_class_references)); + file_put_contents($reference_cache_location, serialize($method_class_references), LOCK_EX); } } @@ -640,9 +641,9 @@ public function setCachedMethodMemberReferences(array $member_references): void $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::CLASS_METHOD_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($member_references)); + file_put_contents($member_cache_location, igbinary_serialize($member_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($member_references)); + file_put_contents($member_cache_location, serialize($member_references), LOCK_EX); } } @@ -657,9 +658,9 @@ public function setCachedMethodDependencies(array $member_references): void $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::METHOD_DEPENDENCIES_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($member_references)); + file_put_contents($member_cache_location, igbinary_serialize($member_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($member_references)); + file_put_contents($member_cache_location, serialize($member_references), LOCK_EX); } } @@ -674,9 +675,9 @@ public function setCachedMethodPropertyReferences(array $property_references): v $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::CLASS_PROPERTY_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($property_references)); + file_put_contents($member_cache_location, igbinary_serialize($property_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($property_references)); + file_put_contents($member_cache_location, serialize($property_references), LOCK_EX); } } @@ -691,9 +692,9 @@ public function setCachedMethodMethodReturnReferences(array $method_return_refer $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::CLASS_METHOD_RETURN_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($method_return_references)); + file_put_contents($member_cache_location, igbinary_serialize($method_return_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($method_return_references)); + file_put_contents($member_cache_location, serialize($method_return_references), LOCK_EX); } } @@ -708,9 +709,9 @@ public function setCachedMethodMissingMemberReferences(array $member_references) $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::METHOD_MISSING_MEMBER_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($member_references)); + file_put_contents($member_cache_location, igbinary_serialize($member_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($member_references)); + file_put_contents($member_cache_location, serialize($member_references), LOCK_EX); } } @@ -725,9 +726,9 @@ public function setCachedFileMemberReferences(array $member_references): void $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::FILE_CLASS_MEMBER_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($member_references)); + file_put_contents($member_cache_location, igbinary_serialize($member_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($member_references)); + file_put_contents($member_cache_location, serialize($member_references), LOCK_EX); } } @@ -742,9 +743,9 @@ public function setCachedFilePropertyReferences(array $property_references): voi $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::FILE_CLASS_PROPERTY_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($property_references)); + file_put_contents($member_cache_location, igbinary_serialize($property_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($property_references)); + file_put_contents($member_cache_location, serialize($property_references), LOCK_EX); } } @@ -759,9 +760,9 @@ public function setCachedFileMethodReturnReferences(array $method_return_referen $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::FILE_METHOD_RETURN_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($method_return_references)); + file_put_contents($member_cache_location, igbinary_serialize($method_return_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($method_return_references)); + file_put_contents($member_cache_location, serialize($method_return_references), LOCK_EX); } } @@ -776,9 +777,9 @@ public function setCachedFileMissingMemberReferences(array $member_references): $member_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::FILE_MISSING_MEMBER_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($member_cache_location, igbinary_serialize($member_references)); + file_put_contents($member_cache_location, igbinary_serialize($member_references), LOCK_EX); } else { - file_put_contents($member_cache_location, serialize($member_references)); + file_put_contents($member_cache_location, serialize($member_references), LOCK_EX); } } @@ -793,9 +794,9 @@ public function setCachedMixedMemberNameReferences(array $references): void $reference_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::UNKNOWN_MEMBER_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($reference_cache_location, igbinary_serialize($references)); + file_put_contents($reference_cache_location, igbinary_serialize($references), LOCK_EX); } else { - file_put_contents($reference_cache_location, serialize($references)); + file_put_contents($reference_cache_location, serialize($references), LOCK_EX); } } @@ -810,9 +811,9 @@ public function setCachedMethodParamUses(array $uses): void $reference_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::METHOD_PARAM_USE_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($reference_cache_location, igbinary_serialize($uses)); + file_put_contents($reference_cache_location, igbinary_serialize($uses), LOCK_EX); } else { - file_put_contents($reference_cache_location, serialize($uses)); + file_put_contents($reference_cache_location, serialize($uses), LOCK_EX); } } @@ -827,9 +828,9 @@ public function setCachedIssues(array $issues): void $issues_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::ISSUES_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($issues_cache_location, igbinary_serialize($issues)); + file_put_contents($issues_cache_location, igbinary_serialize($issues), LOCK_EX); } else { - file_put_contents($issues_cache_location, serialize($issues)); + file_put_contents($issues_cache_location, serialize($issues), LOCK_EX); } } @@ -847,10 +848,10 @@ public function getAnalyzedMethodCache() ) { if ($this->config->use_igbinary) { /** @var array> */ - return igbinary_unserialize(file_get_contents($analyzed_methods_cache_location)); + return igbinary_unserialize(Providers::safeFileGetContents($analyzed_methods_cache_location)); } else { /** @var array> */ - return unserialize(file_get_contents($analyzed_methods_cache_location)); + return unserialize(Providers::safeFileGetContents($analyzed_methods_cache_location)); } } @@ -870,9 +871,9 @@ public function setAnalyzedMethodCache(array $analyzed_methods): void . self::ANALYZED_METHODS_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($analyzed_methods_cache_location, igbinary_serialize($analyzed_methods)); + file_put_contents($analyzed_methods_cache_location, igbinary_serialize($analyzed_methods), LOCK_EX); } else { - file_put_contents($analyzed_methods_cache_location, serialize($analyzed_methods)); + file_put_contents($analyzed_methods_cache_location, serialize($analyzed_methods), LOCK_EX); } } } @@ -893,12 +894,12 @@ public function getFileMapCache() /** * @var array */ - $file_maps_cache = igbinary_unserialize(file_get_contents($file_maps_cache_location)); + $file_maps_cache = igbinary_unserialize(Providers::safeFileGetContents($file_maps_cache_location)); } else { /** * @var array */ - $file_maps_cache = unserialize(file_get_contents($file_maps_cache_location)); + $file_maps_cache = unserialize(Providers::safeFileGetContents($file_maps_cache_location)); } return $file_maps_cache; @@ -918,9 +919,9 @@ public function setFileMapCache(array $file_maps): void $file_maps_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::FILE_MAPS_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($file_maps_cache_location, igbinary_serialize($file_maps)); + file_put_contents($file_maps_cache_location, igbinary_serialize($file_maps), LOCK_EX); } else { - file_put_contents($file_maps_cache_location, serialize($file_maps)); + file_put_contents($file_maps_cache_location, serialize($file_maps), LOCK_EX); } } } @@ -939,10 +940,10 @@ public function getTypeCoverage() ) { if ($this->config->use_igbinary) { /** @var array */ - $type_coverage_cache = igbinary_unserialize(file_get_contents($type_coverage_cache_location)); + $type_coverage_cache = igbinary_unserialize(Providers::safeFileGetContents($type_coverage_cache_location)); } else { /** @var array */ - $type_coverage_cache = unserialize(file_get_contents($type_coverage_cache_location)); + $type_coverage_cache = unserialize(Providers::safeFileGetContents($type_coverage_cache_location)); } return $type_coverage_cache; @@ -962,9 +963,9 @@ public function setTypeCoverage(array $mixed_counts): void $type_coverage_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::TYPE_COVERAGE_CACHE_NAME; if ($this->config->use_igbinary) { - file_put_contents($type_coverage_cache_location, igbinary_serialize($mixed_counts)); + file_put_contents($type_coverage_cache_location, igbinary_serialize($mixed_counts), LOCK_EX); } else { - file_put_contents($type_coverage_cache_location, serialize($mixed_counts)); + file_put_contents($type_coverage_cache_location, serialize($mixed_counts), LOCK_EX); } } } @@ -981,7 +982,7 @@ public function getConfigHashCache() if ($cache_directory && file_exists($config_hash_cache_location) ) { - return file_get_contents($config_hash_cache_location); + return Providers::safeFileGetContents($config_hash_cache_location); } return false; @@ -1000,7 +1001,8 @@ public function setConfigHashCache(string $hash): void file_put_contents( $config_hash_cache_location, - $hash + $hash, + LOCK_EX ); } } diff --git a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php index d52b4165acf..2f9279f12ae 100644 --- a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php @@ -3,13 +3,13 @@ namespace Psalm\Internal\Provider; use Psalm\Config; +use Psalm\Internal\Provider\Providers; use Psalm\Storage\FileStorage; use UnexpectedValueException; use function array_merge; use function dirname; use function file_exists; -use function file_get_contents; use function file_put_contents; use function filemtime; use function get_class; @@ -24,6 +24,7 @@ use function unserialize; use const DIRECTORY_SEPARATOR; +use const LOCK_EX; use const PHP_VERSION_ID; /** @@ -79,9 +80,9 @@ public function writeToCache(FileStorage $storage, string $file_contents): void $storage->hash = $this->getCacheHash($file_path, $file_contents); if ($this->config->use_igbinary) { - file_put_contents($cache_location, igbinary_serialize($storage)); + file_put_contents($cache_location, igbinary_serialize($storage), LOCK_EX); } else { - file_put_contents($cache_location, serialize($storage)); + file_put_contents($cache_location, serialize($storage), LOCK_EX); } } @@ -135,7 +136,7 @@ private function loadFromCache(string $file_path): ?FileStorage if (file_exists($cache_location)) { if ($this->config->use_igbinary) { - $storage = igbinary_unserialize((string)file_get_contents($cache_location)); + $storage = igbinary_unserialize(Providers::safeFileGetContents($cache_location)); if ($storage instanceof FileStorage) { return $storage; @@ -144,7 +145,7 @@ private function loadFromCache(string $file_path): ?FileStorage return null; } - $storage = unserialize((string)file_get_contents($cache_location)); + $storage = unserialize(Providers::safeFileGetContents($cache_location)); if ($storage instanceof FileStorage) { return $storage; diff --git a/src/Psalm/Internal/Provider/ParserCacheProvider.php b/src/Psalm/Internal/Provider/ParserCacheProvider.php index a8f053f09b8..c1c30c27b76 100644 --- a/src/Psalm/Internal/Provider/ParserCacheProvider.php +++ b/src/Psalm/Internal/Provider/ParserCacheProvider.php @@ -5,18 +5,13 @@ use PhpParser; use PhpParser\Node\Stmt; use Psalm\Config; +use Psalm\Internal\Provider\Providers; use RuntimeException; use function clearstatcache; use function error_log; -use function fclose; -use function file_get_contents; use function file_put_contents; use function filemtime; -use function filesize; -use function flock; -use function fopen; -use function fread; use function gettype; use function igbinary_serialize; use function igbinary_unserialize; @@ -33,11 +28,9 @@ use function touch; use function unlink; use function unserialize; -use function usleep; use const DIRECTORY_SEPARATOR; use const LOCK_EX; -use const LOCK_SH; use const SCANDIR_SORT_NONE; /** @@ -108,10 +101,10 @@ public function loadStatementsFromCache( ) { if ($this->use_igbinary) { /** @var list */ - $stmts = igbinary_unserialize((string)file_get_contents($cache_location)); + $stmts = igbinary_unserialize(Providers::safeFileGetContents($cache_location)); } else { /** @var list */ - $stmts = unserialize((string)file_get_contents($cache_location)); + $stmts = unserialize(Providers::safeFileGetContents($cache_location)); } return $stmts; @@ -142,11 +135,11 @@ public function loadExistingStatementsFromCache(string $file_path): ?array if (is_readable($cache_location)) { if ($this->use_igbinary) { /** @var list */ - return igbinary_unserialize((string)file_get_contents($cache_location)) ?: null; + return igbinary_unserialize(Providers::safeFileGetContents($cache_location)) ?: null; } /** @var list */ - return unserialize((string)file_get_contents($cache_location)) ?: null; + return unserialize(Providers::safeFileGetContents($cache_location)) ?: null; } return null; @@ -173,7 +166,7 @@ public function loadExistingFileContentsFromCache(string $file_path): ?string $cache_location = $parser_cache_directory . DIRECTORY_SEPARATOR . $file_cache_key; if (is_readable($cache_location)) { - return file_get_contents($cache_location); + return Providers::safeFileGetContents($cache_location); } return null; @@ -191,29 +184,7 @@ private function getExistingFileContentHashes(): array $file_hashes_path = $root_cache_directory . DIRECTORY_SEPARATOR . self::FILE_HASHES; if ($root_cache_directory && is_readable($file_hashes_path)) { - $fp = fopen($file_hashes_path, 'r'); - $max_wait_cycles = 5; - $has_lock = false; - while ($max_wait_cycles > 0) { - if (flock($fp, LOCK_SH)) { - $has_lock = true; - break; - } - $max_wait_cycles--; - usleep(50000); - } - - if (!$has_lock) { - fclose($fp); - error_log('Could not acquire lock for content hashes file'); - $this->existing_file_content_hashes = []; - - return []; - } - - $hashes_encoded = fread($fp, filesize($file_hashes_path)); - fclose($fp); - + $hashes_encoded = Providers::safeFileGetContents($file_hashes_path); if (!$hashes_encoded) { error_log('Unexpected value when loading from file content hashes'); $this->existing_file_content_hashes = []; @@ -269,9 +240,9 @@ public function saveStatementsToCache( $this->createCacheDirectory($parser_cache_directory); if ($this->use_igbinary) { - file_put_contents($cache_location, igbinary_serialize($stmts)); + file_put_contents($cache_location, igbinary_serialize($stmts), LOCK_EX); } else { - file_put_contents($cache_location, serialize($stmts)); + file_put_contents($cache_location, serialize($stmts), LOCK_EX); } $this->new_file_content_hashes[$file_cache_key] = $file_content_hash; @@ -343,7 +314,7 @@ public function cacheFileContents(string $file_path, string $file_contents): voi $this->createCacheDirectory($parser_cache_directory); - file_put_contents($cache_location, $file_contents); + file_put_contents($cache_location, $file_contents, LOCK_EX); } public function deleteOldParserCaches(float $time_before): int diff --git a/src/Psalm/Internal/Provider/Providers.php b/src/Psalm/Internal/Provider/Providers.php index d42e1004342..8d246b2d1f9 100644 --- a/src/Psalm/Internal/Provider/Providers.php +++ b/src/Psalm/Internal/Provider/Providers.php @@ -2,6 +2,17 @@ namespace Psalm\Internal\Provider; +use RuntimeException; + +use function fclose; +use function filesize; +use function flock; +use function fopen; +use function fread; +use function usleep; + +use const LOCK_SH; + /** * @internal */ @@ -63,4 +74,38 @@ public function __construct( ); $this->file_reference_provider = new FileReferenceProvider($file_reference_cache_provider); } + + public static function safeFileGetContents(string $path): string + { + // no readable validation as that must be done in the caller + $fp = fopen($path, 'r'); + if ($fp === false) { + return ''; + } + $max_wait_cycles = 5; + $has_lock = false; + while ($max_wait_cycles > 0) { + if (flock($fp, LOCK_SH)) { + $has_lock = true; + break; + } + $max_wait_cycles--; + usleep(50000); + } + + if (!$has_lock) { + fclose($fp); + throw new RuntimeException('Could not acquire lock for ' . $path); + } + + $file_size = filesize($path); + $content = ''; + if ( $file_size > 0 ) { + $content = (string) fread($fp, $file_size); + } + + fclose($fp); + + return $content; + } } From 8f6e16add645fa08070d1ec7d909a974a98715cb Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Thu, 11 Aug 2022 16:48:29 +0200 Subject: [PATCH 058/194] added truthy-string alias for non-falsy-string --- src/Psalm/Type/Atomic.php | 1 + tests/TypeCombinationTest.php | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 47d8e573ade..b0d456b65dd 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -210,6 +210,7 @@ public static function create( case 'non-empty-string': return new TNonEmptyString(); + case 'truthy-string': case 'non-falsy-string': return new TNonFalsyString(); diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index 1f3350f3bfb..60a857b6cfe 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -717,6 +717,13 @@ public function providerTestValidTypeCombination(): array 'non-empty-string' ] ], + 'combineTruthyStringAndNonEmptyString' => [ + 'non-empty-string', + [ + 'truthy-string', + 'non-empty-string' + ] + ], 'combineNonFalsyNonEmptyString' => [ 'non-empty-string', [ From 89086382197f0fd47fbfc58d85b9beb3cf63ecec Mon Sep 17 00:00:00 2001 From: Paul Fedorow Date: Fri, 12 Aug 2022 11:08:28 +0200 Subject: [PATCH 059/194] Fix `imageinterlace` function signature --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_80_delta.php | 2 +- dictionaries/CallMap_81_delta.php | 4 ++++ tests/Internal/Codebase/InternalCallMapHandlerTest.php | 1 - 4 files changed, 6 insertions(+), 3 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 329decd3234..143d7d9a189 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -5410,7 +5410,7 @@ 'imagegif' => ['bool', 'image'=>'GdImage', 'file='=>'string|resource|null'], 'imagegrabscreen' => ['false|GdImage'], 'imagegrabwindow' => ['false|GdImage', 'handle'=>'int', 'client_area='=>'int'], -'imageinterlace' => ['int|false', 'image'=>'GdImage', 'enable='=>'int'], +'imageinterlace' => ['bool', 'image'=>'GdImage', 'enable='=>'bool|null'], 'imageistruecolor' => ['bool', 'image'=>'GdImage'], 'imagejpeg' => ['bool', 'image'=>'GdImage', 'file='=>'string|resource|null', 'quality='=>'int'], 'imagelayereffect' => ['bool', 'image'=>'GdImage', 'effect'=>'int'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index 4200608e06a..39edf984020 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -647,7 +647,7 @@ ], 'imageinterlace' => [ 'old' => ['int|false', 'image'=>'resource', 'enable='=>'int'], - 'new' => ['int|false', 'image'=>'GdImage', 'enable='=>'int'], + 'new' => ['int|bool', 'image'=>'GdImage', 'enable='=>'bool|null'], ], 'imageistruecolor' => [ 'old' => ['bool', 'image'=>'resource'], diff --git a/dictionaries/CallMap_81_delta.php b/dictionaries/CallMap_81_delta.php index b6cae16f3d7..622c86614d0 100644 --- a/dictionaries/CallMap_81_delta.php +++ b/dictionaries/CallMap_81_delta.php @@ -233,6 +233,10 @@ 'old' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string'], 'new' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string', 'options='=>'array'], ], + 'imageinterlace' => [ + 'old' => ['int|bool', 'image'=>'GdImage', 'enable='=>'bool|null'], + 'new' => ['bool', 'image'=>'GdImage', 'enable='=>'bool|null'], + ], 'imap_append' => [ 'old' => ['bool', 'imap'=>'resource', 'folder'=>'string', 'message'=>'string', 'options='=>'string', 'internal_date='=>'string'], 'new' => ['bool', 'imap'=>'IMAP\Connection', 'folder'=>'string', 'message'=>'string', 'options='=>'string', 'internal_date='=>'string'], diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index 164fb4143d1..2747997492c 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -112,7 +112,6 @@ class InternalCallMapHandlerTest extends TestCase 'imagefilter', 'imagegd', 'imagegd2', - 'imageinterlace', 'imageopenpolygon', 'imagepolygon', 'imagerotate', From ffe4375a60be682bd4c59355a815bed5f1d5e089 Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 12 Aug 2022 16:03:56 +0300 Subject: [PATCH 060/194] Clarification of `Reflection::getModifierNames()` result type --- dictionaries/CallMap.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 329decd3234..42371c395a9 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -11292,7 +11292,7 @@ 'RedisCluster::zScore' => ['float', 'key'=>'string', 'member'=>'string'], 'RedisCluster::zUnionStore' => ['int', 'Output'=>'string', 'ZSetKeys'=>'array', 'Weights='=>'?array', 'aggregateFunction='=>'string'], 'Reflection::export' => ['?string', 'r'=>'reflector', 'return='=>'bool'], -'Reflection::getModifierNames' => ['array', 'modifiers'=>'int'], +'Reflection::getModifierNames' => ['list', 'modifiers'=>'int'], 'ReflectionClass::__clone' => ['void'], 'ReflectionClass::__construct' => ['void', 'argument'=>'object|class-string'], 'ReflectionClass::__toString' => ['string'], From 4498a523b904bc111a5d7ce3240979e484d3cc8f Mon Sep 17 00:00:00 2001 From: Sergei Predvoditelev Date: Fri, 12 Aug 2022 16:12:36 +0300 Subject: [PATCH 061/194] fix --- dictionaries/CallMap_historical.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 6df5d473acf..f2bb87d83fa 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -5894,7 +5894,7 @@ 'RedisCluster::zScore' => ['float', 'key'=>'string', 'member'=>'string'], 'RedisCluster::zUnionStore' => ['int', 'Output'=>'string', 'ZSetKeys'=>'array', 'Weights='=>'?array', 'aggregateFunction='=>'string'], 'Reflection::export' => ['?string', 'r'=>'reflector', 'return='=>'bool'], - 'Reflection::getModifierNames' => ['array', 'modifiers'=>'int'], + 'Reflection::getModifierNames' => ['list', 'modifiers'=>'int'], 'ReflectionClass::__clone' => ['void'], 'ReflectionClass::__construct' => ['void', 'argument'=>'object|class-string'], 'ReflectionClass::__toString' => ['string'], From 9c67b85f39d92454ec59099b14b535bd2bcd7cab Mon Sep 17 00:00:00 2001 From: Daniel Schmelz Date: Sat, 20 Aug 2022 23:29:03 +0200 Subject: [PATCH 062/194] Fix typos --- docs/annotating_code/supported_annotations.md | 2 +- docs/annotating_code/templated_annotations.md | 4 ++-- docs/annotating_code/type_syntax/scalar_types.md | 2 +- docs/contributing/editing_callmaps.md | 4 ++-- docs/contributing/how_psalm_works.md | 2 +- docs/contributing/philosophy.md | 2 +- docs/manipulating_code/refactoring.md | 2 +- docs/running_psalm/installation.md | 2 +- docs/running_psalm/issues/FalsableReturnStatement.md | 2 +- docs/running_psalm/issues/InvalidExtendClass.md | 2 +- docs/running_psalm/issues/PropertyTypeCoercion.md | 2 +- docs/running_psalm/issues/TaintedCallable.md | 2 +- docs/running_psalm/issues/TaintedLdap.md | 2 +- docs/running_psalm/issues/TaintedTextWithQuotes.md | 2 +- docs/running_psalm/plugins/authoring_plugins.md | 2 +- 15 files changed, 17 insertions(+), 17 deletions(-) diff --git a/docs/annotating_code/supported_annotations.md b/docs/annotating_code/supported_annotations.md index 0337e9e7d41..62d26541eb4 100644 --- a/docs/annotating_code/supported_annotations.md +++ b/docs/annotating_code/supported_annotations.md @@ -509,7 +509,7 @@ class User { ### `@psalm-require-extends` -The `@psalm-require-extends` annotation allows you to define a requirements that a trait imposes on the using class. +The `@psalm-require-extends` annotation allows you to define the requirements that a trait imposes on the using class. ```php ``` -Passing `');alert('injection');//` as a `GET` param here would would cause the `alert` to trigger. +Passing `');alert('injection');//` as a `GET` param here would cause the `alert` to trigger. ## Mitigations diff --git a/docs/running_psalm/plugins/authoring_plugins.md b/docs/running_psalm/plugins/authoring_plugins.md index 494313970f1..cb683af324e 100644 --- a/docs/running_psalm/plugins/authoring_plugins.md +++ b/docs/running_psalm/plugins/authoring_plugins.md @@ -80,7 +80,7 @@ class SomePlugin implements \Psalm\Plugin\EventHandler\AfterStatementAnalysisInt - `AfterFunctionCallAnalysisInterface` - called after Psalm evaluates a function call to any function defined within the project itself. Can alter the return type or perform modifications of the call. - `AfterFunctionLikeAnalysisInterface` - called after Psalm has completed its analysis of a given function-like. - `AfterMethodCallAnalysisInterface` - called after Psalm analyzes a method call. -- `AfterStatementAnalysisInterface` - called after Psalm evaluates an statement. +- `AfterStatementAnalysisInterface` - called after Psalm evaluates a statement. - `BeforeFileAnalysisInterface` - called before Psalm analyzes a file. - `FunctionExistenceProviderInterface` - can be used to override Psalm's builtin function existence checks for one or more functions. - `FunctionParamsProviderInterface.php` - can be used to override Psalm's builtin function parameter lookup for one or more functions. From 63915d1e2c88f96e0deaeec50720b72943d24507 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 22 Aug 2022 16:44:55 +0200 Subject: [PATCH 063/194] added SensitiveParameter, AllowDynamicProperties php 8.2 attributes --- stubs/CoreGenericClasses.phpstub | 12 ++++++++++++ tests/AttributeTest.php | 26 ++++++++++++++++++++++++++ 2 files changed, 38 insertions(+) diff --git a/stubs/CoreGenericClasses.phpstub b/stubs/CoreGenericClasses.phpstub index 0dae5ef34d2..168572e7de7 100644 --- a/stubs/CoreGenericClasses.phpstub +++ b/stubs/CoreGenericClasses.phpstub @@ -501,3 +501,15 @@ final class ReturnTypeWillChange { public function __construct() {} } + +#[Attribute] +final class SensitiveParameter +{ + public function __construct() {} +} + +#[Attribute] +final class AllowDynamicProperties +{ + public function __construct() {} +} diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index efffdbad4de..460bde45b13 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -249,6 +249,32 @@ public function getIterator() [], '8.1' ], + 'allowDynamicProperties' => [ + ' [ + ' [ ' Date: Mon, 22 Aug 2022 16:55:26 +0200 Subject: [PATCH 064/194] imports --- tests/AttributeTest.php | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index 460bde45b13..d8c851d0266 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -254,7 +254,9 @@ public function getIterator() namespace AllowDynamicPropertiesAttribute; - #[\AllowDynamicProperties] + use AllowDynamicProperties; + + #[AllowDynamicProperties] class Foo {}', [], @@ -266,9 +268,11 @@ class Foo namespace SensitiveParameter; + use SensitiveParameter; + class HelloWorld { public function __construct( - #[\SensitiveParameter] string $password + #[SensitiveParameter] string $password ) {} }', [], From 8407cacc023c6471c91703655c0f5c321e8b2c82 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 22 Aug 2022 16:55:32 +0200 Subject: [PATCH 065/194] improve CI error message --- src/Psalm/Internal/CliUtils.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/CliUtils.php b/src/Psalm/Internal/CliUtils.php index f8c88734001..abe54ff20db 100644 --- a/src/Psalm/Internal/CliUtils.php +++ b/src/Psalm/Internal/CliUtils.php @@ -636,7 +636,7 @@ public static function initPhpVersion(array $options, Config $config, ProjectAna if (isset($options['php-version'])) { if (!is_string($options['php-version'])) { - die('Expecting a version number in the format x.y' . PHP_EOL); + die('Expecting a version number in the format x.y, got'. get_debug_type($options['php-version']) . PHP_EOL); } $version = $options['php-version']; $source = 'cli'; From 198347fac7626b269bc45c3249911c661e933ed8 Mon Sep 17 00:00:00 2001 From: Markus Staab Date: Mon, 22 Aug 2022 16:58:23 +0200 Subject: [PATCH 066/194] fix test --- tests/AttributeTest.php | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index d8c851d0266..b77b9d6aa67 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -258,10 +258,8 @@ public function getIterator() #[AllowDynamicProperties] class Foo - {}', - [], - [], - '8.2' + {} + ', ], 'sensitiveParameter' => [ ' [ ' Date: Mon, 22 Aug 2022 16:59:27 +0200 Subject: [PATCH 067/194] Restore CliUtils.php --- src/Psalm/Internal/CliUtils.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/CliUtils.php b/src/Psalm/Internal/CliUtils.php index abe54ff20db..f8c88734001 100644 --- a/src/Psalm/Internal/CliUtils.php +++ b/src/Psalm/Internal/CliUtils.php @@ -636,7 +636,7 @@ public static function initPhpVersion(array $options, Config $config, ProjectAna if (isset($options['php-version'])) { if (!is_string($options['php-version'])) { - die('Expecting a version number in the format x.y, got'. get_debug_type($options['php-version']) . PHP_EOL); + die('Expecting a version number in the format x.y' . PHP_EOL); } $version = $options['php-version']; $source = 'cli'; From 93a293c673431478461144f39f881a0eadc6e419 Mon Sep 17 00:00:00 2001 From: Denis Kuznetsov Date: Tue, 23 Aug 2022 03:24:32 +0300 Subject: [PATCH 068/194] Allow any attribute for complex types in schema --- config.xsd | 50 ++++++++++++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 14 deletions(-) diff --git a/config.xsd b/config.xsd index e942fcbaaf3..c9a4cd88895 100644 --- a/config.xsd +++ b/config.xsd @@ -130,6 +130,7 @@ + @@ -138,24 +139,25 @@ - + - + - + + @@ -165,14 +167,14 @@ - + - + @@ -185,21 +187,21 @@ - + - + - + @@ -207,21 +209,21 @@ - + - + - + @@ -229,6 +231,7 @@ + @@ -237,10 +240,11 @@ + - + @@ -527,7 +531,7 @@ - + @@ -540,11 +544,13 @@ + + @@ -557,12 +563,14 @@ + + @@ -576,11 +584,13 @@ + + @@ -594,11 +604,13 @@ + + @@ -612,11 +624,13 @@ + + @@ -630,11 +644,13 @@ + + @@ -648,11 +664,13 @@ + + @@ -666,28 +684,32 @@ + + - + + + From 4b1adaafecc98f0233f36d2b7a0ab3db7be431fc Mon Sep 17 00:00:00 2001 From: Thomas Gerbet Date: Tue, 23 Aug 2022 14:01:44 +0200 Subject: [PATCH 069/194] Allow *bin2hex and *bin2base64 functions to keep non-empty-string type Those functions should not return a string when they receive a non-empty-string in input. The following example is expected to work: ```php return + * @template T as string + * @param T $string + * @return (T is non-empty-string ? non-empty-string : string) */ function base64_encode(string $string) : string {} +/** + * @psalm-pure + * + * @template T as string + * @param T $string + * @return (T is non-empty-string ? non-empty-string : string) + */ +function bin2hex(string $string): string {} + /** * @psalm-pure * From 6bc714c867b27f5138d3b8224a3524d03ed61818 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 24 Aug 2022 16:04:22 +0200 Subject: [PATCH 070/194] Add support for callable in array_reduce --- .../ArrayReduceReturnTypeProvider.php | 2 +- tests/ClosureTest.php | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php index a9d27ba8774..4632eaecefa 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php @@ -100,7 +100,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $initial_type = $reduce_return_type; - if ($closure_types = $function_call_arg_type->getClosureTypes()) { + if ($closure_types = $function_call_arg_type->getClosureTypes() ?: $function_call_arg_type->getCallableTypes()) { $closure_atomic_type = reset($closure_types); $closure_return_type = $closure_atomic_type->return_type ?: Type::getMixed(); diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index 53e5cc3f9b2..cbd3b3360c4 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -565,6 +565,22 @@ function maker(string $className) { '$result' => 'array{stdClass}' ], ], + 'CallableWithArrayReduce' => [ + ' [ + '$result' => 'int' + ], + ], 'FirstClassCallable:NamedFunction:is_int' => [ ' Date: Wed, 24 Aug 2022 21:31:02 +0200 Subject: [PATCH 071/194] Configure a correct attribute target in stubs/CoreGenericClasses.phpstub --- stubs/CoreGenericClasses.phpstub | 6 +++--- tests/AttributeTest.php | 16 ++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/stubs/CoreGenericClasses.phpstub b/stubs/CoreGenericClasses.phpstub index 168572e7de7..f988fcf26c6 100644 --- a/stubs/CoreGenericClasses.phpstub +++ b/stubs/CoreGenericClasses.phpstub @@ -496,19 +496,19 @@ final class WeakMap implements ArrayAccess, Countable, IteratorAggregate, Traver } -#[Attribute] +#[Attribute(Attribute::TARGET_METHOD)] final class ReturnTypeWillChange { public function __construct() {} } -#[Attribute] +#[Attribute(Attribute::TARGET_PARAMETER)] final class SensitiveParameter { public function __construct() {} } -#[Attribute] +#[Attribute(Attribute::TARGET_CLASS)] final class AllowDynamicProperties { public function __construct() {} diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index b77b9d6aa67..76d511fe853 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -787,6 +787,22 @@ public function anotherMethod(): string false, '8.1', ], + 'sensitiveParameterOnMethod' => [ + ' 'Attribute SensitiveParameter cannot be used on a method', + ], ]; } } From 6a6922d29ef32597e4faa8dac56c22b6efbb9efe Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Wed, 24 Aug 2022 14:19:57 +0200 Subject: [PATCH 072/194] Update call maps for MongoDB extension --- dictionaries/CallMap.php | 461 ++++++++++++++++------------ dictionaries/CallMap_historical.php | 461 ++++++++++++++++------------ 2 files changed, 524 insertions(+), 398 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 3590fe42c7f..1f6ca718260 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -7693,256 +7693,319 @@ 'MongoDB::setReadPreference' => ['bool', 'read_preference'=>'string', 'tags='=>'array'], 'MongoDB::setSlaveOkay' => ['bool', 'ok='=>'bool'], 'MongoDB::setWriteConcern' => ['bool', 'w'=>'mixed', 'wtimeout='=>'int'], -'MongoDB\BSON\Binary::__construct' => ['void', 'data'=>'string', 'type'=>'int'], -'MongoDB\BSON\Binary::__toString' => ['string'], +'MongoDB\BSON\Binary::__construct' => ['void', 'data' => 'string', 'type' => 'int'], 'MongoDB\BSON\Binary::getData' => ['string'], 'MongoDB\BSON\Binary::getType' => ['int'], -'MongoDB\BSON\binary::jsonSerialize' => ['mixed'], -'MongoDB\BSON\binary::serialize' => ['string'], -'MongoDB\BSON\binary::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\binaryinterface::__toString' => ['string'], -'MongoDB\BSON\binaryinterface::getData' => ['string'], -'MongoDB\BSON\binaryinterface::getType' => ['int'], -'MongoDB\BSON\dbpointer::__construct' => ['void'], -'MongoDB\BSON\dbpointer::__toString' => ['string'], -'MongoDB\BSON\dbpointer::jsonSerialize' => ['mixed'], -'MongoDB\BSON\dbpointer::serialize' => ['string'], -'MongoDB\BSON\dbpointer::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\Decimal128::__construct' => ['void', 'value='=>'string'], +'MongoDB\BSON\Binary::__toString' => ['string'], +'MongoDB\BSON\Binary::serialize' => ['string'], +'MongoDB\BSON\Binary::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Binary::jsonSerialize' => ['mixed'], +'MongoDB\BSON\BinaryInterface::getData' => ['void'], +'MongoDB\BSON\BinaryInterface::getType' => ['void'], +'MongoDB\BSON\BinaryInterface::__toString' => ['string'], +'MongoDB\BSON\DBPointer::__toString' => ['string'], +'MongoDB\BSON\DBPointer::serialize' => ['string'], +'MongoDB\BSON\DBPointer::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\DBPointer::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Decimal128::__construct' => ['void', 'value' => 'string'], 'MongoDB\BSON\Decimal128::__toString' => ['string'], -'MongoDB\BSON\decimal128::jsonSerialize' => ['mixed'], -'MongoDB\BSON\decimal128::serialize' => ['string'], -'MongoDB\BSON\decimal128::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\decimal128interface::__toString' => ['string'], -'MongoDB\BSON\fromJSON' => ['string', 'json'=>'string'], -'MongoDB\BSON\fromPHP' => ['string', 'value'=>'array|object'], -'MongoDB\BSON\int64::__construct' => ['void'], -'MongoDB\BSON\int64::__toString' => ['string'], -'MongoDB\BSON\int64::jsonSerialize' => ['mixed'], -'MongoDB\BSON\int64::serialize' => ['string'], -'MongoDB\BSON\int64::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\Javascript::__construct' => ['void', 'code'=>'string', 'scope='=>'array|object'], -'MongoDB\BSON\javascript::__toString' => ['string'], -'MongoDB\BSON\javascript::getCode' => ['string'], -'MongoDB\BSON\javascript::getScope' => ['?object'], -'MongoDB\BSON\javascript::jsonSerialize' => ['mixed'], -'MongoDB\BSON\javascript::serialize' => ['string'], -'MongoDB\BSON\javascript::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\javascriptinterface::__toString' => ['string'], -'MongoDB\BSON\javascriptinterface::getCode' => ['string'], -'MongoDB\BSON\javascriptinterface::getScope' => ['?object'], -'MongoDB\BSON\maxkey::__construct' => ['void'], -'MongoDB\BSON\maxkey::jsonSerialize' => ['mixed'], -'MongoDB\BSON\maxkey::serialize' => ['string'], -'MongoDB\BSON\maxkey::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\minkey::__construct' => ['void'], -'MongoDB\BSON\minkey::jsonSerialize' => ['mixed'], -'MongoDB\BSON\minkey::serialize' => ['string'], -'MongoDB\BSON\minkey::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\ObjectId::__construct' => ['void', 'id='=>'string'], +'MongoDB\BSON\Decimal128::serialize' => ['string'], +'MongoDB\BSON\Decimal128::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Decimal128Interface::__toString' => ['void'], +'MongoDB\BSON\Int64::__toString' => ['string'], +'MongoDB\BSON\Int64::serialize' => ['string'], +'MongoDB\BSON\Int64::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Int64::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Javascript::__construct' => ['void', 'code' => 'string', 'scope=' => 'object|array|null'], +'MongoDB\BSON\Javascript::getCode' => ['string'], +'MongoDB\BSON\Javascript::getScope' => ['?object'], +'MongoDB\BSON\Javascript::__toString' => ['string'], +'MongoDB\BSON\Javascript::serialize' => ['string'], +'MongoDB\BSON\Javascript::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Javascript::jsonSerialize' => ['mixed'], +'MongoDB\BSON\JavascriptInterface::getCode' => ['void'], +'MongoDB\BSON\JavascriptInterface::getScope' => ['void'], +'MongoDB\BSON\JavascriptInterface::__toString' => ['void'], +'MongoDB\BSON\MaxKey::serialize' => ['string'], +'MongoDB\BSON\MaxKey::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], +'MongoDB\BSON\MinKey::serialize' => ['string'], +'MongoDB\BSON\MinKey::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\MinKey::jsonSerialize' => ['mixed'], +'MongoDB\BSON\ObjectId::__construct' => ['void', 'id=' => '?string'], +'MongoDB\BSON\ObjectId::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectId::__toString' => ['string'], -'MongoDB\BSON\objectid::getTimestamp' => ['int'], -'MongoDB\BSON\objectid::jsonSerialize' => ['mixed'], -'MongoDB\BSON\objectid::serialize' => ['string'], -'MongoDB\BSON\objectid::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\objectidinterface::__toString' => ['string'], -'MongoDB\BSON\objectidinterface::getTimestamp' => ['int'], -'MongoDB\BSON\Regex::__construct' => ['void', 'pattern'=>'string', 'flags='=>'string'], -'MongoDB\BSON\Regex::__toString' => ['string'], -'MongoDB\BSON\Regex::getFlags' => ['string'], +'MongoDB\BSON\ObjectId::serialize' => ['string'], +'MongoDB\BSON\ObjectId::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], +'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['void'], +'MongoDB\BSON\ObjectIdInterface::__toString' => ['void'], +'MongoDB\BSON\Regex::__construct' => ['void', 'pattern' => 'string', 'flags=' => 'string'], 'MongoDB\BSON\Regex::getPattern' => ['string'], -'MongoDB\BSON\regex::jsonSerialize' => ['mixed'], -'MongoDB\BSON\regex::serialize' => ['string'], -'MongoDB\BSON\regex::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\regexinterface::__toString' => ['string'], -'MongoDB\BSON\regexinterface::getFlags' => ['string'], -'MongoDB\BSON\regexinterface::getPattern' => ['string'], -'MongoDB\BSON\Serializable::bsonSerialize' => ['array|object'], -'MongoDB\BSON\symbol::__construct' => ['void'], -'MongoDB\BSON\symbol::__toString' => ['string'], -'MongoDB\BSON\symbol::jsonSerialize' => ['mixed'], -'MongoDB\BSON\symbol::serialize' => ['string'], -'MongoDB\BSON\symbol::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment'=>'int', 'timestamp'=>'int'], +'MongoDB\BSON\Regex::getFlags' => ['string'], +'MongoDB\BSON\Regex::__toString' => ['string'], +'MongoDB\BSON\Regex::serialize' => ['string'], +'MongoDB\BSON\Regex::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Regex::jsonSerialize' => ['mixed'], +'MongoDB\BSON\RegexInterface::getPattern' => ['void'], +'MongoDB\BSON\RegexInterface::getFlags' => ['void'], +'MongoDB\BSON\RegexInterface::__toString' => ['void'], +'MongoDB\BSON\Serializable::bsonSerialize' => ['void'], +'MongoDB\BSON\Symbol::__toString' => ['string'], +'MongoDB\BSON\Symbol::serialize' => ['string'], +'MongoDB\BSON\Symbol::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Symbol::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment' => 'string|int', 'timestamp' => 'string|int'], +'MongoDB\BSON\Timestamp::getTimestamp' => ['int'], +'MongoDB\BSON\Timestamp::getIncrement' => ['int'], 'MongoDB\BSON\Timestamp::__toString' => ['string'], -'MongoDB\BSON\timestamp::getIncrement' => ['int'], -'MongoDB\BSON\timestamp::getTimestamp' => ['int'], -'MongoDB\BSON\timestamp::jsonSerialize' => ['mixed'], -'MongoDB\BSON\timestamp::serialize' => ['string'], -'MongoDB\BSON\timestamp::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\timestampinterface::__toString' => ['string'], -'MongoDB\BSON\timestampinterface::getIncrement' => ['int'], -'MongoDB\BSON\timestampinterface::getTimestamp' => ['int'], -'MongoDB\BSON\toJSON' => ['string', 'bson'=>'string'], -'MongoDB\BSON\toPHP' => ['object', 'bson'=>'string', 'typeMap='=>'array'], -'MongoDB\BSON\undefined::__construct' => ['void'], -'MongoDB\BSON\undefined::__toString' => ['string'], -'MongoDB\BSON\undefined::jsonSerialize' => ['mixed'], -'MongoDB\BSON\undefined::serialize' => ['string'], -'MongoDB\BSON\undefined::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data'=>'array'], -'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds='=>'int|DateTimeInterface'], -'MongoDB\BSON\UTCDateTime::__toString' => ['string'], -'MongoDB\BSON\utcdatetime::jsonSerialize' => ['mixed'], -'MongoDB\BSON\utcdatetime::serialize' => ['string'], +'MongoDB\BSON\Timestamp::serialize' => ['string'], +'MongoDB\BSON\Timestamp::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], +'MongoDB\BSON\TimestampInterface::getTimestamp' => ['void'], +'MongoDB\BSON\TimestampInterface::getIncrement' => ['void'], +'MongoDB\BSON\TimestampInterface::__toString' => ['void'], +'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds=' => 'DateTimeInterface|string|int|float|null'], 'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], -'MongoDB\BSON\utcdatetime::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\BSON\utcdatetimeinterface::__toString' => ['string'], -'MongoDB\BSON\utcdatetimeinterface::toDateTime' => ['DateTime'], -'MongoDB\Driver\BulkWrite::__construct' => ['void', 'ordered='=>'bool'], +'MongoDB\BSON\UTCDateTime::__toString' => ['string'], +'MongoDB\BSON\UTCDateTime::serialize' => ['string'], +'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\UTCDateTime::jsonSerialize' => ['mixed'], +'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['void'], +'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['void'], +'MongoDB\BSON\Undefined::__toString' => ['string'], +'MongoDB\BSON\Undefined::serialize' => ['string'], +'MongoDB\BSON\Undefined::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Undefined::jsonSerialize' => ['mixed'], +'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data' => 'array'], +'MongoDB\Driver\BulkWrite::__construct' => ['void', 'options=' => '?array'], 'MongoDB\Driver\BulkWrite::count' => ['int'], -'MongoDB\Driver\BulkWrite::delete' => ['void', 'filter'=>'array|object', 'deleteOptions='=>'array'], -'MongoDB\Driver\BulkWrite::insert' => ['void|MongoDB\BSON\ObjectId', 'document'=>'array|object'], -'MongoDB\Driver\BulkWrite::update' => ['void', 'filter'=>'array|object', 'newObj'=>'array|object', 'updateOptions='=>'array'], -'MongoDB\Driver\Command::__construct' => ['void', 'document'=>'array|object'], -'MongoDB\Driver\Cursor::__construct' => ['void', 'server'=>'Server', 'responseDocument'=>'string'], +'MongoDB\Driver\BulkWrite::delete' => ['void', 'filter' => 'object|array', 'deleteOptions=' => '?array'], +'MongoDB\Driver\BulkWrite::insert' => ['mixed', 'document' => 'object|array'], +'MongoDB\Driver\BulkWrite::update' => ['void', 'filter' => 'object|array', 'newObj' => 'object|array', 'updateOptions=' => '?array'], +'MongoDB\Driver\ClientEncryption::__construct' => ['void', 'options' => 'array'], +'MongoDB\Driver\ClientEncryption::addKeyAltName' => ['?object', 'keyId' => 'MongoDB\BSON\Binary', 'keyAltName' => 'string'], +'MongoDB\Driver\ClientEncryption::createDataKey' => ['MongoDB\BSON\Binary', 'kmsProvider' => 'string', 'options=' => '?array'], +'MongoDB\Driver\ClientEncryption::decrypt' => ['mixed', 'value' => 'MongoDB\BSON\Binary'], +'MongoDB\Driver\ClientEncryption::deleteKey' => ['object', 'keyId' => 'MongoDB\BSON\Binary'], +'MongoDB\Driver\ClientEncryption::encrypt' => ['MongoDB\BSON\Binary', 'value' => 'mixed', 'options=' => '?array'], +'MongoDB\Driver\ClientEncryption::getKey' => ['?object', 'keyId' => 'MongoDB\BSON\Binary'], +'MongoDB\Driver\ClientEncryption::getKeyByAltName' => ['?object', 'keyAltName' => 'string'], +'MongoDB\Driver\ClientEncryption::getKeys' => ['MongoDB\Driver\Cursor'], +'MongoDB\Driver\ClientEncryption::removeKeyAltName' => ['?object', 'keyId' => 'MongoDB\BSON\Binary', 'keyAltName' => 'string'], +'MongoDB\Driver\ClientEncryption::rewrapManyDataKey' => ['object', 'filter' => 'object|array', 'options=' => '?array'], +'MongoDB\Driver\Command::__construct' => ['void', 'document' => 'object|array', 'commandOptions=' => '?array'], +'MongoDB\Driver\Cursor::current' => ['object|array|null'], 'MongoDB\Driver\Cursor::getId' => ['MongoDB\Driver\CursorId'], 'MongoDB\Driver\Cursor::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\Cursor::isDead' => ['bool'], -'MongoDB\Driver\Cursor::setTypeMap' => ['void', 'typemap'=>'array'], +'MongoDB\Driver\Cursor::key' => ['?int'], +'MongoDB\Driver\Cursor::next' => ['void'], +'MongoDB\Driver\Cursor::rewind' => ['void'], +'MongoDB\Driver\Cursor::setTypeMap' => ['void', 'typemap' => 'array'], 'MongoDB\Driver\Cursor::toArray' => ['array'], -'MongoDB\Driver\CursorId::__construct' => ['void', 'id'=>'string'], +'MongoDB\Driver\Cursor::valid' => ['bool'], 'MongoDB\Driver\CursorId::__toString' => ['string'], 'MongoDB\Driver\CursorId::serialize' => ['string'], -'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized'=>'string'], -'mongodb\driver\exception\commandexception::getResultDocument' => ['object'], -'MongoDB\Driver\Exception\RuntimeException::__clone' => ['void'], -'MongoDB\Driver\Exception\RuntimeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?RuntimeException|?Throwable'], +'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized' => ''], +'MongoDB\Driver\CursorInterface::current' => ['object|array|null'], +'MongoDB\Driver\CursorInterface::getId' => ['void'], +'MongoDB\Driver\CursorInterface::getServer' => ['void'], +'MongoDB\Driver\CursorInterface::isDead' => ['void'], +'MongoDB\Driver\CursorInterface::key' => ['?int'], +'MongoDB\Driver\CursorInterface::next' => ['void'], +'MongoDB\Driver\CursorInterface::rewind' => ['void'], +'MongoDB\Driver\CursorInterface::setTypeMap' => ['void', 'typemap' => 'array'], +'MongoDB\Driver\CursorInterface::toArray' => ['void'], +'MongoDB\Driver\CursorInterface::valid' => ['bool'], +'MongoDB\Driver\Exception\AuthenticationException::__toString' => ['string'], +'MongoDB\Driver\Exception\BulkWriteException::__toString' => ['string'], +'MongoDB\Driver\Exception\CommandException::getResultDocument' => ['object'], +'MongoDB\Driver\Exception\CommandException::__toString' => ['string'], +'MongoDB\Driver\Exception\ConnectionException::__toString' => ['string'], +'MongoDB\Driver\Exception\ConnectionTimeoutException::__toString' => ['string'], +'MongoDB\Driver\Exception\EncryptionException::__toString' => ['string'], +'MongoDB\Driver\Exception\Exception::__toString' => ['string'], +'MongoDB\Driver\Exception\ExecutionTimeoutException::__toString' => ['string'], +'MongoDB\Driver\Exception\InvalidArgumentException::__toString' => ['string'], +'MongoDB\Driver\Exception\LogicException::__toString' => ['string'], +'MongoDB\Driver\Exception\RuntimeException::hasErrorLabel' => ['bool', 'errorLabel' => 'string'], 'MongoDB\Driver\Exception\RuntimeException::__toString' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::__wakeup' => ['void'], -'MongoDB\Driver\Exception\RuntimeException::getCode' => ['int'], -'MongoDB\Driver\Exception\RuntimeException::getFile' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::getLine' => ['int'], -'MongoDB\Driver\Exception\RuntimeException::getMessage' => ['string'], -'MongoDB\Driver\Exception\RuntimeException::getPrevious' => ['RuntimeException|Throwable'], -'MongoDB\Driver\Exception\RuntimeException::getTrace' => ['list\',args?:array}>'], -'MongoDB\Driver\Exception\RuntimeException::getTraceAsString' => ['string'], -'mongodb\driver\exception\runtimeexception::hasErrorLabel' => ['bool', 'errorLabel'=>'string'], -'MongoDB\Driver\Exception\WriteException::__clone' => ['void'], -'MongoDB\Driver\Exception\WriteException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?RuntimeException|?Throwable'], -'MongoDB\Driver\Exception\WriteException::__toString' => ['string'], -'MongoDB\Driver\Exception\WriteException::__wakeup' => ['void'], -'MongoDB\Driver\Exception\WriteException::getCode' => ['int'], -'MongoDB\Driver\Exception\WriteException::getFile' => ['string'], -'MongoDB\Driver\Exception\WriteException::getLine' => ['int'], -'MongoDB\Driver\Exception\WriteException::getMessage' => ['string'], -'MongoDB\Driver\Exception\WriteException::getPrevious' => ['RuntimeException|Throwable'], -'MongoDB\Driver\Exception\WriteException::getTrace' => ['list\',args?:array}>'], -'MongoDB\Driver\Exception\WriteException::getTraceAsString' => ['string'], +'MongoDB\Driver\Exception\SSLConnectionException::__toString' => ['string'], +'MongoDB\Driver\Exception\ServerException::__toString' => ['string'], +'MongoDB\Driver\Exception\UnexpectedValueException::__toString' => ['string'], 'MongoDB\Driver\Exception\WriteException::getWriteResult' => ['MongoDB\Driver\WriteResult'], -'MongoDB\Driver\Manager::__construct' => ['void', 'uri'=>'string', 'options='=>'array', 'driverOptions='=>'array'], -'MongoDB\Driver\Manager::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'bulk'=>'MongoDB\Driver\BulkWrite', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'readPreference='=>'MongoDB\Driver\ReadPreference'], -'MongoDB\Driver\Manager::executeDelete' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'filter'=>'array|object', 'deleteOptions='=>'array', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::executeInsert' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'document'=>'array|object', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'query'=>'MongoDB\Driver\Query', 'readPreference='=>'MongoDB\Driver\ReadPreference'], -'mongodb\driver\manager::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], -'mongodb\driver\manager::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], -'MongoDB\Driver\Manager::executeUpdate' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'filter'=>'array|object', 'newObj'=>'array|object', 'updateOptions='=>'array', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], -'mongodb\driver\manager::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], +'MongoDB\Driver\Exception\WriteException::__toString' => ['string'], +'MongoDB\Driver\Manager::__construct' => ['void', 'uri=' => '?string', 'uriOptions=' => '?array', 'driverOptions=' => '?array'], +'MongoDB\Driver\Manager::addSubscriber' => ['void', 'subscriber' => 'MongoDB\Driver\Monitoring\Subscriber'], +'MongoDB\Driver\Manager::createClientEncryption' => ['MongoDB\Driver\ClientEncryption', 'options' => 'array'], +'MongoDB\Driver\Manager::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace' => 'string', 'bulk' => 'MongoDB\Driver\BulkWrite', 'options=' => 'MongoDB\Driver\WriteConcern|array|null'], +'MongoDB\Driver\Manager::executeCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Manager::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace' => 'string', 'query' => 'MongoDB\Driver\Query', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Manager::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], +'MongoDB\Driver\Manager::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], +'MongoDB\Driver\Manager::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], +'MongoDB\Driver\Manager::getEncryptedFieldsMap' => ['object|array|null'], 'MongoDB\Driver\Manager::getReadConcern' => ['MongoDB\Driver\ReadConcern'], 'MongoDB\Driver\Manager::getReadPreference' => ['MongoDB\Driver\ReadPreference'], -'MongoDB\Driver\Manager::getServers' => ['MongoDB\Driver\Server[]'], +'MongoDB\Driver\Manager::getServers' => ['array'], 'MongoDB\Driver\Manager::getWriteConcern' => ['MongoDB\Driver\WriteConcern'], -'MongoDB\Driver\Manager::selectServer' => ['MongoDB\Driver\Server', 'readPreference'=>'MongoDB\Driver\ReadPreference'], -'mongodb\driver\manager::startSession' => ['MongoDB\Driver\Session', 'options='=>'array'], -'mongodb\driver\monitoring\commandfailedevent::getCommandName' => ['string'], -'mongodb\driver\monitoring\commandfailedevent::getDurationMicros' => ['int'], -'mongodb\driver\monitoring\commandfailedevent::getError' => ['Exception'], -'mongodb\driver\monitoring\commandfailedevent::getOperationId' => ['string'], -'mongodb\driver\monitoring\commandfailedevent::getReply' => ['object'], -'mongodb\driver\monitoring\commandfailedevent::getRequestId' => ['string'], -'mongodb\driver\monitoring\commandfailedevent::getServer' => ['MongoDB\Driver\Server'], -'mongodb\driver\monitoring\commandstartedevent::getCommand' => ['object'], -'mongodb\driver\monitoring\commandstartedevent::getCommandName' => ['string'], -'mongodb\driver\monitoring\commandstartedevent::getDatabaseName' => ['string'], -'mongodb\driver\monitoring\commandstartedevent::getOperationId' => ['string'], -'mongodb\driver\monitoring\commandstartedevent::getRequestId' => ['string'], -'mongodb\driver\monitoring\commandstartedevent::getServer' => ['MongoDB\Driver\Server'], -'mongodb\driver\monitoring\commandsubscriber::commandFailed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandFailedEvent'], -'mongodb\driver\monitoring\commandsubscriber::commandStarted' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandStartedEvent'], -'mongodb\driver\monitoring\commandsubscriber::commandSucceeded' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandSucceededEvent'], -'mongodb\driver\monitoring\commandsucceededevent::getCommandName' => ['string'], -'mongodb\driver\monitoring\commandsucceededevent::getDurationMicros' => ['int'], -'mongodb\driver\monitoring\commandsucceededevent::getOperationId' => ['string'], -'mongodb\driver\monitoring\commandsucceededevent::getReply' => ['object'], -'mongodb\driver\monitoring\commandsucceededevent::getRequestId' => ['string'], -'mongodb\driver\monitoring\commandsucceededevent::getServer' => ['MongoDB\Driver\Server'], -'MongoDB\Driver\Query::__construct' => ['void', 'filter'=>'array|object', 'queryOptions='=>'array'], -'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level='=>'string'], -'MongoDB\Driver\ReadConcern::bsonSerialize' => ['object'], +'MongoDB\Driver\Manager::removeSubscriber' => ['void', 'subscriber' => 'MongoDB\Driver\Monitoring\Subscriber'], +'MongoDB\Driver\Manager::selectServer' => ['MongoDB\Driver\Server', 'readPreference=' => '?MongoDB\Driver\ReadPreference'], +'MongoDB\Driver\Manager::startSession' => ['MongoDB\Driver\Session', 'options=' => '?array'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getCommandName' => ['string'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getError' => ['Exception'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getOperationId' => ['string'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getReply' => ['object'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getRequestId' => ['string'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\CommandFailedEvent::getServerConnectionId' => ['?int'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getCommand' => ['object'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getCommandName' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getDatabaseName' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getOperationId' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getRequestId' => ['string'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\CommandStartedEvent::getServerConnectionId' => ['?int'], +'MongoDB\Driver\Monitoring\CommandSubscriber::commandStarted' => ['void', 'event' => 'MongoDB\Driver\Monitoring\CommandStartedEvent'], +'MongoDB\Driver\Monitoring\CommandSubscriber::commandSucceeded' => ['void', 'event' => 'MongoDB\Driver\Monitoring\CommandSucceededEvent'], +'MongoDB\Driver\Monitoring\CommandSubscriber::commandFailed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\CommandFailedEvent'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getCommandName' => ['string'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getOperationId' => ['string'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getReply' => ['object'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getRequestId' => ['string'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServerConnectionId' => ['?int'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverChanged' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerChangedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverClosed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerClosedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverOpening' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerOpeningEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatFailed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatStarted' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatSucceeded' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyChanged' => ['void', 'event' => 'MongoDB\Driver\Monitoring\TopologyChangedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyClosed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\TopologyClosedEvent'], +'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyOpening' => ['void', 'event' => 'MongoDB\Driver\Monitoring\TopologyOpeningEvent'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getNewDescription' => ['MongoDB\Driver\ServerDescription'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getPreviousDescription' => ['MongoDB\Driver\ServerDescription'], +'MongoDB\Driver\Monitoring\ServerChangedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\ServerClosedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerClosedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerClosedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getError' => ['Exception'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::isAwaited' => ['bool'], +'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::isAwaited' => ['bool'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getDurationMicros' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getReply' => ['object'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::isAwaited' => ['bool'], +'MongoDB\Driver\Monitoring\ServerOpeningEvent::getPort' => ['int'], +'MongoDB\Driver\Monitoring\ServerOpeningEvent::getHost' => ['string'], +'MongoDB\Driver\Monitoring\ServerOpeningEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\TopologyChangedEvent::getNewDescription' => ['MongoDB\Driver\TopologyDescription'], +'MongoDB\Driver\Monitoring\TopologyChangedEvent::getPreviousDescription' => ['MongoDB\Driver\TopologyDescription'], +'MongoDB\Driver\Monitoring\TopologyChangedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\TopologyClosedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Monitoring\TopologyOpeningEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], +'MongoDB\Driver\Query::__construct' => ['void', 'filter' => 'object|array', 'queryOptions=' => '?array'], +'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level=' => '?string'], 'MongoDB\Driver\ReadConcern::getLevel' => ['?string'], 'MongoDB\Driver\ReadConcern::isDefault' => ['bool'], +'MongoDB\Driver\ReadConcern::bsonSerialize' => ['object|array'], 'MongoDB\Driver\ReadConcern::serialize' => ['string'], -'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode'=>'string|int', 'tagSets='=>'array', 'options='=>'array'], -'MongoDB\Driver\ReadPreference::bsonSerialize' => ['object'], -'MongoDB\Driver\ReadPreference::getHedge' => ['object|null'], +'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized' => ''], +'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode' => 'string|int', 'tagSets=' => '?array', 'options=' => '?array'], +'MongoDB\Driver\ReadPreference::getHedge' => ['?object'], 'MongoDB\Driver\ReadPreference::getMaxStalenessSeconds' => ['int'], 'MongoDB\Driver\ReadPreference::getMode' => ['int'], 'MongoDB\Driver\ReadPreference::getModeString' => ['string'], 'MongoDB\Driver\ReadPreference::getTagSets' => ['array'], +'MongoDB\Driver\ReadPreference::bsonSerialize' => ['object|array'], 'MongoDB\Driver\ReadPreference::serialize' => ['string'], -'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized'=>'string'], -'MongoDB\Driver\Server::__construct' => ['void', 'host'=>'string', 'port'=>'string', 'options='=>'array', 'driverOptions='=>'array'], -'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'zwrite'=>'BulkWrite'], -'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command'], -'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'zquery'=>'Query'], -'mongodb\driver\server::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], -'mongodb\driver\server::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], -'mongodb\driver\server::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], +'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized' => ''], +'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace' => 'string', 'bulkWrite' => 'MongoDB\Driver\BulkWrite', 'options=' => 'MongoDB\Driver\WriteConcern|array|null'], +'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace' => 'string', 'query' => 'MongoDB\Driver\Query', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], +'MongoDB\Driver\Server::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], +'MongoDB\Driver\Server::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], +'MongoDB\Driver\Server::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], 'MongoDB\Driver\Server::getHost' => ['string'], 'MongoDB\Driver\Server::getInfo' => ['array'], -'MongoDB\Driver\Server::getLatency' => ['int'], +'MongoDB\Driver\Server::getLatency' => ['?int'], 'MongoDB\Driver\Server::getPort' => ['int'], -'MongoDB\Driver\Server::getState' => [''], +'MongoDB\Driver\Server::getServerDescription' => ['MongoDB\Driver\ServerDescription'], 'MongoDB\Driver\Server::getTags' => ['array'], 'MongoDB\Driver\Server::getType' => ['int'], 'MongoDB\Driver\Server::isArbiter' => ['bool'], -'MongoDB\Driver\Server::isDelayed' => [''], 'MongoDB\Driver\Server::isHidden' => ['bool'], 'MongoDB\Driver\Server::isPassive' => ['bool'], 'MongoDB\Driver\Server::isPrimary' => ['bool'], 'MongoDB\Driver\Server::isSecondary' => ['bool'], -'mongodb\driver\session::__construct' => ['void'], -'mongodb\driver\session::abortTransaction' => ['void'], -'mongodb\driver\session::advanceClusterTime' => ['void', 'clusterTime'=>'array|object'], -'mongodb\driver\session::advanceOperationTime' => ['void', 'operationTime'=>'MongoDB\BSON\TimestampInterface'], -'mongodb\driver\session::commitTransaction' => ['void'], -'mongodb\driver\session::endSession' => ['void'], -'mongodb\driver\session::getClusterTime' => ['?object'], -'mongodb\driver\session::getLogicalSessionId' => ['object'], -'mongodb\driver\session::getOperationTime' => ['MongoDB\BSON\Timestamp|null'], -'mongodb\driver\session::getTransactionOptions' => ['array|null'], -'mongodb\driver\session::getTransactionState' => ['string'], -'mongodb\driver\session::startTransaction' => ['void', 'options'=>'array|object'], -'MongoDB\Driver\WriteConcern::__construct' => ['void', 'wstring'=>'string|int', 'wtimeout='=>'int', 'journal='=>'bool'], -'MongoDB\Driver\WriteConcern::bsonSerialize' => ['object'], +'MongoDB\Driver\ServerApi::__construct' => ['void', 'version' => 'string', 'strict=' => '?bool', 'deprecationErrors=' => '?bool'], +'MongoDB\Driver\ServerApi::bsonSerialize' => ['object|array'], +'MongoDB\Driver\ServerApi::serialize' => ['string'], +'MongoDB\Driver\ServerApi::unserialize' => ['void', 'serialized' => ''], +'MongoDB\Driver\ServerDescription::getHelloResponse' => ['array'], +'MongoDB\Driver\ServerDescription::getHost' => ['string'], +'MongoDB\Driver\ServerDescription::getLastUpdateTime' => ['int'], +'MongoDB\Driver\ServerDescription::getPort' => ['int'], +'MongoDB\Driver\ServerDescription::getRoundTripTime' => ['?int'], +'MongoDB\Driver\ServerDescription::getType' => ['string'], +'MongoDB\Driver\Session::abortTransaction' => ['void'], +'MongoDB\Driver\Session::advanceClusterTime' => ['void', 'clusterTime' => 'object|array'], +'MongoDB\Driver\Session::advanceOperationTime' => ['void', 'operationTime' => 'MongoDB\BSON\TimestampInterface'], +'MongoDB\Driver\Session::commitTransaction' => ['void'], +'MongoDB\Driver\Session::endSession' => ['void'], +'MongoDB\Driver\Session::getClusterTime' => ['?object'], +'MongoDB\Driver\Session::getLogicalSessionId' => ['object'], +'MongoDB\Driver\Session::getOperationTime' => ['?MongoDB\BSON\Timestamp'], +'MongoDB\Driver\Session::getServer' => ['?MongoDB\Driver\Server'], +'MongoDB\Driver\Session::getTransactionOptions' => ['?array'], +'MongoDB\Driver\Session::getTransactionState' => ['string'], +'MongoDB\Driver\Session::isDirty' => ['bool'], +'MongoDB\Driver\Session::isInTransaction' => ['bool'], +'MongoDB\Driver\Session::startTransaction' => ['void', 'options=' => '?array'], +'MongoDB\Driver\TopologyDescription::getServers' => ['array'], +'MongoDB\Driver\TopologyDescription::getType' => ['string'], +'MongoDB\Driver\TopologyDescription::hasReadableServer' => ['bool', 'readPreference=' => '?MongoDB\Driver\ReadPreference'], +'MongoDB\Driver\TopologyDescription::hasWritableServer' => ['bool'], +'MongoDB\Driver\WriteConcern::__construct' => ['void', 'w' => 'string|int', 'wtimeout=' => '?int', 'journal=' => '?bool'], 'MongoDB\Driver\WriteConcern::getJournal' => ['?bool'], -'MongoDB\Driver\WriteConcern::getJurnal' => ['?bool'], -'MongoDB\Driver\WriteConcern::getW' => ['int|null|string'], +'MongoDB\Driver\WriteConcern::getW' => ['string|int|null'], 'MongoDB\Driver\WriteConcern::getWtimeout' => ['int'], 'MongoDB\Driver\WriteConcern::isDefault' => ['bool'], +'MongoDB\Driver\WriteConcern::bsonSerialize' => ['object|array'], 'MongoDB\Driver\WriteConcern::serialize' => ['string'], -'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized'=>'string'], +'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized' => ''], 'MongoDB\Driver\WriteConcernError::getCode' => ['int'], -'MongoDB\Driver\WriteConcernError::getInfo' => ['mixed'], +'MongoDB\Driver\WriteConcernError::getInfo' => ['?object'], 'MongoDB\Driver\WriteConcernError::getMessage' => ['string'], 'MongoDB\Driver\WriteError::getCode' => ['int'], 'MongoDB\Driver\WriteError::getIndex' => ['int'], -'MongoDB\Driver\WriteError::getInfo' => ['mixed'], +'MongoDB\Driver\WriteError::getInfo' => ['?object'], 'MongoDB\Driver\WriteError::getMessage' => ['string'], -'MongoDB\Driver\WriteException::getWriteResult' => [''], -'MongoDB\Driver\WriteResult::getDeletedCount' => ['?int'], -'MongoDB\Driver\WriteResult::getInfo' => [''], 'MongoDB\Driver\WriteResult::getInsertedCount' => ['?int'], 'MongoDB\Driver\WriteResult::getMatchedCount' => ['?int'], 'MongoDB\Driver\WriteResult::getModifiedCount' => ['?int'], -'MongoDB\Driver\WriteResult::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\WriteResult::getDeletedCount' => ['?int'], 'MongoDB\Driver\WriteResult::getUpsertedCount' => ['?int'], +'MongoDB\Driver\WriteResult::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\WriteResult::getUpsertedIds' => ['array'], -'MongoDB\Driver\WriteResult::getWriteConcernError' => ['MongoDB\Driver\WriteConcernError|null'], -'MongoDB\Driver\WriteResult::getWriteErrors' => ['MongoDB\Driver\WriteError[]'], +'MongoDB\Driver\WriteResult::getWriteConcernError' => ['?MongoDB\Driver\WriteConcernError'], +'MongoDB\Driver\WriteResult::getWriteErrors' => ['array'], 'MongoDB\Driver\WriteResult::isAcknowledged' => ['bool'], 'MongoDBRef::create' => ['array', 'collection'=>'string', 'id'=>'mixed', 'database='=>'string'], 'MongoDBRef::get' => ['?array', 'db'=>'MongoDB', 'ref'=>'array'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index f2bb87d83fa..f2bf1d49284 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -3965,213 +3965,319 @@ 'MongoDBRef::create' => ['array', 'collection'=>'string', 'id'=>'mixed', 'database='=>'string'], 'MongoDBRef::get' => ['?array', 'db'=>'MongoDB', 'ref'=>'array'], 'MongoDBRef::isRef' => ['bool', 'ref'=>'mixed'], - 'MongoDB\BSON\Binary::__construct' => ['void', 'data'=>'string', 'type'=>'int'], - 'MongoDB\BSON\Binary::__toString' => ['string'], + 'MongoDB\BSON\Binary::__construct' => ['void', 'data' => 'string', 'type' => 'int'], 'MongoDB\BSON\Binary::getData' => ['string'], 'MongoDB\BSON\Binary::getType' => ['int'], - 'MongoDB\BSON\Decimal128::__construct' => ['void', 'value='=>'string'], + 'MongoDB\BSON\Binary::__toString' => ['string'], + 'MongoDB\BSON\Binary::serialize' => ['string'], + 'MongoDB\BSON\Binary::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Binary::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\BinaryInterface::getData' => ['void'], + 'MongoDB\BSON\BinaryInterface::getType' => ['void'], + 'MongoDB\BSON\BinaryInterface::__toString' => ['string'], + 'MongoDB\BSON\DBPointer::__toString' => ['string'], + 'MongoDB\BSON\DBPointer::serialize' => ['string'], + 'MongoDB\BSON\DBPointer::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\DBPointer::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\Decimal128::__construct' => ['void', 'value' => 'string'], 'MongoDB\BSON\Decimal128::__toString' => ['string'], - 'MongoDB\BSON\Javascript::__construct' => ['void', 'code'=>'string', 'scope='=>'array|object'], - 'MongoDB\BSON\ObjectId::__construct' => ['void', 'id='=>'string'], + 'MongoDB\BSON\Decimal128::serialize' => ['string'], + 'MongoDB\BSON\Decimal128::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\Decimal128Interface::__toString' => ['void'], + 'MongoDB\BSON\Int64::__toString' => ['string'], + 'MongoDB\BSON\Int64::serialize' => ['string'], + 'MongoDB\BSON\Int64::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Int64::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\Javascript::__construct' => ['void', 'code' => 'string', 'scope=' => 'object|array|null'], + 'MongoDB\BSON\Javascript::getCode' => ['string'], + 'MongoDB\BSON\Javascript::getScope' => ['?object'], + 'MongoDB\BSON\Javascript::__toString' => ['string'], + 'MongoDB\BSON\Javascript::serialize' => ['string'], + 'MongoDB\BSON\Javascript::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Javascript::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\JavascriptInterface::getCode' => ['void'], + 'MongoDB\BSON\JavascriptInterface::getScope' => ['void'], + 'MongoDB\BSON\JavascriptInterface::__toString' => ['void'], + 'MongoDB\BSON\MaxKey::serialize' => ['string'], + 'MongoDB\BSON\MaxKey::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\MinKey::serialize' => ['string'], + 'MongoDB\BSON\MinKey::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\MinKey::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\ObjectId::__construct' => ['void', 'id=' => '?string'], + 'MongoDB\BSON\ObjectId::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectId::__toString' => ['string'], - 'MongoDB\BSON\Regex::__construct' => ['void', 'pattern'=>'string', 'flags='=>'string'], - 'MongoDB\BSON\Regex::__toString' => ['string'], - 'MongoDB\BSON\Regex::getFlags' => ['string'], + 'MongoDB\BSON\ObjectId::serialize' => ['string'], + 'MongoDB\BSON\ObjectId::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['void'], + 'MongoDB\BSON\ObjectIdInterface::__toString' => ['void'], + 'MongoDB\BSON\Regex::__construct' => ['void', 'pattern' => 'string', 'flags=' => 'string'], 'MongoDB\BSON\Regex::getPattern' => ['string'], - 'MongoDB\BSON\Serializable::bsonSerialize' => ['array|object'], - 'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment'=>'int', 'timestamp'=>'int'], + 'MongoDB\BSON\Regex::getFlags' => ['string'], + 'MongoDB\BSON\Regex::__toString' => ['string'], + 'MongoDB\BSON\Regex::serialize' => ['string'], + 'MongoDB\BSON\Regex::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Regex::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\RegexInterface::getPattern' => ['void'], + 'MongoDB\BSON\RegexInterface::getFlags' => ['void'], + 'MongoDB\BSON\RegexInterface::__toString' => ['void'], + 'MongoDB\BSON\Serializable::bsonSerialize' => ['void'], + 'MongoDB\BSON\Symbol::__toString' => ['string'], + 'MongoDB\BSON\Symbol::serialize' => ['string'], + 'MongoDB\BSON\Symbol::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Symbol::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment' => 'string|int', 'timestamp' => 'string|int'], + 'MongoDB\BSON\Timestamp::getTimestamp' => ['int'], + 'MongoDB\BSON\Timestamp::getIncrement' => ['int'], 'MongoDB\BSON\Timestamp::__toString' => ['string'], - 'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds='=>'int|DateTimeInterface'], - 'MongoDB\BSON\UTCDateTime::__toString' => ['string'], + 'MongoDB\BSON\Timestamp::serialize' => ['string'], + 'MongoDB\BSON\Timestamp::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\TimestampInterface::getTimestamp' => ['void'], + 'MongoDB\BSON\TimestampInterface::getIncrement' => ['void'], + 'MongoDB\BSON\TimestampInterface::__toString' => ['void'], + 'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds=' => 'DateTimeInterface|string|int|float|null'], 'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], - 'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data'=>'array'], - 'MongoDB\BSON\binary::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\binary::serialize' => ['string'], - 'MongoDB\BSON\binary::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\binaryinterface::__toString' => ['string'], - 'MongoDB\BSON\binaryinterface::getData' => ['string'], - 'MongoDB\BSON\binaryinterface::getType' => ['int'], - 'MongoDB\BSON\dbpointer::__construct' => ['void'], - 'MongoDB\BSON\dbpointer::__toString' => ['string'], - 'MongoDB\BSON\dbpointer::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\dbpointer::serialize' => ['string'], - 'MongoDB\BSON\dbpointer::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\decimal128::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\decimal128::serialize' => ['string'], - 'MongoDB\BSON\decimal128::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\decimal128interface::__toString' => ['string'], - 'MongoDB\BSON\fromJSON' => ['string', 'json'=>'string'], - 'MongoDB\BSON\fromPHP' => ['string', 'value'=>'array|object'], - 'MongoDB\BSON\int64::__construct' => ['void'], - 'MongoDB\BSON\int64::__toString' => ['string'], - 'MongoDB\BSON\int64::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\int64::serialize' => ['string'], - 'MongoDB\BSON\int64::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\javascript::__toString' => ['string'], - 'MongoDB\BSON\javascript::getCode' => ['string'], - 'MongoDB\BSON\javascript::getScope' => ['?object'], - 'MongoDB\BSON\javascript::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\javascript::serialize' => ['string'], - 'MongoDB\BSON\javascript::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\javascriptinterface::__toString' => ['string'], - 'MongoDB\BSON\javascriptinterface::getCode' => ['string'], - 'MongoDB\BSON\javascriptinterface::getScope' => ['?object'], - 'MongoDB\BSON\maxkey::__construct' => ['void'], - 'MongoDB\BSON\maxkey::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\maxkey::serialize' => ['string'], - 'MongoDB\BSON\maxkey::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\minkey::__construct' => ['void'], - 'MongoDB\BSON\minkey::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\minkey::serialize' => ['string'], - 'MongoDB\BSON\minkey::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\objectid::getTimestamp' => ['int'], - 'MongoDB\BSON\objectid::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\objectid::serialize' => ['string'], - 'MongoDB\BSON\objectid::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\objectidinterface::__toString' => ['string'], - 'MongoDB\BSON\objectidinterface::getTimestamp' => ['int'], - 'MongoDB\BSON\regex::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\regex::serialize' => ['string'], - 'MongoDB\BSON\regex::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\regexinterface::__toString' => ['string'], - 'MongoDB\BSON\regexinterface::getFlags' => ['string'], - 'MongoDB\BSON\regexinterface::getPattern' => ['string'], - 'MongoDB\BSON\symbol::__construct' => ['void'], - 'MongoDB\BSON\symbol::__toString' => ['string'], - 'MongoDB\BSON\symbol::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\symbol::serialize' => ['string'], - 'MongoDB\BSON\symbol::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\timestamp::getIncrement' => ['int'], - 'MongoDB\BSON\timestamp::getTimestamp' => ['int'], - 'MongoDB\BSON\timestamp::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\timestamp::serialize' => ['string'], - 'MongoDB\BSON\timestamp::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\timestampinterface::__toString' => ['string'], - 'MongoDB\BSON\timestampinterface::getIncrement' => ['int'], - 'MongoDB\BSON\timestampinterface::getTimestamp' => ['int'], - 'MongoDB\BSON\toJSON' => ['string', 'bson'=>'string'], - 'MongoDB\BSON\toPHP' => ['object', 'bson'=>'string', 'typeMap='=>'array'], - 'MongoDB\BSON\undefined::__construct' => ['void'], - 'MongoDB\BSON\undefined::__toString' => ['string'], - 'MongoDB\BSON\undefined::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\undefined::serialize' => ['string'], - 'MongoDB\BSON\undefined::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\utcdatetime::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\utcdatetime::serialize' => ['string'], - 'MongoDB\BSON\utcdatetime::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\BSON\utcdatetimeinterface::__toString' => ['string'], - 'MongoDB\BSON\utcdatetimeinterface::toDateTime' => ['DateTime'], - 'MongoDB\Driver\BulkWrite::__construct' => ['void', 'ordered='=>'bool'], + 'MongoDB\BSON\UTCDateTime::__toString' => ['string'], + 'MongoDB\BSON\UTCDateTime::serialize' => ['string'], + 'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\UTCDateTime::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['void'], + 'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['void'], + 'MongoDB\BSON\Undefined::__toString' => ['string'], + 'MongoDB\BSON\Undefined::serialize' => ['string'], + 'MongoDB\BSON\Undefined::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Undefined::jsonSerialize' => ['mixed'], + 'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data' => 'array'], + 'MongoDB\Driver\BulkWrite::__construct' => ['void', 'options=' => '?array'], 'MongoDB\Driver\BulkWrite::count' => ['int'], - 'MongoDB\Driver\BulkWrite::delete' => ['void', 'filter'=>'array|object', 'deleteOptions='=>'array'], - 'MongoDB\Driver\BulkWrite::insert' => ['void|MongoDB\BSON\ObjectId', 'document'=>'array|object'], - 'MongoDB\Driver\BulkWrite::update' => ['void', 'filter'=>'array|object', 'newObj'=>'array|object', 'updateOptions='=>'array'], - 'MongoDB\Driver\Command::__construct' => ['void', 'document'=>'array|object'], - 'MongoDB\Driver\Cursor::__construct' => ['void', 'server'=>'Server', 'responseDocument'=>'string'], + 'MongoDB\Driver\BulkWrite::delete' => ['void', 'filter' => 'object|array', 'deleteOptions=' => '?array'], + 'MongoDB\Driver\BulkWrite::insert' => ['mixed', 'document' => 'object|array'], + 'MongoDB\Driver\BulkWrite::update' => ['void', 'filter' => 'object|array', 'newObj' => 'object|array', 'updateOptions=' => '?array'], + 'MongoDB\Driver\ClientEncryption::__construct' => ['void', 'options' => 'array'], + 'MongoDB\Driver\ClientEncryption::addKeyAltName' => ['?object', 'keyId' => 'MongoDB\BSON\Binary', 'keyAltName' => 'string'], + 'MongoDB\Driver\ClientEncryption::createDataKey' => ['MongoDB\BSON\Binary', 'kmsProvider' => 'string', 'options=' => '?array'], + 'MongoDB\Driver\ClientEncryption::decrypt' => ['mixed', 'value' => 'MongoDB\BSON\Binary'], + 'MongoDB\Driver\ClientEncryption::deleteKey' => ['object', 'keyId' => 'MongoDB\BSON\Binary'], + 'MongoDB\Driver\ClientEncryption::encrypt' => ['MongoDB\BSON\Binary', 'value' => 'mixed', 'options=' => '?array'], + 'MongoDB\Driver\ClientEncryption::getKey' => ['?object', 'keyId' => 'MongoDB\BSON\Binary'], + 'MongoDB\Driver\ClientEncryption::getKeyByAltName' => ['?object', 'keyAltName' => 'string'], + 'MongoDB\Driver\ClientEncryption::getKeys' => ['MongoDB\Driver\Cursor'], + 'MongoDB\Driver\ClientEncryption::removeKeyAltName' => ['?object', 'keyId' => 'MongoDB\BSON\Binary', 'keyAltName' => 'string'], + 'MongoDB\Driver\ClientEncryption::rewrapManyDataKey' => ['object', 'filter' => 'object|array', 'options=' => '?array'], + 'MongoDB\Driver\Command::__construct' => ['void', 'document' => 'object|array', 'commandOptions=' => '?array'], + 'MongoDB\Driver\Cursor::current' => ['object|array|null'], 'MongoDB\Driver\Cursor::getId' => ['MongoDB\Driver\CursorId'], 'MongoDB\Driver\Cursor::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\Cursor::isDead' => ['bool'], - 'MongoDB\Driver\Cursor::setTypeMap' => ['void', 'typemap'=>'array'], + 'MongoDB\Driver\Cursor::key' => ['?int'], + 'MongoDB\Driver\Cursor::next' => ['void'], + 'MongoDB\Driver\Cursor::rewind' => ['void'], + 'MongoDB\Driver\Cursor::setTypeMap' => ['void', 'typemap' => 'array'], 'MongoDB\Driver\Cursor::toArray' => ['array'], - 'MongoDB\Driver\CursorId::__construct' => ['void', 'id'=>'string'], + 'MongoDB\Driver\Cursor::valid' => ['bool'], 'MongoDB\Driver\CursorId::__toString' => ['string'], 'MongoDB\Driver\CursorId::serialize' => ['string'], - 'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\Driver\Exception\RuntimeException::__clone' => ['void'], - 'MongoDB\Driver\Exception\RuntimeException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?RuntimeException|?Throwable'], + 'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\Driver\CursorInterface::current' => ['object|array|null'], + 'MongoDB\Driver\CursorInterface::getId' => ['void'], + 'MongoDB\Driver\CursorInterface::getServer' => ['void'], + 'MongoDB\Driver\CursorInterface::isDead' => ['void'], + 'MongoDB\Driver\CursorInterface::key' => ['?int'], + 'MongoDB\Driver\CursorInterface::next' => ['void'], + 'MongoDB\Driver\CursorInterface::rewind' => ['void'], + 'MongoDB\Driver\CursorInterface::setTypeMap' => ['void', 'typemap' => 'array'], + 'MongoDB\Driver\CursorInterface::toArray' => ['void'], + 'MongoDB\Driver\CursorInterface::valid' => ['bool'], + 'MongoDB\Driver\Exception\AuthenticationException::__toString' => ['string'], + 'MongoDB\Driver\Exception\BulkWriteException::__toString' => ['string'], + 'MongoDB\Driver\Exception\CommandException::getResultDocument' => ['object'], + 'MongoDB\Driver\Exception\CommandException::__toString' => ['string'], + 'MongoDB\Driver\Exception\ConnectionException::__toString' => ['string'], + 'MongoDB\Driver\Exception\ConnectionTimeoutException::__toString' => ['string'], + 'MongoDB\Driver\Exception\EncryptionException::__toString' => ['string'], + 'MongoDB\Driver\Exception\Exception::__toString' => ['string'], + 'MongoDB\Driver\Exception\ExecutionTimeoutException::__toString' => ['string'], + 'MongoDB\Driver\Exception\InvalidArgumentException::__toString' => ['string'], + 'MongoDB\Driver\Exception\LogicException::__toString' => ['string'], + 'MongoDB\Driver\Exception\RuntimeException::hasErrorLabel' => ['bool', 'errorLabel' => 'string'], 'MongoDB\Driver\Exception\RuntimeException::__toString' => ['string'], - 'MongoDB\Driver\Exception\RuntimeException::__wakeup' => ['void'], - 'MongoDB\Driver\Exception\RuntimeException::getCode' => ['int'], - 'MongoDB\Driver\Exception\RuntimeException::getFile' => ['string'], - 'MongoDB\Driver\Exception\RuntimeException::getLine' => ['int'], - 'MongoDB\Driver\Exception\RuntimeException::getMessage' => ['string'], - 'MongoDB\Driver\Exception\RuntimeException::getPrevious' => ['RuntimeException|Throwable'], - 'MongoDB\Driver\Exception\RuntimeException::getTrace' => ['list\',args?:array}>'], - 'MongoDB\Driver\Exception\RuntimeException::getTraceAsString' => ['string'], - 'MongoDB\Driver\Exception\WriteException::__clone' => ['void'], - 'MongoDB\Driver\Exception\WriteException::__construct' => ['void', 'message='=>'string', 'code='=>'int', 'previous='=>'?RuntimeException|?Throwable'], - 'MongoDB\Driver\Exception\WriteException::__toString' => ['string'], - 'MongoDB\Driver\Exception\WriteException::__wakeup' => ['void'], - 'MongoDB\Driver\Exception\WriteException::getCode' => ['int'], - 'MongoDB\Driver\Exception\WriteException::getFile' => ['string'], - 'MongoDB\Driver\Exception\WriteException::getLine' => ['int'], - 'MongoDB\Driver\Exception\WriteException::getMessage' => ['string'], - 'MongoDB\Driver\Exception\WriteException::getPrevious' => ['RuntimeException|Throwable'], - 'MongoDB\Driver\Exception\WriteException::getTrace' => ['list\',args?:array}>'], - 'MongoDB\Driver\Exception\WriteException::getTraceAsString' => ['string'], + 'MongoDB\Driver\Exception\SSLConnectionException::__toString' => ['string'], + 'MongoDB\Driver\Exception\ServerException::__toString' => ['string'], + 'MongoDB\Driver\Exception\UnexpectedValueException::__toString' => ['string'], 'MongoDB\Driver\Exception\WriteException::getWriteResult' => ['MongoDB\Driver\WriteResult'], - 'MongoDB\Driver\Manager::__construct' => ['void', 'uri'=>'string', 'options='=>'array', 'driverOptions='=>'array'], - 'MongoDB\Driver\Manager::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'bulk'=>'MongoDB\Driver\BulkWrite', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], - 'MongoDB\Driver\Manager::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'readPreference='=>'MongoDB\Driver\ReadPreference'], - 'MongoDB\Driver\Manager::executeDelete' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'filter'=>'array|object', 'deleteOptions='=>'array', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], - 'MongoDB\Driver\Manager::executeInsert' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'document'=>'array|object', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], - 'MongoDB\Driver\Manager::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'query'=>'MongoDB\Driver\Query', 'readPreference='=>'MongoDB\Driver\ReadPreference'], - 'MongoDB\Driver\Manager::executeUpdate' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'filter'=>'array|object', 'newObj'=>'array|object', 'updateOptions='=>'array', 'writeConcern='=>'MongoDB\Driver\WriteConcern'], + 'MongoDB\Driver\Exception\WriteException::__toString' => ['string'], + 'MongoDB\Driver\Manager::__construct' => ['void', 'uri=' => '?string', 'uriOptions=' => '?array', 'driverOptions=' => '?array'], + 'MongoDB\Driver\Manager::addSubscriber' => ['void', 'subscriber' => 'MongoDB\Driver\Monitoring\Subscriber'], + 'MongoDB\Driver\Manager::createClientEncryption' => ['MongoDB\Driver\ClientEncryption', 'options' => 'array'], + 'MongoDB\Driver\Manager::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace' => 'string', 'bulk' => 'MongoDB\Driver\BulkWrite', 'options=' => 'MongoDB\Driver\WriteConcern|array|null'], + 'MongoDB\Driver\Manager::executeCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], + 'MongoDB\Driver\Manager::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace' => 'string', 'query' => 'MongoDB\Driver\Query', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], + 'MongoDB\Driver\Manager::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], + 'MongoDB\Driver\Manager::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], + 'MongoDB\Driver\Manager::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], + 'MongoDB\Driver\Manager::getEncryptedFieldsMap' => ['object|array|null'], 'MongoDB\Driver\Manager::getReadConcern' => ['MongoDB\Driver\ReadConcern'], 'MongoDB\Driver\Manager::getReadPreference' => ['MongoDB\Driver\ReadPreference'], - 'MongoDB\Driver\Manager::getServers' => ['MongoDB\Driver\Server[]'], + 'MongoDB\Driver\Manager::getServers' => ['array'], 'MongoDB\Driver\Manager::getWriteConcern' => ['MongoDB\Driver\WriteConcern'], - 'MongoDB\Driver\Manager::selectServer' => ['MongoDB\Driver\Server', 'readPreference'=>'MongoDB\Driver\ReadPreference'], - 'MongoDB\Driver\Query::__construct' => ['void', 'filter'=>'array|object', 'queryOptions='=>'array'], - 'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level='=>'string'], - 'MongoDB\Driver\ReadConcern::bsonSerialize' => ['object'], + 'MongoDB\Driver\Manager::removeSubscriber' => ['void', 'subscriber' => 'MongoDB\Driver\Monitoring\Subscriber'], + 'MongoDB\Driver\Manager::selectServer' => ['MongoDB\Driver\Server', 'readPreference=' => '?MongoDB\Driver\ReadPreference'], + 'MongoDB\Driver\Manager::startSession' => ['MongoDB\Driver\Session', 'options=' => '?array'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getCommandName' => ['string'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getDurationMicros' => ['int'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getError' => ['Exception'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getOperationId' => ['string'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getReply' => ['object'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getRequestId' => ['string'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getServer' => ['MongoDB\Driver\Server'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\CommandFailedEvent::getServerConnectionId' => ['?int'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getCommand' => ['object'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getCommandName' => ['string'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getDatabaseName' => ['string'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getOperationId' => ['string'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getRequestId' => ['string'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getServer' => ['MongoDB\Driver\Server'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\CommandStartedEvent::getServerConnectionId' => ['?int'], + 'MongoDB\Driver\Monitoring\CommandSubscriber::commandStarted' => ['void', 'event' => 'MongoDB\Driver\Monitoring\CommandStartedEvent'], + 'MongoDB\Driver\Monitoring\CommandSubscriber::commandSucceeded' => ['void', 'event' => 'MongoDB\Driver\Monitoring\CommandSucceededEvent'], + 'MongoDB\Driver\Monitoring\CommandSubscriber::commandFailed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\CommandFailedEvent'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getCommandName' => ['string'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getDurationMicros' => ['int'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getOperationId' => ['string'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getReply' => ['object'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getRequestId' => ['string'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServer' => ['MongoDB\Driver\Server'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServiceId' => ['?MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\CommandSucceededEvent::getServerConnectionId' => ['?int'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverChanged' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerChangedEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverClosed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerClosedEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverOpening' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerOpeningEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatFailed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatStarted' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::serverHeartbeatSucceeded' => ['void', 'event' => 'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyChanged' => ['void', 'event' => 'MongoDB\Driver\Monitoring\TopologyChangedEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyClosed' => ['void', 'event' => 'MongoDB\Driver\Monitoring\TopologyClosedEvent'], + 'MongoDB\Driver\Monitoring\SDAMSubscriber::topologyOpening' => ['void', 'event' => 'MongoDB\Driver\Monitoring\TopologyOpeningEvent'], + 'MongoDB\Driver\Monitoring\ServerChangedEvent::getPort' => ['int'], + 'MongoDB\Driver\Monitoring\ServerChangedEvent::getHost' => ['string'], + 'MongoDB\Driver\Monitoring\ServerChangedEvent::getNewDescription' => ['MongoDB\Driver\ServerDescription'], + 'MongoDB\Driver\Monitoring\ServerChangedEvent::getPreviousDescription' => ['MongoDB\Driver\ServerDescription'], + 'MongoDB\Driver\Monitoring\ServerChangedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\ServerClosedEvent::getPort' => ['int'], + 'MongoDB\Driver\Monitoring\ServerClosedEvent::getHost' => ['string'], + 'MongoDB\Driver\Monitoring\ServerClosedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getDurationMicros' => ['int'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getError' => ['Exception'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getPort' => ['int'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::getHost' => ['string'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatFailedEvent::isAwaited' => ['bool'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::getPort' => ['int'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::getHost' => ['string'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatStartedEvent::isAwaited' => ['bool'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getDurationMicros' => ['int'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getReply' => ['object'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getPort' => ['int'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::getHost' => ['string'], + 'MongoDB\Driver\Monitoring\ServerHeartbeatSucceededEvent::isAwaited' => ['bool'], + 'MongoDB\Driver\Monitoring\ServerOpeningEvent::getPort' => ['int'], + 'MongoDB\Driver\Monitoring\ServerOpeningEvent::getHost' => ['string'], + 'MongoDB\Driver\Monitoring\ServerOpeningEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\TopologyChangedEvent::getNewDescription' => ['MongoDB\Driver\TopologyDescription'], + 'MongoDB\Driver\Monitoring\TopologyChangedEvent::getPreviousDescription' => ['MongoDB\Driver\TopologyDescription'], + 'MongoDB\Driver\Monitoring\TopologyChangedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\TopologyClosedEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Monitoring\TopologyOpeningEvent::getTopologyId' => ['MongoDB\BSON\ObjectId'], + 'MongoDB\Driver\Query::__construct' => ['void', 'filter' => 'object|array', 'queryOptions=' => '?array'], + 'MongoDB\Driver\ReadConcern::__construct' => ['void', 'level=' => '?string'], 'MongoDB\Driver\ReadConcern::getLevel' => ['?string'], 'MongoDB\Driver\ReadConcern::isDefault' => ['bool'], + 'MongoDB\Driver\ReadConcern::bsonSerialize' => ['object|array'], 'MongoDB\Driver\ReadConcern::serialize' => ['string'], - 'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode'=>'string|int', 'tagSets='=>'array', 'options='=>'array'], - 'MongoDB\Driver\ReadPreference::bsonSerialize' => ['object'], - 'MongoDB\Driver\ReadPreference::getHedge' => ['object|null'], + 'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode' => 'string|int', 'tagSets=' => '?array', 'options=' => '?array'], + 'MongoDB\Driver\ReadPreference::getHedge' => ['?object'], 'MongoDB\Driver\ReadPreference::getMaxStalenessSeconds' => ['int'], 'MongoDB\Driver\ReadPreference::getMode' => ['int'], 'MongoDB\Driver\ReadPreference::getModeString' => ['string'], 'MongoDB\Driver\ReadPreference::getTagSets' => ['array'], + 'MongoDB\Driver\ReadPreference::bsonSerialize' => ['object|array'], 'MongoDB\Driver\ReadPreference::serialize' => ['string'], - 'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized'=>'string'], - 'MongoDB\Driver\Server::__construct' => ['void', 'host'=>'string', 'port'=>'string', 'options='=>'array', 'driverOptions='=>'array'], - 'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace'=>'string', 'zwrite'=>'BulkWrite'], - 'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command'], - 'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace'=>'string', 'zquery'=>'Query'], + 'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace' => 'string', 'bulkWrite' => 'MongoDB\Driver\BulkWrite', 'options=' => 'MongoDB\Driver\WriteConcern|array|null'], + 'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], + 'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace' => 'string', 'query' => 'MongoDB\Driver\Query', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], + 'MongoDB\Driver\Server::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], + 'MongoDB\Driver\Server::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], + 'MongoDB\Driver\Server::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => '?array'], 'MongoDB\Driver\Server::getHost' => ['string'], 'MongoDB\Driver\Server::getInfo' => ['array'], - 'MongoDB\Driver\Server::getLatency' => ['int'], + 'MongoDB\Driver\Server::getLatency' => ['?int'], 'MongoDB\Driver\Server::getPort' => ['int'], - 'MongoDB\Driver\Server::getState' => [''], + 'MongoDB\Driver\Server::getServerDescription' => ['MongoDB\Driver\ServerDescription'], 'MongoDB\Driver\Server::getTags' => ['array'], 'MongoDB\Driver\Server::getType' => ['int'], 'MongoDB\Driver\Server::isArbiter' => ['bool'], - 'MongoDB\Driver\Server::isDelayed' => [''], 'MongoDB\Driver\Server::isHidden' => ['bool'], 'MongoDB\Driver\Server::isPassive' => ['bool'], 'MongoDB\Driver\Server::isPrimary' => ['bool'], 'MongoDB\Driver\Server::isSecondary' => ['bool'], - 'MongoDB\Driver\WriteConcern::__construct' => ['void', 'wstring'=>'string|int', 'wtimeout='=>'int', 'journal='=>'bool'], - 'MongoDB\Driver\WriteConcern::bsonSerialize' => ['object'], + 'MongoDB\Driver\ServerApi::__construct' => ['void', 'version' => 'string', 'strict=' => '?bool', 'deprecationErrors=' => '?bool'], + 'MongoDB\Driver\ServerApi::bsonSerialize' => ['object|array'], + 'MongoDB\Driver\ServerApi::serialize' => ['string'], + 'MongoDB\Driver\ServerApi::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\Driver\ServerDescription::getHelloResponse' => ['array'], + 'MongoDB\Driver\ServerDescription::getHost' => ['string'], + 'MongoDB\Driver\ServerDescription::getLastUpdateTime' => ['int'], + 'MongoDB\Driver\ServerDescription::getPort' => ['int'], + 'MongoDB\Driver\ServerDescription::getRoundTripTime' => ['?int'], + 'MongoDB\Driver\ServerDescription::getType' => ['string'], + 'MongoDB\Driver\Session::abortTransaction' => ['void'], + 'MongoDB\Driver\Session::advanceClusterTime' => ['void', 'clusterTime' => 'object|array'], + 'MongoDB\Driver\Session::advanceOperationTime' => ['void', 'operationTime' => 'MongoDB\BSON\TimestampInterface'], + 'MongoDB\Driver\Session::commitTransaction' => ['void'], + 'MongoDB\Driver\Session::endSession' => ['void'], + 'MongoDB\Driver\Session::getClusterTime' => ['?object'], + 'MongoDB\Driver\Session::getLogicalSessionId' => ['object'], + 'MongoDB\Driver\Session::getOperationTime' => ['?MongoDB\BSON\Timestamp'], + 'MongoDB\Driver\Session::getServer' => ['?MongoDB\Driver\Server'], + 'MongoDB\Driver\Session::getTransactionOptions' => ['?array'], + 'MongoDB\Driver\Session::getTransactionState' => ['string'], + 'MongoDB\Driver\Session::isDirty' => ['bool'], + 'MongoDB\Driver\Session::isInTransaction' => ['bool'], + 'MongoDB\Driver\Session::startTransaction' => ['void', 'options=' => '?array'], + 'MongoDB\Driver\TopologyDescription::getServers' => ['array'], + 'MongoDB\Driver\TopologyDescription::getType' => ['string'], + 'MongoDB\Driver\TopologyDescription::hasReadableServer' => ['bool', 'readPreference=' => '?MongoDB\Driver\ReadPreference'], + 'MongoDB\Driver\TopologyDescription::hasWritableServer' => ['bool'], + 'MongoDB\Driver\WriteConcern::__construct' => ['void', 'w' => 'string|int', 'wtimeout=' => '?int', 'journal=' => '?bool'], 'MongoDB\Driver\WriteConcern::getJournal' => ['?bool'], - 'MongoDB\Driver\WriteConcern::getJurnal' => ['?bool'], - 'MongoDB\Driver\WriteConcern::getW' => ['int|null|string'], + 'MongoDB\Driver\WriteConcern::getW' => ['string|int|null'], 'MongoDB\Driver\WriteConcern::getWtimeout' => ['int'], 'MongoDB\Driver\WriteConcern::isDefault' => ['bool'], + 'MongoDB\Driver\WriteConcern::bsonSerialize' => ['object|array'], 'MongoDB\Driver\WriteConcern::serialize' => ['string'], - 'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized'=>'string'], + 'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized' => ''], 'MongoDB\Driver\WriteConcernError::getCode' => ['int'], - 'MongoDB\Driver\WriteConcernError::getInfo' => ['mixed'], + 'MongoDB\Driver\WriteConcernError::getInfo' => ['?object'], 'MongoDB\Driver\WriteConcernError::getMessage' => ['string'], 'MongoDB\Driver\WriteError::getCode' => ['int'], 'MongoDB\Driver\WriteError::getIndex' => ['int'], - 'MongoDB\Driver\WriteError::getInfo' => ['mixed'], + 'MongoDB\Driver\WriteError::getInfo' => ['?object'], 'MongoDB\Driver\WriteError::getMessage' => ['string'], - 'MongoDB\Driver\WriteException::getWriteResult' => [''], - 'MongoDB\Driver\WriteResult::getDeletedCount' => ['?int'], - 'MongoDB\Driver\WriteResult::getInfo' => [''], 'MongoDB\Driver\WriteResult::getInsertedCount' => ['?int'], 'MongoDB\Driver\WriteResult::getMatchedCount' => ['?int'], 'MongoDB\Driver\WriteResult::getModifiedCount' => ['?int'], - 'MongoDB\Driver\WriteResult::getServer' => ['MongoDB\Driver\Server'], + 'MongoDB\Driver\WriteResult::getDeletedCount' => ['?int'], 'MongoDB\Driver\WriteResult::getUpsertedCount' => ['?int'], + 'MongoDB\Driver\WriteResult::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\WriteResult::getUpsertedIds' => ['array'], - 'MongoDB\Driver\WriteResult::getWriteConcernError' => ['MongoDB\Driver\WriteConcernError|null'], - 'MongoDB\Driver\WriteResult::getWriteErrors' => ['MongoDB\Driver\WriteError[]'], + 'MongoDB\Driver\WriteResult::getWriteConcernError' => ['?MongoDB\Driver\WriteConcernError'], + 'MongoDB\Driver\WriteResult::getWriteErrors' => ['array'], 'MongoDB\Driver\WriteResult::isAcknowledged' => ['bool'], 'MongoDate::__construct' => ['void', 'second='=>'int', 'usecond='=>'int'], 'MongoDate::__toString' => ['string'], @@ -13002,49 +13108,6 @@ 'mkdir' => ['bool', 'directory'=>'string', 'permissions='=>'int', 'recursive='=>'bool', 'context='=>'resource'], 'mktime' => ['int|false', 'hour='=>'int', 'minute='=>'int', 'second='=>'int', 'month='=>'int', 'day='=>'int', 'year='=>'int'], 'money_format' => ['string', 'format'=>'string', 'value'=>'float'], - 'mongodb\driver\exception\commandexception::getResultDocument' => ['object'], - 'mongodb\driver\exception\runtimeexception::hasErrorLabel' => ['bool', 'errorLabel'=>'string'], - 'mongodb\driver\manager::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], - 'mongodb\driver\manager::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], - 'mongodb\driver\manager::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], - 'mongodb\driver\manager::startSession' => ['MongoDB\Driver\Session', 'options='=>'array'], - 'mongodb\driver\monitoring\commandfailedevent::getCommandName' => ['string'], - 'mongodb\driver\monitoring\commandfailedevent::getDurationMicros' => ['int'], - 'mongodb\driver\monitoring\commandfailedevent::getError' => ['Exception'], - 'mongodb\driver\monitoring\commandfailedevent::getOperationId' => ['string'], - 'mongodb\driver\monitoring\commandfailedevent::getReply' => ['object'], - 'mongodb\driver\monitoring\commandfailedevent::getRequestId' => ['string'], - 'mongodb\driver\monitoring\commandfailedevent::getServer' => ['MongoDB\Driver\Server'], - 'mongodb\driver\monitoring\commandstartedevent::getCommand' => ['object'], - 'mongodb\driver\monitoring\commandstartedevent::getCommandName' => ['string'], - 'mongodb\driver\monitoring\commandstartedevent::getDatabaseName' => ['string'], - 'mongodb\driver\monitoring\commandstartedevent::getOperationId' => ['string'], - 'mongodb\driver\monitoring\commandstartedevent::getRequestId' => ['string'], - 'mongodb\driver\monitoring\commandstartedevent::getServer' => ['MongoDB\Driver\Server'], - 'mongodb\driver\monitoring\commandsubscriber::commandFailed' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandFailedEvent'], - 'mongodb\driver\monitoring\commandsubscriber::commandStarted' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandStartedEvent'], - 'mongodb\driver\monitoring\commandsubscriber::commandSucceeded' => ['void', 'event'=>'MongoDB\Driver\Monitoring\CommandSucceededEvent'], - 'mongodb\driver\monitoring\commandsucceededevent::getCommandName' => ['string'], - 'mongodb\driver\monitoring\commandsucceededevent::getDurationMicros' => ['int'], - 'mongodb\driver\monitoring\commandsucceededevent::getOperationId' => ['string'], - 'mongodb\driver\monitoring\commandsucceededevent::getReply' => ['object'], - 'mongodb\driver\monitoring\commandsucceededevent::getRequestId' => ['string'], - 'mongodb\driver\monitoring\commandsucceededevent::getServer' => ['MongoDB\Driver\Server'], - 'mongodb\driver\server::executeReadCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], - 'mongodb\driver\server::executeReadWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], - 'mongodb\driver\server::executeWriteCommand' => ['MongoDB\Driver\Cursor', 'db'=>'string', 'command'=>'MongoDB\Driver\Command', 'options='=>'array'], - 'mongodb\driver\session::__construct' => ['void'], - 'mongodb\driver\session::abortTransaction' => ['void'], - 'mongodb\driver\session::advanceClusterTime' => ['void', 'clusterTime'=>'array|object'], - 'mongodb\driver\session::advanceOperationTime' => ['void', 'operationTime'=>'MongoDB\BSON\TimestampInterface'], - 'mongodb\driver\session::commitTransaction' => ['void'], - 'mongodb\driver\session::endSession' => ['void'], - 'mongodb\driver\session::getClusterTime' => ['?object'], - 'mongodb\driver\session::getLogicalSessionId' => ['object'], - 'mongodb\driver\session::getOperationTime' => ['MongoDB\BSON\Timestamp|null'], - 'mongodb\driver\session::getTransactionOptions' => ['array|null'], - 'mongodb\driver\session::getTransactionState' => ['string'], - 'mongodb\driver\session::startTransaction' => ['void', 'options'=>'array|object'], 'monitor_custom_event' => ['void', 'class'=>'string', 'text'=>'string', 'severe='=>'int', 'user_data='=>'mixed'], 'monitor_httperror_event' => ['void', 'error_code'=>'int', 'url'=>'string', 'severe='=>'int'], 'monitor_license_info' => ['array'], From 5b8f611e7318ceddbd0c381b4bacd03728da59cb Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Thu, 25 Aug 2022 15:27:04 +0200 Subject: [PATCH 073/194] Handle tentative return types for interfaces correctly --- dictionaries/CallMap.php | 47 +++++++++++++---------------- dictionaries/CallMap_historical.php | 47 +++++++++++++---------------- 2 files changed, 42 insertions(+), 52 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 1f6ca718260..f8dfb0dccc8 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -7700,8 +7700,8 @@ 'MongoDB\BSON\Binary::serialize' => ['string'], 'MongoDB\BSON\Binary::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\Binary::jsonSerialize' => ['mixed'], -'MongoDB\BSON\BinaryInterface::getData' => ['void'], -'MongoDB\BSON\BinaryInterface::getType' => ['void'], +'MongoDB\BSON\BinaryInterface::getData' => ['string'], +'MongoDB\BSON\BinaryInterface::getType' => ['int'], 'MongoDB\BSON\BinaryInterface::__toString' => ['string'], 'MongoDB\BSON\DBPointer::__toString' => ['string'], 'MongoDB\BSON\DBPointer::serialize' => ['string'], @@ -7712,7 +7712,7 @@ 'MongoDB\BSON\Decimal128::serialize' => ['string'], 'MongoDB\BSON\Decimal128::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], -'MongoDB\BSON\Decimal128Interface::__toString' => ['void'], +'MongoDB\BSON\Decimal128Interface::__toString' => ['string'], 'MongoDB\BSON\Int64::__toString' => ['string'], 'MongoDB\BSON\Int64::serialize' => ['string'], 'MongoDB\BSON\Int64::unserialize' => ['void', 'serialized' => ''], @@ -7724,9 +7724,9 @@ 'MongoDB\BSON\Javascript::serialize' => ['string'], 'MongoDB\BSON\Javascript::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\Javascript::jsonSerialize' => ['mixed'], -'MongoDB\BSON\JavascriptInterface::getCode' => ['void'], -'MongoDB\BSON\JavascriptInterface::getScope' => ['void'], -'MongoDB\BSON\JavascriptInterface::__toString' => ['void'], +'MongoDB\BSON\JavascriptInterface::getCode' => ['string'], +'MongoDB\BSON\JavascriptInterface::getScope' => ['?object'], +'MongoDB\BSON\JavascriptInterface::__toString' => ['string'], 'MongoDB\BSON\MaxKey::serialize' => ['string'], 'MongoDB\BSON\MaxKey::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], @@ -7739,8 +7739,8 @@ 'MongoDB\BSON\ObjectId::serialize' => ['string'], 'MongoDB\BSON\ObjectId::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], -'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['void'], -'MongoDB\BSON\ObjectIdInterface::__toString' => ['void'], +'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['int'], +'MongoDB\BSON\ObjectIdInterface::__toString' => ['string'], 'MongoDB\BSON\Regex::__construct' => ['void', 'pattern' => 'string', 'flags=' => 'string'], 'MongoDB\BSON\Regex::getPattern' => ['string'], 'MongoDB\BSON\Regex::getFlags' => ['string'], @@ -7748,10 +7748,10 @@ 'MongoDB\BSON\Regex::serialize' => ['string'], 'MongoDB\BSON\Regex::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\Regex::jsonSerialize' => ['mixed'], -'MongoDB\BSON\RegexInterface::getPattern' => ['void'], -'MongoDB\BSON\RegexInterface::getFlags' => ['void'], -'MongoDB\BSON\RegexInterface::__toString' => ['void'], -'MongoDB\BSON\Serializable::bsonSerialize' => ['void'], +'MongoDB\BSON\RegexInterface::getPattern' => ['string'], +'MongoDB\BSON\RegexInterface::getFlags' => ['string'], +'MongoDB\BSON\RegexInterface::__toString' => ['string'], +'MongoDB\BSON\Serializable::bsonSerialize' => ['object|array'], 'MongoDB\BSON\Symbol::__toString' => ['string'], 'MongoDB\BSON\Symbol::serialize' => ['string'], 'MongoDB\BSON\Symbol::unserialize' => ['void', 'serialized' => ''], @@ -7763,17 +7763,17 @@ 'MongoDB\BSON\Timestamp::serialize' => ['string'], 'MongoDB\BSON\Timestamp::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], -'MongoDB\BSON\TimestampInterface::getTimestamp' => ['void'], -'MongoDB\BSON\TimestampInterface::getIncrement' => ['void'], -'MongoDB\BSON\TimestampInterface::__toString' => ['void'], +'MongoDB\BSON\TimestampInterface::getTimestamp' => ['int'], +'MongoDB\BSON\TimestampInterface::getIncrement' => ['int'], +'MongoDB\BSON\TimestampInterface::__toString' => ['string'], 'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds=' => 'DateTimeInterface|string|int|float|null'], 'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], 'MongoDB\BSON\UTCDateTime::__toString' => ['string'], 'MongoDB\BSON\UTCDateTime::serialize' => ['string'], 'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\UTCDateTime::jsonSerialize' => ['mixed'], -'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['void'], -'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['void'], +'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['DateTime'], +'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['string'], 'MongoDB\BSON\Undefined::__toString' => ['string'], 'MongoDB\BSON\Undefined::serialize' => ['string'], 'MongoDB\BSON\Undefined::unserialize' => ['void', 'serialized' => ''], @@ -7809,16 +7809,11 @@ 'MongoDB\Driver\CursorId::__toString' => ['string'], 'MongoDB\Driver\CursorId::serialize' => ['string'], 'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized' => ''], -'MongoDB\Driver\CursorInterface::current' => ['object|array|null'], -'MongoDB\Driver\CursorInterface::getId' => ['void'], -'MongoDB\Driver\CursorInterface::getServer' => ['void'], -'MongoDB\Driver\CursorInterface::isDead' => ['void'], -'MongoDB\Driver\CursorInterface::key' => ['?int'], -'MongoDB\Driver\CursorInterface::next' => ['void'], -'MongoDB\Driver\CursorInterface::rewind' => ['void'], +'MongoDB\Driver\CursorInterface::getId' => ['MongoDB\Driver\CursorId'], +'MongoDB\Driver\CursorInterface::getServer' => ['MongoDB\Driver\Server'], +'MongoDB\Driver\CursorInterface::isDead' => ['bool'], 'MongoDB\Driver\CursorInterface::setTypeMap' => ['void', 'typemap' => 'array'], -'MongoDB\Driver\CursorInterface::toArray' => ['void'], -'MongoDB\Driver\CursorInterface::valid' => ['bool'], +'MongoDB\Driver\CursorInterface::toArray' => ['array'], 'MongoDB\Driver\Exception\AuthenticationException::__toString' => ['string'], 'MongoDB\Driver\Exception\BulkWriteException::__toString' => ['string'], 'MongoDB\Driver\Exception\CommandException::getResultDocument' => ['object'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index f2bf1d49284..74057a56163 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -3972,8 +3972,8 @@ 'MongoDB\BSON\Binary::serialize' => ['string'], 'MongoDB\BSON\Binary::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\Binary::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\BinaryInterface::getData' => ['void'], - 'MongoDB\BSON\BinaryInterface::getType' => ['void'], + 'MongoDB\BSON\BinaryInterface::getData' => ['string'], + 'MongoDB\BSON\BinaryInterface::getType' => ['int'], 'MongoDB\BSON\BinaryInterface::__toString' => ['string'], 'MongoDB\BSON\DBPointer::__toString' => ['string'], 'MongoDB\BSON\DBPointer::serialize' => ['string'], @@ -3984,7 +3984,7 @@ 'MongoDB\BSON\Decimal128::serialize' => ['string'], 'MongoDB\BSON\Decimal128::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\Decimal128Interface::__toString' => ['void'], + 'MongoDB\BSON\Decimal128Interface::__toString' => ['string'], 'MongoDB\BSON\Int64::__toString' => ['string'], 'MongoDB\BSON\Int64::serialize' => ['string'], 'MongoDB\BSON\Int64::unserialize' => ['void', 'serialized' => ''], @@ -3996,9 +3996,9 @@ 'MongoDB\BSON\Javascript::serialize' => ['string'], 'MongoDB\BSON\Javascript::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\Javascript::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\JavascriptInterface::getCode' => ['void'], - 'MongoDB\BSON\JavascriptInterface::getScope' => ['void'], - 'MongoDB\BSON\JavascriptInterface::__toString' => ['void'], + 'MongoDB\BSON\JavascriptInterface::getCode' => ['string'], + 'MongoDB\BSON\JavascriptInterface::getScope' => ['?object'], + 'MongoDB\BSON\JavascriptInterface::__toString' => ['string'], 'MongoDB\BSON\MaxKey::serialize' => ['string'], 'MongoDB\BSON\MaxKey::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], @@ -4011,8 +4011,8 @@ 'MongoDB\BSON\ObjectId::serialize' => ['string'], 'MongoDB\BSON\ObjectId::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['void'], - 'MongoDB\BSON\ObjectIdInterface::__toString' => ['void'], + 'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['int'], + 'MongoDB\BSON\ObjectIdInterface::__toString' => ['string'], 'MongoDB\BSON\Regex::__construct' => ['void', 'pattern' => 'string', 'flags=' => 'string'], 'MongoDB\BSON\Regex::getPattern' => ['string'], 'MongoDB\BSON\Regex::getFlags' => ['string'], @@ -4020,10 +4020,10 @@ 'MongoDB\BSON\Regex::serialize' => ['string'], 'MongoDB\BSON\Regex::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\Regex::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\RegexInterface::getPattern' => ['void'], - 'MongoDB\BSON\RegexInterface::getFlags' => ['void'], - 'MongoDB\BSON\RegexInterface::__toString' => ['void'], - 'MongoDB\BSON\Serializable::bsonSerialize' => ['void'], + 'MongoDB\BSON\RegexInterface::getPattern' => ['string'], + 'MongoDB\BSON\RegexInterface::getFlags' => ['string'], + 'MongoDB\BSON\RegexInterface::__toString' => ['string'], + 'MongoDB\BSON\Serializable::bsonSerialize' => ['object|array'], 'MongoDB\BSON\Symbol::__toString' => ['string'], 'MongoDB\BSON\Symbol::serialize' => ['string'], 'MongoDB\BSON\Symbol::unserialize' => ['void', 'serialized' => ''], @@ -4035,17 +4035,17 @@ 'MongoDB\BSON\Timestamp::serialize' => ['string'], 'MongoDB\BSON\Timestamp::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\TimestampInterface::getTimestamp' => ['void'], - 'MongoDB\BSON\TimestampInterface::getIncrement' => ['void'], - 'MongoDB\BSON\TimestampInterface::__toString' => ['void'], + 'MongoDB\BSON\TimestampInterface::getTimestamp' => ['int'], + 'MongoDB\BSON\TimestampInterface::getIncrement' => ['int'], + 'MongoDB\BSON\TimestampInterface::__toString' => ['string'], 'MongoDB\BSON\UTCDateTime::__construct' => ['void', 'milliseconds=' => 'DateTimeInterface|string|int|float|null'], 'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], 'MongoDB\BSON\UTCDateTime::__toString' => ['string'], 'MongoDB\BSON\UTCDateTime::serialize' => ['string'], 'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'serialized' => ''], 'MongoDB\BSON\UTCDateTime::jsonSerialize' => ['mixed'], - 'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['void'], - 'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['void'], + 'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['DateTime'], + 'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['string'], 'MongoDB\BSON\Undefined::__toString' => ['string'], 'MongoDB\BSON\Undefined::serialize' => ['string'], 'MongoDB\BSON\Undefined::unserialize' => ['void', 'serialized' => ''], @@ -4081,16 +4081,11 @@ 'MongoDB\Driver\CursorId::__toString' => ['string'], 'MongoDB\Driver\CursorId::serialize' => ['string'], 'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized' => ''], - 'MongoDB\Driver\CursorInterface::current' => ['object|array|null'], - 'MongoDB\Driver\CursorInterface::getId' => ['void'], - 'MongoDB\Driver\CursorInterface::getServer' => ['void'], - 'MongoDB\Driver\CursorInterface::isDead' => ['void'], - 'MongoDB\Driver\CursorInterface::key' => ['?int'], - 'MongoDB\Driver\CursorInterface::next' => ['void'], - 'MongoDB\Driver\CursorInterface::rewind' => ['void'], + 'MongoDB\Driver\CursorInterface::getId' => ['MongoDB\Driver\CursorId'], + 'MongoDB\Driver\CursorInterface::getServer' => ['MongoDB\Driver\Server'], + 'MongoDB\Driver\CursorInterface::isDead' => ['bool'], 'MongoDB\Driver\CursorInterface::setTypeMap' => ['void', 'typemap' => 'array'], - 'MongoDB\Driver\CursorInterface::toArray' => ['void'], - 'MongoDB\Driver\CursorInterface::valid' => ['bool'], + 'MongoDB\Driver\CursorInterface::toArray' => ['array'], 'MongoDB\Driver\Exception\AuthenticationException::__toString' => ['string'], 'MongoDB\Driver\Exception\BulkWriteException::__toString' => ['string'], 'MongoDB\Driver\Exception\CommandException::getResultDocument' => ['object'], From 48bf5496d8c2678cbbd24129837b41e2678c36ff Mon Sep 17 00:00:00 2001 From: Andreas Braun Date: Fri, 26 Aug 2022 10:03:08 +0200 Subject: [PATCH 074/194] Add missing parameter type for Serializable::unserialize --- dictionaries/CallMap.php | 36 ++++++++++++++--------------- dictionaries/CallMap_historical.php | 36 ++++++++++++++--------------- 2 files changed, 36 insertions(+), 36 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index f8dfb0dccc8..70655310d70 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -7698,46 +7698,46 @@ 'MongoDB\BSON\Binary::getType' => ['int'], 'MongoDB\BSON\Binary::__toString' => ['string'], 'MongoDB\BSON\Binary::serialize' => ['string'], -'MongoDB\BSON\Binary::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Binary::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Binary::jsonSerialize' => ['mixed'], 'MongoDB\BSON\BinaryInterface::getData' => ['string'], 'MongoDB\BSON\BinaryInterface::getType' => ['int'], 'MongoDB\BSON\BinaryInterface::__toString' => ['string'], 'MongoDB\BSON\DBPointer::__toString' => ['string'], 'MongoDB\BSON\DBPointer::serialize' => ['string'], -'MongoDB\BSON\DBPointer::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\DBPointer::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\DBPointer::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Decimal128::__construct' => ['void', 'value' => 'string'], 'MongoDB\BSON\Decimal128::__toString' => ['string'], 'MongoDB\BSON\Decimal128::serialize' => ['string'], -'MongoDB\BSON\Decimal128::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Decimal128::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Decimal128Interface::__toString' => ['string'], 'MongoDB\BSON\Int64::__toString' => ['string'], 'MongoDB\BSON\Int64::serialize' => ['string'], -'MongoDB\BSON\Int64::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Int64::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Int64::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Javascript::__construct' => ['void', 'code' => 'string', 'scope=' => 'object|array|null'], 'MongoDB\BSON\Javascript::getCode' => ['string'], 'MongoDB\BSON\Javascript::getScope' => ['?object'], 'MongoDB\BSON\Javascript::__toString' => ['string'], 'MongoDB\BSON\Javascript::serialize' => ['string'], -'MongoDB\BSON\Javascript::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Javascript::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Javascript::jsonSerialize' => ['mixed'], 'MongoDB\BSON\JavascriptInterface::getCode' => ['string'], 'MongoDB\BSON\JavascriptInterface::getScope' => ['?object'], 'MongoDB\BSON\JavascriptInterface::__toString' => ['string'], 'MongoDB\BSON\MaxKey::serialize' => ['string'], -'MongoDB\BSON\MaxKey::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\MaxKey::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], 'MongoDB\BSON\MinKey::serialize' => ['string'], -'MongoDB\BSON\MinKey::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\MinKey::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\MinKey::jsonSerialize' => ['mixed'], 'MongoDB\BSON\ObjectId::__construct' => ['void', 'id=' => '?string'], 'MongoDB\BSON\ObjectId::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectId::__toString' => ['string'], 'MongoDB\BSON\ObjectId::serialize' => ['string'], -'MongoDB\BSON\ObjectId::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\ObjectId::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], 'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectIdInterface::__toString' => ['string'], @@ -7746,7 +7746,7 @@ 'MongoDB\BSON\Regex::getFlags' => ['string'], 'MongoDB\BSON\Regex::__toString' => ['string'], 'MongoDB\BSON\Regex::serialize' => ['string'], -'MongoDB\BSON\Regex::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Regex::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Regex::jsonSerialize' => ['mixed'], 'MongoDB\BSON\RegexInterface::getPattern' => ['string'], 'MongoDB\BSON\RegexInterface::getFlags' => ['string'], @@ -7754,14 +7754,14 @@ 'MongoDB\BSON\Serializable::bsonSerialize' => ['object|array'], 'MongoDB\BSON\Symbol::__toString' => ['string'], 'MongoDB\BSON\Symbol::serialize' => ['string'], -'MongoDB\BSON\Symbol::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Symbol::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Symbol::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment' => 'string|int', 'timestamp' => 'string|int'], 'MongoDB\BSON\Timestamp::getTimestamp' => ['int'], 'MongoDB\BSON\Timestamp::getIncrement' => ['int'], 'MongoDB\BSON\Timestamp::__toString' => ['string'], 'MongoDB\BSON\Timestamp::serialize' => ['string'], -'MongoDB\BSON\Timestamp::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Timestamp::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], 'MongoDB\BSON\TimestampInterface::getTimestamp' => ['int'], 'MongoDB\BSON\TimestampInterface::getIncrement' => ['int'], @@ -7770,13 +7770,13 @@ 'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], 'MongoDB\BSON\UTCDateTime::__toString' => ['string'], 'MongoDB\BSON\UTCDateTime::serialize' => ['string'], -'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\UTCDateTime::jsonSerialize' => ['mixed'], 'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['DateTime'], 'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['string'], 'MongoDB\BSON\Undefined::__toString' => ['string'], 'MongoDB\BSON\Undefined::serialize' => ['string'], -'MongoDB\BSON\Undefined::unserialize' => ['void', 'serialized' => ''], +'MongoDB\BSON\Undefined::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Undefined::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data' => 'array'], 'MongoDB\Driver\BulkWrite::__construct' => ['void', 'options=' => '?array'], @@ -7808,7 +7808,7 @@ 'MongoDB\Driver\Cursor::valid' => ['bool'], 'MongoDB\Driver\CursorId::__toString' => ['string'], 'MongoDB\Driver\CursorId::serialize' => ['string'], -'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized' => ''], +'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\Driver\CursorInterface::getId' => ['MongoDB\Driver\CursorId'], 'MongoDB\Driver\CursorInterface::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\CursorInterface::isDead' => ['bool'], @@ -7921,7 +7921,7 @@ 'MongoDB\Driver\ReadConcern::isDefault' => ['bool'], 'MongoDB\Driver\ReadConcern::bsonSerialize' => ['object|array'], 'MongoDB\Driver\ReadConcern::serialize' => ['string'], -'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized' => ''], +'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode' => 'string|int', 'tagSets=' => '?array', 'options=' => '?array'], 'MongoDB\Driver\ReadPreference::getHedge' => ['?object'], 'MongoDB\Driver\ReadPreference::getMaxStalenessSeconds' => ['int'], @@ -7930,7 +7930,7 @@ 'MongoDB\Driver\ReadPreference::getTagSets' => ['array'], 'MongoDB\Driver\ReadPreference::bsonSerialize' => ['object|array'], 'MongoDB\Driver\ReadPreference::serialize' => ['string'], -'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized' => ''], +'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace' => 'string', 'bulkWrite' => 'MongoDB\Driver\BulkWrite', 'options=' => 'MongoDB\Driver\WriteConcern|array|null'], 'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], 'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace' => 'string', 'query' => 'MongoDB\Driver\Query', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], @@ -7952,7 +7952,7 @@ 'MongoDB\Driver\ServerApi::__construct' => ['void', 'version' => 'string', 'strict=' => '?bool', 'deprecationErrors=' => '?bool'], 'MongoDB\Driver\ServerApi::bsonSerialize' => ['object|array'], 'MongoDB\Driver\ServerApi::serialize' => ['string'], -'MongoDB\Driver\ServerApi::unserialize' => ['void', 'serialized' => ''], +'MongoDB\Driver\ServerApi::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\Driver\ServerDescription::getHelloResponse' => ['array'], 'MongoDB\Driver\ServerDescription::getHost' => ['string'], 'MongoDB\Driver\ServerDescription::getLastUpdateTime' => ['int'], @@ -7984,7 +7984,7 @@ 'MongoDB\Driver\WriteConcern::isDefault' => ['bool'], 'MongoDB\Driver\WriteConcern::bsonSerialize' => ['object|array'], 'MongoDB\Driver\WriteConcern::serialize' => ['string'], -'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized' => ''], +'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\Driver\WriteConcernError::getCode' => ['int'], 'MongoDB\Driver\WriteConcernError::getInfo' => ['?object'], 'MongoDB\Driver\WriteConcernError::getMessage' => ['string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 74057a56163..6f50add98a1 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -3970,46 +3970,46 @@ 'MongoDB\BSON\Binary::getType' => ['int'], 'MongoDB\BSON\Binary::__toString' => ['string'], 'MongoDB\BSON\Binary::serialize' => ['string'], - 'MongoDB\BSON\Binary::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Binary::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Binary::jsonSerialize' => ['mixed'], 'MongoDB\BSON\BinaryInterface::getData' => ['string'], 'MongoDB\BSON\BinaryInterface::getType' => ['int'], 'MongoDB\BSON\BinaryInterface::__toString' => ['string'], 'MongoDB\BSON\DBPointer::__toString' => ['string'], 'MongoDB\BSON\DBPointer::serialize' => ['string'], - 'MongoDB\BSON\DBPointer::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\DBPointer::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\DBPointer::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Decimal128::__construct' => ['void', 'value' => 'string'], 'MongoDB\BSON\Decimal128::__toString' => ['string'], 'MongoDB\BSON\Decimal128::serialize' => ['string'], - 'MongoDB\BSON\Decimal128::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Decimal128::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Decimal128::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Decimal128Interface::__toString' => ['string'], 'MongoDB\BSON\Int64::__toString' => ['string'], 'MongoDB\BSON\Int64::serialize' => ['string'], - 'MongoDB\BSON\Int64::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Int64::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Int64::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Javascript::__construct' => ['void', 'code' => 'string', 'scope=' => 'object|array|null'], 'MongoDB\BSON\Javascript::getCode' => ['string'], 'MongoDB\BSON\Javascript::getScope' => ['?object'], 'MongoDB\BSON\Javascript::__toString' => ['string'], 'MongoDB\BSON\Javascript::serialize' => ['string'], - 'MongoDB\BSON\Javascript::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Javascript::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Javascript::jsonSerialize' => ['mixed'], 'MongoDB\BSON\JavascriptInterface::getCode' => ['string'], 'MongoDB\BSON\JavascriptInterface::getScope' => ['?object'], 'MongoDB\BSON\JavascriptInterface::__toString' => ['string'], 'MongoDB\BSON\MaxKey::serialize' => ['string'], - 'MongoDB\BSON\MaxKey::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\MaxKey::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\MaxKey::jsonSerialize' => ['mixed'], 'MongoDB\BSON\MinKey::serialize' => ['string'], - 'MongoDB\BSON\MinKey::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\MinKey::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\MinKey::jsonSerialize' => ['mixed'], 'MongoDB\BSON\ObjectId::__construct' => ['void', 'id=' => '?string'], 'MongoDB\BSON\ObjectId::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectId::__toString' => ['string'], 'MongoDB\BSON\ObjectId::serialize' => ['string'], - 'MongoDB\BSON\ObjectId::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\ObjectId::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\ObjectId::jsonSerialize' => ['mixed'], 'MongoDB\BSON\ObjectIdInterface::getTimestamp' => ['int'], 'MongoDB\BSON\ObjectIdInterface::__toString' => ['string'], @@ -4018,7 +4018,7 @@ 'MongoDB\BSON\Regex::getFlags' => ['string'], 'MongoDB\BSON\Regex::__toString' => ['string'], 'MongoDB\BSON\Regex::serialize' => ['string'], - 'MongoDB\BSON\Regex::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Regex::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Regex::jsonSerialize' => ['mixed'], 'MongoDB\BSON\RegexInterface::getPattern' => ['string'], 'MongoDB\BSON\RegexInterface::getFlags' => ['string'], @@ -4026,14 +4026,14 @@ 'MongoDB\BSON\Serializable::bsonSerialize' => ['object|array'], 'MongoDB\BSON\Symbol::__toString' => ['string'], 'MongoDB\BSON\Symbol::serialize' => ['string'], - 'MongoDB\BSON\Symbol::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Symbol::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Symbol::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Timestamp::__construct' => ['void', 'increment' => 'string|int', 'timestamp' => 'string|int'], 'MongoDB\BSON\Timestamp::getTimestamp' => ['int'], 'MongoDB\BSON\Timestamp::getIncrement' => ['int'], 'MongoDB\BSON\Timestamp::__toString' => ['string'], 'MongoDB\BSON\Timestamp::serialize' => ['string'], - 'MongoDB\BSON\Timestamp::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Timestamp::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Timestamp::jsonSerialize' => ['mixed'], 'MongoDB\BSON\TimestampInterface::getTimestamp' => ['int'], 'MongoDB\BSON\TimestampInterface::getIncrement' => ['int'], @@ -4042,13 +4042,13 @@ 'MongoDB\BSON\UTCDateTime::toDateTime' => ['DateTime'], 'MongoDB\BSON\UTCDateTime::__toString' => ['string'], 'MongoDB\BSON\UTCDateTime::serialize' => ['string'], - 'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\UTCDateTime::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\UTCDateTime::jsonSerialize' => ['mixed'], 'MongoDB\BSON\UTCDateTimeInterface::toDateTime' => ['DateTime'], 'MongoDB\BSON\UTCDateTimeInterface::__toString' => ['string'], 'MongoDB\BSON\Undefined::__toString' => ['string'], 'MongoDB\BSON\Undefined::serialize' => ['string'], - 'MongoDB\BSON\Undefined::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\BSON\Undefined::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\BSON\Undefined::jsonSerialize' => ['mixed'], 'MongoDB\BSON\Unserializable::bsonUnserialize' => ['void', 'data' => 'array'], 'MongoDB\Driver\BulkWrite::__construct' => ['void', 'options=' => '?array'], @@ -4080,7 +4080,7 @@ 'MongoDB\Driver\Cursor::valid' => ['bool'], 'MongoDB\Driver\CursorId::__toString' => ['string'], 'MongoDB\Driver\CursorId::serialize' => ['string'], - 'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\Driver\CursorId::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\Driver\CursorInterface::getId' => ['MongoDB\Driver\CursorId'], 'MongoDB\Driver\CursorInterface::getServer' => ['MongoDB\Driver\Server'], 'MongoDB\Driver\CursorInterface::isDead' => ['bool'], @@ -4193,7 +4193,7 @@ 'MongoDB\Driver\ReadConcern::isDefault' => ['bool'], 'MongoDB\Driver\ReadConcern::bsonSerialize' => ['object|array'], 'MongoDB\Driver\ReadConcern::serialize' => ['string'], - 'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\Driver\ReadConcern::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\Driver\ReadPreference::__construct' => ['void', 'mode' => 'string|int', 'tagSets=' => '?array', 'options=' => '?array'], 'MongoDB\Driver\ReadPreference::getHedge' => ['?object'], 'MongoDB\Driver\ReadPreference::getMaxStalenessSeconds' => ['int'], @@ -4202,7 +4202,7 @@ 'MongoDB\Driver\ReadPreference::getTagSets' => ['array'], 'MongoDB\Driver\ReadPreference::bsonSerialize' => ['object|array'], 'MongoDB\Driver\ReadPreference::serialize' => ['string'], - 'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\Driver\ReadPreference::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\Driver\Server::executeBulkWrite' => ['MongoDB\Driver\WriteResult', 'namespace' => 'string', 'bulkWrite' => 'MongoDB\Driver\BulkWrite', 'options=' => 'MongoDB\Driver\WriteConcern|array|null'], 'MongoDB\Driver\Server::executeCommand' => ['MongoDB\Driver\Cursor', 'db' => 'string', 'command' => 'MongoDB\Driver\Command', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], 'MongoDB\Driver\Server::executeQuery' => ['MongoDB\Driver\Cursor', 'namespace' => 'string', 'query' => 'MongoDB\Driver\Query', 'options=' => 'MongoDB\Driver\ReadPreference|array|null'], @@ -4224,7 +4224,7 @@ 'MongoDB\Driver\ServerApi::__construct' => ['void', 'version' => 'string', 'strict=' => '?bool', 'deprecationErrors=' => '?bool'], 'MongoDB\Driver\ServerApi::bsonSerialize' => ['object|array'], 'MongoDB\Driver\ServerApi::serialize' => ['string'], - 'MongoDB\Driver\ServerApi::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\Driver\ServerApi::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\Driver\ServerDescription::getHelloResponse' => ['array'], 'MongoDB\Driver\ServerDescription::getHost' => ['string'], 'MongoDB\Driver\ServerDescription::getLastUpdateTime' => ['int'], @@ -4256,7 +4256,7 @@ 'MongoDB\Driver\WriteConcern::isDefault' => ['bool'], 'MongoDB\Driver\WriteConcern::bsonSerialize' => ['object|array'], 'MongoDB\Driver\WriteConcern::serialize' => ['string'], - 'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized' => ''], + 'MongoDB\Driver\WriteConcern::unserialize' => ['void', 'serialized' => 'string'], 'MongoDB\Driver\WriteConcernError::getCode' => ['int'], 'MongoDB\Driver\WriteConcernError::getInfo' => ['?object'], 'MongoDB\Driver\WriteConcernError::getMessage' => ['string'], From d7097281ba7b8bbabf7ea3fe038f85db7c6fd266 Mon Sep 17 00:00:00 2001 From: Aleksandr Zhuravlev Date: Wed, 31 Aug 2022 21:02:20 +1200 Subject: [PATCH 075/194] trim(), ltrim(), rtrim() now keep lowercase string attribute --- stubs/CoreGenericFunctions.phpstub | 14 +++++++++++++- tests/FunctionCallTest.php | 27 +++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index 720ba46e1d8..6cc5eec57a4 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -566,6 +566,12 @@ function strpos($haystack, $needle, int $offset = 0) : int {} /** * @psalm-pure * + * @return ( + * $string is class-string + * ? ($characters is '\\' ? class-string : string) + * : ($string is lowercase-string ? lowercase-string : string) + * ) + * * @psalm-flow ($string) -> return */ function trim(string $string, string $characters = " \t\n\r\0\x0B") : string {} @@ -573,7 +579,11 @@ function trim(string $string, string $characters = " \t\n\r\0\x0B") : string {} /** * @psalm-pure * - * @return ($string is class-string ? ($characters is '\\' ? class-string : string) : string) + * @return ( + * $string is class-string + * ? ($characters is '\\' ? class-string : string) + * : ($string is lowercase-string ? lowercase-string : string) + * ) * * @psalm-flow ($string) -> return */ @@ -582,6 +592,8 @@ function ltrim(string $string, string $characters = " \t\n\r\0\x0B") : string {} /** * @psalm-pure * + * @return ($string is lowercase-string ? lowercase-string : string) + * * @psalm-flow ($string) -> return */ function rtrim(string $string, string $characters = " \t\n\r\0\x0B") : string {} diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 955cc9b13e3..be4f4c32b17 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1785,6 +1785,33 @@ function sayHello(string $needle): void { [], '8.0', ], + 'trimSavesLowercaseAttribute' => [ + ' [ + '$b===' => 'lowercase-string', + ], + ], + 'ltrimSavesLowercaseAttribute' => [ + ' [ + '$b===' => 'lowercase-string', + ], + ], + 'rtrimSavesLowercaseAttribute' => [ + ' [ + '$b===' => 'lowercase-string', + ], + ], ]; } From 969c7a098be545cf453687ed86a68899d9a3781c Mon Sep 17 00:00:00 2001 From: Semyon <7ionmail@gmail.com> Date: Fri, 2 Sep 2022 17:37:10 +0300 Subject: [PATCH 076/194] Make ctype_digit and ctype_lower work as assertions --- .../Statements/Expression/AssertionFinder.php | 2 + tests/TypeReconciliation/ConditionalTest.php | 52 +++++++++++++++++++ 2 files changed, 54 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 0fa53eaf5e5..1dcc97346a9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -103,6 +103,8 @@ class AssertionFinder 'is_scalar' => ['scalar', [Type::class, 'getScalar']], 'is_iterable' => ['iterable'], 'is_countable' => ['countable'], + 'ctype_digit' => ['numeric-string', [Type::class, 'getNumericString']], + 'ctype_lower' => ['non-empty-lowercase-string', [Type::class, 'getNonEmptyLowercaseString']], ]; /** diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index ac5ee27f322..295597be4a3 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -2848,6 +2848,58 @@ function matches(string $value): bool { return true; }' ], + 'ctypeDigitMakesStringNumeric' => [ + ' [ + ' [ + '$int' => 'int<48, 57>|int<256, 1000>' + ] + ], + 'ctypeLowerMakesStringLowercase' => [ + ' [ + ' [ + '$int' => 'int<97, 122>' + ] + ], ]; } From a6710791e42aada572fb0b0f43e62eff22d68b11 Mon Sep 17 00:00:00 2001 From: Alex <93376818+sashashura@users.noreply.github.com> Date: Fri, 2 Sep 2022 19:16:42 +0100 Subject: [PATCH 077/194] Update build-phar.yml Signed-off-by: sashashura <93376818+sashashura@users.noreply.github.com> --- .github/workflows/build-phar.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/build-phar.yml b/.github/workflows/build-phar.yml index 357a36011bf..ffbf8aacfe4 100644 --- a/.github/workflows/build-phar.yml +++ b/.github/workflows/build-phar.yml @@ -8,8 +8,13 @@ on: types: - published +permissions: + contents: read + jobs: pre_job: + permissions: + actions: write runs-on: ubuntu-latest outputs: should_skip: ${{ steps.skip_check.outputs.should_skip }} @@ -24,6 +29,8 @@ jobs: paths: '["bin/**", "assets/**", "build/**", "dictionaries/**", "src/**", "stubs/**", "psalm", "psalm-language-server", "psalm-plugin", "psalm-refactor", "psalter", "box.json.dist", "composer.json", "config.xsd", "keys.asc.gpg", "scoper.inc.php"]' build-phar: + permissions: + contents: write # for release needs: pre_job if: ${{ needs.pre_job.outputs.should_skip != 'true' }} runs-on: ubuntu-latest From f0a8810cf50b2afdea562282f702aa098106a062 Mon Sep 17 00:00:00 2001 From: Semyon <7ionmail@gmail.com> Date: Wed, 7 Sep 2022 15:49:35 +0300 Subject: [PATCH 078/194] Fix ctype_digit assertion bug --- .../Statements/Expression/AssertionFinder.php | 2 +- tests/TypeReconciliation/ConditionalTest.php | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 1dcc97346a9..1603d1655dd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -103,7 +103,7 @@ class AssertionFinder 'is_scalar' => ['scalar', [Type::class, 'getScalar']], 'is_iterable' => ['iterable'], 'is_countable' => ['countable'], - 'ctype_digit' => ['numeric-string', [Type::class, 'getNumericString']], + 'ctype_digit' => ['=numeric-string', [Type::class, 'getNumericString']], 'ctype_lower' => ['non-empty-lowercase-string', [Type::class, 'getNonEmptyLowercaseString']], ]; diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index 295597be4a3..3b7d0148bd5 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -2862,6 +2862,20 @@ function bar(mixed $m): void } ', ], + 'ctypeDigitMakesStringNumericButDoesntProveOtherwise' => [ + ' [ ' Date: Wed, 7 Sep 2022 13:20:31 +0200 Subject: [PATCH 079/194] report invalidCasing when using a class that is not user defined too (e.g. new DateTime) --- .../Internal/Analyzer/ClassLikeAnalyzer.php | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index 0b4934e8de0..51387fc2cdf 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -361,16 +361,14 @@ public static function checkFullyQualifiedClassLikeName( || ($interface_exists && !$codebase->interfaceHasCorrectCasing($fq_class_name)) || ($enum_exists && !$codebase->classlikes->enumHasCorrectCasing($fq_class_name)) ) { - if ($codebase->classlikes->isUserDefined(strtolower($aliased_name))) { - IssueBuffer::maybeAdd( - new InvalidClass( - 'Class, interface or enum ' . $fq_class_name . ' has wrong casing', - $code_location, - $fq_class_name - ), - $suppressed_issues - ); - } + IssueBuffer::maybeAdd( + new InvalidClass( + 'Class, interface or enum ' . $fq_class_name . ' has wrong casing', + $code_location, + $fq_class_name + ), + $suppressed_issues + ); } } From 1a10654cb3fa5a3a8eac30b3d59520bf0cd00c3d Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 8 Sep 2022 12:01:14 +0200 Subject: [PATCH 080/194] fix tests --- tests/IntRangeTest.php | 2 +- tests/MethodCallTest.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/IntRangeTest.php b/tests/IntRangeTest.php index 5aa37992772..7d3491b73f8 100644 --- a/tests/IntRangeTest.php +++ b/tests/IntRangeTest.php @@ -687,7 +687,7 @@ function doAnalysis(): void /** @var string $secret */ $length = strlen($secret); if ($length > 16) { - throw new exception(""); + throw new Exception(""); } assert($length === 1); diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 62860e8f209..f30f1115fdc 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -979,7 +979,7 @@ public static function new() : self { class Datetime extends \DateTime { - public static function createFromInterface(\DatetimeInterface $datetime): \DateTime + public static function createFromInterface(\DateTimeInterface $datetime): \DateTime { return parent::createFromInterface($datetime); } From 15046c932b59b8c817f16601507cc81321d5997a Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 8 Sep 2022 18:51:33 +0200 Subject: [PATCH 081/194] preg_replace with anchor will always only have 1 replacement, add limit for clarity and performance --- src/Psalm/Codebase.php | 2 +- src/Psalm/Config.php | 8 ++++---- src/Psalm/Config/Creator.php | 2 +- src/Psalm/Config/FileFilter.php | 2 +- src/Psalm/Context.php | 2 +- src/Psalm/DocComment.php | 8 ++++---- .../Internal/Analyzer/ClassLikeAnalyzer.php | 2 +- .../Internal/Analyzer/NamespaceAnalyzer.php | 2 +- .../Expression/Call/FunctionCallAnalyzer.php | 2 +- .../Statements/Expression/CallAnalyzer.php | 4 ++-- src/Psalm/Internal/Cli/LanguageServer.php | 2 +- src/Psalm/Internal/Cli/Psalm.php | 2 +- src/Psalm/Internal/Cli/Psalter.php | 2 +- src/Psalm/Internal/Cli/Refactor.php | 2 +- src/Psalm/Internal/Codebase/Analyzer.php | 2 +- src/Psalm/Internal/Codebase/ClassLikes.php | 4 ++-- src/Psalm/Internal/Codebase/Properties.php | 16 ++++++++-------- src/Psalm/Internal/MethodIdentifier.php | 4 ++-- .../Reflector/ClassLikeDocblockParser.php | 2 +- .../Reflector/ClassLikeNodeScanner.php | 4 ++-- .../Reflector/FunctionLikeDocblockParser.php | 6 +++--- src/Psalm/Internal/Scanner/DocblockParser.php | 4 ++-- 22 files changed, 42 insertions(+), 42 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index cb72efa8b76..1de35369467 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1669,7 +1669,7 @@ public function getCompletionItemsForPartialSymbol( ) { $file_contents = $this->getFileContents($file_path); - $class_name = preg_replace('/^.*\\\/', '', $fq_class_name); + $class_name = preg_replace('/^.*\\\/', '', $fq_class_name, 1); if ($aliases->uses_end) { $position = self::getPositionFromOffset($aliases->uses_end, $file_contents); diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index d9d81bfd5fb..0f6ef130ca8 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -1313,7 +1313,7 @@ public function setCustomErrorLevel(string $issue_key, string $error_level): voi private function loadFileExtensions(SimpleXMLElement $extensions): void { foreach ($extensions as $extension) { - $extension_name = preg_replace('/^\.?/', '', (string)$extension['name']); + $extension_name = preg_replace('/^\.?/', '', (string)$extension['name'], 1); $this->file_extensions[] = $extension_name; if (isset($extension['scanner'])) { @@ -1507,7 +1507,7 @@ private function getPluginClassForPath(Codebase $codebase, string $path, string public function shortenFileName(string $to): string { if (!is_file($to)) { - return preg_replace('/^' . preg_quote($this->base_dir, '/') . '/', '', $to); + return preg_replace('/^' . preg_quote($this->base_dir, '/') . '/', '', $to, 1); } $from = $this->base_dir; @@ -1679,7 +1679,7 @@ public static function getParentIssueType(string $issue_type): ?string } if (strpos($issue_type, 'Possibly') === 0) { - $stripped_issue_type = preg_replace('/^Possibly(False|Null)?/', '', $issue_type); + $stripped_issue_type = preg_replace('/^Possibly(False|Null)?/', '', $issue_type, 1); if (strpos($stripped_issue_type, 'Invalid') === false && strpos($stripped_issue_type, 'Un') !== 0) { $stripped_issue_type = 'Invalid' . $stripped_issue_type; @@ -1693,7 +1693,7 @@ public static function getParentIssueType(string $issue_type): ?string } if (preg_match('/^(False|Null)[A-Z]/', $issue_type) && !strpos($issue_type, 'Reference')) { - return preg_replace('/^(False|Null)/', 'Invalid', $issue_type); + return preg_replace('/^(False|Null)/', 'Invalid', $issue_type, 1); } if ($issue_type === 'UndefinedInterfaceMethod') { diff --git a/src/Psalm/Config/Creator.php b/src/Psalm/Config/Creator.php index f67eb5cf3cd..5a541dca888 100644 --- a/src/Psalm/Config/Creator.php +++ b/src/Psalm/Config/Creator.php @@ -242,7 +242,7 @@ private static function getPsr4Or0Paths(string $current_dir, array $composer_jso continue; } - $path = preg_replace('@[\\\\/]$@', '', $path); + $path = preg_replace('@[/\\\]$@', '', $path, 1); if ($path !== 'tests') { $nodes[] = ''; diff --git a/src/Psalm/Config/FileFilter.php b/src/Psalm/Config/FileFilter.php index 76f53d6be2e..de83e148285 100644 --- a/src/Psalm/Config/FileFilter.php +++ b/src/Psalm/Config/FileFilter.php @@ -421,7 +421,7 @@ function (): bool { */ protected static function slashify(string $str): string { - return preg_replace('/\/?$/', DIRECTORY_SEPARATOR, $str); + return rtrim( $str, DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR; } public function allows(string $file_name, bool $case_sensitive = false): bool diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php index 73e642af92e..1acfb9d7d37 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -752,7 +752,7 @@ public function hasVariable(string $var_name): bool return false; } - $stripped_var = preg_replace('/(->|\[).*$/', '', $var_name); + $stripped_var = preg_replace('/(->|\[).*$/', '', $var_name, 1); if ($stripped_var !== '$this' || $var_name !== $stripped_var) { $this->referenced_var_ids[$var_name] = true; diff --git a/src/Psalm/DocComment.php b/src/Psalm/DocComment.php index ae3cd6ca32a..f9cbc8287c6 100644 --- a/src/Psalm/DocComment.php +++ b/src/Psalm/DocComment.php @@ -64,9 +64,9 @@ public static function parse(string $docblock, ?int $line_number = null, bool $p { // Strip off comments. $docblock = trim($docblock); - $docblock = preg_replace('@^/\*\*@', '', $docblock); - $docblock = preg_replace('@\*/$@', '', $docblock); - $docblock = preg_replace('@^[ \t]*\*@m', '', $docblock); + $docblock = preg_replace('@^/\*\*@', '', $docblock, 1); + $docblock = preg_replace('@\*/$@', '', $docblock, 1); + $docblock = preg_replace('@^[ \t]*\*@m', '', $docblock, 1); // Normalize multi-line @specials. $lines = explode("\n", $docblock); @@ -157,7 +157,7 @@ public static function parse(string $docblock, ?int $line_number = null, bool $p // Trim any empty lines off the front, but leave the indent level if there // is one. - $docblock = preg_replace('/^\s*\n/', '', $docblock); + $docblock = preg_replace('/^\s*\n/', '', $docblock, 1); foreach ($special as $special_key => $_) { if (strpos($special_key, 'psalm-') === 0) { diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index 0b4934e8de0..ca4b7e9f986 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -231,7 +231,7 @@ public static function checkFullyQualifiedClassLikeName( return null; } - $fq_class_name = preg_replace('/^\\\/', '', $fq_class_name); + $fq_class_name = preg_replace('/^\\\/', '', $fq_class_name, 1); if (in_array($fq_class_name, ['callable', 'iterable', 'self', 'static', 'parent'], true)) { return true; diff --git a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php index 06413e6bbbf..a3d658b6e01 100644 --- a/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/NamespaceAnalyzer.php @@ -221,7 +221,7 @@ public static function isWithinAny(string $calling_identifier, array $identifier */ public static function getNameSpaceRoot(string $fullyQualifiedClassName): string { - $root_namespace = preg_replace('/^([^\\\]+).*/', '$1', $fullyQualifiedClassName); + $root_namespace = preg_replace('/^([^\\\]+).*/', '$1', $fullyQualifiedClassName, 1); if ($root_namespace === "") { throw new InvalidArgumentException("Invalid classname \"$fullyQualifiedClassName\""); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index d98cf1fc825..96f26a09497 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -735,7 +735,7 @@ private static function getAnalyzeNamedExpression( if (strpos($var_type_part->value, '::')) { $parts = explode('::', strtolower($var_type_part->value)); $fq_class_name = $parts[0]; - $fq_class_name = preg_replace('/^\\\\/', '', $fq_class_name); + $fq_class_name = preg_replace('/^\\\/', '', $fq_class_name, 1); $potential_method_id = new MethodIdentifier($fq_class_name, $parts[1]); } else { $function_call_info->new_function_name = new VirtualFullyQualified( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 9cc9d85098e..e5c7942365a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -522,7 +522,7 @@ public static function getFunctionIdsFromCallableArg( } if ($callable_arg instanceof PhpParser\Node\Scalar\String_) { - $potential_id = preg_replace('/^\\\/', '', $callable_arg->value); + $potential_id = preg_replace('/^\\\/', '', $callable_arg->value, 1); if (preg_match('/^[A-Za-z0-9_]+(\\\[A-Za-z0-9_]+)*(::[A-Za-z0-9_]+)?$/', $potential_id)) { return [$potential_id]; @@ -552,7 +552,7 @@ public static function getFunctionIdsFromCallableArg( } if ($class_arg instanceof PhpParser\Node\Scalar\String_) { - return [preg_replace('/^\\\/', '', $class_arg->value) . '::' . $method_name_arg->value]; + return [preg_replace('/^\\\/', '', $class_arg->value, 1) . '::' . $method_name_arg->value]; } if ($class_arg instanceof PhpParser\Node\Expr\ClassConstFetch diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index e8689ee968c..634cdee16a4 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -93,7 +93,7 @@ public static function run(array $argv): void array_map( function (string $arg) use ($valid_long_options): void { if (strpos($arg, '--') === 0 && $arg !== '--') { - $arg_name = preg_replace('/=.*$/', '', substr($arg, 2)); + $arg_name = preg_replace('/=.*$/', '', substr($arg, 2), 1); if (!in_array($arg_name, $valid_long_options, true) && !in_array($arg_name . ':', $valid_long_options, true) diff --git a/src/Psalm/Internal/Cli/Psalm.php b/src/Psalm/Internal/Cli/Psalm.php index 86031b7b614..a01a47aa05b 100644 --- a/src/Psalm/Internal/Cli/Psalm.php +++ b/src/Psalm/Internal/Cli/Psalm.php @@ -431,7 +431,7 @@ private static function validateCliArguments(array $args): void array_map( function (string $arg): void { if (strpos($arg, '--') === 0 && $arg !== '--') { - $arg_name = preg_replace('/=.*$/', '', substr($arg, 2)); + $arg_name = preg_replace('/=.*$/', '', substr($arg, 2), 1); if (!in_array($arg_name, self::LONG_OPTIONS) && !in_array($arg_name . ':', self::LONG_OPTIONS) diff --git a/src/Psalm/Internal/Cli/Psalter.php b/src/Psalm/Internal/Cli/Psalter.php index a29491475cf..7db204c5f61 100644 --- a/src/Psalm/Internal/Cli/Psalter.php +++ b/src/Psalm/Internal/Cli/Psalter.php @@ -432,7 +432,7 @@ private static function validateCliArguments(array $args): void array_map( function (string $arg): void { if (strpos($arg, '--') === 0 && $arg !== '--') { - $arg_name = preg_replace('/=.*$/', '', substr($arg, 2)); + $arg_name = preg_replace('/=.*$/', '', substr($arg, 2), 1); if ($arg_name === 'alter') { // valid option for psalm, ignored by psalter diff --git a/src/Psalm/Internal/Cli/Refactor.php b/src/Psalm/Internal/Cli/Refactor.php index 1c87f4e84a2..864e9d4aebd 100644 --- a/src/Psalm/Internal/Cli/Refactor.php +++ b/src/Psalm/Internal/Cli/Refactor.php @@ -82,7 +82,7 @@ public static function run(array $argv): void array_map( function (string $arg) use ($valid_long_options): void { if (strpos($arg, '--') === 0 && $arg !== '--') { - $arg_name = preg_replace('/=.*$/', '', substr($arg, 2)); + $arg_name = preg_replace('/=.*$/', '', substr($arg, 2), 1); if ($arg_name === 'refactor') { // valid option for psalm, ignored by psalter diff --git a/src/Psalm/Internal/Codebase/Analyzer.php b/src/Psalm/Internal/Codebase/Analyzer.php index 36c927131d3..a2d06d4eaaa 100644 --- a/src/Psalm/Internal/Codebase/Analyzer.php +++ b/src/Psalm/Internal/Codebase/Analyzer.php @@ -773,7 +773,7 @@ public function loadCachedResults(ProjectAnalyzer $project_analyzer): void $method_param_uses[$member_id] ); - $member_stub = preg_replace('/::.*$/', '::*', $member_id); + $member_stub = preg_replace('/::.*$/', '::*', $member_id, 1); if (isset($all_referencing_methods[$member_stub])) { $newly_invalidated_methods = array_merge( diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index 372d1da901f..659e421ecf8 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -185,7 +185,7 @@ private function collectPredefinedClassLikes(): void $predefined_classes = get_declared_classes(); foreach ($predefined_classes as $predefined_class) { - $predefined_class = preg_replace('/^\\\/', '', $predefined_class); + $predefined_class = preg_replace('/^\\\/', '', $predefined_class, 1); /** @psalm-suppress ArgumentTypeCoercion */ $reflection_class = new ReflectionClass($predefined_class); @@ -201,7 +201,7 @@ private function collectPredefinedClassLikes(): void $predefined_interfaces = get_declared_interfaces(); foreach ($predefined_interfaces as $predefined_interface) { - $predefined_interface = preg_replace('/^\\\/', '', $predefined_interface); + $predefined_interface = preg_replace('/^\\\/', '', $predefined_interface, 1); /** @psalm-suppress ArgumentTypeCoercion */ $reflection_class = new ReflectionClass($predefined_interface); diff --git a/src/Psalm/Internal/Codebase/Properties.php b/src/Psalm/Internal/Codebase/Properties.php index 2a0137a5db6..ba5604ea850 100644 --- a/src/Psalm/Internal/Codebase/Properties.php +++ b/src/Psalm/Internal/Codebase/Properties.php @@ -83,8 +83,8 @@ public function propertyExists( ?Context $context = null, ?CodeLocation $code_location = null ): bool { - // remove trailing backslash if it exists - $property_id = preg_replace('/^\\\\/', '', $property_id); + // remove leading backslash if it exists + $property_id = ltrim( $property_id, '\\' ); [$fq_class_name, $property_name] = explode('::$', $property_id); $fq_class_name_lc = strtolower($fq_class_name); @@ -248,8 +248,8 @@ public function getAppearingClassForProperty( public function getStorage(string $property_id): PropertyStorage { - // remove trailing backslash if it exists - $property_id = preg_replace('/^\\\\/', '', $property_id); + // remove leading backslash if it exists + $property_id = ltrim( $property_id, '\\' ); [$fq_class_name, $property_name] = explode('::$', $property_id); @@ -269,8 +269,8 @@ public function getStorage(string $property_id): PropertyStorage public function hasStorage(string $property_id): bool { - // remove trailing backslash if it exists - $property_id = preg_replace('/^\\\\/', '', $property_id); + // remove leading backslash if it exists + $property_id = ltrim( $property_id, '\\' ); [$fq_class_name, $property_name] = explode('::$', $property_id); @@ -291,8 +291,8 @@ public function getPropertyType( ?StatementsSource $source = null, ?Context $context = null ): ?Union { - // remove trailing backslash if it exists - $property_id = preg_replace('/^\\\\/', '', $property_id); + // remove leading backslash if it exists + $property_id = ltrim( $property_id, '\\' ); [$fq_class_name, $property_name] = explode('::$', $property_id); diff --git a/src/Psalm/Internal/MethodIdentifier.php b/src/Psalm/Internal/MethodIdentifier.php index cf4c81ba63d..1abb0ecded1 100644 --- a/src/Psalm/Internal/MethodIdentifier.php +++ b/src/Psalm/Internal/MethodIdentifier.php @@ -56,8 +56,8 @@ public static function fromMethodIdReference(string $method_id): self if (!static::isValidMethodIdReference($method_id)) { throw new InvalidArgumentException('Invalid method id reference provided: ' . $method_id); } - // remove trailing backslash if it exists - $method_id = preg_replace('/^\\\\/', '', $method_id); + // remove leading backslash if it exists + $method_id = ltrim($method_id, '\\'); $method_id_parts = explode('::', $method_id); return new self($method_id_parts[0], strtolower($method_id_parts[1])); } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php index 4572df37da3..aa2adb811c0 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -506,7 +506,7 @@ protected static function addMagicPropertyToInfo( ) { $line_parts[1] = str_replace('&', '', $line_parts[1]); - $line_parts[1] = preg_replace('/,$/', '', $line_parts[1]); + $line_parts[1] = preg_replace('/,$/', '', $line_parts[1], 1); $end = $offset + strlen($line_parts[0]); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 91fbfbfee14..656bab44db9 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -1838,8 +1838,8 @@ private static function getTypeAliasesFromCommentLines( $type_string = str_replace("\n", '', implode('', $var_line_parts)); - $type_string = preg_replace('/>[^>^\}]*$/', '>', $type_string); - $type_string = preg_replace('/\}[^>^\}]*$/', '}', $type_string); + $type_string = preg_replace('/>[^>^\}]*$/', '>', $type_string, 1); + $type_string = preg_replace('/\}[^>^\}]*$/', '}', $type_string, 1); try { $type_tokens = TypeTokenizer::getFullyQualifiedTokens( diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index 341d0f63918..2eb713b9458 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -76,7 +76,7 @@ public static function parse( ) { $line_parts[1] = str_replace('&', '', $line_parts[1]); - $line_parts[1] = preg_replace('/,$/', '', $line_parts[1]); + $line_parts[1] = preg_replace('/,$/', '', $line_parts[1], 1); $end = $offset + strlen($line_parts[0]); @@ -152,7 +152,7 @@ public static function parse( throw new IncorrectDocblockException('Misplaced variable'); } - $line_parts[1] = preg_replace('/,$/', '', $line_parts[1]); + $line_parts[1] = preg_replace('/,$/', '', $line_parts[1], 1); $info->params_out[] = [ 'name' => trim($line_parts[1]), @@ -340,7 +340,7 @@ public static function parse( throw new IncorrectDocblockException('Misplaced variable'); } - $line_parts[1] = preg_replace('/,$/', '', $line_parts[1]); + $line_parts[1] = preg_replace('/,$/', '', $line_parts[1], 1); $info->globals[] = [ 'name' => $line_parts[1], diff --git a/src/Psalm/Internal/Scanner/DocblockParser.php b/src/Psalm/Internal/Scanner/DocblockParser.php index 2ddf134e62c..22ae1ed258f 100644 --- a/src/Psalm/Internal/Scanner/DocblockParser.php +++ b/src/Psalm/Internal/Scanner/DocblockParser.php @@ -111,7 +111,7 @@ public static function parse(string $docblock, int $offsetStart): ParsedDocblock // Strip the leading *, if present. $text = $lines[$k]; $text = str_replace("\t", ' ', $text); - $text = preg_replace('/^ *\*/', '', $text); + $text = preg_replace('/^ *\*/', '', $text, 1); $lines[$k] = $text; } @@ -142,7 +142,7 @@ public static function parse(string $docblock, int $offsetStart): ParsedDocblock // Trim any empty lines off the front, but leave the indent level if there // is one. - $docblock = preg_replace('/^\s*\n/', '', $docblock); + $docblock = preg_replace('/^\s*\n/', '', $docblock, 1); $parsed = new ParsedDocblock($docblock, $special, $first_line_padding ?: ''); From d0984f4e472f70da50b881f7b403cd6f1c126c42 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 8 Sep 2022 19:21:04 +0200 Subject: [PATCH 082/194] fix psalm internal errors reported by new checks --- src/Psalm/Config.php | 3 +-- src/Psalm/Internal/Codebase/ClassLikes.php | 8 -------- src/Psalm/Internal/PluginManager/ConfigFile.php | 10 +++++----- 3 files changed, 6 insertions(+), 15 deletions(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index d9d81bfd5fb..9ac69ea7d42 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -6,7 +6,7 @@ use Composer\Semver\Constraint\Constraint; use Composer\Semver\VersionParser; use DOMDocument; -use DomElement; +use DOMElement; use InvalidArgumentException; use LogicException; use OutOfBoundsException; @@ -752,7 +752,6 @@ private static function validateXmlConfig(string $base_dir, string $file_content $psalm_nodes = $dom_document->getElementsByTagName('psalm'); - /** @var DomElement|null */ $psalm_node = $psalm_nodes->item(0); if (!$psalm_node) { diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index 372d1da901f..d19b202fef6 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -784,14 +784,6 @@ public function traitHasCorrectCase(string $fq_trait_name): bool return isset($this->existing_traits[$fq_trait_name]); } - /** - * @param lowercase-string $fq_class_name - */ - public function isUserDefined(string $fq_class_name): bool - { - return $this->classlike_storage_provider->get($fq_class_name)->user_defined; - } - public function getTraitNode(string $fq_trait_name): PhpParser\Node\Stmt\Trait_ { $fq_trait_name_lc = strtolower($fq_trait_name); diff --git a/src/Psalm/Internal/PluginManager/ConfigFile.php b/src/Psalm/Internal/PluginManager/ConfigFile.php index a945dd24931..ccabdc12b39 100644 --- a/src/Psalm/Internal/PluginManager/ConfigFile.php +++ b/src/Psalm/Internal/PluginManager/ConfigFile.php @@ -3,7 +3,7 @@ namespace Psalm\Internal\PluginManager; use DOMDocument; -use DomElement; +use DOMElement; use Psalm\Config; use RuntimeException; @@ -51,7 +51,7 @@ public function getConfig(): Config public function removePlugin(string $plugin_class): void { $config_xml = $this->readXml(); - /** @var DomElement */ + /** @var DOMElement */ $psalm_root = $config_xml->getElementsByTagName('psalm')[0]; $plugins_elements = $psalm_root->getElementsByTagName('plugins'); if (!$plugins_elements->length) { @@ -59,7 +59,7 @@ public function removePlugin(string $plugin_class): void return; } - /** @var DomElement */ + /** @var DOMElement */ $plugins_element = $plugins_elements->item(0); $plugin_elements = $plugins_element->getElementsByTagName('pluginClass'); @@ -82,7 +82,7 @@ public function removePlugin(string $plugin_class): void public function addPlugin(string $plugin_class): void { $config_xml = $this->readXml(); - /** @var DomElement */ + /** @var DOMElement */ $psalm_root = $config_xml->getElementsByTagName('psalm')->item(0); $plugins_elements = $psalm_root->getElementsByTagName('plugins'); if (!$plugins_elements->length) { @@ -91,7 +91,7 @@ public function addPlugin(string $plugin_class): void $psalm_root->appendChild($plugins_element); } } else { - /** @var DomElement */ + /** @var DOMElement */ $plugins_element = $plugins_elements->item(0); } From 249d61ec1b19550878c3598bcb6e94f79cad6754 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 8 Sep 2022 14:02:26 +0200 Subject: [PATCH 083/194] classlike_alias incorrect casing not handled correctly --- src/Psalm/Internal/Codebase/ClassLikes.php | 23 +++++++++++-------- src/Psalm/Internal/Codebase/Scanner.php | 4 ++-- .../Reflector/ExpressionScanner.php | 2 -- src/Psalm/Storage/FileStorage.php | 2 +- 4 files changed, 16 insertions(+), 15 deletions(-) diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index f81e888620f..65422140d98 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -131,7 +131,12 @@ class ClassLikes /** * @var array */ - private $classlike_aliases = []; + private $classlike_aliases_map = []; + + /** + * @var array + */ + private $existing_classlike_aliases = []; /** * @var array @@ -750,7 +755,7 @@ public function classHasCorrectCasing(string $fq_class_name): bool return true; } - if (isset($this->classlike_aliases[strtolower($fq_class_name)])) { + if (isset($this->existing_classlike_aliases[$fq_class_name])) { return true; } @@ -759,7 +764,7 @@ public function classHasCorrectCasing(string $fq_class_name): bool public function interfaceHasCorrectCasing(string $fq_interface_name): bool { - if (isset($this->classlike_aliases[strtolower($fq_interface_name)])) { + if (isset($this->existing_classlike_aliases[$fq_interface_name])) { return true; } @@ -768,7 +773,7 @@ public function interfaceHasCorrectCasing(string $fq_interface_name): bool public function enumHasCorrectCasing(string $fq_enum_name): bool { - if (isset($this->classlike_aliases[strtolower($fq_enum_name)])) { + if (isset($this->existing_classlike_aliases[$fq_enum_name])) { return true; } @@ -777,7 +782,7 @@ public function enumHasCorrectCasing(string $fq_enum_name): bool public function traitHasCorrectCase(string $fq_trait_name): bool { - if (isset($this->classlike_aliases[strtolower($fq_trait_name)])) { + if (isset($this->existing_classlike_aliases[$fq_trait_name])) { return true; } @@ -820,12 +825,10 @@ public function getTraitNode(string $fq_trait_name): PhpParser\Node\Stmt\Trait_ throw new UnexpectedValueException('Could not locate trait statement'); } - /** - * @param lowercase-string $alias_name - */ public function addClassAlias(string $fq_class_name, string $alias_name): void { - $this->classlike_aliases[$alias_name] = $fq_class_name; + $this->classlike_aliases_map[strtolower($alias_name)] = $fq_class_name; + $this->existing_classlike_aliases[$alias_name] = true; } public function getUnAliasedName(string $alias_name): string @@ -835,7 +838,7 @@ public function getUnAliasedName(string $alias_name): string return $alias_name; } - $result = $this->classlike_aliases[$alias_name_lc] ?? $alias_name; + $result = $this->classlike_aliases_map[$alias_name_lc] ?? $alias_name; if ($result === $alias_name) { return $result; } diff --git a/src/Psalm/Internal/Codebase/Scanner.php b/src/Psalm/Internal/Codebase/Scanner.php index fb6e1bf5c63..b45044a0431 100644 --- a/src/Psalm/Internal/Codebase/Scanner.php +++ b/src/Psalm/Internal/Codebase/Scanner.php @@ -605,7 +605,7 @@ private function scanFile( } foreach ($file_storage->classlikes_in_file as $fq_classlike_name) { - $this->codebase->exhumeClassLikeStorage(strtolower($fq_classlike_name), $file_path); + $this->codebase->exhumeClassLikeStorage($fq_classlike_name, $file_path); } foreach ($file_storage->required_classes as $fq_classlike_name) { @@ -736,7 +736,7 @@ function () use ($fq_class_name): ?ReflectionClass { $new_fq_class_name_lc = strtolower($new_fq_class_name); if ($new_fq_class_name_lc !== $fq_class_name_lc) { - $classlikes->addClassAlias($new_fq_class_name, $fq_class_name_lc); + $classlikes->addClassAlias($new_fq_class_name, $fq_class_name); $fq_class_name_lc = $new_fq_class_name_lc; } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php index 2a6dbf71eb9..3cc4bd7350b 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ExpressionScanner.php @@ -257,8 +257,6 @@ private static function registerClassMapFunctionCall( $second_arg_value = substr($second_arg_value, 1); } - $second_arg_value = strtolower($second_arg_value); - $codebase->classlikes->addClassAlias( $first_arg_value, $second_arg_value diff --git a/src/Psalm/Storage/FileStorage.php b/src/Psalm/Storage/FileStorage.php index df09117dbea..fcde3b3a6ad 100644 --- a/src/Psalm/Storage/FileStorage.php +++ b/src/Psalm/Storage/FileStorage.php @@ -86,7 +86,7 @@ class FileStorage public $type_aliases = []; /** - * @var array + * @var array */ public $classlike_aliases = []; From c450d95727794fce33a8f372cd8777bef9b2257c Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 8 Sep 2022 14:33:20 +0200 Subject: [PATCH 084/194] fix inconsistent function naming --- src/Psalm/Codebase.php | 4 ++-- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 2 +- src/Psalm/Internal/Codebase/ClassLikes.php | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 1de35369467..c74ba4637ae 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -793,9 +793,9 @@ public function interfaceHasCorrectCasing(string $fq_interface_name): bool return $this->classlikes->interfaceHasCorrectCasing($fq_interface_name); } - public function traitHasCorrectCase(string $fq_trait_name): bool + public function traitHasCorrectCasing(string $fq_trait_name): bool { - return $this->classlikes->traitHasCorrectCase($fq_trait_name); + return $this->classlikes->traitHasCorrectCasing($fq_trait_name); } /** diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index cb4e63491ba..f55fbaaa90d 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -1370,7 +1370,7 @@ private function analyzeTraitUse( return false; } - if (!$codebase->traitHasCorrectCase($fq_trait_name)) { + if (!$codebase->traitHasCorrectCasing($fq_trait_name)) { if (IssueBuffer::accepts( new UndefinedTrait( 'Trait ' . $fq_trait_name . ' has wrong casing', diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index 65422140d98..85abeeb0912 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -780,7 +780,7 @@ public function enumHasCorrectCasing(string $fq_enum_name): bool return isset($this->existing_enums[$fq_enum_name]); } - public function traitHasCorrectCase(string $fq_trait_name): bool + public function traitHasCorrectCasing(string $fq_trait_name): bool { if (isset($this->existing_classlike_aliases[$fq_trait_name])) { return true; From 4c6abccfb2a14c71bc9b4edea5696b6724aa43e1 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 8 Sep 2022 14:41:55 +0200 Subject: [PATCH 085/194] fix tests --- tests/Internal/Codebase/ClassLikesTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/Internal/Codebase/ClassLikesTest.php b/tests/Internal/Codebase/ClassLikesTest.php index b04beb38f41..8168d17a875 100644 --- a/tests/Internal/Codebase/ClassLikesTest.php +++ b/tests/Internal/Codebase/ClassLikesTest.php @@ -30,7 +30,7 @@ public function setUp(): void public function testWillDetectClassImplementingAliasedInterface(): void { - $this->classlikes->addClassAlias('Foo', 'bar'); + $this->classlikes->addClassAlias('Foo', 'Bar'); $classStorage = new ClassLikeStorage('Baz'); $classStorage->class_implements['bar'] = 'Bar'; @@ -42,9 +42,9 @@ public function testWillDetectClassImplementingAliasedInterface(): void public function testWillResolveAliasedAliases(): void { - $this->classlikes->addClassAlias('Foo', 'bar'); - $this->classlikes->addClassAlias('Bar', 'baz'); - $this->classlikes->addClassAlias('Baz', 'qoo'); + $this->classlikes->addClassAlias('Foo', 'Bar'); + $this->classlikes->addClassAlias('Bar', 'Baz'); + $this->classlikes->addClassAlias('Baz', 'Qoo'); self::assertSame('Foo', $this->classlikes->getUnAliasedName('Qoo')); } From 6e4c1823d6da316614fa17e45264f6aa13886925 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 4 Aug 2022 00:45:33 +0200 Subject: [PATCH 086/194] partial revert nullable type for curl_multi_getcontent Fix https://github.com/vimeo/psalm/issues/8351 Partially reverts https://github.com/vimeo/psalm/commit/f28ac7377778e281c1b406251dd839f88ea4622e --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_80_delta.php | 4 ++-- dictionaries/CallMap_historical.php | 2 +- tests/Internal/Codebase/InternalCallMapHandlerTest.php | 1 + 4 files changed, 5 insertions(+), 4 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 219e6372641..8c9eeb8a338 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1678,7 +1678,7 @@ 'curl_multi_close' => ['void', 'multi_handle'=>'CurlMultiHandle'], 'curl_multi_errno' => ['int', 'multi_handle'=>'CurlMultiHandle'], 'curl_multi_exec' => ['int', 'multi_handle'=>'CurlMultiHandle', '&w_still_running'=>'int'], -'curl_multi_getcontent' => ['?string', 'handle'=>'CurlHandle'], +'curl_multi_getcontent' => ['string', 'handle'=>'CurlHandle'], 'curl_multi_info_read' => ['array|false', 'multi_handle'=>'CurlMultiHandle', '&w_queued_messages='=>'int'], 'curl_multi_init' => ['CurlMultiHandle|false'], 'curl_multi_remove_handle' => ['int', 'multi_handle'=>'CurlMultiHandle', 'handle'=>'CurlHandle'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index afeb1bc4133..21f4f6665a0 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -282,8 +282,8 @@ 'new' => ['int', 'multi_handle'=>'CurlMultiHandle', '&w_still_running'=>'int'], ], 'curl_multi_getcontent' => [ - 'old' => ['?string', 'ch'=>'resource'], - 'new' => ['?string', 'handle'=>'CurlHandle'], + 'old' => ['string', 'ch'=>'resource'], + 'new' => ['string', 'handle'=>'CurlHandle'], ], 'curl_multi_info_read' => [ 'old' => ['array|false', 'mh'=>'resource', '&w_msgs_in_queue='=>'int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index a5c4875aa71..43652148102 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -10135,7 +10135,7 @@ 'curl_multi_add_handle' => ['int', 'mh'=>'resource', 'ch'=>'resource'], 'curl_multi_close' => ['void', 'mh'=>'resource'], 'curl_multi_exec' => ['int', 'mh'=>'resource', '&w_still_running'=>'int'], - 'curl_multi_getcontent' => ['?string', 'ch'=>'resource'], + 'curl_multi_getcontent' => ['string', 'ch'=>'resource'], 'curl_multi_info_read' => ['array|false', 'mh'=>'resource', '&w_msgs_in_queue='=>'int'], 'curl_multi_init' => ['resource|false'], 'curl_multi_remove_handle' => ['int', 'mh'=>'resource', 'ch'=>'resource'], diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index 164fb4143d1..7ac15128d7b 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -343,6 +343,7 @@ class InternalCallMapHandlerTest extends TestCase 'cal_from_jd', 'collator_get_strength', 'curl_multi_init', + 'curl_multi_getcontent', // issue #8351 'date_add', 'date_date_set', 'date_diff', From 8da5f5eb1aa526aacc00982ac533f534a267508f Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 16 Aug 2022 15:48:49 +0200 Subject: [PATCH 087/194] use exceptions instead of error_log for ParserCacheProvider * use exceptions instead of error_log for ParserCacheProvider like all other cache providers do * remove duplicate code in ParserCacheProvider * use same hash as other cache providers * update Config.php cache directory creation to use same code as ParserCacheProvider --- src/Psalm/Config.php | 17 +- .../Internal/Provider/ParserCacheProvider.php | 184 ++++++++---------- .../Provider/ParserInstanceCacheProvider.php | 12 ++ 3 files changed, 109 insertions(+), 104 deletions(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 6dd9b63d125..534c8252906 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -35,6 +35,7 @@ use Psalm\Plugin\PluginEntryPointInterface; use Psalm\Progress\Progress; use Psalm\Progress\VoidProgress; +use RuntimeException; use SimpleXMLElement; use SimpleXMLIterator; use Throwable; @@ -55,7 +56,6 @@ use function clearstatcache; use function count; use function dirname; -use function error_log; use function explode; use function extension_loaded; use function file_exists; @@ -1013,8 +1013,19 @@ private static function fromXmlAndPaths( chdir($config->base_dir); } - if (is_dir($config->cache_directory) === false && @mkdir($config->cache_directory, 0777, true) === false) { - error_log('Could not create cache directory: ' . $config->cache_directory); + if (!is_dir($config->cache_directory)) { + try { + if (mkdir($config->cache_directory, 0777, true) === false) { + // any other error than directory already exists/permissions issue + throw new RuntimeException('Failed to create Psalm cache directory for unknown reasons'); + } + } catch (RuntimeException $e) { + if (!is_dir($config->cache_directory)) { + // rethrow the error with default message + // it contains the reason why creation failed + throw $e; + } + } } if ($cwd) { diff --git a/src/Psalm/Internal/Provider/ParserCacheProvider.php b/src/Psalm/Internal/Provider/ParserCacheProvider.php index c1c30c27b76..dc36c13f54c 100644 --- a/src/Psalm/Internal/Provider/ParserCacheProvider.php +++ b/src/Psalm/Internal/Provider/ParserCacheProvider.php @@ -7,12 +7,13 @@ use Psalm\Config; use Psalm\Internal\Provider\Providers; use RuntimeException; +use UnexpectedValueException; use function clearstatcache; -use function error_log; use function file_put_contents; use function filemtime; use function gettype; +use function hash; use function igbinary_serialize; use function igbinary_unserialize; use function is_array; @@ -21,7 +22,6 @@ use function is_writable; use function json_decode; use function json_encode; -use function md5; use function mkdir; use function scandir; use function serialize; @@ -42,31 +42,33 @@ class ParserCacheProvider private const PARSER_CACHE_DIRECTORY = 'php-parser'; private const FILE_CONTENTS_CACHE_DIRECTORY = 'file-caches'; + /** + * @var Config + */ + private $config; + /** * A map of filename hashes to contents hashes * * @var array|null */ - private $existing_file_content_hashes; + protected $existing_file_content_hashes; /** * A map of recently-added filename hashes to contents hashes * * @var array */ - private $new_file_content_hashes = []; + protected $new_file_content_hashes = []; /** * @var bool */ private $use_file_cache; - /** @var bool */ - private $use_igbinary; - public function __construct(Config $config, bool $use_file_cache = true) { - $this->use_igbinary = $config->use_igbinary; + $this->config = $config; $this->use_file_cache = $use_file_cache; } @@ -78,28 +80,22 @@ public function loadStatementsFromCache( int $file_modified_time, string $file_content_hash ): ?array { - $root_cache_directory = Config::getInstance()->getCacheDirectory(); - - if (!$root_cache_directory) { + if (!$this->use_file_cache) { return null; } - $file_cache_key = $this->getParserCacheKey( - $file_path - ); + $cache_location = $this->getCacheLocationForPath($file_path, self::PARSER_CACHE_DIRECTORY); - $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::PARSER_CACHE_DIRECTORY; + $file_cache_key = $this->getParserCacheKey($file_path); $file_content_hashes = $this->new_file_content_hashes + $this->getExistingFileContentHashes(); - $cache_location = $parser_cache_directory . DIRECTORY_SEPARATOR . $file_cache_key; - if (isset($file_content_hashes[$file_cache_key]) && $file_content_hash === $file_content_hashes[$file_cache_key] && is_readable($cache_location) && filemtime($cache_location) > $file_modified_time ) { - if ($this->use_igbinary) { + if ($this->config->use_igbinary) { /** @var list */ $stmts = igbinary_unserialize(Providers::safeFileGetContents($cache_location)); } else { @@ -118,22 +114,14 @@ public function loadStatementsFromCache( */ public function loadExistingStatementsFromCache(string $file_path): ?array { - $root_cache_directory = Config::getInstance()->getCacheDirectory(); - - if (!$root_cache_directory) { + if (!$this->use_file_cache) { return null; } - $file_cache_key = $this->getParserCacheKey( - $file_path - ); - - $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::PARSER_CACHE_DIRECTORY; - - $cache_location = $parser_cache_directory . DIRECTORY_SEPARATOR . $file_cache_key; + $cache_location = $this->getCacheLocationForPath($file_path, self::PARSER_CACHE_DIRECTORY); if (is_readable($cache_location)) { - if ($this->use_igbinary) { + if ($this->config->use_igbinary) { /** @var list */ return igbinary_unserialize(Providers::safeFileGetContents($cache_location)) ?: null; } @@ -151,19 +139,7 @@ public function loadExistingFileContentsFromCache(string $file_path): ?string return null; } - $root_cache_directory = Config::getInstance()->getCacheDirectory(); - - if (!$root_cache_directory) { - return null; - } - - $file_cache_key = $this->getParserCacheKey( - $file_path - ); - - $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::FILE_CONTENTS_CACHE_DIRECTORY; - - $cache_location = $parser_cache_directory . DIRECTORY_SEPARATOR . $file_cache_key; + $cache_location = $this->getCacheLocationForPath($file_path, self::FILE_CONTENTS_CACHE_DIRECTORY); if (is_readable($cache_location)) { return Providers::safeFileGetContents($cache_location); @@ -177,35 +153,37 @@ public function loadExistingFileContentsFromCache(string $file_path): ?string */ private function getExistingFileContentHashes(): array { - $config = Config::getInstance(); - $root_cache_directory = $config->getCacheDirectory(); + if (!$this->use_file_cache) { + return []; + } if ($this->existing_file_content_hashes === null) { + $root_cache_directory = $this->config->getCacheDirectory(); $file_hashes_path = $root_cache_directory . DIRECTORY_SEPARATOR . self::FILE_HASHES; - if ($root_cache_directory && is_readable($file_hashes_path)) { - $hashes_encoded = Providers::safeFileGetContents($file_hashes_path); - if (!$hashes_encoded) { - error_log('Unexpected value when loading from file content hashes'); - $this->existing_file_content_hashes = []; - - return []; - } + if (!$root_cache_directory) { + throw new UnexpectedValueException('No cache directory defined'); + } - $hashes_decoded = json_decode($hashes_encoded, true); + if (!is_readable($file_hashes_path)) { + // might not exist yet + $this->existing_file_content_hashes = []; + return $this->existing_file_content_hashes; + } - if (!is_array($hashes_decoded)) { - error_log('Unexpected value ' . gettype($hashes_decoded)); - $this->existing_file_content_hashes = []; + $hashes_encoded = Providers::safeFileGetContents($file_hashes_path); + if (!$hashes_encoded) { + throw new UnexpectedValueException('File content hashes should be in cache'); + } - return []; - } + $hashes_decoded = json_decode($hashes_encoded, true); - /** @var array $hashes_decoded */ - $this->existing_file_content_hashes = $hashes_decoded; - } else { - $this->existing_file_content_hashes = []; + if (!is_array($hashes_decoded)) { + throw new UnexpectedValueException('File content hashes are of invalid type ' . gettype($hashes_decoded)); } + + /** @var array $hashes_decoded */ + $this->existing_file_content_hashes = $hashes_decoded; } return $this->existing_file_content_hashes; @@ -220,31 +198,18 @@ public function saveStatementsToCache( array $stmts, bool $touch_only ): void { - $root_cache_directory = Config::getInstance()->getCacheDirectory(); - - if (!$root_cache_directory) { - return; - } - - $file_cache_key = $this->getParserCacheKey( - $file_path - ); - - $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::PARSER_CACHE_DIRECTORY; - - $cache_location = $parser_cache_directory . DIRECTORY_SEPARATOR . $file_cache_key; + $cache_location = $this->getCacheLocationForPath($file_path, self::PARSER_CACHE_DIRECTORY, !$touch_only); if ($touch_only) { touch($cache_location); } else { - $this->createCacheDirectory($parser_cache_directory); - - if ($this->use_igbinary) { + if ($this->config->use_igbinary) { file_put_contents($cache_location, igbinary_serialize($stmts), LOCK_EX); } else { file_put_contents($cache_location, serialize($stmts), LOCK_EX); } + $file_cache_key = $this->getParserCacheKey($file_path); $this->new_file_content_hashes[$file_cache_key] = $file_content_hash; } } @@ -268,7 +233,11 @@ public function addNewFileContentHashes(array $file_content_hashes): void public function saveFileContentHashes(): void { - $root_cache_directory = Config::getInstance()->getCacheDirectory(); + if (!$this->use_file_cache) { + return; + } + + $root_cache_directory = $this->config->getCacheDirectory(); if (!$root_cache_directory) { return; @@ -298,28 +267,17 @@ public function cacheFileContents(string $file_path, string $file_contents): voi return; } - $root_cache_directory = Config::getInstance()->getCacheDirectory(); - - if (!$root_cache_directory) { - return; - } - - $file_cache_key = $this->getParserCacheKey( - $file_path - ); - - $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::FILE_CONTENTS_CACHE_DIRECTORY; - - $cache_location = $parser_cache_directory . DIRECTORY_SEPARATOR . $file_cache_key; - - $this->createCacheDirectory($parser_cache_directory); + $cache_location = $this->getCacheLocationForPath($file_path, self::FILE_CONTENTS_CACHE_DIRECTORY, true); file_put_contents($cache_location, $file_contents, LOCK_EX); } public function deleteOldParserCaches(float $time_before): int { - $cache_directory = Config::getInstance()->getCacheDirectory(); + $cache_directory = $this->config->getCacheDirectory(); + + $this->existing_file_content_hashes = null; + $this->new_file_content_hashes = []; if (!$cache_directory) { return 0; @@ -349,22 +307,46 @@ public function deleteOldParserCaches(float $time_before): int return $removed_count; } - private function getParserCacheKey(string $file_name): string + private function getParserCacheKey(string $file_path): string { - return md5($file_name) . ($this->use_igbinary ? '-igbinary' : '') . '-r'; + if (PHP_VERSION_ID >= 80100) { + $hash = hash('xxh128', $file_path); + } else { + $hash = hash('md4', $file_path); + } + + return $hash . ($this->config->use_igbinary ? '-igbinary' : '') . '-r'; } - private function createCacheDirectory(string $parser_cache_directory): void + + private function getCacheLocationForPath(string $file_path, string $subdirectory, bool $create_directory = false): string { - if (!is_dir($parser_cache_directory)) { + $root_cache_directory = $this->config->getCacheDirectory(); + + if (!$root_cache_directory) { + throw new UnexpectedValueException('No cache directory defined'); + } + + $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . $subdirectory; + + if ($create_directory && !is_dir($parser_cache_directory)) { try { - mkdir($parser_cache_directory, 0777, true); + if (mkdir($parser_cache_directory, 0777, true) === false) { + // any other error than directory already exists/permissions issue + throw new RuntimeException('Failed to create ' . $parser_cache_directory . ' cache directory for unknown reasons'); + } } catch (RuntimeException $e) { // Race condition (#4483) if (!is_dir($parser_cache_directory)) { - error_log('Could not create parser cache directory: ' . $parser_cache_directory); + // rethrow the error with default message + // it contains the reason why creation failed + throw $e; } } } + + return $parser_cache_directory + . DIRECTORY_SEPARATOR + . $this->getParserCacheKey($file_path); } } diff --git a/tests/Internal/Provider/ParserInstanceCacheProvider.php b/tests/Internal/Provider/ParserInstanceCacheProvider.php index 9b81bfcef87..766772cd600 100644 --- a/tests/Internal/Provider/ParserInstanceCacheProvider.php +++ b/tests/Internal/Provider/ParserInstanceCacheProvider.php @@ -82,6 +82,18 @@ public function cacheFileContents(string $file_path, string $file_contents): voi $this->file_contents_cache[$file_path] = $file_contents; } + public function deleteOldParserCaches(float $time_before): int + { + $this->existing_file_content_hashes = null; + $this->new_file_content_hashes = []; + + $this->file_contents_cache = []; + $this->file_content_hash = []; + $this->statements_cache = []; + $this->statements_cache_time = []; + return 0; + } + public function saveFileContentHashes(): void { } From 4726454f49095bdfca0e024f850af079362927a8 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 16 Aug 2022 15:53:28 +0200 Subject: [PATCH 088/194] update leftover md5 in provider to commonly used hash Revert "update leftover md5 in provider to commonly used hash" This reverts commit 66337ecf50446dca8650a0812ebfe516d1993e06. partially put back Update StatementsProvider.php --- src/Psalm/Internal/Provider/StatementsProvider.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Provider/StatementsProvider.php b/src/Psalm/Internal/Provider/StatementsProvider.php index 431cb1585d4..75e47809b6e 100644 --- a/src/Psalm/Internal/Provider/StatementsProvider.php +++ b/src/Psalm/Internal/Provider/StatementsProvider.php @@ -26,6 +26,7 @@ use function array_merge; use function count; use function filemtime; +use function hash; use function md5; use function strlen; use function strpos; @@ -132,7 +133,11 @@ public function getStatementsForFile(string $file_path, string $php_version, ?Pr $config = Config::getInstance(); - $file_content_hash = md5($version . $file_contents); + if (PHP_VERSION_ID >= 80100) { + $file_content_hash = hash('xxh128', $version . $file_contents); + } else { + $file_content_hash = hash('md4', $version . $file_contents); + } if (!$this->parser_cache_provider || (!$config->isInProjectDirs($file_path) && strpos($file_path, 'vendor')) @@ -239,6 +244,7 @@ function (int $_): bool { array_flip($unchanged_signature_members) ); + // do NOT change this to hash, it will fail on Windows for whatever reason $file_path_hash = md5($file_path); $changed_members = array_map( From 8ac86f0a4d99d65ee8932c0a74e79d2b6a5c2f9a Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 18 Aug 2022 13:39:46 +0200 Subject: [PATCH 089/194] use consistent race condition dir creation code in all places in cache --- .../ClassLikeStorageCacheProvider.php | 15 +++++++- .../Provider/FileReferenceCacheProvider.php | 36 +++++++++++++------ .../Provider/FileStorageCacheProvider.php | 15 +++++++- 3 files changed, 54 insertions(+), 12 deletions(-) diff --git a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php index c7fa19acd0c..a6aa8e7b129 100644 --- a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php @@ -5,6 +5,7 @@ use Psalm\Config; use Psalm\Internal\Provider\Providers; use Psalm\Storage\ClassLikeStorage; +use RuntimeException; use UnexpectedValueException; use function array_merge; @@ -168,7 +169,19 @@ private function getCacheLocationForClass( $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::CLASS_CACHE_DIRECTORY; if ($create_directory && !is_dir($parser_cache_directory)) { - mkdir($parser_cache_directory, 0777, true); + try { + if (mkdir($parser_cache_directory, 0777, true) === false) { + // any other error than directory already exists/permissions issue + throw new RuntimeException('Failed to create ' . $parser_cache_directory . ' cache directory for unknown reasons'); + } + } catch (RuntimeException $e) { + // Race condition (#4483) + if (!is_dir($parser_cache_directory)) { + // rethrow the error with default message + // it contains the reason why creation failed + throw $e; + } + } } $data = $file_path ? strtolower($file_path) . ' ' : ''; diff --git a/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php b/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php index e512aacf29b..76f12afb09f 100644 --- a/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php @@ -5,6 +5,7 @@ use Psalm\Config; use Psalm\Internal\Codebase\Analyzer; use Psalm\Internal\Provider\Providers; +use RuntimeException; use UnexpectedValueException; use function file_exists; @@ -12,6 +13,7 @@ use function igbinary_serialize; use function igbinary_unserialize; use function is_array; +use function is_dir; use function is_readable; use function mkdir; use function serialize; @@ -992,18 +994,32 @@ public function setConfigHashCache(string $hash): void { $cache_directory = Config::getInstance()->getCacheDirectory(); - if ($cache_directory) { - if (!file_exists($cache_directory)) { - mkdir($cache_directory, 0777, true); + if (!$cache_directory) { + return; + } + + if (!is_dir($cache_directory)) { + try { + if (mkdir($cache_directory, 0777, true) === false) { + // any other error than directory already exists/permissions issue + throw new RuntimeException('Failed to create ' . $cache_directory . ' cache directory for unknown reasons'); + } + } catch (RuntimeException $e) { + // Race condition (#4483) + if (!is_dir($cache_directory)) { + // rethrow the error with default message + // it contains the reason why creation failed + throw $e; + } } + } - $config_hash_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::CONFIG_HASH_CACHE_NAME; + $config_hash_cache_location = $cache_directory . DIRECTORY_SEPARATOR . self::CONFIG_HASH_CACHE_NAME; - file_put_contents( - $config_hash_cache_location, - $hash, - LOCK_EX - ); - } + file_put_contents( + $config_hash_cache_location, + $hash, + LOCK_EX + ); } } diff --git a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php index 2f9279f12ae..ddcbf22a633 100644 --- a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php @@ -5,6 +5,7 @@ use Psalm\Config; use Psalm\Internal\Provider\Providers; use Psalm\Storage\FileStorage; +use RuntimeException; use UnexpectedValueException; use function array_merge; @@ -168,7 +169,19 @@ private function getCacheLocationForPath(string $file_path, bool $create_directo $parser_cache_directory = $root_cache_directory . DIRECTORY_SEPARATOR . self::FILE_STORAGE_CACHE_DIRECTORY; if ($create_directory && !is_dir($parser_cache_directory)) { - mkdir($parser_cache_directory, 0777, true); + try { + if (mkdir($parser_cache_directory, 0777, true) === false) { + // any other error than directory already exists/permissions issue + throw new RuntimeException('Failed to create ' . $parser_cache_directory . ' cache directory for unknown reasons'); + } + } catch (RuntimeException $e) { + // Race condition (#4483) + if (!is_dir($parser_cache_directory)) { + // rethrow the error with default message + // it contains the reason why creation failed + throw $e; + } + } } if (PHP_VERSION_ID >= 80100) { From 978f37e421a1c4a96cc0b9b0445deca97ea443aa Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 24 Aug 2022 12:18:46 +0200 Subject: [PATCH 090/194] improve unlinking potential race condition * fix rare race condition on file cache unlink * remove unnecessary reset() * improve code readability using variable --- src/Psalm/Config.php | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 534c8252906..380e8399751 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -2298,20 +2298,29 @@ public static function removeCacheDirectory(string $dir): void continue; } + $full_path = $dir . '/' . $object; + // if it was deleted in the meantime/race condition with other psalm process - if (!file_exists($dir . '/' . $object)) { + if (!file_exists($full_path)) { continue; } - if (filetype($dir . '/' . $object) === 'dir') { - self::removeCacheDirectory($dir . '/' . $object); + if (filetype($full_path) === 'dir') { + self::removeCacheDirectory($full_path); } else { - unlink($dir . '/' . $object); + try { + unlink($full_path); + } catch (RuntimeException $e) { + clearstatcache(true, $full_path); + if (file_exists($full_path)) { + // rethrow the error with default message + // it contains the reason why deletion failed + throw $e; + } + } } } - reset($objects); - // may have been removed in the meantime clearstatcache(true, $dir); if (is_dir($dir)) { From 62df25a741a3a5f24c373e7ee8f7b66bc7e954b7 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 8 Sep 2022 23:57:12 +0200 Subject: [PATCH 091/194] fix test cache inconsistency --- tests/Internal/Provider/FakeParserCacheProvider.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/Internal/Provider/FakeParserCacheProvider.php b/tests/Internal/Provider/FakeParserCacheProvider.php index 06b31afd84b..36b1dbb3d50 100644 --- a/tests/Internal/Provider/FakeParserCacheProvider.php +++ b/tests/Internal/Provider/FakeParserCacheProvider.php @@ -33,6 +33,14 @@ public function cacheFileContents(string $file_path, string $file_contents): voi { } + public function deleteOldParserCaches(float $time_before): int + { + $this->existing_file_content_hashes = null; + $this->new_file_content_hashes = []; + + return 0; + } + public function saveFileContentHashes(): void { } From 4b2841580e45015136fbbf10f634bdfffe61f341 Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Tue, 13 Sep 2022 09:50:21 -0400 Subject: [PATCH 092/194] Pin version of PHPStan phpdoc parser for slevomat rules --- composer.json | 1 + 1 file changed, 1 insertion(+) diff --git a/composer.json b/composer.json index ddbe93f4c60..b710a9bcb83 100644 --- a/composer.json +++ b/composer.json @@ -52,6 +52,7 @@ "phpunit/phpunit": "^9.0", "psalm/plugin-phpunit": "^0.16", "slevomat/coding-standard": "^7.0", + "phpstan/phpdoc-parser": "1.6.4", "squizlabs/php_codesniffer": "^3.5", "symfony/process": "^4.3 || ^5.0 || ^6.0", "weirdan/prophecy-shim": "^1.0 || ^2.0" From 6f298d2af2c7f11e1176b19c73a8d9363c88fc29 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Tue, 13 Sep 2022 12:33:47 -0400 Subject: [PATCH 093/194] Fix phpcs violations --- src/Psalm/Config.php | 1 - src/Psalm/Config/FileFilter.php | 4 ++-- src/Psalm/Internal/Codebase/Properties.php | 10 ++++----- src/Psalm/Internal/MethodIdentifier.php | 2 +- .../ClassLikeStorageCacheProvider.php | 4 +++- .../Provider/FileReferenceCacheProvider.php | 12 ++++++++--- .../Provider/FileStorageCacheProvider.php | 4 +++- .../Internal/Provider/ParserCacheProvider.php | 21 +++++++++++++------ src/Psalm/Internal/Provider/Providers.php | 2 +- .../ArrayReduceReturnTypeProvider.php | 4 +++- .../Internal/Provider/StatementsProvider.php | 2 ++ 11 files changed, 44 insertions(+), 22 deletions(-) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index 380e8399751..c97021c5ed3 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -6,7 +6,6 @@ use Composer\Semver\Constraint\Constraint; use Composer\Semver\VersionParser; use DOMDocument; -use DOMElement; use InvalidArgumentException; use LogicException; use OutOfBoundsException; diff --git a/src/Psalm/Config/FileFilter.php b/src/Psalm/Config/FileFilter.php index de83e148285..37e370ff5db 100644 --- a/src/Psalm/Config/FileFilter.php +++ b/src/Psalm/Config/FileFilter.php @@ -15,10 +15,10 @@ use function is_dir; use function is_iterable; use function preg_match; -use function preg_replace; use function readlink; use function realpath; use function restore_error_handler; +use function rtrim; use function set_error_handler; use function str_replace; use function stripos; @@ -421,7 +421,7 @@ function (): bool { */ protected static function slashify(string $str): string { - return rtrim( $str, DIRECTORY_SEPARATOR ) . DIRECTORY_SEPARATOR; + return rtrim($str, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR; } public function allows(string $file_name, bool $case_sensitive = false): bool diff --git a/src/Psalm/Internal/Codebase/Properties.php b/src/Psalm/Internal/Codebase/Properties.php index ba5604ea850..94ff1ff08a1 100644 --- a/src/Psalm/Internal/Codebase/Properties.php +++ b/src/Psalm/Internal/Codebase/Properties.php @@ -15,7 +15,7 @@ use UnexpectedValueException; use function explode; -use function preg_replace; +use function ltrim; use function strtolower; /** @@ -84,7 +84,7 @@ public function propertyExists( ?CodeLocation $code_location = null ): bool { // remove leading backslash if it exists - $property_id = ltrim( $property_id, '\\' ); + $property_id = ltrim($property_id, '\\'); [$fq_class_name, $property_name] = explode('::$', $property_id); $fq_class_name_lc = strtolower($fq_class_name); @@ -249,7 +249,7 @@ public function getAppearingClassForProperty( public function getStorage(string $property_id): PropertyStorage { // remove leading backslash if it exists - $property_id = ltrim( $property_id, '\\' ); + $property_id = ltrim($property_id, '\\'); [$fq_class_name, $property_name] = explode('::$', $property_id); @@ -270,7 +270,7 @@ public function getStorage(string $property_id): PropertyStorage public function hasStorage(string $property_id): bool { // remove leading backslash if it exists - $property_id = ltrim( $property_id, '\\' ); + $property_id = ltrim($property_id, '\\'); [$fq_class_name, $property_name] = explode('::$', $property_id); @@ -292,7 +292,7 @@ public function getPropertyType( ?Context $context = null ): ?Union { // remove leading backslash if it exists - $property_id = ltrim( $property_id, '\\' ); + $property_id = ltrim($property_id, '\\'); [$fq_class_name, $property_name] = explode('::$', $property_id); diff --git a/src/Psalm/Internal/MethodIdentifier.php b/src/Psalm/Internal/MethodIdentifier.php index 1abb0ecded1..309af8e5259 100644 --- a/src/Psalm/Internal/MethodIdentifier.php +++ b/src/Psalm/Internal/MethodIdentifier.php @@ -6,7 +6,7 @@ use function explode; use function is_string; -use function preg_replace; +use function ltrim; use function strpos; use function strtolower; diff --git a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php index a6aa8e7b129..abd34da2d9c 100644 --- a/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/ClassLikeStorageCacheProvider.php @@ -172,7 +172,9 @@ private function getCacheLocationForClass( try { if (mkdir($parser_cache_directory, 0777, true) === false) { // any other error than directory already exists/permissions issue - throw new RuntimeException('Failed to create ' . $parser_cache_directory . ' cache directory for unknown reasons'); + throw new RuntimeException( + 'Failed to create ' . $parser_cache_directory . ' cache directory for unknown reasons' + ); } } catch (RuntimeException $e) { // Race condition (#4483) diff --git a/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php b/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php index 76f12afb09f..a1b3eb1a10c 100644 --- a/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileReferenceCacheProvider.php @@ -942,10 +942,14 @@ public function getTypeCoverage() ) { if ($this->config->use_igbinary) { /** @var array */ - $type_coverage_cache = igbinary_unserialize(Providers::safeFileGetContents($type_coverage_cache_location)); + $type_coverage_cache = igbinary_unserialize( + Providers::safeFileGetContents($type_coverage_cache_location) + ); } else { /** @var array */ - $type_coverage_cache = unserialize(Providers::safeFileGetContents($type_coverage_cache_location)); + $type_coverage_cache = unserialize( + Providers::safeFileGetContents($type_coverage_cache_location) + ); } return $type_coverage_cache; @@ -1002,7 +1006,9 @@ public function setConfigHashCache(string $hash): void try { if (mkdir($cache_directory, 0777, true) === false) { // any other error than directory already exists/permissions issue - throw new RuntimeException('Failed to create ' . $cache_directory . ' cache directory for unknown reasons'); + throw new RuntimeException( + 'Failed to create ' . $cache_directory . ' cache directory for unknown reasons' + ); } } catch (RuntimeException $e) { // Race condition (#4483) diff --git a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php index ddcbf22a633..ede2789a817 100644 --- a/src/Psalm/Internal/Provider/FileStorageCacheProvider.php +++ b/src/Psalm/Internal/Provider/FileStorageCacheProvider.php @@ -172,7 +172,9 @@ private function getCacheLocationForPath(string $file_path, bool $create_directo try { if (mkdir($parser_cache_directory, 0777, true) === false) { // any other error than directory already exists/permissions issue - throw new RuntimeException('Failed to create ' . $parser_cache_directory . ' cache directory for unknown reasons'); + throw new RuntimeException( + 'Failed to create ' . $parser_cache_directory . ' cache directory for unknown reasons' + ); } } catch (RuntimeException $e) { // Race condition (#4483) diff --git a/src/Psalm/Internal/Provider/ParserCacheProvider.php b/src/Psalm/Internal/Provider/ParserCacheProvider.php index dc36c13f54c..8c7dac6e0a4 100644 --- a/src/Psalm/Internal/Provider/ParserCacheProvider.php +++ b/src/Psalm/Internal/Provider/ParserCacheProvider.php @@ -31,6 +31,7 @@ use const DIRECTORY_SEPARATOR; use const LOCK_EX; +use const PHP_VERSION_ID; use const SCANDIR_SORT_NONE; /** @@ -179,7 +180,9 @@ private function getExistingFileContentHashes(): array $hashes_decoded = json_decode($hashes_encoded, true); if (!is_array($hashes_decoded)) { - throw new UnexpectedValueException('File content hashes are of invalid type ' . gettype($hashes_decoded)); + throw new UnexpectedValueException( + 'File content hashes are of invalid type ' . gettype($hashes_decoded) + ); } /** @var array $hashes_decoded */ @@ -243,8 +246,9 @@ public function saveFileContentHashes(): void return; } - // directory was removed - // most likely due to a race condition with other psalm instances that were manually started at the same time + // directory was removed most likely due to a race condition + // with other psalm instances that were manually started at + // the same time clearstatcache(true, $root_cache_directory); if (!is_dir($root_cache_directory)) { return; @@ -319,8 +323,11 @@ private function getParserCacheKey(string $file_path): string } - private function getCacheLocationForPath(string $file_path, string $subdirectory, bool $create_directory = false): string - { + private function getCacheLocationForPath( + string $file_path, + string $subdirectory, + bool $create_directory = false + ): string { $root_cache_directory = $this->config->getCacheDirectory(); if (!$root_cache_directory) { @@ -333,7 +340,9 @@ private function getCacheLocationForPath(string $file_path, string $subdirectory try { if (mkdir($parser_cache_directory, 0777, true) === false) { // any other error than directory already exists/permissions issue - throw new RuntimeException('Failed to create ' . $parser_cache_directory . ' cache directory for unknown reasons'); + throw new RuntimeException( + 'Failed to create ' . $parser_cache_directory . ' cache directory for unknown reasons' + ); } } catch (RuntimeException $e) { // Race condition (#4483) diff --git a/src/Psalm/Internal/Provider/Providers.php b/src/Psalm/Internal/Provider/Providers.php index 8d246b2d1f9..f8392f88242 100644 --- a/src/Psalm/Internal/Provider/Providers.php +++ b/src/Psalm/Internal/Provider/Providers.php @@ -100,7 +100,7 @@ public static function safeFileGetContents(string $path): string $file_size = filesize($path); $content = ''; - if ( $file_size > 0 ) { + if ($file_size > 0) { $content = (string) fread($fp, $file_size); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php index 4632eaecefa..d67a4634493 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayReduceReturnTypeProvider.php @@ -100,7 +100,9 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $initial_type = $reduce_return_type; - if ($closure_types = $function_call_arg_type->getClosureTypes() ?: $function_call_arg_type->getCallableTypes()) { + $closure_types = $function_call_arg_type->getClosureTypes() ?: $function_call_arg_type->getCallableTypes(); + + if ($closure_types) { $closure_atomic_type = reset($closure_types); $closure_return_type = $closure_atomic_type->return_type ?: Type::getMixed(); diff --git a/src/Psalm/Internal/Provider/StatementsProvider.php b/src/Psalm/Internal/Provider/StatementsProvider.php index 75e47809b6e..7e1ad9f77be 100644 --- a/src/Psalm/Internal/Provider/StatementsProvider.php +++ b/src/Psalm/Internal/Provider/StatementsProvider.php @@ -31,6 +31,8 @@ use function strlen; use function strpos; +use const PHP_VERSION_ID; + /** * @internal */ From 7429bc203e118ba9ccbfba58de1a43dee25fc5f3 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Tue, 13 Sep 2022 12:35:16 -0400 Subject: [PATCH 094/194] Ignore php-parser issue --- psalm.xml.dist | 1 + 1 file changed, 1 insertion(+) diff --git a/psalm.xml.dist b/psalm.xml.dist index 135cadc6691..e1ba4dab41d 100644 --- a/psalm.xml.dist +++ b/psalm.xml.dist @@ -34,6 +34,7 @@ + From 95bb71f8a2d7f3eadc3e42fcfa27ab72641d32f4 Mon Sep 17 00:00:00 2001 From: Matt Brown Date: Tue, 13 Sep 2022 12:39:15 -0400 Subject: [PATCH 095/194] Support PHP 7.1 in require-dev restriction --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/composer.json b/composer.json index b710a9bcb83..72d44663cd6 100644 --- a/composer.json +++ b/composer.json @@ -52,7 +52,7 @@ "phpunit/phpunit": "^9.0", "psalm/plugin-phpunit": "^0.16", "slevomat/coding-standard": "^7.0", - "phpstan/phpdoc-parser": "1.6.4", + "phpstan/phpdoc-parser": "1.2.* || 1.6.4", "squizlabs/php_codesniffer": "^3.5", "symfony/process": "^4.3 || ^5.0 || ^6.0", "weirdan/prophecy-shim": "^1.0 || ^2.0" From 17ca8ef014442f5793c08faddc36ceca0a0d15d2 Mon Sep 17 00:00:00 2001 From: George Steel Date: Mon, 12 Sep 2022 13:13:44 +0100 Subject: [PATCH 096/194] `date_get_last_errors()`, `DateTime::getLastErrors()` may return false Up to PHP 8.2, these functions return false if no previous date operations have been performed. In PHP 8.2, false is returned after a date operation that yields neither warnings nor errors: https://3v4l.org/HBq0q https://3v4l.org/3QsKY Signed-off-by: George Steel --- dictionaries/CallMap.php | 6 +++--- dictionaries/CallMap_historical.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 1d89ed805ae..60003565b66 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1723,7 +1723,7 @@ 'date_default_timezone_set' => ['bool', 'timezoneId'=>'string'], 'date_diff' => ['DateInterval|false', 'baseObject'=>'DateTimeInterface', 'targetObject'=>'DateTimeInterface', 'absolute='=>'bool'], 'date_format' => ['string', 'object'=>'DateTimeInterface', 'format'=>'string'], -'date_get_last_errors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], +'date_get_last_errors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], 'date_interval_create_from_date_string' => ['DateInterval', 'datetime'=>'string'], 'date_interval_format' => ['string', 'object'=>'DateInterval', 'format'=>'string'], 'date_isodate_set' => ['DateTime|false', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'dayOfWeek='=>'int|mixed'], @@ -1782,7 +1782,7 @@ 'DateTime::createFromInterface' => ['self', 'object' => 'DateTimeInterface'], 'DateTime::diff' => ['DateInterval|false', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTime::format' => ['string', 'format'=>'string'], -'DateTime::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], +'DateTime::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], 'DateTime::getOffset' => ['int'], 'DateTime::getTimestamp' => ['int|false'], 'DateTime::getTimezone' => ['DateTimeZone|false'], @@ -1796,7 +1796,7 @@ 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], 'DateTimeImmutable::createFromInterface' => ['self', 'object' => 'DateTimeInterface'], -'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], +'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], 'DateTimeInterface::getOffset' => ['int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 79545d9053f..6488f5be3ab 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1044,7 +1044,7 @@ 'DateTime::createFromFormat' => ['static|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], 'DateTime::diff' => ['DateInterval|false', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTime::format' => ['string|false', 'format'=>'string'], - 'DateTime::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], + 'DateTime::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], 'DateTime::getOffset' => ['int'], 'DateTime::getTimestamp' => ['int|false'], 'DateTime::getTimezone' => ['DateTimeZone|false'], @@ -1057,7 +1057,7 @@ 'DateTime::sub' => ['static', 'interval'=>'DateInterval'], 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], - 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], + 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], 'DateTimeInterface::getOffset' => ['int'], @@ -10254,7 +10254,7 @@ 'date_default_timezone_set' => ['bool', 'timezoneId'=>'string'], 'date_diff' => ['DateInterval|false', 'baseObject'=>'DateTimeInterface', 'targetObject'=>'DateTimeInterface', 'absolute='=>'bool'], 'date_format' => ['string|false', 'object'=>'DateTimeInterface', 'format'=>'string'], - 'date_get_last_errors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], + 'date_get_last_errors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], 'date_interval_create_from_date_string' => ['DateInterval', 'datetime'=>'string'], 'date_interval_format' => ['string', 'object'=>'DateInterval', 'format'=>'string'], 'date_isodate_set' => ['DateTime|false', 'object'=>'DateTime', 'year'=>'int', 'week'=>'int', 'dayOfWeek='=>'int|mixed'], From 32aedbac58be50bda6ecf53c73e63414818a7fd8 Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Tue, 6 Sep 2022 11:05:40 +0200 Subject: [PATCH 097/194] Add dateTimeModify return type provider --- .../Provider/MethodReturnTypeProvider.php | 2 + .../DateTimeModifyReturnTypeProvider.php | 61 ++++++++++++ tests/DateTimeTest.php | 96 +++++++++++++++++++ tests/MethodCallTest.php | 2 +- 4 files changed, 160 insertions(+), 1 deletion(-) create mode 100644 src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php create mode 100644 tests/DateTimeTest.php diff --git a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php index f29dc77d6cc..a4203e0d7e1 100644 --- a/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/MethodReturnTypeProvider.php @@ -7,6 +7,7 @@ use Psalm\CodeLocation; use Psalm\Context; use Psalm\Internal\Provider\ReturnTypeProvider\ClosureFromCallableReturnTypeProvider; +use Psalm\Internal\Provider\ReturnTypeProvider\DateTimeModifyReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\DomNodeAppendChild; use Psalm\Internal\Provider\ReturnTypeProvider\ImagickPixelColorReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\PdoStatementReturnTypeProvider; @@ -58,6 +59,7 @@ public function __construct() $this->registerClass(SimpleXmlElementAsXml::class); $this->registerClass(PdoStatementReturnTypeProvider::class); $this->registerClass(ClosureFromCallableReturnTypeProvider::class); + $this->registerClass(DateTimeModifyReturnTypeProvider::class); } /** diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php new file mode 100644 index 00000000000..4d474dda537 --- /dev/null +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php @@ -0,0 +1,61 @@ +getSource(); + $call_args = $event->getCallArgs(); + $method_name_lowercase = $event->getMethodNameLowercase(); + if ( + !$statements_source instanceof StatementsAnalyzer + || $method_name_lowercase !== 'modify' + || !isset($call_args[0]) + ) { + return null; + } + + $first_arg = $call_args[0]->value; + $first_arg_type = $statements_source->node_data->getType($first_arg); + if (!$first_arg_type) { + return null; + } + + $has_date_time = false; + $has_false = false; + foreach ($first_arg_type->getAtomicTypes() as $type_part) { + if (!$type_part instanceof TLiteralString) { + return null; + } + + if (@(new \DateTime())->modify($type_part->value) === false) { + $has_false = true; + } else { + $has_date_time = true; + } + } + + if ($has_false && !$has_date_time) { + return Type::getFalse(); + } + if ($has_date_time && !$has_false) { + return Type::parseString($event->getFqClasslikeName()); + } + + return null; + } +} diff --git a/tests/DateTimeTest.php b/tests/DateTimeTest.php new file mode 100644 index 00000000000..6eb3b572038 --- /dev/null +++ b/tests/DateTimeTest.php @@ -0,0 +1,96 @@ +,error_levels?:string[]}> + */ + public function providerValidCodeParse(): iterable + { + return [ + 'modify' => [ + 'modify(getString()); + $b = $dateTimeImmutable->modify(getString()); + ', + 'assertions' => [ + '$a' => 'DateTime|false', + '$b' => 'DateTimeImmutable|false', + ], + ], + 'modifyWithValidConstant' => [ + 'modify(getString()); + $b = $dateTimeImmutable->modify(getString()); + ', + 'assertions' => [ + '$a' => 'DateTime', + '$b' => 'DateTimeImmutable', + ], + ], + 'modifyWithInvalidConstant' => [ + 'modify(getString()); + $b = $dateTimeImmutable->modify(getString()); + ', + 'assertions' => [ + '$a' => 'false', + '$b' => 'false', + ], + ], + 'modifyWithBothConstant' => [ + 'modify(getString()); + $b = $dateTimeImmutable->modify(getString()); + ', + 'assertions' => [ + '$a' => 'DateTime|false', + '$b' => 'DateTimeImmutable|false', + ], + ], + ]; + } +} diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index f30f1115fdc..cb3103fdeb8 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -272,7 +272,7 @@ final class MyDate extends DateTimeImmutable {} $b = (new DateTimeImmutable())->modify("+3 hours");', 'assertions' => [ '$yesterday' => 'MyDate|false', - '$b' => 'DateTimeImmutable|false', + '$b' => 'DateTimeImmutable', ], ], 'magicCall' => [ From fec5c8ab03daaa99c2ece9054cd5ce0599fb5f4f Mon Sep 17 00:00:00 2001 From: Vincent Langlet Date: Wed, 14 Sep 2022 00:55:32 +0200 Subject: [PATCH 098/194] Fix cs --- .../ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php index 4d474dda537..1677ba64f6f 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Provider\ReturnTypeProvider; +use DateTime; use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Plugin\EventHandler\Event\MethodReturnTypeProviderEvent; use Psalm\Plugin\EventHandler\MethodReturnTypeProviderInterface; @@ -21,8 +22,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) $statements_source = $event->getSource(); $call_args = $event->getCallArgs(); $method_name_lowercase = $event->getMethodNameLowercase(); - if ( - !$statements_source instanceof StatementsAnalyzer + if (!$statements_source instanceof StatementsAnalyzer || $method_name_lowercase !== 'modify' || !isset($call_args[0]) ) { @@ -42,7 +42,7 @@ public static function getMethodReturnType(MethodReturnTypeProviderEvent $event) return null; } - if (@(new \DateTime())->modify($type_part->value) === false) { + if (@(new DateTime())->modify($type_part->value) === false) { $has_false = true; } else { $has_date_time = true; From 7bc29a91eb6532d0dacdb0c9d3775bdd9ff6e799 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 30 Jul 2022 01:29:05 +0200 Subject: [PATCH 099/194] make superglobals more specific Update VariableFetchAnalyzer.php --- src/Psalm/Codebase.php | 2 +- .../Fetch/VariableFetchAnalyzer.php | 134 ++++++++++++++++-- .../Analyzer/Statements/GlobalAnalyzer.php | 6 +- .../Internal/Type/AssertionReconciler.php | 2 +- 4 files changed, 131 insertions(+), 13 deletions(-) diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index c74ba4637ae..dcf5c0b906a 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -1088,7 +1088,7 @@ public function getSymbolInformation(string $file_path, string $symbol): ?array } if (strpos($symbol, '$') === 0) { - $type = VariableFetchAnalyzer::getGlobalType($symbol); + $type = VariableFetchAnalyzer::getGlobalType($symbol, $this->analysis_php_version_id); if (!$type->isMixed()) { return ['type' => 'analysis_php_version_id); self::taintVariable($statements_analyzer, $var_name, $type, $stmt); @@ -522,7 +532,7 @@ public static function isSuperGlobal(string $var_id): bool ); } - public static function getGlobalType(string $var_id): Union + public static function getGlobalType(string $var_id, int $codebase_analysis_php_version_id): Union { $config = Config::getInstance(); @@ -531,26 +541,132 @@ public static function getGlobalType(string $var_id): Union } if ($var_id === '$argv') { + // only in CLI, null otherwise return new Union([ - new TArray([Type::getInt(), Type::getString()]), + new TNonEmptyList(Type::getString()), + new TNull() ]); } if ($var_id === '$argc') { - return Type::getInt(); + // only in CLI, null otherwise + return new Union([ + new TIntRange(1, null), + new TNull() + ]); + } + + if (!self::isSuperGlobal($var_id)) { + return Type::getMixed(); } if ($var_id === '$http_response_header') { return new Union([ - new TList(Type::getString()) + new TList(Type::getNonEmptyString()) + ]); + } + + if ($var_id === '$GLOBALS') { + return new Union([ + new TNonEmptyArray([ + Type::getNonEmptyString(), + Type::getMixed() + ]) ]); } - if (self::isSuperGlobal($var_id)) { - $type = Type::getArray(); - if ($var_id === '$_SESSION') { - $type->possibly_undefined = true; + if ($var_id === '$_COOKIE') { + $type = new TArray( + [ + Type::getNonEmptyString(), + Type::getString(), + ] + ); + + return new Union([$type]); + } + + if (in_array($var_id, array('$_GET', '$_POST', '$_REQUEST'), true)) { + $array_key = new Union([new TNonEmptyString(), new TInt()]); + $array = new TNonEmptyArray( + [ + $array_key, + new Union([ + new TString(), + new TArray([ + $array_key, + Type::getMixed() + ]) + ]) + ] + ); + + $type = new TArray( + [ + $array_key, + new Union([new TString(), $array]), + ] + ); + + return new Union([$type]); + } + + if ($var_id === '$_SERVER' || $var_id === '$_ENV') { + $type = new TArray( + [ + Type::getNonEmptyString(), + new Union([new TFloat(), new TIntRange(1, null), new TString(), new TNonEmptyList(Type::getString())]), + ] + ); + + return new Union([$type]); + } + + if ($var_id === '$_FILES') { + $values = [ + 'name' => new Union([ + new TString(), + new TNonEmptyList(Type::getString()), + ]), + 'type' => new Union([ + new TString(), + new TNonEmptyList(Type::getString()), + ]), + 'size' => new Union([ + new TInt(), + new TNonEmptyList(Type::getInt()), + ]), + 'tmp_name' => new Union([ + new TString(), + new TNonEmptyList(Type::getString()), + ]), + 'error' => new Union([ + new TInt(), + new TNonEmptyList(Type::getInt()), + ]), + ]; + + if ($codebase_analysis_php_version_id >= 81000) { + $values['full_path'] = new Union([ + new TString(), + new TNonEmptyList(Type::getString()), + ]); } + + $type = new TKeyedArray($values); + + return new Union([$type]); + } + + if ($var_id === '$_SESSION') { + // keys must be string + $type = new Union([ + new TArray([ + Type::getNonEmptyString(), + Type::getMixed(), + ]) + ]); + $type->possibly_undefined = true; return $type; } diff --git a/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php index c716bc2bfbc..d4280bd2a6e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/GlobalAnalyzer.php @@ -33,6 +33,7 @@ public static function analyze( ); } + $codebase = $statements_analyzer->getCodebase(); $source = $statements_analyzer->getSource(); $function_storage = $source instanceof FunctionLikeAnalyzer ? $source->getFunctionLikeStorage($statements_analyzer) @@ -44,7 +45,8 @@ public static function analyze( $var_id = '$' . $var->name; if ($var->name === 'argv' || $var->name === 'argc') { - $context->vars_in_scope[$var_id] = VariableFetchAnalyzer::getGlobalType($var_id); + $context->vars_in_scope[$var_id] = + VariableFetchAnalyzer::getGlobalType($var_id, $codebase->analysis_php_version_id); } elseif (isset($function_storage->global_types[$var_id])) { $context->vars_in_scope[$var_id] = clone $function_storage->global_types[$var_id]; $context->vars_possibly_in_scope[$var_id] = true; @@ -52,7 +54,7 @@ public static function analyze( $context->vars_in_scope[$var_id] = $global_context && $global_context->hasVariable($var_id) ? clone $global_context->vars_in_scope[$var_id] - : VariableFetchAnalyzer::getGlobalType($var_id); + : VariableFetchAnalyzer::getGlobalType($var_id, $codebase->analysis_php_version_id); $context->vars_possibly_in_scope[$var_id] = true; diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index d1611accfa8..e72954cbaf0 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -117,7 +117,7 @@ public static function reconcile( && is_string($key) && VariableFetchAnalyzer::isSuperGlobal($key) ) { - $existing_var_type = VariableFetchAnalyzer::getGlobalType($key); + $existing_var_type = VariableFetchAnalyzer::getGlobalType($key, $codebase->analysis_php_version_id); } if ($existing_var_type === null) { From 5c39e66b15f32f7c5ff579c5830de17cadeb4fc4 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 10 Sep 2022 13:06:17 +0200 Subject: [PATCH 100/194] fix tests --- docs/running_psalm/issues/MixedArgument.md | 2 +- docs/running_psalm/issues/MixedArrayAccess.md | 2 +- .../issues/MixedArrayAssignment.md | 2 +- docs/running_psalm/issues/MixedArrayOffset.md | 2 +- docs/running_psalm/issues/MixedAssignment.md | 8 ++++---- docs/running_psalm/issues/MixedClone.md | 2 +- .../running_psalm/issues/MixedFunctionCall.md | 2 +- .../issues/MixedInferredReturnType.md | 2 +- docs/running_psalm/issues/MixedOperand.md | 2 +- .../issues/MixedReturnStatement.md | 2 +- .../issues/MixedStringOffsetAssignment.md | 2 +- tests/ArrayAssignmentTest.php | 2 +- tests/ArrayFunctionCallTest.php | 4 ++-- tests/AssertAnnotationTest.php | 2 +- tests/FileUpdates/TemporaryUpdateTest.php | 12 ++++++------ tests/FunctionCallTest.php | 6 +++--- tests/Internal/CliUtilsTest.php | 2 +- tests/JsonOutputTest.php | 8 ++++---- tests/LanguageServer/SymbolLookupTest.php | 6 +++--- tests/ReturnTypeTest.php | 2 +- tests/TaintTest.php | 19 ++++++++++--------- tests/Template/ClassTemplateTest.php | 2 +- tests/Template/ConditionalReturnTypeTest.php | 2 +- tests/TypeReconciliation/EmptyTest.php | 2 +- .../src/FileWithErrors.php | 7 ++++++- tests/fixtures/expected_taint_graph.dot | 13 +++++++++---- 26 files changed, 64 insertions(+), 53 deletions(-) diff --git a/docs/running_psalm/issues/MixedArgument.md b/docs/running_psalm/issues/MixedArgument.md index 896f01efe43..a7f1f5a1642 100644 --- a/docs/running_psalm/issues/MixedArgument.md +++ b/docs/running_psalm/issues/MixedArgument.md @@ -6,5 +6,5 @@ Emitted when Psalm cannot determine the type of an argument [ ' 5, "b" => 12, "c" => null], function(?int $i) { - return $_GET["a"]; + return $GLOBALS["a"]; } );', 'error_message' => 'MixedArgumentTypeCoercion', diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index 22442e8e1c5..8dead02b7d6 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -513,7 +513,7 @@ function assertIntOrFoo($b) : void { } /** @psalm-suppress MixedAssignment */ - $a = $_GET["a"]; + $a = $GLOBALS["a"]; assertIntOrFoo($a); diff --git a/tests/FileUpdates/TemporaryUpdateTest.php b/tests/FileUpdates/TemporaryUpdateTest.php index 529b8f2ff07..cff307f3669 100644 --- a/tests/FileUpdates/TemporaryUpdateTest.php +++ b/tests/FileUpdates/TemporaryUpdateTest.php @@ -217,7 +217,7 @@ public function foo() { } public function bar() { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -232,7 +232,7 @@ public function foo() : int { } public function bar() { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -247,7 +247,7 @@ public function foo() : int { } public function bar() : int { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -268,7 +268,7 @@ public function foo() : int { } public function bar() : int { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -285,7 +285,7 @@ public function foo() : int { } public function bar() : int { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', @@ -303,7 +303,7 @@ public function foo() : int { } public function bar() : int { - $a = $_GET["foo"]; + $a = $GLOBALS["foo"]; return $this->foo(); } }', diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index be4f4c32b17..e57a1d432b9 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -128,7 +128,7 @@ function foo() { } 'noRedundantConditionAfterMixedOrEmptyArrayCountCheck' => [ ' [ '$a' => 'false|int', @@ -1481,7 +1481,7 @@ function test() : void { $y2 = date("Y", 10000); $F2 = date("F", 10000); /** @psalm-suppress MixedArgument */ - $F3 = date("F", $_GET["F3"]);', + $F3 = date("F", $GLOBALS["F3"]);', [ '$y' => 'numeric-string', '$m' => 'numeric-string', diff --git a/tests/Internal/CliUtilsTest.php b/tests/Internal/CliUtilsTest.php index f6f6ab0511e..a2bfe9cb6fc 100644 --- a/tests/Internal/CliUtilsTest.php +++ b/tests/Internal/CliUtilsTest.php @@ -19,7 +19,7 @@ class CliUtilsTest extends TestCase protected function setUp(): void { global $argv; - $this->argv = $argv; + $this->argv = $argv ?? []; } protected function tearDown(): void diff --git a/tests/JsonOutputTest.php b/tests/JsonOutputTest.php index f151cfc820e..975ac545fce 100644 --- a/tests/JsonOutputTest.php +++ b/tests/JsonOutputTest.php @@ -123,11 +123,11 @@ function fooFoo() { 'assertCancelsMixedAssignment' => [ ' 'Docblock-defined type int for $a is always int', + assert(is_string($a)); + if (is_string($a)) {}', + 'message' => 'Docblock-defined type string for $a is always string', 'line' => 4, - 'error' => 'is_int($a)', + 'error' => 'is_string($a)', ], ]; } diff --git a/tests/LanguageServer/SymbolLookupTest.php b/tests/LanguageServer/SymbolLookupTest.php index c5ab588f23f..be928a96dc2 100644 --- a/tests/LanguageServer/SymbolLookupTest.php +++ b/tests/LanguageServer/SymbolLookupTest.php @@ -78,7 +78,7 @@ function qux(int $a, int $b) : int { return $a + $b; } - $_SERVER;' + $_SESSION;' ); new FileAnalyzer($this->project_analyzer, 'somefile.php', 'somefile.php'); @@ -111,9 +111,9 @@ function qux(int $a, int $b) : int { $this->assertNotNull($information); $this->assertSame("getSymbolInformation('somefile.php', '$_SERVER'); + $information = $codebase->getSymbolInformation('somefile.php', '$_SESSION'); $this->assertNotNull($information); - $this->assertSame("", $information['type']); + $this->assertSame("", $information['type']); $information = $codebase->getSymbolInformation('somefile.php', '$my_global'); $this->assertNotNull($information); diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php index 9c522ee9df1..5642079d89e 100644 --- a/tests/ReturnTypeTest.php +++ b/tests/ReturnTypeTest.php @@ -1213,7 +1213,7 @@ function fooFoo(): A { * @psalm-suppress UndefinedClass */ function fooFoo(): A { - return $_GET["a"]; + return $GLOBALS["a"]; } fooFoo()->bar();', diff --git a/tests/TaintTest.php b/tests/TaintTest.php index 302ca24348f..1c7620e4c06 100644 --- a/tests/TaintTest.php +++ b/tests/TaintTest.php @@ -458,13 +458,6 @@ public static function slugify(string $url) : string { echo $a[0]["b"];', ], - 'intUntainted' => [ - ' [ ' [ ' 'TaintedHtml', ], 'foreachArg' => [ diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index ee84dbce884..6b94c0f9db0 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -1425,7 +1425,7 @@ public function __construct(array $elements = []) } /** @psalm-suppress MixedArgument */ - $c = new ArrayCollection($_GET["a"]);', + $c = new ArrayCollection($GLOBALS["a"]);', [ '$c' => 'ArrayCollection', ], diff --git a/tests/Template/ConditionalReturnTypeTest.php b/tests/Template/ConditionalReturnTypeTest.php index f01263a2fdd..15e4e9ee41f 100644 --- a/tests/Template/ConditionalReturnTypeTest.php +++ b/tests/Template/ConditionalReturnTypeTest.php @@ -40,7 +40,7 @@ public function getAttribute(?string $name, string $default = "") $a = (new A)->getAttribute("colour", "red"); // typed as string $b = (new A)->getAttribute(null); // typed as array /** @psalm-suppress MixedArgument */ - $c = (new A)->getAttribute($_GET["foo"]); // typed as string|array', + $c = (new A)->getAttribute($GLOBALS["foo"]); // typed as string|array', [ '$a' => 'string', '$b' => 'array', diff --git a/tests/TypeReconciliation/EmptyTest.php b/tests/TypeReconciliation/EmptyTest.php index 1d92af7130d..873d75c5459 100644 --- a/tests/TypeReconciliation/EmptyTest.php +++ b/tests/TypeReconciliation/EmptyTest.php @@ -200,7 +200,7 @@ function foo(int $t) : void { ' "$_GET['abc']-src/FileWithErrors.php:345-349" - "$_GET['abc']-src/FileWithErrors.php:345-349" -> "coalesce-src/FileWithErrors.php:345-363" + "$_GET:src/FileWithErrors.php:413" -> "$_GET['abc']-src/FileWithErrors.php:413-417" + "$_GET:src/FileWithErrors.php:440" -> "$_GET['abc']-src/FileWithErrors.php:440-444" + "$_GET:src/FileWithErrors.php:456" -> "$_GET['abc']-src/FileWithErrors.php:456-460" + "$_GET['abc']-src/FileWithErrors.php:440-444" -> "call to is_string-src/FileWithErrors.php:440-451" + "$_GET['abc']-src/FileWithErrors.php:456-460" -> "call to echo-src/FileWithErrors.php:407-473" "$s-src/FileWithErrors.php:109-110" -> "variable-use" -> "acme\sampleproject\bar" "$s-src/FileWithErrors.php:162-163" -> "variable-use" -> "acme\sampleproject\baz" "$s-src/FileWithErrors.php:215-216" -> "variable-use" -> "acme\sampleproject\bat" @@ -10,6 +13,8 @@ digraph Taints { "acme\sampleproject\bat#1" -> "$s-src/FileWithErrors.php:215-216" "acme\sampleproject\baz#1" -> "$s-src/FileWithErrors.php:162-163" "acme\sampleproject\foo#1" -> "$s-src/FileWithErrors.php:57-58" - "call to echo-src/FileWithErrors.php:335-364" -> "echo#1-src/filewitherrors.php:330" - "coalesce-src/FileWithErrors.php:345-363" -> "call to echo-src/FileWithErrors.php:335-364" + "call to echo-src/FileWithErrors.php:335-367" -> "echo#1-src/filewitherrors.php:330" + "call to echo-src/FileWithErrors.php:407-473" -> "echo#1-src/filewitherrors.php:402" + "call to is_string-src/FileWithErrors.php:440-451" -> "is_string#1-src/filewitherrors.php:430" + "coalesce-src/FileWithErrors.php:345-366" -> "call to echo-src/FileWithErrors.php:335-367" } From a3cb10c085c6d217149209f632d5b146cf0da47d Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 11 Sep 2022 17:03:19 +0200 Subject: [PATCH 101/194] make $_SERVER more detailed --- .../Fetch/VariableFetchAnalyzer.php | 112 ++++++++++++++++-- 1 file changed, 104 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 60ef3b9f287..8b3f8de7a7a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -22,7 +22,6 @@ use Psalm\IssueBuffer; use Psalm\Type; use Psalm\Type\Atomic\TArray; -use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIntRange; use Psalm\Type\Atomic\TKeyedArray; @@ -612,14 +611,111 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ } if ($var_id === '$_SERVER' || $var_id === '$_ENV') { - $type = new TArray( - [ - Type::getNonEmptyString(), - new Union([new TFloat(), new TIntRange(1, null), new TString(), new TNonEmptyList(Type::getString())]), - ] - ); + $string_helper = Type::getString(); + $string_helper->possibly_undefined = true; - return new Union([$type]); + $non_empty_string_helper = Type::getNonEmptyString(); + $non_empty_string_helper->possibly_undefined = true; + + $argv_helper = new Union([ + new TNonEmptyList(Type::getString()) + ]); + $argv_helper->possibly_undefined = true; + + $argc_helper = new Union([ + new TIntRange(1, null) + ]); + $argc_helper->possibly_undefined = true; + + $request_time_helper = new Union([ + new TIntRange(time(), null) + ]); + $request_time_helper->possibly_undefined = true; + + $request_time_float_helper = Type::getFloat(); + $request_time_float_helper->possibly_undefined = true; + + $detailed_type = new TKeyedArray([ + // https://www.php.net/manual/en/reserved.variables.server.php + 'PHP_SELF' => $non_empty_string_helper, + 'argv' => $argv_helper, + 'argc' => $argc_helper, + 'GATEWAY_INTERFACE' => $non_empty_string_helper, + 'SERVER_ADDR' => $non_empty_string_helper, + 'SERVER_NAME' => $non_empty_string_helper, + 'SERVER_SOFTWARE' => $non_empty_string_helper, + 'SERVER_PROTOCOL' => $non_empty_string_helper, + 'REQUEST_METHOD' => $non_empty_string_helper, + 'REQUEST_TIME' => $request_time_helper, + 'REQUEST_TIME_FLOAT' => $request_time_float_helper, + 'QUERY_STRING' => $string_helper, + 'DOCUMENT_ROOT' => $non_empty_string_helper, + 'HTTP_ACCEPT' => $non_empty_string_helper, + 'HTTP_ACCEPT_CHARSET' => $non_empty_string_helper, + 'HTTP_ACCEPT_ENCODING' => $non_empty_string_helper, + 'HTTP_ACCEPT_LANGUAGE' => $non_empty_string_helper, + 'HTTP_CONNECTION' => $non_empty_string_helper, + 'HTTP_HOST' => $non_empty_string_helper, + 'HTTP_REFERER' => $non_empty_string_helper, + 'HTTP_USER_AGENT' => $non_empty_string_helper, + 'HTTPS' => $string_helper, + 'REMOTE_ADDR' => $non_empty_string_helper, + 'REMOTE_HOST' => $non_empty_string_helper, + 'REMOTE_PORT' => $string_helper, + 'REMOTE_USER' => $non_empty_string_helper, + 'REDIRECT_REMOTE_USER' => $non_empty_string_helper, + 'SCRIPT_FILENAME' => $non_empty_string_helper, + 'SERVER_ADMIN' => $non_empty_string_helper, + 'SERVER_PORT' => $non_empty_string_helper, + 'SERVER_SIGNATURE' => $non_empty_string_helper, + 'PATH_TRANSLATED' => $non_empty_string_helper, + 'SCRIPT_NAME' => $non_empty_string_helper, + 'REQUEST_URI' => $non_empty_string_helper, + 'PHP_AUTH_DIGEST' => $non_empty_string_helper, + 'PHP_AUTH_USER' => $non_empty_string_helper, + 'PHP_AUTH_PW' => $non_empty_string_helper, + 'AUTH_TYPE' => $non_empty_string_helper, + 'PATH_INFO' => $non_empty_string_helper, + 'ORIG_PATH_INFO' => $non_empty_string_helper, + // misc from RFC not included above already http://www.faqs.org/rfcs/rfc3875.html + 'CONTENT_LENGTH' => $string_helper, + 'CONTENT_TYPE' => $string_helper, + // common, misc stuff + 'FCGI_ROLE' => $non_empty_string_helper, + 'HOME' => $non_empty_string_helper, + 'HTTP_CACHE_CONTROL' => $non_empty_string_helper, + 'HTTP_COOKIE' => $non_empty_string_helper, + 'HTTP_PRIORITY' => $non_empty_string_helper, + 'PATH' => $non_empty_string_helper, + 'REDIRECT_STATUS' => $non_empty_string_helper, + 'REQUEST_SCHEME' => $non_empty_string_helper, + 'USER' => $non_empty_string_helper, + // common, misc headers + 'HTTP_UPGRADE_INSECURE_REQUESTS' => $non_empty_string_helper, + 'HTTP_X_FORWARDED_PROTO' => $non_empty_string_helper, + 'HTTP_CLIENT_IP' => $non_empty_string_helper, + 'HTTP_X_REAL_IP' => $non_empty_string_helper, + 'HTTP_X_FORWARDED_FOR' => $non_empty_string_helper, + 'HTTP_CF_CONNECTING_IP' => $non_empty_string_helper, + 'HTTP_CF_IPCOUNTRY' => $non_empty_string_helper, + 'HTTP_CF_VISITOR' => $non_empty_string_helper, + 'HTTP_CDN_LOOP' => $non_empty_string_helper, + // common, misc browser headers + 'HTTP_DNT' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_DEST' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_USER' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_MODE' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_SITE' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA_PLATFORM' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA_MOBILE' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA' => $non_empty_string_helper, + ]); + + // generic case for all other elements + $detailed_type->previous_key_type = Type::getNonEmptyString(); + $detailed_type->previous_value_type = Type::getString(); + + return new Union([$detailed_type]); } if ($var_id === '$_FILES') { From b701c7074b97e8f2e347d9b847587be25bd6d716 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 11 Sep 2022 18:18:45 +0200 Subject: [PATCH 102/194] fix tests for detailed $_SERVER --- tests/AssertAnnotationTest.php | 6 +++--- tests/TypeReconciliation/ConditionalTest.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/AssertAnnotationTest.php b/tests/AssertAnnotationTest.php index 8dead02b7d6..9ecb98208c5 100644 --- a/tests/AssertAnnotationTest.php +++ b/tests/AssertAnnotationTest.php @@ -375,7 +375,7 @@ function isInvalidString(?string $myVar) : bool { echo "Ma chaine " . $myString; }', ], - 'assertServerVar' => [ + 'assertSessionVar' => [ ' [ diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index 3b7d0148bd5..fb1e10efa92 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -768,10 +768,10 @@ function d(?iterable $foo): void { } }', ], - 'isStringServerVar' => [ + 'isStringSessionVar' => [ ' [ From a2118c65d3ef06cb3cdf5b8b19d18a2f32048f0c Mon Sep 17 00:00:00 2001 From: hirokinoue <70567194+hirokinoue@users.noreply.github.com> Date: Sat, 17 Sep 2022 22:11:28 +0900 Subject: [PATCH 103/194] add test case reproducing issue #7428 --- .../TypeReconciliation/ArrayKeyExistsTest.php | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 0f8bfe16498..6367f4eed5d 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -2,6 +2,8 @@ namespace Psalm\Tests\TypeReconciliation; +use Psalm\Config; +use Psalm\Context; use Psalm\Tests\TestCase; use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; @@ -430,4 +432,31 @@ function go(array $options): void { ], ]; } + + public function testAllowPropertyFetchAsNeedle(): void + { + Config::getInstance()->ensure_array_int_offsets_exist = true; + + $this->addFile( + 'somefile.php', + ' $bar */ + $bar = []; + + if (array_key_exists($foo->status, $bar)) { + echo $bar[$foo->status]; + }' + ); + + $this->analyzeFile('somefile.php', new Context()); + } } From faf4e8ef84d6433b5994290bc488f042301561d9 Mon Sep 17 00:00:00 2001 From: hirokinoue <70567194+hirokinoue@users.noreply.github.com> Date: Sat, 17 Sep 2022 22:36:16 +0900 Subject: [PATCH 104/194] allow PropertyFetch node to behave like Variable node --- .../Analyzer/Statements/Expression/AssertionFinder.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 1603d1655dd..eb537d018e2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -3685,7 +3685,9 @@ private static function getArrayKeyExistsAssertions( } else { $first_var_name = null; } - } elseif ($expr->getArgs()[0]->value instanceof PhpParser\Node\Expr\Variable + } elseif (($expr->getArgs()[0]->value instanceof PhpParser\Node\Expr\Variable + || $expr->getArgs()[0]->value instanceof PhpParser\Node\Expr\PropertyFetch + ) && $source instanceof StatementsAnalyzer && ($first_var_type = $source->node_data->getType($expr->getArgs()[0]->value)) ) { From 2f870776e24d078bb8e0cce38cdbdf33b6aa48a5 Mon Sep 17 00:00:00 2001 From: hirokinoue <70567194+hirokinoue@users.noreply.github.com> Date: Sat, 17 Sep 2022 23:02:25 +0900 Subject: [PATCH 105/194] add test case derived from issue #7428 --- .../TypeReconciliation/ArrayKeyExistsTest.php | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/TypeReconciliation/ArrayKeyExistsTest.php b/tests/TypeReconciliation/ArrayKeyExistsTest.php index 6367f4eed5d..90fa7d938a1 100644 --- a/tests/TypeReconciliation/ArrayKeyExistsTest.php +++ b/tests/TypeReconciliation/ArrayKeyExistsTest.php @@ -459,4 +459,29 @@ class Foo { $this->analyzeFile('somefile.php', new Context()); } + + public function testAllowStaticPropertyFetchAsNeedle(): void + { + Config::getInstance()->ensure_array_int_offsets_exist = true; + + $this->addFile( + 'somefile.php', + ' $bar */ + $bar = []; + + if (array_key_exists(Foo::$status, $bar)) { + echo $bar[Foo::$status]; + }' + ); + + $this->analyzeFile('somefile.php', new Context()); + } } From 2cf131fb45f8b211e4b11e148519ed390a5323a2 Mon Sep 17 00:00:00 2001 From: hirokinoue <70567194+hirokinoue@users.noreply.github.com> Date: Sat, 17 Sep 2022 23:08:29 +0900 Subject: [PATCH 106/194] allow StaticPropertyFetch node to behave like Variable node --- .../Internal/Analyzer/Statements/Expression/AssertionFinder.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index eb537d018e2..1cd6ed62f41 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -3687,6 +3687,7 @@ private static function getArrayKeyExistsAssertions( } } elseif (($expr->getArgs()[0]->value instanceof PhpParser\Node\Expr\Variable || $expr->getArgs()[0]->value instanceof PhpParser\Node\Expr\PropertyFetch + || $expr->getArgs()[0]->value instanceof PhpParser\Node\Expr\StaticPropertyFetch ) && $source instanceof StatementsAnalyzer && ($first_var_type = $source->node_data->getType($expr->getArgs()[0]->value)) From 9071e877a8d240bdf3ac7b32ba4f3549d5f83f52 Mon Sep 17 00:00:00 2001 From: hirokinoue <70567194+hirokinoue@users.noreply.github.com> Date: Sun, 18 Sep 2022 01:15:09 +0900 Subject: [PATCH 107/194] fix according to psalm analysis --- .../Analyzer/Statements/Expression/AssertionFinder.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 1cd6ed62f41..029d9422cf9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -3653,8 +3653,8 @@ private static function getArrayKeyExistsAssertions( ) : null; - if ($array_root) { - if ($first_var_name === null && isset($expr->getArgs()[0])) { + if ($array_root && isset($expr->getArgs()[0])) { + if ($first_var_name === null) { $first_arg = $expr->getArgs()[0]; if ($first_arg->value instanceof PhpParser\Node\Scalar\String_) { From 602e26edd4acbc3b77ebd5bbf06a5f36c2692e24 Mon Sep 17 00:00:00 2001 From: HypeMC Date: Sat, 17 Sep 2022 21:21:33 +0200 Subject: [PATCH 108/194] Fix array_column with object and column name null --- .../ArrayColumnReturnTypeProvider.php | 9 +- tests/ReturnTypeProvider/ArrayColumnTest.php | 92 +++++++++++++++++++ 2 files changed, 95 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php index 89e68a1eaa4..b9d45b1c3ef 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayColumnReturnTypeProvider.php @@ -36,7 +36,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return Type::getMixed(); } - $row_shape = null; + $row_type = $row_shape = null; $input_array_not_empty = false; // calculate row shape @@ -45,7 +45,6 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev && $first_arg_type->hasArray() ) { $input_array = $first_arg_type->getAtomicTypes()['array']; - $row_type = null; if ($input_array instanceof TKeyedArray) { $row_type = $input_array->getGenericArrayType()->type_params[1]; } elseif ($input_array instanceof TArray) { @@ -104,7 +103,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } $result_key_type = Type::getArrayKey(); - $result_element_type = null; + $result_element_type = null !== $row_type && $value_column_name_is_null ? $row_type : null; $have_at_least_one_res = false; // calculate results if ($row_shape instanceof TKeyedArray) { @@ -116,9 +115,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } //array_column skips undefined elements so resulting type is necessarily defined $result_element_type->possibly_undefined = false; - } elseif ($value_column_name_is_null) { - $result_element_type = new Union([$row_shape]); - } else { + } elseif (!$value_column_name_is_null) { $result_element_type = Type::getMixed(); } diff --git a/tests/ReturnTypeProvider/ArrayColumnTest.php b/tests/ReturnTypeProvider/ArrayColumnTest.php index 5b5cf4d2276..400dad77465 100644 --- a/tests/ReturnTypeProvider/ArrayColumnTest.php +++ b/tests/ReturnTypeProvider/ArrayColumnTest.php @@ -3,10 +3,12 @@ namespace Psalm\Tests\ReturnTypeProvider; use Psalm\Tests\TestCase; +use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait; use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait; class ArrayColumnTest extends TestCase { + use InvalidCodeAnalysisTestTrait; use ValidCodeAnalysisTestTrait; public function providerValidCodeParse(): iterable @@ -61,5 +63,95 @@ function f(array $shape): array { } ', ]; + + yield 'arrayColumnWithObjectsAndColumnNameNull' => [ + 'foo(); + } + ', + ]; + + yield 'arrayColumnWithIntersectionAndColumnNameNull' => [ + 'foo(); + $instance->bar(); + } + ', + ]; + + yield 'arrayColumnWithArrayAndColumnNameNull' => [ + ' "", "instance" => new C]], null, "name") as $array) { + $array["instance"]->foo(); + } + ', + ]; + + yield 'arrayColumnWithListOfObject' => [ + ' $instances */ + $instances = []; + foreach (array_column($instances, null, "name") as $instance) { + foo($instance); + } + ', + ]; + + yield 'arrayColumnWithListOfArrays' => [ + ' $arrays */ + $arrays = []; + foreach (array_column($arrays, null, "name") as $array) { + foo($array); + } + ', + ]; + } + + public function providerInvalidCodeParse(): iterable + { + yield 'arrayColumnWithArrayAndColumnNameNull' => [ + ' $arrays */ + $arrays = []; + foreach (array_column($arrays, null, "name") as $array) { + $array["instance"]->foo(); + } + ', + 'error_message' => 'MixedMethodCall', + ]; } } From ee16caf596a784af2dbac0decdeacf85a042eceb Mon Sep 17 00:00:00 2001 From: HypeMC Date: Sat, 17 Sep 2022 22:20:01 +0200 Subject: [PATCH 109/194] Make template constraints examples in docs consistent --- docs/annotating_code/templated_annotations.md | 2 +- docs/annotating_code/type_syntax/conditional_types.md | 4 ++-- docs/running_psalm/issues/InvalidTemplateParam.md | 2 +- docs/running_psalm/issues/NonInvariantDocblockPropertyType.md | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/annotating_code/templated_annotations.md b/docs/annotating_code/templated_annotations.md index 512a83ab17a..ed1d8054494 100644 --- a/docs/annotating_code/templated_annotations.md +++ b/docs/annotating_code/templated_annotations.md @@ -229,7 +229,7 @@ Templated types aren't limited to key-value pairs, and you can re-use templates ```php */ diff --git a/docs/annotating_code/type_syntax/conditional_types.md b/docs/annotating_code/type_syntax/conditional_types.md index 3d57dab3357..00dacca29a4 100644 --- a/docs/annotating_code/type_syntax/conditional_types.md +++ b/docs/annotating_code/type_syntax/conditional_types.md @@ -18,7 +18,7 @@ Let's suppose we want to make a userland implementation of PHP's numeric additio Date: Sat, 17 Sep 2022 23:46:19 +0200 Subject: [PATCH 110/194] Document the object with properties syntax --- .../type_syntax/atomic_types.md | 1 + .../type_syntax/object_types.md | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) diff --git a/docs/annotating_code/type_syntax/atomic_types.md b/docs/annotating_code/type_syntax/atomic_types.md index 5fa957be454..093dad1b0a7 100644 --- a/docs/annotating_code/type_syntax/atomic_types.md +++ b/docs/annotating_code/type_syntax/atomic_types.md @@ -21,6 +21,7 @@ Atomic types are the basic building block of all type information used in Psalm. ## [Object types](object_types.md) - [object](object_types.md) +- [object{foo: string}](object_types.md) - [Exception, Foo\MyClass and Foo\MyClass](object_types.md) - [Generator](object_types.md) diff --git a/docs/annotating_code/type_syntax/object_types.md b/docs/annotating_code/type_syntax/object_types.md index 00af6c79e82..18de65d71a3 100644 --- a/docs/annotating_code/type_syntax/object_types.md +++ b/docs/annotating_code/type_syntax/object_types.md @@ -2,6 +2,25 @@ `object`, `stdClass`, `Foo`, `Bar\Baz` etc. are examples of object types. These types are also valid types in PHP. +#### Object properties + +Psalm supports specifying the properties of an object and their expected types, e.g.: + +```php +/** @param object{foo: string} $obj */ +function takesObject(object $obj) : string { + return $obj->foo; +} + +takesObject((object) ["foo" => "hello"]); +``` + +Optional properties can be denoted by a trailing `?`, e.g.: + +```php +/** @param object{optional?: string} */ +``` + #### Generic object types Psalm supports using generic object types like `ArrayObject`. Any generic object should be typehinted with appropriate [`@template` tags](../templated_annotations.md). From eb93f692513274bcce179de265be7fbe03409efe Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sat, 17 Sep 2022 19:37:32 +0200 Subject: [PATCH 111/194] Add null-type to several DOM-functions --- dictionaries/CallMap.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index bcdd10cdb39..98d47ea3a36 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -2105,7 +2105,7 @@ 'DOMDocument::createTextNode' => ['DOMText|false', 'content'=>'string'], 'DOMDocument::getElementById' => ['?DOMElement', 'elementid'=>'string'], 'DOMDocument::getElementsByTagName' => ['DOMNodeList', 'name'=>'string'], -'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string', 'localname'=>'string'], +'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMDocument::importNode' => ['DOMNode|false', 'importednode'=>'DOMNode', 'deep='=>'bool'], 'DOMDocument::load' => ['DOMDocument|bool', 'filename'=>'string', 'options='=>'int'], 'DOMDocument::loadHTML' => ['bool', 'source'=>'string', 'options='=>'int'], @@ -2137,23 +2137,23 @@ 'DOMElement::get_elements_by_tagname' => ['array', 'name'=>'string'], 'DOMElement::getAttribute' => ['string', 'name'=>'string'], 'DOMElement::getAttributeNode' => ['DOMAttr', 'name'=>'string'], -'DOMElement::getAttributeNodeNS' => ['DOMAttr', 'namespaceuri'=>'string', 'localname'=>'string'], -'DOMElement::getAttributeNS' => ['string', 'namespaceuri'=>'string', 'localname'=>'string'], +'DOMElement::getAttributeNodeNS' => ['DOMAttr', 'namespaceuri'=>'string|null', 'localname'=>'string'], +'DOMElement::getAttributeNS' => ['string', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::getElementsByTagName' => ['DOMNodeList', 'name'=>'string'], -'DOMElement::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string', 'localname'=>'string'], +'DOMElement::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::has_attribute' => ['bool', 'name'=>'string'], 'DOMElement::hasAttribute' => ['bool', 'name'=>'string'], -'DOMElement::hasAttributeNS' => ['bool', 'namespaceuri'=>'string', 'localname'=>'string'], +'DOMElement::hasAttributeNS' => ['bool', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::remove_attribute' => ['bool', 'name'=>'string'], 'DOMElement::removeAttribute' => ['bool', 'name'=>'string'], 'DOMElement::removeAttributeNode' => ['bool', 'oldnode'=>'DOMAttr'], -'DOMElement::removeAttributeNS' => ['bool', 'namespaceuri'=>'string', 'localname'=>'string'], +'DOMElement::removeAttributeNS' => ['bool', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::set_attribute' => ['DomAttribute', 'name'=>'string', 'value'=>'string'], 'DOMElement::set_attribute_node' => ['DomNode', 'attr'=>'DOMNode'], 'DOMElement::setAttribute' => ['DOMAttr|false', 'name'=>'string', 'value'=>'string'], 'DOMElement::setAttributeNode' => ['?DOMAttr', 'attr'=>'DOMAttr'], 'DOMElement::setAttributeNodeNS' => ['DOMAttr', 'attr'=>'DOMAttr'], -'DOMElement::setAttributeNS' => ['void', 'namespaceuri'=>'string', 'qualifiedname'=>'string', 'value'=>'string'], +'DOMElement::setAttributeNS' => ['void', 'namespaceuri'=>'string|null', 'qualifiedname'=>'string', 'value'=>'string'], 'DOMElement::setIdAttribute' => ['void', 'name'=>'string', 'isid'=>'bool'], 'DOMElement::setIdAttributeNode' => ['void', 'attr'=>'DOMAttr', 'isid'=>'bool'], 'DOMElement::setIdAttributeNS' => ['void', 'namespaceuri'=>'string', 'localname'=>'string', 'isid'=>'bool'], From 3b737480bdc051181229e23add085e36e93147e1 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 18 Sep 2022 11:34:40 +0200 Subject: [PATCH 112/194] Fix CallMap_historical --- dictionaries/CallMap_historical.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index b0fb9eebf43..c5b135785cc 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -936,7 +936,7 @@ 'DOMDocument::createTextNode' => ['DOMText|false', 'content'=>'string'], 'DOMDocument::getElementById' => ['?DOMElement', 'elementid'=>'string'], 'DOMDocument::getElementsByTagName' => ['DOMNodeList', 'name'=>'string'], - 'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string', 'localname'=>'string'], + 'DOMDocument::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMDocument::importNode' => ['DOMNode|false', 'importednode'=>'DOMNode', 'deep='=>'bool'], 'DOMDocument::load' => ['DOMDocument|bool', 'filename'=>'string', 'options='=>'int'], 'DOMDocument::loadHTML' => ['bool', 'source'=>'string', 'options='=>'int'], @@ -958,23 +958,23 @@ 'DOMDocumentFragment::appendXML' => ['bool', 'data'=>'string'], 'DOMElement::__construct' => ['void', 'name'=>'string', 'value='=>'string', 'uri='=>'string'], 'DOMElement::getAttribute' => ['string', 'name'=>'string'], - 'DOMElement::getAttributeNS' => ['string', 'namespaceuri'=>'string', 'localname'=>'string'], + 'DOMElement::getAttributeNS' => ['string', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::getAttributeNode' => ['DOMAttr', 'name'=>'string'], - 'DOMElement::getAttributeNodeNS' => ['DOMAttr', 'namespaceuri'=>'string', 'localname'=>'string'], + 'DOMElement::getAttributeNodeNS' => ['DOMAttr', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::getElementsByTagName' => ['DOMNodeList', 'name'=>'string'], - 'DOMElement::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string', 'localname'=>'string'], + 'DOMElement::getElementsByTagNameNS' => ['DOMNodeList', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::get_attribute' => ['string', 'name'=>'string'], 'DOMElement::get_attribute_node' => ['DomAttribute', 'name'=>'string'], 'DOMElement::get_elements_by_tagname' => ['array', 'name'=>'string'], 'DOMElement::hasAttribute' => ['bool', 'name'=>'string'], - 'DOMElement::hasAttributeNS' => ['bool', 'namespaceuri'=>'string', 'localname'=>'string'], + 'DOMElement::hasAttributeNS' => ['bool', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::has_attribute' => ['bool', 'name'=>'string'], 'DOMElement::removeAttribute' => ['bool', 'name'=>'string'], - 'DOMElement::removeAttributeNS' => ['bool', 'namespaceuri'=>'string', 'localname'=>'string'], + 'DOMElement::removeAttributeNS' => ['bool', 'namespaceuri'=>'string|null', 'localname'=>'string'], 'DOMElement::removeAttributeNode' => ['bool', 'oldnode'=>'DOMAttr'], 'DOMElement::remove_attribute' => ['bool', 'name'=>'string'], 'DOMElement::setAttribute' => ['DOMAttr|false', 'name'=>'string', 'value'=>'string'], - 'DOMElement::setAttributeNS' => ['void', 'namespaceuri'=>'string', 'qualifiedname'=>'string', 'value'=>'string'], + 'DOMElement::setAttributeNS' => ['void', 'namespaceuri'=>'string|null', 'qualifiedname'=>'string', 'value'=>'string'], 'DOMElement::setAttributeNode' => ['?DOMAttr', 'attr'=>'DOMAttr'], 'DOMElement::setAttributeNodeNS' => ['DOMAttr', 'attr'=>'DOMAttr'], 'DOMElement::setIdAttribute' => ['void', 'name'=>'string', 'isid'=>'bool'], From 2a315bef45033c13f149b431d3443725299937f8 Mon Sep 17 00:00:00 2001 From: Tim van Dijen Date: Sun, 18 Sep 2022 11:53:09 +0200 Subject: [PATCH 113/194] Fix some more --- dictionaries/CallMap.php | 4 ++-- dictionaries/CallMap_historical.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 98d47ea3a36..e16876d5ea3 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -2094,12 +2094,12 @@ 'DOMComment::__construct' => ['void', 'value='=>'string'], 'DOMDocument::__construct' => ['void', 'version='=>'string', 'encoding='=>'string'], 'DOMDocument::createAttribute' => ['DOMAttr|false', 'name'=>'string'], -'DOMDocument::createAttributeNS' => ['DOMAttr|false', 'namespaceuri'=>'string', 'qualifiedname'=>'string'], +'DOMDocument::createAttributeNS' => ['DOMAttr|false', 'namespaceuri'=>'string|null', 'qualifiedname'=>'string'], 'DOMDocument::createCDATASection' => ['DOMCDATASection|false', 'data'=>'string'], 'DOMDocument::createComment' => ['DOMComment|false', 'data'=>'string'], 'DOMDocument::createDocumentFragment' => ['DOMDocumentFragment|false'], 'DOMDocument::createElement' => ['DOMElement|false', 'name'=>'string', 'value='=>'string'], -'DOMDocument::createElementNS' => ['DOMElement|false', 'namespaceuri'=>'string', 'qualifiedname'=>'string', 'value='=>'string'], +'DOMDocument::createElementNS' => ['DOMElement|false', 'namespaceuri'=>'string|null', 'qualifiedname'=>'string', 'value='=>'string'], 'DOMDocument::createEntityReference' => ['DOMEntityReference|false', 'name'=>'string'], 'DOMDocument::createProcessingInstruction' => ['DOMProcessingInstruction|false', 'target'=>'string', 'data='=>'string'], 'DOMDocument::createTextNode' => ['DOMText|false', 'content'=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index c5b135785cc..997fc467be1 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -925,12 +925,12 @@ 'DOMComment::__construct' => ['void', 'value='=>'string'], 'DOMDocument::__construct' => ['void', 'version='=>'string', 'encoding='=>'string'], 'DOMDocument::createAttribute' => ['DOMAttr|false', 'name'=>'string'], - 'DOMDocument::createAttributeNS' => ['DOMAttr|false', 'namespaceuri'=>'string', 'qualifiedname'=>'string'], + 'DOMDocument::createAttributeNS' => ['DOMAttr|false', 'namespaceuri'=>'string|null', 'qualifiedname'=>'string'], 'DOMDocument::createCDATASection' => ['DOMCDATASection|false', 'data'=>'string'], 'DOMDocument::createComment' => ['DOMComment|false', 'data'=>'string'], 'DOMDocument::createDocumentFragment' => ['DOMDocumentFragment|false'], 'DOMDocument::createElement' => ['DOMElement|false', 'name'=>'string', 'value='=>'string'], - 'DOMDocument::createElementNS' => ['DOMElement|false', 'namespaceuri'=>'string', 'qualifiedname'=>'string', 'value='=>'string'], + 'DOMDocument::createElementNS' => ['DOMElement|false', 'namespaceuri'=>'string|null', 'qualifiedname'=>'string', 'value='=>'string'], 'DOMDocument::createEntityReference' => ['DOMEntityReference|false', 'name'=>'string'], 'DOMDocument::createProcessingInstruction' => ['DOMProcessingInstruction|false', 'target'=>'string', 'data='=>'string'], 'DOMDocument::createTextNode' => ['DOMText|false', 'content'=>'string'], From e2e6265ba18c13604ab8261f73f79ecf884decae Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 15 Sep 2022 19:49:36 +0200 Subject: [PATCH 114/194] ignore nullable issues for $argv/$argc --- .../Expression/Fetch/VariableFetchAnalyzer.php | 14 ++++++++++++-- tests/Internal/CliUtilsTest.php | 4 ++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 8b3f8de7a7a..303caa17ce1 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -541,18 +541,28 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ if ($var_id === '$argv') { // only in CLI, null otherwise - return new Union([ + $argv_nullable = new Union([ new TNonEmptyList(Type::getString()), new TNull() ]); + // use TNull explicitly instead of this + // as it will cause weird errors due to ignore_nullable_issues true + // e.g. InvalidPropertyAssignmentValue + // $this->argv 'list' cannot be assigned type 'non-empty-list' + // $argv_nullable->possibly_undefined = true; + $argv_nullable->ignore_nullable_issues = true; + return $argv_nullable; } if ($var_id === '$argc') { // only in CLI, null otherwise - return new Union([ + $argc_nullable = new Union([ new TIntRange(1, null), new TNull() ]); + // $argc_nullable->possibly_undefined = true; + $argc_nullable->ignore_nullable_issues = true; + return $argc_nullable; } if (!self::isSuperGlobal($var_id)) { diff --git a/tests/Internal/CliUtilsTest.php b/tests/Internal/CliUtilsTest.php index a2bfe9cb6fc..928eb0152f1 100644 --- a/tests/Internal/CliUtilsTest.php +++ b/tests/Internal/CliUtilsTest.php @@ -12,14 +12,14 @@ class CliUtilsTest extends TestCase { /** - * @var array + * @var list */ private $argv = []; protected function setUp(): void { global $argv; - $this->argv = $argv ?? []; + $this->argv = $argv; } protected function tearDown(): void From 44785fadfca997c4e47019c7aa413f3acdb004a3 Mon Sep 17 00:00:00 2001 From: dennis Date: Tue, 13 Sep 2022 11:28:26 +0200 Subject: [PATCH 115/194] Introduce test with code inside namespace --- .../ThrowsBlockAdditionTest.php | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/FileManipulation/ThrowsBlockAdditionTest.php b/tests/FileManipulation/ThrowsBlockAdditionTest.php index ae9eabf415d..cf192ee04e3 100644 --- a/tests/FileManipulation/ThrowsBlockAdditionTest.php +++ b/tests/FileManipulation/ThrowsBlockAdditionTest.php @@ -118,6 +118,30 @@ function foo(string $s): string { ['MissingThrowsDocblock'], true, ], + 'addThrowsAnnotationToFunctionInNamespace' => [ + ' Date: Tue, 13 Sep 2022 11:31:20 +0200 Subject: [PATCH 116/194] Ensure @throws annotations reference valid Exception classes --- .../Internal/FileManipulation/FunctionDocblockManipulator.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php index ca652333ea8..6cc3c594681 100644 --- a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php @@ -405,6 +405,7 @@ private function getDocblock(): string $inferredThrowsClause = array_reduce( $this->throwsExceptions, function (string $throwsClause, string $exception) { + $exception = '\\' . $exception; return $throwsClause === '' ? $exception : $throwsClause.'|'.$exception; }, '' From b88d2890e29155d56727d9bdb7c3d551da774440 Mon Sep 17 00:00:00 2001 From: dennis Date: Tue, 13 Sep 2022 11:32:19 +0200 Subject: [PATCH 117/194] Fix existing tests --- tests/FileManipulation/ThrowsBlockAdditionTest.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/FileManipulation/ThrowsBlockAdditionTest.php b/tests/FileManipulation/ThrowsBlockAdditionTest.php index cf192ee04e3..af4876ecd6f 100644 --- a/tests/FileManipulation/ThrowsBlockAdditionTest.php +++ b/tests/FileManipulation/ThrowsBlockAdditionTest.php @@ -20,7 +20,7 @@ function foo(string $s): string { }', ' Date: Sat, 17 Sep 2022 09:58:49 +0200 Subject: [PATCH 118/194] Add test to document cross namespace behaviour --- .../ThrowsBlockAdditionTest.php | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/tests/FileManipulation/ThrowsBlockAdditionTest.php b/tests/FileManipulation/ThrowsBlockAdditionTest.php index af4876ecd6f..02cb974e86e 100644 --- a/tests/FileManipulation/ThrowsBlockAdditionTest.php +++ b/tests/FileManipulation/ThrowsBlockAdditionTest.php @@ -142,6 +142,44 @@ function foo(string $s): string { ['MissingThrowsDocblock'], true, ], + 'addThrowsAnnotationToFunctionFromFunctionFromOtherNamespace' => [ + ' Date: Sat, 17 Sep 2022 10:55:19 +0200 Subject: [PATCH 119/194] Improved class name generation for @throws annotation --- src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php | 9 ++++++++- .../FileManipulation/FunctionDocblockManipulator.php | 1 - tests/FileManipulation/ThrowsBlockAdditionTest.php | 8 ++++---- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 385a954c946..c9c63238b8e 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -720,7 +720,14 @@ public function analyze( } if (!$is_expected) { - $missingThrowsDocblockErrors[] = $possibly_thrown_exception; + $missing_docblock_exception = new TNamedObject($possibly_thrown_exception); + $missingThrowsDocblockErrors[] = $missing_docblock_exception->toNamespacedString( + $this->source->getNamespace(), + $this->source->getAliasedClassesFlipped(), + $this->source->getFQCLN(), + true + ); + foreach ($codelocations as $codelocation) { // issues are suppressed in ThrowAnalyzer, CallAnalyzer, etc. IssueBuffer::maybeAdd( diff --git a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php index 6cc3c594681..ca652333ea8 100644 --- a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php @@ -405,7 +405,6 @@ private function getDocblock(): string $inferredThrowsClause = array_reduce( $this->throwsExceptions, function (string $throwsClause, string $exception) { - $exception = '\\' . $exception; return $throwsClause === '' ? $exception : $throwsClause.'|'.$exception; }, '' diff --git a/tests/FileManipulation/ThrowsBlockAdditionTest.php b/tests/FileManipulation/ThrowsBlockAdditionTest.php index 02cb974e86e..b5a57c1924d 100644 --- a/tests/FileManipulation/ThrowsBlockAdditionTest.php +++ b/tests/FileManipulation/ThrowsBlockAdditionTest.php @@ -20,7 +20,7 @@ function foo(string $s): string { }', ' Date: Sat, 17 Sep 2022 10:56:29 +0200 Subject: [PATCH 120/194] Test to describe use statements being applied --- .../ThrowsBlockAdditionTest.php | 40 +++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/tests/FileManipulation/ThrowsBlockAdditionTest.php b/tests/FileManipulation/ThrowsBlockAdditionTest.php index b5a57c1924d..41f4e4ae2df 100644 --- a/tests/FileManipulation/ThrowsBlockAdditionTest.php +++ b/tests/FileManipulation/ThrowsBlockAdditionTest.php @@ -180,6 +180,46 @@ function bar(): void { ['MissingThrowsDocblock'], true, ], + 'addThrowsAnnotationAccountsForUseStatements' => [ + ' Date: Wed, 3 Aug 2022 21:43:37 +0200 Subject: [PATCH 121/194] fix type for (string) true --- .../Statements/Expression/CastAnalyzer.php | 27 ++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 439ee3bf19e..b75176e2b41 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -23,6 +23,7 @@ use Psalm\Type\Atomic\Scalar; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TBool; +use Psalm\Type\Atomic\TClosedResource; use Psalm\Type\Atomic\TFalse; use Psalm\Type\Atomic\TFloat; use Psalm\Type\Atomic\TInt; @@ -33,6 +34,9 @@ use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Atomic\TNonEmptyArray; +use Psalm\Type\Atomic\TNonEmptyList; +use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Atomic\TNonspecificLiteralInt; use Psalm\Type\Atomic\TNonspecificLiteralString; use Psalm\Type\Atomic\TNull; @@ -42,6 +46,7 @@ use Psalm\Type\Atomic\TResource; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\Atomic\TTrue; use Psalm\Type\Union; use function array_merge; @@ -352,8 +357,28 @@ public static function castStringAttempt( continue; } + if ($atomic_type instanceof TTrue + ) { + $valid_strings[] = new TLiteralString('1'); + continue; + } + + if ($atomic_type instanceof TBool + ) { + $valid_strings[] = new TLiteralString('1'); + $valid_strings[] = new TLiteralString(''); + continue; + } + + if ($atomic_type instanceof TClosedResource + || $atomic_type instanceof TResource + ) { + $castable_types[] = new TNonEmptyString(); + + continue; + } + if ($atomic_type instanceof TMixed - || $atomic_type instanceof TResource || $atomic_type instanceof Scalar ) { $castable_types[] = new TString(); From 3bec76acb8c89637d3c6c4817f6a9f18ec49f3e4 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 3 Aug 2022 22:15:41 +0200 Subject: [PATCH 122/194] fix invalid casts for int https://psalm.dev/r/0d284c6f48 --- .../Statements/Expression/CastAnalyzer.php | 272 ++++++++++++++++-- 1 file changed, 243 insertions(+), 29 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index b75176e2b41..4d45367cad5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -52,7 +52,6 @@ use function array_merge; use function array_pop; use function array_values; -use function count; use function get_class; class CastAnalyzer @@ -67,45 +66,27 @@ public static function analyze( return false; } - $as_int = true; - $valid_int_type = null; $maybe_type = $statements_analyzer->node_data->getType($stmt->expr); if ($maybe_type) { if ($maybe_type->isInt()) { - $valid_int_type = $maybe_type; if (!$maybe_type->from_calculation) { self::handleRedundantCast($maybe_type, $statements_analyzer, $stmt); } } - if (count($maybe_type->getAtomicTypes()) === 1 - && $maybe_type->getSingleAtomic() instanceof TBool) { - $as_int = false; - $type = new Union([ - new TLiteralInt(0), - new TLiteralInt(1), - ]); - - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph - ) { - $type->parent_nodes = $maybe_type->parent_nodes; - } - - $statements_analyzer->node_data->setType($stmt, $type); - } + $type = self::castIntAttempt( + $statements_analyzer, + $context, + $maybe_type, + $stmt->expr, + true + ); + } else { + $type = Type::getInt(); } - if ($as_int) { - $type = $valid_int_type ?? Type::getInt(); - - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph - ) { - $type->parent_nodes = $maybe_type->parent_nodes ?? []; - } - - $statements_analyzer->node_data->setType($stmt, $type); - } + $statements_analyzer->node_data->setType($stmt, $type); return true; } @@ -305,6 +286,239 @@ public static function analyze( return false; } + public static function castIntAttempt( + StatementsAnalyzer $statements_analyzer, + Context $context, + Union $stmt_type, + PhpParser\Node\Expr $stmt, + bool $explicit_cast = false + ): Union { + $codebase = $statements_analyzer->getCodebase(); + + $possibly_unwanted_cast = []; + $invalid_casts = []; + $valid_ints = []; + $castable_types = []; + + $atomic_types = $stmt_type->getAtomicTypes(); + + $parent_nodes = []; + + if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { + $parent_nodes = $stmt_type->parent_nodes; + } + + while ($atomic_types) { + $atomic_type = array_pop($atomic_types); + + if ($atomic_type instanceof TInt) { + $valid_ints[] = $atomic_type; + + continue; + } + + if ($atomic_type instanceof TFloat) { + if ($atomic_type instanceof TLiteralFloat) { + $valid_ints[] = new TLiteralInt((int) $atomic_type->value); + } else { + $castable_types[] = new TInt(); + } + + continue; + } + + if ($atomic_type instanceof TString) { + if ($atomic_type instanceof TLiteralString && (int) $atomic_type->value !== 0) { + $valid_ints[] = new TLiteralInt((int) $atomic_type->value); + } elseif ($atomic_type instanceof TNumericString) { + $castable_types[] = new TInt(); + } else { + // any normal string + $valid_ints[] = new TLiteralInt(0); + } + + continue; + } + + if ($atomic_type instanceof TNull || $atomic_type instanceof TFalse) { + $valid_ints[] = new TLiteralInt(0); + continue; + } + + if ($atomic_type instanceof TTrue) { + $valid_ints[] = new TLiteralInt(1); + continue; + } + + if ($atomic_type instanceof TBool) { + // do NOT use TIntRange here, as it will cause invalid behavior, e.g. bitwiseAssignment + $valid_ints[] = new TLiteralInt(0); + $valid_ints[] = new TLiteralInt(1); + continue; + } + + // could be invalid, but allow it, as it is allowed for TString below too + if ($atomic_type instanceof TMixed + || $atomic_type instanceof TClosedResource + || $atomic_type instanceof TResource + || $atomic_type instanceof Scalar + ) { + $castable_types[] = new TInt(); + + continue; + } + + if ($atomic_type instanceof TNamedObject + || $atomic_type instanceof TObjectWithProperties + ) { + $intersection_types = [$atomic_type]; + + if ($atomic_type->extra_types) { + $intersection_types = array_merge($intersection_types, $atomic_type->extra_types); + } + + foreach ($intersection_types as $intersection_type) { + if ($intersection_type instanceof TNamedObject) { + $intersection_method_id = new MethodIdentifier( + $intersection_type->value, + '__tostring' + ); + + if ($codebase->methods->methodExists( + $intersection_method_id, + $context->calling_method_id, + new CodeLocation($statements_analyzer->getSource(), $stmt) + )) { + $return_type = $codebase->methods->getMethodReturnType( + $intersection_method_id, + $self_class + ) ?? Type::getString(); + + $declaring_method_id = $codebase->methods->getDeclaringMethodId($intersection_method_id); + + MethodCallReturnTypeFetcher::taintMethodCallResult( + $statements_analyzer, + $return_type, + $stmt, + $stmt, + [], + $intersection_method_id, + $declaring_method_id, + $intersection_type->value . '::__toString', + $context + ); + + if ($statements_analyzer->data_flow_graph) { + $parent_nodes = array_merge($return_type->parent_nodes, $parent_nodes); + } + + foreach ($return_type->getAtomicTypes() as $sub_atomic_type) { + if ($sub_atomic_type instanceof TLiteralString + && (int) $sub_atomic_type->value !== 0 + ) { + $valid_ints[] = new TLiteralInt((int) $sub_atomic_type->value); + } elseif ($sub_atomic_type instanceof TNumericString) { + $castable_types[] = new TInt(); + } else { + $valid_ints[] = new TLiteralInt(0); + } + } + + continue 2; + } + } + + if ($intersection_type instanceof TObjectWithProperties + && isset($intersection_type->methods['__toString']) + ) { + $castable_types[] = new TInt(); + + continue 2; + } + } + } + + if ($atomic_type instanceof TNonEmptyArray + || $atomic_type instanceof TNonEmptyList + ) { + $possibly_unwanted_cast[] = $atomic_type->getId(); + + $valid_ints[] = new TLiteralInt(1); + + continue; + } + + if ($atomic_type instanceof TArray + || $atomic_type instanceof TList + || $atomic_type instanceof TKeyedArray + ) { + // if type is not specific, it can be both 0 or 1, depending on whether the array has data or not + // welcome to off-by-one hell if that happens :-) + $possibly_unwanted_cast[] = $atomic_type->getId(); + + $valid_ints[] = new TLiteralInt(0); + $valid_ints[] = new TLiteralInt(1); + + continue; + } + + if ($atomic_type instanceof TTemplateParam) { + $atomic_types = array_merge($atomic_types, $atomic_type->as->getAtomicTypes()); + + continue; + } + + $invalid_casts[] = $atomic_type->getId(); + } + + if ($invalid_casts) { + if ( $valid_ints || $castable_types ) { + IssueBuffer::maybeAdd( + new PossiblyInvalidCast( + $invalid_casts[0] . ' cannot be cast to int', + new CodeLocation( $statements_analyzer->getSource(), $stmt ) + ), + $statements_analyzer->getSuppressedIssues() + ); + } else { + IssueBuffer::maybeAdd( + new InvalidCast( + $invalid_casts[0] . ' cannot be cast to int', + new CodeLocation( $statements_analyzer->getSource(), $stmt ) + ), + $statements_analyzer->getSuppressedIssues() + ); + } + } elseif (!empty($possibly_unwanted_cast)) { + IssueBuffer::maybeAdd( + new PossiblyInvalidCast( + 'Casting ' . $possibly_unwanted_cast[0] . ' to int has possibly unintended value of 1', + new CodeLocation( $statements_analyzer->getSource(), $stmt ) + ), + $statements_analyzer->getSuppressedIssues() + ); + } elseif ($explicit_cast && !$castable_types) { + // todo: emit error here + } + + $valid_types = array_merge($valid_ints, $castable_types); + + if (!$valid_types) { + $int_type = Type::getInt(); + } else { + $int_type = TypeCombiner::combine( + $valid_types, + $codebase + ); + } + + if ($statements_analyzer->data_flow_graph) { + $int_type->parent_nodes = $parent_nodes; + } + + return $int_type; + } + public static function castStringAttempt( StatementsAnalyzer $statements_analyzer, Context $context, From 39ec75523e2aa7134b0e785cf99bb626df8b5ca8 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 3 Aug 2022 22:30:45 +0200 Subject: [PATCH 123/194] same for float --- .../Statements/Expression/CastAnalyzer.php | 247 +++++++++++++++++- 1 file changed, 241 insertions(+), 6 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 4d45367cad5..3d26e272e95 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -102,13 +102,16 @@ public static function analyze( if ($maybe_type->isFloat()) { self::handleRedundantCast($maybe_type, $statements_analyzer, $stmt); } - } - - $type = Type::getFloat(); - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph - ) { - $type->parent_nodes = $maybe_type->parent_nodes ?? []; + $type = self::castFloatAttempt( + $statements_analyzer, + $context, + $maybe_type, + $stmt->expr, + true + ); + } else { + $type = Type::getFloat(); } $statements_analyzer->node_data->setType($stmt, $type); @@ -519,6 +522,238 @@ public static function castIntAttempt( return $int_type; } + public static function castFloatAttempt( + StatementsAnalyzer $statements_analyzer, + Context $context, + Union $stmt_type, + PhpParser\Node\Expr $stmt, + bool $explicit_cast = false + ): Union { + $codebase = $statements_analyzer->getCodebase(); + + $possibly_unwanted_cast = []; + $invalid_casts = []; + $valid_floats = []; + $castable_types = []; + + $atomic_types = $stmt_type->getAtomicTypes(); + + $parent_nodes = []; + + if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { + $parent_nodes = $stmt_type->parent_nodes; + } + + while ($atomic_types) { + $atomic_type = array_pop($atomic_types); + + if ($atomic_type instanceof TFloat) { + $valid_floats[] = $atomic_type; + + continue; + } + + if ($atomic_type instanceof TInt) { + if ($atomic_type instanceof TLiteralInt) { + $valid_floats[] = new TLiteralFloat((float) $atomic_type->value); + } else { + $castable_types[] = new TFloat(); + } + + continue; + } + + if ($atomic_type instanceof TString) { + if ($atomic_type instanceof TLiteralString && (float) $atomic_type->value !== 0.0) { + $valid_floats[] = new TLiteralFloat((float) $atomic_type->value); + } elseif ($atomic_type instanceof TNumericString) { + $castable_types[] = new TFloat(); + } else { + // any normal string + $valid_floats[] = new TLiteralFloat(0.0); + } + + continue; + } + + if ($atomic_type instanceof TNull || $atomic_type instanceof TFalse) { + $valid_floats[] = new TLiteralFloat(0.0); + continue; + } + + if ($atomic_type instanceof TTrue) { + $valid_floats[] = new TLiteralFloat(1.0); + continue; + } + + if ($atomic_type instanceof TBool) { + $valid_floats[] = new TLiteralFloat(0.0); + $valid_floats[] = new TLiteralFloat(1.0); + continue; + } + + // could be invalid, but allow it, as it is allowed for TString below too + if ($atomic_type instanceof TMixed + || $atomic_type instanceof TClosedResource + || $atomic_type instanceof TResource + || $atomic_type instanceof Scalar + ) { + $castable_types[] = new TFloat(); + + continue; + } + + if ($atomic_type instanceof TNamedObject + || $atomic_type instanceof TObjectWithProperties + ) { + $intersection_types = [$atomic_type]; + + if ($atomic_type->extra_types) { + $intersection_types = array_merge($intersection_types, $atomic_type->extra_types); + } + + foreach ($intersection_types as $intersection_type) { + if ($intersection_type instanceof TNamedObject) { + $intersection_method_id = new MethodIdentifier( + $intersection_type->value, + '__tostring' + ); + + if ($codebase->methods->methodExists( + $intersection_method_id, + $context->calling_method_id, + new CodeLocation($statements_analyzer->getSource(), $stmt) + )) { + $return_type = $codebase->methods->getMethodReturnType( + $intersection_method_id, + $self_class + ) ?? Type::getString(); + + $declaring_method_id = $codebase->methods->getDeclaringMethodId($intersection_method_id); + + MethodCallReturnTypeFetcher::taintMethodCallResult( + $statements_analyzer, + $return_type, + $stmt, + $stmt, + [], + $intersection_method_id, + $declaring_method_id, + $intersection_type->value . '::__toString', + $context + ); + + if ($statements_analyzer->data_flow_graph) { + $parent_nodes = array_merge($return_type->parent_nodes, $parent_nodes); + } + + foreach ($return_type->getAtomicTypes() as $sub_atomic_type) { + if ($sub_atomic_type instanceof TLiteralString + && (float) $sub_atomic_type->value !== 0.0 + ) { + $valid_floats[] = new TLiteralFloat((float) $sub_atomic_type->value); + } elseif ($sub_atomic_type instanceof TNumericString) { + $castable_types[] = new TFloat(); + } else { + $valid_floats[] = new TLiteralFloat(0.0); + } + } + + continue 2; + } + } + + if ($intersection_type instanceof TObjectWithProperties + && isset($intersection_type->methods['__toString']) + ) { + $castable_types[] = new TFloat(); + + continue 2; + } + } + } + + if ($atomic_type instanceof TNonEmptyArray + || $atomic_type instanceof TNonEmptyList + ) { + $possibly_unwanted_cast[] = $atomic_type->getId(); + + $valid_floats[] = new TLiteralFloat(1.0); + + continue; + } + + if ($atomic_type instanceof TArray + || $atomic_type instanceof TList + || $atomic_type instanceof TKeyedArray + ) { + // if type is not specific, it can be both 0 or 1, depending on whether the array has data or not + // welcome to off-by-one hell if that happens :-) + $possibly_unwanted_cast[] = $atomic_type->getId(); + + $valid_floats[] = new TLiteralFloat(0.0); + $valid_floats[] = new TLiteralFloat(1.0); + + continue; + } + + if ($atomic_type instanceof TTemplateParam) { + $atomic_types = array_merge($atomic_types, $atomic_type->as->getAtomicTypes()); + + continue; + } + + $invalid_casts[] = $atomic_type->getId(); + } + + if ($invalid_casts) { + if ( $valid_floats || $castable_types ) { + IssueBuffer::maybeAdd( + new PossiblyInvalidCast( + $invalid_casts[0] . ' cannot be cast to float', + new CodeLocation( $statements_analyzer->getSource(), $stmt ) + ), + $statements_analyzer->getSuppressedIssues() + ); + } else { + IssueBuffer::maybeAdd( + new InvalidCast( + $invalid_casts[0] . ' cannot be cast to float', + new CodeLocation( $statements_analyzer->getSource(), $stmt ) + ), + $statements_analyzer->getSuppressedIssues() + ); + } + } elseif (!empty($possibly_unwanted_cast)) { + IssueBuffer::maybeAdd( + new PossiblyInvalidCast( + 'Casting ' . $possibly_unwanted_cast[0] . ' to float has possibly unintended value of 1.0', + new CodeLocation( $statements_analyzer->getSource(), $stmt ) + ), + $statements_analyzer->getSuppressedIssues() + ); + } elseif ($explicit_cast && !$castable_types) { + // todo: emit error here + } + + $valid_types = array_merge($valid_floats, $castable_types); + + if (!$valid_types) { + $float_type = Type::getFloat(); + } else { + $float_type = TypeCombiner::combine( + $valid_types, + $codebase + ); + } + + if ($statements_analyzer->data_flow_graph) { + $float_type->parent_nodes = $parent_nodes; + } + + return $float_type; + } + public static function castStringAttempt( StatementsAnalyzer $statements_analyzer, Context $context, From d32efb0619dbc2ac277d3283a04c13f55c7a76e0 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Wed, 3 Aug 2022 23:48:49 +0200 Subject: [PATCH 124/194] float/int always 1 on "error", no PossiblyInvalidCasts by default --- .../Statements/Expression/CastAnalyzer.php | 58 +++++++------------ 1 file changed, 22 insertions(+), 36 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 3d26e272e95..f5af439823b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -471,32 +471,25 @@ public static function castIntAttempt( continue; } + // always 1 for "error" cases + $valid_ints[] = new TLiteralInt(1); + $invalid_casts[] = $atomic_type->getId(); } if ($invalid_casts) { - if ( $valid_ints || $castable_types ) { - IssueBuffer::maybeAdd( - new PossiblyInvalidCast( - $invalid_casts[0] . ' cannot be cast to int', - new CodeLocation( $statements_analyzer->getSource(), $stmt ) - ), - $statements_analyzer->getSuppressedIssues() - ); - } else { - IssueBuffer::maybeAdd( - new InvalidCast( - $invalid_casts[0] . ' cannot be cast to int', - new CodeLocation( $statements_analyzer->getSource(), $stmt ) - ), - $statements_analyzer->getSuppressedIssues() - ); - } + IssueBuffer::maybeAdd( + new InvalidCast( + $invalid_casts[0] . ' cannot be cast to int', + new CodeLocation($statements_analyzer->getSource(), $stmt) + ), + $statements_analyzer->getSuppressedIssues() + ); } elseif (!empty($possibly_unwanted_cast)) { IssueBuffer::maybeAdd( new PossiblyInvalidCast( 'Casting ' . $possibly_unwanted_cast[0] . ' to int has possibly unintended value of 1', - new CodeLocation( $statements_analyzer->getSource(), $stmt ) + new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() ); @@ -703,32 +696,25 @@ public static function castFloatAttempt( continue; } + // always 1.0 for "error" cases + $valid_floats[] = new TLiteralFloat(1.0); + $invalid_casts[] = $atomic_type->getId(); } if ($invalid_casts) { - if ( $valid_floats || $castable_types ) { - IssueBuffer::maybeAdd( - new PossiblyInvalidCast( - $invalid_casts[0] . ' cannot be cast to float', - new CodeLocation( $statements_analyzer->getSource(), $stmt ) - ), - $statements_analyzer->getSuppressedIssues() - ); - } else { - IssueBuffer::maybeAdd( - new InvalidCast( - $invalid_casts[0] . ' cannot be cast to float', - new CodeLocation( $statements_analyzer->getSource(), $stmt ) - ), - $statements_analyzer->getSuppressedIssues() - ); - } + IssueBuffer::maybeAdd( + new InvalidCast( + $invalid_casts[0] . ' cannot be cast to float', + new CodeLocation($statements_analyzer->getSource(), $stmt) + ), + $statements_analyzer->getSuppressedIssues() + ); } elseif (!empty($possibly_unwanted_cast)) { IssueBuffer::maybeAdd( new PossiblyInvalidCast( 'Casting ' . $possibly_unwanted_cast[0] . ' to float has possibly unintended value of 1.0', - new CodeLocation( $statements_analyzer->getSource(), $stmt ) + new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() ); From c3eebe25792c6a1a6b2ac6f23e1c258fa280ff36 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 4 Aug 2022 00:14:06 +0200 Subject: [PATCH 125/194] be less strict for generic string type --- .../Statements/Expression/CastAnalyzer.php | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index f5af439823b..61c28c0a727 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -331,13 +331,14 @@ public static function castIntAttempt( } if ($atomic_type instanceof TString) { - if ($atomic_type instanceof TLiteralString && (int) $atomic_type->value !== 0) { + if ($atomic_type instanceof TLiteralString) { $valid_ints[] = new TLiteralInt((int) $atomic_type->value); } elseif ($atomic_type instanceof TNumericString) { $castable_types[] = new TInt(); } else { - // any normal string - $valid_ints[] = new TLiteralInt(0); + // any normal string is technically $valid_int[] = new TLiteralInt(0); + // however we cannot be certain that it's not inferred, therefore less strict + $castable_types[] = new TInt(); } continue; @@ -416,14 +417,14 @@ public static function castIntAttempt( } foreach ($return_type->getAtomicTypes() as $sub_atomic_type) { - if ($sub_atomic_type instanceof TLiteralString - && (int) $sub_atomic_type->value !== 0 - ) { + if ($sub_atomic_type instanceof TLiteralString) { $valid_ints[] = new TLiteralInt((int) $sub_atomic_type->value); } elseif ($sub_atomic_type instanceof TNumericString) { $castable_types[] = new TInt(); } else { - $valid_ints[] = new TLiteralInt(0); + // any normal string is technically $valid_int[] = new TLiteralInt(0); + // however we cannot be certain that it's not inferred, therefore less strict + $castable_types[] = new TInt(); } } @@ -557,13 +558,14 @@ public static function castFloatAttempt( } if ($atomic_type instanceof TString) { - if ($atomic_type instanceof TLiteralString && (float) $atomic_type->value !== 0.0) { + if ($atomic_type instanceof TLiteralString) { $valid_floats[] = new TLiteralFloat((float) $atomic_type->value); } elseif ($atomic_type instanceof TNumericString) { $castable_types[] = new TFloat(); } else { - // any normal string - $valid_floats[] = new TLiteralFloat(0.0); + // any normal string is technically $valid_floats[] = new TLiteralFloat(0.0); + // however we cannot be certain that it's not inferred, therefore less strict + $castable_types[] = new TFloat(); } continue; @@ -641,14 +643,14 @@ public static function castFloatAttempt( } foreach ($return_type->getAtomicTypes() as $sub_atomic_type) { - if ($sub_atomic_type instanceof TLiteralString - && (float) $sub_atomic_type->value !== 0.0 - ) { + if ($sub_atomic_type instanceof TLiteralString) { $valid_floats[] = new TLiteralFloat((float) $sub_atomic_type->value); } elseif ($sub_atomic_type instanceof TNumericString) { $castable_types[] = new TFloat(); } else { - $valid_floats[] = new TLiteralFloat(0.0); + // any normal string is technically $valid_int[] = new TLiteralInt(0); + // however we cannot be certain that it's not inferred, therefore less strict + $castable_types[] = new TFloat(); } } From 7cdad99645dd48dab1d639f97f89949e3155a02b Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Fri, 9 Sep 2022 03:04:14 +0200 Subject: [PATCH 126/194] add RiskyCast --- config.xsd | 1 + docs/running_psalm/error_levels.md | 1 + docs/running_psalm/issues.md | 1 + docs/running_psalm/issues/RiskyCast.md | 17 +++++++++++++ .../Statements/Expression/CastAnalyzer.php | 25 ++++++++++--------- src/Psalm/Issue/RiskyCast.php | 9 +++++++ 6 files changed, 42 insertions(+), 12 deletions(-) create mode 100644 docs/running_psalm/issues/RiskyCast.md create mode 100644 src/Psalm/Issue/RiskyCast.php diff --git a/config.xsd b/config.xsd index c9a4cd88895..5a0dab58d68 100644 --- a/config.xsd +++ b/config.xsd @@ -462,6 +462,7 @@ + diff --git a/docs/running_psalm/error_levels.md b/docs/running_psalm/error_levels.md index c58cca026ff..38e628bfd93 100644 --- a/docs/running_psalm/error_levels.md +++ b/docs/running_psalm/error_levels.md @@ -187,6 +187,7 @@ These issues are treated as errors at level 3 and below. - [PossiblyUndefinedMethod](issues/PossiblyUndefinedMethod.md) - [PossiblyUndefinedVariable](issues/PossiblyUndefinedVariable.md) - [PropertyTypeCoercion](issues/PropertyTypeCoercion.md) + - [RiskyCast](issues/RiskyCast.md) ## Errors ignored at level 5 and higher diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md index 50385f60e31..20ddd0eca76 100644 --- a/docs/running_psalm/issues.md +++ b/docs/running_psalm/issues.md @@ -214,6 +214,7 @@ - [RedundantPropertyInitializationCheck](issues/RedundantPropertyInitializationCheck.md) - [ReferenceConstraintViolation](issues/ReferenceConstraintViolation.md) - [ReservedWord](issues/ReservedWord.md) + - [RiskyCast](issues/RiskyCast.md) - [StringIncrement](issues/StringIncrement.md) - [TaintedCallable](issues/TaintedCallable.md) - [TaintedCookie](issues/TaintedCookie.md) diff --git a/docs/running_psalm/issues/RiskyCast.md b/docs/running_psalm/issues/RiskyCast.md new file mode 100644 index 00000000000..ef90d6e37b8 --- /dev/null +++ b/docs/running_psalm/issues/RiskyCast.md @@ -0,0 +1,17 @@ +# RiskyCast + +Emitted when attempting to cast an array to int or float + +```php +getCodebase(); - $possibly_unwanted_cast = []; + $risky_cast = []; $invalid_casts = []; $valid_ints = []; $castable_types = []; @@ -445,7 +446,7 @@ public static function castIntAttempt( if ($atomic_type instanceof TNonEmptyArray || $atomic_type instanceof TNonEmptyList ) { - $possibly_unwanted_cast[] = $atomic_type->getId(); + $risky_cast[] = $atomic_type->getId(); $valid_ints[] = new TLiteralInt(1); @@ -458,7 +459,7 @@ public static function castIntAttempt( ) { // if type is not specific, it can be both 0 or 1, depending on whether the array has data or not // welcome to off-by-one hell if that happens :-) - $possibly_unwanted_cast[] = $atomic_type->getId(); + $risky_cast[] = $atomic_type->getId(); $valid_ints[] = new TLiteralInt(0); $valid_ints[] = new TLiteralInt(1); @@ -486,10 +487,10 @@ public static function castIntAttempt( ), $statements_analyzer->getSuppressedIssues() ); - } elseif (!empty($possibly_unwanted_cast)) { + } elseif ($risky_cast) { IssueBuffer::maybeAdd( - new PossiblyInvalidCast( - 'Casting ' . $possibly_unwanted_cast[0] . ' to int has possibly unintended value of 1', + new RiskyCast( + 'Casting ' . $risky_cast[0] . ' to int has possibly unintended value of 0/1', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() @@ -525,7 +526,7 @@ public static function castFloatAttempt( ): Union { $codebase = $statements_analyzer->getCodebase(); - $possibly_unwanted_cast = []; + $risky_cast = []; $invalid_casts = []; $valid_floats = []; $castable_types = []; @@ -671,7 +672,7 @@ public static function castFloatAttempt( if ($atomic_type instanceof TNonEmptyArray || $atomic_type instanceof TNonEmptyList ) { - $possibly_unwanted_cast[] = $atomic_type->getId(); + $risky_cast[] = $atomic_type->getId(); $valid_floats[] = new TLiteralFloat(1.0); @@ -684,7 +685,7 @@ public static function castFloatAttempt( ) { // if type is not specific, it can be both 0 or 1, depending on whether the array has data or not // welcome to off-by-one hell if that happens :-) - $possibly_unwanted_cast[] = $atomic_type->getId(); + $risky_cast[] = $atomic_type->getId(); $valid_floats[] = new TLiteralFloat(0.0); $valid_floats[] = new TLiteralFloat(1.0); @@ -712,10 +713,10 @@ public static function castFloatAttempt( ), $statements_analyzer->getSuppressedIssues() ); - } elseif (!empty($possibly_unwanted_cast)) { + } elseif ($risky_cast) { IssueBuffer::maybeAdd( - new PossiblyInvalidCast( - 'Casting ' . $possibly_unwanted_cast[0] . ' to float has possibly unintended value of 1.0', + new RiskyCast( + 'Casting ' . $risky_cast[0] . ' to float has possibly unintended value of 0.0/1.0', new CodeLocation($statements_analyzer->getSource(), $stmt) ), $statements_analyzer->getSuppressedIssues() diff --git a/src/Psalm/Issue/RiskyCast.php b/src/Psalm/Issue/RiskyCast.php new file mode 100644 index 00000000000..6576d430aeb --- /dev/null +++ b/src/Psalm/Issue/RiskyCast.php @@ -0,0 +1,9 @@ + Date: Fri, 9 Sep 2022 04:22:54 +0200 Subject: [PATCH 127/194] fix psalm-internal risky casts --- src/Psalm/Internal/Cli/LanguageServer.php | 3 ++- src/Psalm/Internal/Cli/Psalter.php | 3 ++- src/Psalm/Internal/Cli/Refactor.php | 3 ++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Cli/LanguageServer.php b/src/Psalm/Internal/Cli/LanguageServer.php index 634cdee16a4..1a0144def8c 100644 --- a/src/Psalm/Internal/Cli/LanguageServer.php +++ b/src/Psalm/Internal/Cli/LanguageServer.php @@ -32,6 +32,7 @@ use function in_array; use function ini_set; use function is_array; +use function is_numeric; use function is_string; use function preg_replace; use function realpath; @@ -298,7 +299,7 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class $find_unused_code = 'auto'; } - if (isset($options['disable-on-change'])) { + if (isset($options['disable-on-change']) && is_numeric($options['disable-on-change'])) { $project_analyzer->onchange_line_limit = (int) $options['disable-on-change']; } diff --git a/src/Psalm/Internal/Cli/Psalter.php b/src/Psalm/Internal/Cli/Psalter.php index 7db204c5f61..12e025fce7e 100644 --- a/src/Psalm/Internal/Cli/Psalter.php +++ b/src/Psalm/Internal/Cli/Psalter.php @@ -45,6 +45,7 @@ use function ini_set; use function is_array; use function is_dir; +use function is_numeric; use function is_string; use function microtime; use function pathinfo; @@ -230,7 +231,7 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class chdir($current_dir); } - $threads = isset($options['threads']) ? (int)$options['threads'] : 1; + $threads = isset($options['threads']) && is_numeric($options['threads']) ? (int)$options['threads'] : 1; if (isset($options['no-cache'])) { $providers = new Providers( diff --git a/src/Psalm/Internal/Cli/Refactor.php b/src/Psalm/Internal/Cli/Refactor.php index 864e9d4aebd..a3b5a114b37 100644 --- a/src/Psalm/Internal/Cli/Refactor.php +++ b/src/Psalm/Internal/Cli/Refactor.php @@ -35,6 +35,7 @@ use function in_array; use function ini_set; use function is_array; +use function is_numeric; use function is_string; use function max; use function microtime; @@ -284,7 +285,7 @@ function () use ($current_dir, $options, $vendor_dir): ?\Composer\Autoload\Class chdir($current_dir); } - $threads = isset($options['threads']) + $threads = isset($options['threads']) && is_numeric($options['threads']) ? (int)$options['threads'] : max(1, ProjectAnalyzer::getCpuCount() - 2); From bf1c0320fd7584aa6f287946e44b34f3f2c1872e Mon Sep 17 00:00:00 2001 From: Ricardo Boss Date: Sun, 16 Jan 2022 21:33:04 +0100 Subject: [PATCH 128/194] Cherry-pick: Try to provide literal int types when possible (fixes #6966) (#7071) * Fixed vimeo/psalm#6966 * Only accept >= 0 values for mode argument in round() * Made round() only return float or literal float values and remove unneeded test * Registered RoundReturnTypeProvider * Updated cast analyzer to handle single string literal int values as literal ints * Fixed psalm errors * Fix invalid property accesses * Addressed comments * Added Tests * Marked RoundReturnTypeProvider as internal * Fixed CS --- dictionaries/CallMap.php | 2 +- dictionaries/CallMap_historical.php | 2 +- .../Provider/FunctionReturnTypeProvider.php | 2 + .../RoundReturnTypeProvider.php | 78 +++++++++++++++++++ tests/FunctionCallTest.php | 9 ++- tests/TypeReconciliation/ValueTest.php | 8 ++ 6 files changed, 98 insertions(+), 3 deletions(-) create mode 100644 src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index df6b3456d8a..df1739c6609 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -11714,7 +11714,7 @@ 'rewind' => ['bool', 'stream'=>'resource'], 'rewinddir' => ['null|false', 'dir_handle='=>'resource'], 'rmdir' => ['bool', 'directory'=>'string', 'context='=>'resource'], -'round' => ['float', 'num'=>'float', 'precision='=>'int', 'mode='=>'int'], +'round' => ['float', 'num'=>'float', 'precision='=>'int', 'mode='=>'0|positive-int'], 'rpm_close' => ['bool', 'rpmr'=>'resource'], 'rpm_get_tag' => ['mixed', 'rpmr'=>'resource', 'tagnum'=>'int'], 'rpm_is_valid' => ['bool', 'filename'=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 036c5c710c7..a8ad6acd48e 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -14751,7 +14751,7 @@ 'rewind' => ['bool', 'stream'=>'resource'], 'rewinddir' => ['null|false', 'dir_handle='=>'resource'], 'rmdir' => ['bool', 'directory'=>'string', 'context='=>'resource'], - 'round' => ['float', 'num'=>'float', 'precision='=>'int', 'mode='=>'int'], + 'round' => ['float', 'num'=>'float', 'precision='=>'int', 'mode='=>'0|positive-int'], 'rpm_close' => ['bool', 'rpmr'=>'resource'], 'rpm_get_tag' => ['mixed', 'rpmr'=>'resource', 'tagnum'=>'int'], 'rpm_is_valid' => ['bool', 'filename'=>'string'], diff --git a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php index 8b124ea5532..e4636d5c3a0 100644 --- a/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php @@ -35,6 +35,7 @@ use Psalm\Internal\Provider\ReturnTypeProvider\MktimeReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\ParseUrlReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\RandReturnTypeProvider; +use Psalm\Internal\Provider\ReturnTypeProvider\RoundReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\StrReplaceReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\StrTrReturnTypeProvider; use Psalm\Internal\Provider\ReturnTypeProvider\TriggerErrorReturnTypeProvider; @@ -109,6 +110,7 @@ public function __construct() $this->registerClass(TriggerErrorReturnTypeProvider::class); $this->registerClass(RandReturnTypeProvider::class); $this->registerClass(InArrayReturnTypeProvider::class); + $this->registerClass(RoundReturnTypeProvider::class); } /** diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php new file mode 100644 index 00000000000..eb8ee561f54 --- /dev/null +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/RoundReturnTypeProvider.php @@ -0,0 +1,78 @@ + + */ + public static function getFunctionIds(): array + { + return ['round']; + } + + public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Type\Union + { + $call_args = $event->getCallArgs(); + if (count($call_args) === 0) { + return null; + } + + $statements_source = $event->getStatementsSource(); + $nodeTypeProvider = $statements_source->getNodeTypeProvider(); + + $num_arg = $nodeTypeProvider->getType($call_args[0]->value); + + $precision_val = 0; + if ($statements_source instanceof StatementsAnalyzer && count($call_args) > 1) { + $type = $statements_source->node_data->getType($call_args[1]->value); + + if ($type !== null && $type->isSingle()) { + $atomic_type = array_values($type->getAtomicTypes())[0]; + if ($atomic_type instanceof Type\Atomic\TLiteralInt) { + $precision_val = $atomic_type->value; + } + } + } + + $mode_val = PHP_ROUND_HALF_UP; + if ($statements_source instanceof StatementsAnalyzer && count($call_args) > 2) { + $type = $statements_source->node_data->getType($call_args[2]->value); + + if ($type !== null && $type->isSingle()) { + $atomic_type = array_values($type->getAtomicTypes())[0]; + if ($atomic_type instanceof Type\Atomic\TLiteralInt) { + /** @var positive-int|0 $mode_val */ + $mode_val = $atomic_type->value; + } + } + } + + if ($num_arg !== null && $num_arg->isSingle()) { + $num_type = array_values($num_arg->getAtomicTypes())[0]; + if ($num_type instanceof Type\Atomic\TLiteralFloat || $num_type instanceof Type\Atomic\TLiteralInt) { + $rounded_val = round($num_type->value, $precision_val, $mode_val); + return new Type\Union([new Type\Atomic\TLiteralFloat($rounded_val)]); + } + } + + return new Type\Union([new Type\Atomic\TFloat()]); + } +} diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index e57a1d432b9..9b457ce600d 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -31,7 +31,6 @@ function filter(array $strings): array { } ' ], - 'typedArrayWithDefault' => [ ' 'lowercase-string', ], ], + 'round_literalValue' => [ + ' [ + '$a===' => 'float(10.36)', + ], + ], ]; } diff --git a/tests/TypeReconciliation/ValueTest.php b/tests/TypeReconciliation/ValueTest.php index b5963ac4125..cda14767394 100644 --- a/tests/TypeReconciliation/ValueTest.php +++ b/tests/TypeReconciliation/ValueTest.php @@ -909,6 +909,14 @@ function foo(string $s) : void { $interval = \DateInterval::createFromDateString("30 дней"); if ($interval === false) {}', ], + 'literalInt' => [ + ' [ + '$a===' => '5', + ], + ], ]; } From d69be4b9a271446f1295a0c89b528c5eafdf5a12 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Fri, 16 Sep 2022 14:19:26 +0200 Subject: [PATCH 129/194] objects even with __toString methods cannot be cast to int/float --- .../Statements/Expression/CastAnalyzer.php | 159 +++++------------- tests/ToStringTest.php | 12 ++ 2 files changed, 55 insertions(+), 116 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 039243fd00a..334a466e9bd 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -54,9 +54,18 @@ use function array_pop; use function array_values; use function get_class; +use function strtolower; class CastAnalyzer { + /** @var string[] */ + private const PSEUDO_CASTABLE_CLASSES = [ + 'SimpleXMLElement', + 'DOMNode', + 'GMP', + 'Decimal\Decimal', + ]; + public static function analyze( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\Cast $stmt, @@ -78,7 +87,6 @@ public static function analyze( $type = self::castIntAttempt( $statements_analyzer, - $context, $maybe_type, $stmt->expr, true @@ -106,7 +114,6 @@ public static function analyze( $type = self::castFloatAttempt( $statements_analyzer, - $context, $maybe_type, $stmt->expr, true @@ -292,7 +299,6 @@ public static function analyze( public static function castIntAttempt( StatementsAnalyzer $statements_analyzer, - Context $context, Union $stmt_type, PhpParser\Node\Expr $stmt, bool $explicit_cast = false @@ -373,9 +379,7 @@ public static function castIntAttempt( continue; } - if ($atomic_type instanceof TNamedObject - || $atomic_type instanceof TObjectWithProperties - ) { + if ($atomic_type instanceof TNamedObject) { $intersection_types = [$atomic_type]; if ($atomic_type->extra_types) { @@ -383,62 +387,25 @@ public static function castIntAttempt( } foreach ($intersection_types as $intersection_type) { - if ($intersection_type instanceof TNamedObject) { - $intersection_method_id = new MethodIdentifier( - $intersection_type->value, - '__tostring' - ); - - if ($codebase->methods->methodExists( - $intersection_method_id, - $context->calling_method_id, - new CodeLocation($statements_analyzer->getSource(), $stmt) - )) { - $return_type = $codebase->methods->getMethodReturnType( - $intersection_method_id, - $self_class - ) ?? Type::getString(); - - $declaring_method_id = $codebase->methods->getDeclaringMethodId($intersection_method_id); - - MethodCallReturnTypeFetcher::taintMethodCallResult( - $statements_analyzer, - $return_type, - $stmt, - $stmt, - [], - $intersection_method_id, - $declaring_method_id, - $intersection_type->value . '::__toString', - $context - ); - - if ($statements_analyzer->data_flow_graph) { - $parent_nodes = array_merge($return_type->parent_nodes, $parent_nodes); - } - - foreach ($return_type->getAtomicTypes() as $sub_atomic_type) { - if ($sub_atomic_type instanceof TLiteralString) { - $valid_ints[] = new TLiteralInt((int) $sub_atomic_type->value); - } elseif ($sub_atomic_type instanceof TNumericString) { - $castable_types[] = new TInt(); - } else { - // any normal string is technically $valid_int[] = new TLiteralInt(0); - // however we cannot be certain that it's not inferred, therefore less strict - $castable_types[] = new TInt(); - } - } - - continue 2; - } + if (!$intersection_type instanceof TNamedObject) { + continue; } - if ($intersection_type instanceof TObjectWithProperties - && isset($intersection_type->methods['__toString']) - ) { - $castable_types[] = new TInt(); + // prevent "Could not get class storage for mixed" + if (!$codebase->classExists($intersection_type->value)) { + continue; + } - continue 2; + foreach (self::PSEUDO_CASTABLE_CLASSES as $pseudo_castable_class) { + if (strtolower($intersection_type->value) === strtolower($pseudo_castable_class) + || $codebase->classExtends( + $intersection_type->value, + $pseudo_castable_class + ) + ) { + $castable_types[] = new TInt(); + continue 3; + } } } } @@ -519,7 +486,6 @@ public static function castIntAttempt( public static function castFloatAttempt( StatementsAnalyzer $statements_analyzer, - Context $context, Union $stmt_type, PhpParser\Node\Expr $stmt, bool $explicit_cast = false @@ -599,9 +565,7 @@ public static function castFloatAttempt( continue; } - if ($atomic_type instanceof TNamedObject - || $atomic_type instanceof TObjectWithProperties - ) { + if ($atomic_type instanceof TNamedObject) { $intersection_types = [$atomic_type]; if ($atomic_type->extra_types) { @@ -609,62 +573,25 @@ public static function castFloatAttempt( } foreach ($intersection_types as $intersection_type) { - if ($intersection_type instanceof TNamedObject) { - $intersection_method_id = new MethodIdentifier( - $intersection_type->value, - '__tostring' - ); - - if ($codebase->methods->methodExists( - $intersection_method_id, - $context->calling_method_id, - new CodeLocation($statements_analyzer->getSource(), $stmt) - )) { - $return_type = $codebase->methods->getMethodReturnType( - $intersection_method_id, - $self_class - ) ?? Type::getString(); - - $declaring_method_id = $codebase->methods->getDeclaringMethodId($intersection_method_id); - - MethodCallReturnTypeFetcher::taintMethodCallResult( - $statements_analyzer, - $return_type, - $stmt, - $stmt, - [], - $intersection_method_id, - $declaring_method_id, - $intersection_type->value . '::__toString', - $context - ); - - if ($statements_analyzer->data_flow_graph) { - $parent_nodes = array_merge($return_type->parent_nodes, $parent_nodes); - } - - foreach ($return_type->getAtomicTypes() as $sub_atomic_type) { - if ($sub_atomic_type instanceof TLiteralString) { - $valid_floats[] = new TLiteralFloat((float) $sub_atomic_type->value); - } elseif ($sub_atomic_type instanceof TNumericString) { - $castable_types[] = new TFloat(); - } else { - // any normal string is technically $valid_int[] = new TLiteralInt(0); - // however we cannot be certain that it's not inferred, therefore less strict - $castable_types[] = new TFloat(); - } - } - - continue 2; - } + if (!$intersection_type instanceof TNamedObject) { + continue; } - if ($intersection_type instanceof TObjectWithProperties - && isset($intersection_type->methods['__toString']) - ) { - $castable_types[] = new TFloat(); + // prevent "Could not get class storage for mixed" + if (!$codebase->classExists($intersection_type->value)) { + continue; + } - continue 2; + foreach (self::PSEUDO_CASTABLE_CLASSES as $pseudo_castable_class) { + if (strtolower($intersection_type->value) === strtolower($pseudo_castable_class) + || $codebase->classExtends( + $intersection_type->value, + $pseudo_castable_class + ) + ) { + $castable_types[] = new TFloat(); + continue 3; + } } } } diff --git a/tests/ToStringTest.php b/tests/ToStringTest.php index 1a35aae8b73..40874ca4e94 100644 --- a/tests/ToStringTest.php +++ b/tests/ToStringTest.php @@ -505,6 +505,18 @@ public function __toString(): string ', 'error_message' => 'ImplicitToStringCast' ], + 'toStringTypecastNonString' => [ + ' 'InvalidCast', + ], ]; } } From d69e06242617d89a66bcc30843acdd30af1ce23c Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 20 Sep 2022 10:59:46 +0200 Subject: [PATCH 130/194] add RiskyCast tests --- tests/ToStringTest.php | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/ToStringTest.php b/tests/ToStringTest.php index 40874ca4e94..f9101fa9cbb 100644 --- a/tests/ToStringTest.php +++ b/tests/ToStringTest.php @@ -517,6 +517,16 @@ function __toString(): string { echo (int) $foo;', 'error_message' => 'InvalidCast', ], + 'riskyArrayToIntCast' => [ + ' 'RiskyCast', + ], + 'riskyArrayToFloatCast' => [ + ' 'RiskyCast', + ], ]; } } From ce76158085afa6928b018bc559765784db59523e Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Tue, 16 Aug 2022 17:28:54 +0200 Subject: [PATCH 131/194] fix crash in ['bool', 'typelib_name'=>'string', 'case_insensitive='=>'true'], ], 'count' => [ - 'old' => ['int', 'value'=>'Countable|array|SimpleXMLElement|ResourceBundle', 'mode='=>'int'], + 'old' => ['int', 'value'=>'Countable|array|SimpleXMLElement', 'mode='=>'int'], 'new' => ['int', 'value'=>'Countable|array', 'mode='=>'int'], ], 'count_chars' => [ diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index a8ad6acd48e..0fc9a97fbd0 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -10085,7 +10085,7 @@ 'copy' => ['bool', 'from'=>'string', 'to'=>'string', 'context='=>'resource'], 'cos' => ['float', 'num'=>'float'], 'cosh' => ['float', 'num'=>'float'], - 'count' => ['int', 'value'=>'Countable|array|SimpleXMLElement|ResourceBundle', 'mode='=>'int'], + 'count' => ['int', 'value'=>'Countable|array|SimpleXMLElement', 'mode='=>'int'], 'count_chars' => ['array|false', 'input'=>'string', 'mode='=>'0|1|2'], 'count_chars\'1' => ['string|false', 'input'=>'string', 'mode='=>'3|4'], 'crack_check' => ['bool', 'dictionary'=>'', 'password'=>'string'], diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index f5be423c569..2f02ad18355 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -51,6 +51,7 @@ class InternalCallMapHandlerTest extends TestCase private static $ignoredFunctions = [ 'apcu_entry', 'array_multisort', + 'count', // #8346 'bcdiv', 'bcmod', 'bcpowmod', From 328561d3880c2e267b211ff307f82e3189e7a9fb Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 22 Sep 2022 00:41:06 +0200 Subject: [PATCH 132/194] add code for faster debugging next time --- tests/Internal/Codebase/InternalCallMapHandlerTest.php | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index 2f02ad18355..0821cd9cae5 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -51,7 +51,6 @@ class InternalCallMapHandlerTest extends TestCase private static $ignoredFunctions = [ 'apcu_entry', 'array_multisort', - 'count', // #8346 'bcdiv', 'bcmod', 'bcpowmod', @@ -520,7 +519,13 @@ public function testIgnoresAreSortedAndUnique(): void /** @var string */ $function = is_int($key) ? $value : $key; - $this->assertGreaterThan(0, strcmp($function, $previousFunction)); + $diff = strcmp($function, $previousFunction); + if ($diff <= 0) { + // faster debugging errors in tests + echo "\n" . $previousFunction . "\n" . $function . "\n"; + } + + $this->assertGreaterThan(0, $diff); $previousFunction = $function; } } From e803af4cd47f1bb368ba80d880c7b08d39d10651 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 22 Sep 2022 02:10:51 +0200 Subject: [PATCH 133/194] use cache for declared function when available before falling back to stubs fixes return type issues reported for the wrong file --- src/Psalm/Internal/Codebase/Functions.php | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Codebase/Functions.php b/src/Psalm/Internal/Codebase/Functions.php index ca882a0c6dd..a05723d923f 100644 --- a/src/Psalm/Internal/Codebase/Functions.php +++ b/src/Psalm/Internal/Codebase/Functions.php @@ -84,8 +84,9 @@ public function getStorage( $function_id = substr($function_id, 1); } + $from_stubs = false; if (isset(self::$stubbed_functions[$function_id])) { - return self::$stubbed_functions[$function_id]; + $from_stubs = self::$stubbed_functions[$function_id]; } $file_storage = null; @@ -117,6 +118,10 @@ public function getStorage( return $this->reflection->getFunctionStorage($function_id); } + if ($from_stubs) { + return $from_stubs; + } + throw new UnexpectedValueException( 'Expecting non-empty $root_file_path and $checked_file_path' ); @@ -135,6 +140,10 @@ public function getStorage( } } + if ($from_stubs) { + return $from_stubs; + } + throw new UnexpectedValueException( 'Expecting ' . $function_id . ' to have storage in ' . $checked_file_path ); @@ -145,6 +154,10 @@ public function getStorage( $declaring_file_storage = $this->file_storage_provider->get($declaring_file_path); if (!isset($declaring_file_storage->functions[$function_id])) { + if ($from_stubs) { + return $from_stubs; + } + throw new UnexpectedValueException( 'Not expecting ' . $function_id . ' to not have storage in ' . $declaring_file_path ); From b1c0c2d8a1a482491b6219cb6fd77b1e1de4c8d4 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Thu, 22 Sep 2022 00:10:27 +0200 Subject: [PATCH 134/194] add hideAllErrorsExceptPassedFiles config option for files only (not directories, since that wouldn't make practical sense) --- src/Psalm/Config.php | 13 +++++++++++++ src/Psalm/Internal/Analyzer/ProjectAnalyzer.php | 6 ++++++ 2 files changed, 19 insertions(+) diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index c97021c5ed3..72d7ae02b4c 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -295,6 +295,11 @@ class Config */ public $hide_external_errors = false; + /** + * @var bool + */ + public $hide_all_errors_except_passed_files = false; + /** @var bool */ public $allow_includes = true; @@ -926,6 +931,7 @@ private static function fromXmlAndPaths( 'useDocblockPropertyTypes' => 'use_docblock_property_types', 'throwExceptionOnError' => 'throw_exception', 'hideExternalErrors' => 'hide_external_errors', + 'hideAllErrorsExceptPassedFiles' => 'hide_all_errors_except_passed_files', 'resolveFromConfigFile' => 'resolve_from_config_file', 'allowFileIncludes' => 'allow_includes', 'strictBinaryOperands' => 'strict_binary_operands', @@ -1567,6 +1573,13 @@ public function reportIssueInFile(string $issue_type, string $file_path): bool $project_analyzer = ProjectAnalyzer::getInstance(); + // if the option is set and at least one file is passed via CLI + if ($this->hide_all_errors_except_passed_files + && $project_analyzer->check_paths_files + && !in_array($file_path, $project_analyzer->check_paths_files, true)) { + return false; + } + $codebase = $project_analyzer->getCodebase(); if (!$this->hide_external_errors) { diff --git a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php index 40b3316df2a..21cc4ca0893 100644 --- a/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ProjectAnalyzer.php @@ -204,6 +204,11 @@ class ProjectAnalyzer */ public $provide_completion = false; + /** + * @var list + */ + public $check_paths_files = []; + /** * @var array */ @@ -1178,6 +1183,7 @@ public function checkPaths(array $paths_to_check): void if (is_dir($path)) { $this->checkDirWithConfig($path, $this->config, true); } elseif (is_file($path)) { + $this->check_paths_files[] = $path; $this->codebase->addFilesToAnalyze([$path => $path]); $this->config->hide_external_errors = $this->config->isInProjectDirs($path); } From b68ac865e1ab2ed97d9e83471c3795b93b7d3c7f Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sat, 24 Sep 2022 10:44:12 +0200 Subject: [PATCH 135/194] add docs --- docs/running_psalm/configuration.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/running_psalm/configuration.md b/docs/running_psalm/configuration.md index f24d4542cc8..6a7f4856bc2 100644 --- a/docs/running_psalm/configuration.md +++ b/docs/running_psalm/configuration.md @@ -400,6 +400,14 @@ Useful in testing, this makes Psalm throw a regular-old exception when it encoun ``` Whether or not to show issues in files that are used by your project files, but which are not included in ``. Defaults to `false`. +#### hideAllErrorsExceptPassedFiles +```xml + +``` +Whether or not to report issues only for files that were passed explicitly as arguments in CLI. This means any files that are loaded with require/include will not report either, if not set in CLI. Useful if you want to only check errors in a single or selected files. Defaults to `false`. + #### cacheDirectory ```xml Date: Mon, 3 Oct 2022 10:45:36 +0200 Subject: [PATCH 136/194] Immutable Unions --- .github/workflows/bcc.yml | 4 +- .gitignore | 1 + UPGRADING.md | 9 + psalm-baseline.xml | 62 +- src/Psalm/Context.php | 14 +- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 12 +- .../Analyzer/FunctionLikeAnalyzer.php | 7 +- .../Internal/Analyzer/MethodComparator.php | 23 +- .../Statements/Expression/ArrayAnalyzer.php | 4 +- .../Assignment/ArrayAssignmentAnalyzer.php | 20 +- .../Expression/AssignmentAnalyzer.php | 5 +- .../Expression/BinaryOp/ConcatAnalyzer.php | 15 +- .../Expression/BitwiseNotAnalyzer.php | 1 + .../Expression/Call/ArgumentAnalyzer.php | 5 +- .../Expression/Call/ArgumentsAnalyzer.php | 16 +- .../Call/ArrayFunctionArgumentsAnalyzer.php | 6 +- .../Call/FunctionCallReturnTypeFetcher.php | 15 +- .../ExistingAtomicMethodCallAnalyzer.php | 8 +- .../Method/MethodCallReturnTypeFetcher.php | 2 +- .../Call/Method/MissingMethodCallHandler.php | 4 +- .../Expression/Call/MethodCallAnalyzer.php | 4 +- .../Expression/Call/StaticCallAnalyzer.php | 6 +- .../StaticMethod/AtomicStaticCallAnalyzer.php | 8 +- .../ExistingAtomicStaticCallAnalyzer.php | 2 +- .../Statements/Expression/CallAnalyzer.php | 5 +- .../Statements/Expression/CastAnalyzer.php | 4 +- .../Expression/Fetch/ArrayFetchAnalyzer.php | 73 +- .../Fetch/AtomicPropertyFetchAnalyzer.php | 2 +- .../Fetch/InstancePropertyFetchAnalyzer.php | 8 +- .../Expression/SimpleTypeInferer.php | 2 +- .../Statements/Expression/YieldAnalyzer.php | 2 +- .../Analyzer/Statements/ReturnAnalyzer.php | 6 +- .../Analyzer/Statements/UnsetAnalyzer.php | 4 +- src/Psalm/Internal/Codebase/ClassLikes.php | 8 +- src/Psalm/Internal/Codebase/Methods.php | 8 +- .../Reflector/ClassLikeNodeScanner.php | 2 +- .../Reflector/FunctionLikeDocblockScanner.php | 6 +- .../Reflector/FunctionLikeNodeScanner.php | 2 +- .../PhpVisitor/Reflector/TypeHintResolver.php | 2 +- .../ArrayFilterReturnTypeProvider.php | 4 +- ...rayPointerAdjustmentReturnTypeProvider.php | 2 +- .../ArrayPopReturnTypeProvider.php | 2 +- .../FilterVarReturnTypeProvider.php | 8 +- .../FirstArgStringReturnTypeProvider.php | 7 +- .../StrReplaceReturnTypeProvider.php | 3 +- src/Psalm/Internal/ReferenceConstraint.php | 16 +- .../Internal/Type/AssertionReconciler.php | 2 + .../Comparator/CallableTypeComparator.php | 11 +- .../Type/Comparator/UnionTypeComparator.php | 13 +- .../Type/NegatedAssertionReconciler.php | 13 +- .../Type/SimpleAssertionReconciler.php | 23 +- .../Type/SimpleNegatedAssertionReconciler.php | 25 +- .../Type/TemplateInferredTypeReplacer.php | 28 +- .../Type/TemplateStandinTypeReplacer.php | 12 +- src/Psalm/Internal/Type/TypeParser.php | 6 +- src/Psalm/Storage/Possibilities.php | 2 +- src/Psalm/Type.php | 6 +- src/Psalm/Type/Atomic.php | 14 +- src/Psalm/Type/Atomic/CallableTrait.php | 4 +- src/Psalm/Type/Atomic/GenericTrait.php | 4 +- src/Psalm/Type/Atomic/TClassStringMap.php | 2 +- src/Psalm/Type/Atomic/TConditional.php | 2 +- src/Psalm/Type/Atomic/TKeyedArray.php | 4 +- src/Psalm/Type/Atomic/TList.php | 2 +- .../Type/Atomic/TObjectWithProperties.php | 4 +- src/Psalm/Type/Atomic/TTemplateKeyOf.php | 2 +- .../Type/Atomic/TTemplatePropertiesOf.php | 4 +- src/Psalm/Type/Atomic/TTemplateValueOf.php | 2 +- src/Psalm/Type/MutableUnion.php | 455 +++++ src/Psalm/Type/Reconciler.php | 19 +- src/Psalm/Type/Union.php | 1480 +---------------- src/Psalm/Type/UnionTrait.php | 1287 ++++++++++++++ tests/ArrayAccessTest.php | 2 +- tests/Template/ClassTemplateTest.php | 2 +- 74 files changed, 2134 insertions(+), 1725 deletions(-) create mode 100644 src/Psalm/Type/MutableUnion.php create mode 100644 src/Psalm/Type/UnionTrait.php diff --git a/.github/workflows/bcc.yml b/.github/workflows/bcc.yml index 8bc58be9bb3..5cc8c319458 100644 --- a/.github/workflows/bcc.yml +++ b/.github/workflows/bcc.yml @@ -13,7 +13,7 @@ jobs: tools: composer:v2 coverage: none - - uses: actions/checkout@v2 + - uses: actions/checkout@v3 with: fetch-depth: 0 @@ -24,7 +24,7 @@ jobs: echo "::set-output name=vcs_cache::$(composer config cache-vcs-dir)" - name: Cache composer cache - uses: actions/cache@v2 + uses: actions/cache@v3 with: path: | ${{ steps.composer-cache.outputs.files_cache }} diff --git a/.gitignore b/.gitignore index a060c7ed342..3d4eb36dc0d 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ /tests/fixtures/symlinktest/* .idea/ +.vscode/ diff --git a/UPGRADING.md b/UPGRADING.md index 9f2ad3f0dd1..6e176c69ebd 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -1,5 +1,14 @@ # Upgrading from Psalm 4 to Psalm 5 ## Changed +- [BC] `Psalm\Type\Union`s are now partially immutable, mutator methods were removed and moved into `Psalm\Type\MutableUnion`. + To modify a union type, use the new `Psalm\Type\Union::getBuilder` method to turn a `Psalm\Type\Union` into a `Psalm\Type\MutableUnion`: once you're done, use `Psalm\Type\MutableUnion::freeze` to get a new `Psalm\Type\Union`. + Methods removed from `Psalm\Type\Union` and moved into `Psalm\Type\MutableUnion`: + - `replaceTypes` + - `addType` + - `removeType` + - `substitute` + - `replaceClassLike` + - [BC] TPositiveInt has been removed and replaced by TIntRange - [BC] The parameter `$php_version` of `Psalm\Type\Atomic::create()` renamed diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 95396607e4d..e140ba1a087 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + $comment_block->tags['variablesfrom'][0] @@ -112,6 +112,9 @@ + + verifyType + $non_existent_method_ids[0] $parts[1] @@ -289,6 +292,14 @@ $cs[0] + + + $callable + + + TCallable|TClosure|null + + $combination->array_type_params[1] @@ -311,31 +322,6 @@ array_keys($template_type_map[$template_param_name])[0] - - - VirtualClass - - - - - VirtualFunction - - - - - VirtualInterface - - - - - VirtualTrait - - - - - VirtualConst - - array_keys($template_type_map[$value])[0] @@ -346,12 +332,36 @@ $this->type_params[1] + + + + + + $allow_mutations + $failed_reconciliation + $from_template_default + $has_mutations + $initialized_class + $reference_free + + $type[0] $type[0][0] + + + $ignore_isset + + + + + allFloatLiterals + allFloatLiterals + + $subNodes['expr'] diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php index 286cbf72ee4..8b894dad1d8 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -485,7 +485,10 @@ public function update( if ((!$new_type || !$old_type->equals($new_type)) && ($new_type || count($existing_type->getAtomicTypes()) > 1) ) { - $existing_type->substitute($old_type, $new_type); + $existing_type = $existing_type + ->getBuilder() + ->substitute($old_type, $new_type) + ->freeze(); if ($new_type && $new_type->from_docblock) { $existing_type->setFromDocblock(); @@ -770,18 +773,23 @@ public function removeDescendents( $statements_analyzer ); - foreach ($this->vars_in_scope as $var_id => $type) { + foreach ($this->vars_in_scope as $var_id => &$type) { if (preg_match('/' . preg_quote($remove_var_id, '/') . '[\]\[\-]/', $var_id)) { $this->remove($var_id, false); } + $builder = null; foreach ($type->getAtomicTypes() as $atomic_type) { if ($atomic_type instanceof DependentType && $atomic_type->getVarId() === $remove_var_id ) { - $type->addType($atomic_type->getReplacement()); + $builder ??= $type->getBuilder(); + $builder->addType($atomic_type->getReplacement()); } } + if ($builder) { + $type = $builder->freeze(); + } } } diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index ff9bed24448..d585d398584 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -791,12 +791,12 @@ public static function addContextProperties( $template_result = new TemplateResult([], $lower_bounds); - TemplateInferredTypeReplacer::replace( + $guide_property_type = TemplateInferredTypeReplacer::replace( $guide_property_type, $template_result, $codebase ); - TemplateInferredTypeReplacer::replace( + $property_type = TemplateInferredTypeReplacer::replace( $property_type, $template_result, $codebase @@ -1294,7 +1294,11 @@ static function (FunctionLikeParameter $param): PhpParser\Node\Arg { ); } elseif (!$property_storage->has_default) { if (isset($this->inferred_property_types[$property_name])) { - $this->inferred_property_types[$property_name]->addType(new TNull()); + $this->inferred_property_types[$property_name] = + $this->inferred_property_types[$property_name] + ->getBuilder() + ->addType(new TNull()) + ->freeze(); $this->inferred_property_types[$property_name]->setFromDocblock(); } } @@ -1543,7 +1547,7 @@ private function analyzeProperty( } if ($suggested_type && !$property_storage->has_default && $property_storage->is_static) { - $suggested_type->addType(new TNull()); + $suggested_type = $suggested_type->getBuilder()->addType(new TNull())->freeze(); } if ($suggested_type && !$suggested_type->isNull()) { diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 29fc51a3080..735dc7fef0a 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -992,8 +992,9 @@ private function processParams( if ($signature_type && $signature_type_location && $signature_type->hasObjectType()) { $referenced_type = $signature_type; if ($referenced_type->isNullable()) { - $referenced_type = clone $referenced_type; + $referenced_type = $referenced_type->getBuilder(); $referenced_type->removeType('null'); + $referenced_type = $referenced_type->freeze(); } [$start, $end] = $signature_type_location->getSelectionBounds(); $codebase->analyzer->addOffsetReference( @@ -1844,9 +1845,9 @@ private function getFunctionInformation( $this->storage->if_this_is_type ); - foreach ($context->vars_in_scope as $var_name => $var_type) { + foreach ($context->vars_in_scope as $var_name => &$var_type) { if (0 === mb_strpos($var_name, '$this->')) { - TemplateInferredTypeReplacer::replace($var_type, $template_result, $codebase); + $var_type = TemplateInferredTypeReplacer::replace($var_type, $template_result, $codebase); } } diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index 58476245364..f6bfe8db4b9 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -362,7 +362,7 @@ private static function compareMethodParams( $guide_param_signature_type = $guide_param->type; $or_null_guide_param_signature_type = $guide_param->signature_type - ? clone $guide_param->signature_type + ? $guide_param->signature_type->getBuilder() : null; if ($or_null_guide_param_signature_type) { @@ -729,29 +729,34 @@ private static function compareMethodDocblockParams( } } - foreach ($implementer_method_storage_param_type->getAtomicTypes() as $k => $t) { + $builder = $implementer_method_storage_param_type->getBuilder(); + foreach ($builder->getAtomicTypes() as $k => $t) { if ($t instanceof TTemplateParam && strpos($t->defining_class, 'fn-') === 0 ) { - $implementer_method_storage_param_type->removeType($k); + $builder->removeType($k); foreach ($t->as->getAtomicTypes() as $as_t) { - $implementer_method_storage_param_type->addType($as_t); + $builder->addType($as_t); } } } + $implementer_method_storage_param_type = $builder->freeze(); - foreach ($guide_method_storage_param_type->getAtomicTypes() as $k => $t) { + $builder = $guide_method_storage_param_type->getBuilder(); + foreach ($builder->getAtomicTypes() as $k => $t) { if ($t instanceof TTemplateParam && strpos($t->defining_class, 'fn-') === 0 ) { - $guide_method_storage_param_type->removeType($k); + $builder->removeType($k); foreach ($t->as->getAtomicTypes() as $as_t) { - $guide_method_storage_param_type->addType($as_t); + $builder->addType($as_t); } } } + $guide_method_storage_param_type = $builder->freeze(); + unset($builder); if ($implementer_classlike_storage->template_extended_params) { self::transformTemplates( @@ -1055,7 +1060,7 @@ private static function compareMethodDocblockReturnTypes( private static function transformTemplates( array $template_extended_params, string $base_class_name, - Union $templated_type, + Union &$templated_type, Codebase $codebase ): void { if (isset($template_extended_params[$base_class_name])) { @@ -1092,7 +1097,7 @@ private static function transformTemplates( $template_result = new TemplateResult([], $template_types); - TemplateInferredTypeReplacer::replace( + $templated_type = TemplateInferredTypeReplacer::replace( $templated_type, $template_result, $codebase diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index f33c0d82ad4..a3c55b1b1c0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -225,10 +225,10 @@ public static function analyze( } if ($bad_types && $good_types) { - $item_key_type->substitute( + $item_key_type = $item_key_type->getBuilder()->substitute( TypeCombiner::combine($bad_types, $codebase), TypeCombiner::combine($good_types, $codebase) - ); + )->freeze(); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index c78f72ef44a..e0bd6b08401 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -218,7 +218,9 @@ public static function updateArrayType( $new_child_type = $root_type; } + $new_child_type = $new_child_type->getBuilder(); $new_child_type->removeType('null'); + $new_child_type = $new_child_type->freeze(); if (!$root_type->hasObjectType()) { $root_type = $new_child_type; @@ -295,7 +297,7 @@ private static function updateTypeWithKeyValues( $has_matching_objectlike_property = false; $has_matching_string = false; - $child_stmt_type = clone $child_stmt_type; + $child_stmt_type = $child_stmt_type->getBuilder(); foreach ($child_stmt_type->getAtomicTypes() as $type) { if ($type instanceof TTemplateParam) { @@ -351,7 +353,7 @@ private static function updateTypeWithKeyValues( } } - $child_stmt_type->bustCache(); + $child_stmt_type = $child_stmt_type->freeze(); if (!$has_matching_objectlike_property && !$has_matching_string) { if (count($key_values) === 1) { @@ -511,9 +513,7 @@ private static function updateArrayAssignmentChildType( } } - $array_atomic_key_type = ArrayFetchAnalyzer::replaceOffsetTypeWithInts( - $key_type - ); + $array_atomic_key_type = ArrayFetchAnalyzer::replaceOffsetTypeWithInts($key_type); } else { $array_atomic_key_type = Type::getArrayKey(); } @@ -557,7 +557,7 @@ private static function updateArrayAssignmentChildType( ] ); - TemplateInferredTypeReplacer::replace( + $value_type = TemplateInferredTypeReplacer::replace( $value_type, $template_result, $codebase @@ -742,17 +742,21 @@ private static function analyzeNestedArrayAssignment( $is_last = $i === count($child_stmts) - 1; + $child_stmt_dim_type_or_int = $child_stmt_dim_type ?? Type::getInt(); $child_stmt_type = ArrayFetchAnalyzer::getArrayAccessTypeGivenOffset( $statements_analyzer, $child_stmt, $array_type, - $child_stmt_dim_type ?? Type::getInt(), + $child_stmt_dim_type_or_int, true, $extended_var_id, $context, $assign_value, !$is_last ? null : $assignment_type ); + if ($child_stmt->dim) { + $statements_analyzer->node_data->setType($child_stmt->dim, $child_stmt_dim_type_or_int); + } $statements_analyzer->node_data->setType( $child_stmt, @@ -886,8 +890,10 @@ private static function analyzeNestedArrayAssignment( ); } + $new_child_type = $new_child_type->getBuilder(); $new_child_type->removeType('null'); $new_child_type->possibly_undefined = false; + $new_child_type = $new_child_type->freeze(); if (!$child_stmt_type->hasObjectType()) { $child_stmt_type = $new_child_type; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index c787b2443c9..c185eb579d5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -1561,7 +1561,10 @@ private static function analyzeDestructuringAssignment( if (($context->error_suppressing && ($offset || $can_be_empty)) || $has_null ) { - $context->vars_in_scope[$list_var_id]->addType(new TNull); + $context->vars_in_scope[$list_var_id] = $context->vars_in_scope[$list_var_id] + ->getBuilder() + ->addType(new TNull) + ->freeze(); } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index a4f3924df52..9539eceaf7f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -36,6 +36,7 @@ use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Atomic\TNonspecificLiteralString; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TNumericString; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Union; @@ -189,9 +190,11 @@ public static function analyze( } if (!$literal_concat) { - $numeric_type = Type::getNumericString(); - $numeric_type->addType(new TInt()); - $numeric_type->addType(new TFloat()); + $numeric_type = new Union([ + new TNumericString, + new TInt, + new TFloat + ]); $left_is_numeric = UnionTypeComparator::isContainedBy( $codebase, $left_type, @@ -212,8 +215,7 @@ public static function analyze( } } - $lowercase_type = clone $numeric_type; - $lowercase_type->addType(new TLowercaseString()); + $lowercase_type = $numeric_type->getBuilder()->addType(new TLowercaseString())->freeze(); $all_lowercase = UnionTypeComparator::isContainedBy( $codebase, @@ -225,8 +227,7 @@ public static function analyze( $lowercase_type ); - $non_empty_string = clone $numeric_type; - $non_empty_string->addType(new TNonEmptyString()); + $non_empty_string = $numeric_type->getBuilder()->addType(new TNonEmptyString())->freeze(); $has_non_empty = UnionTypeComparator::isContainedBy( $codebase, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php index 878dc11eca8..8774fbf5227 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php @@ -44,6 +44,7 @@ public static function analyze( $unacceptable_type = null; $has_valid_operand = false; + $stmt_expr_type = $stmt_expr_type->getBuilder(); foreach ($stmt_expr_type->getAtomicTypes() as $type_string => $type_part) { if ($type_part instanceof TInt || $type_part instanceof TString) { if ($type_part instanceof TLiteralInt) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index e87585606f5..34cf5152e7e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -834,6 +834,7 @@ public static function verifyType( if ($param_type->hasCallableType() && $param_type->isSingle()) { // we do this replacement early because later we don't have access to the // $statements_analyzer, which is necessary to understand string function names + $input_type = $input_type->getBuilder(); foreach ($input_type->getAtomicTypes() as $key => $atomic_type) { if (!$atomic_type instanceof TLiteralString || InternalCallMapHandler::inCallMap($atomic_type->value) @@ -854,6 +855,7 @@ public static function verifyType( $input_type->addType($candidate_callable); } } + $input_type = $input_type->freeze(); } $union_comparison_results = new TypeComparisonResult(); @@ -1384,9 +1386,10 @@ private static function coerceValueAfterGatekeeperArgument( $was_cloned = false; if ($input_type->isNullable() && !$param_type->isNullable()) { - $input_type = clone $input_type; + $input_type = $input_type->getBuilder(); $was_cloned = true; $input_type->removeType('null'); + $input_type = $input_type->freeze(); } if ($input_type->getId() === $param_type->getId()) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 68b90dc676d..424c748f36b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -494,7 +494,7 @@ private static function handleHighOrderFuncCallArg( // The map function expects callable(A):B as second param // We know that previous arg type is list where the int is the A template. // Then we can replace callable(A): B to callable(int):B using $inferred_template_result. - TemplateInferredTypeReplacer::replace( + $replaced_container_hof_atomic = TemplateInferredTypeReplacer::replace( $replaced_container_hof_atomic, $inferred_template_result, $codebase @@ -601,7 +601,7 @@ private static function handleClosureArg( $context->calling_method_id ?: $context->calling_function_id ); - TemplateInferredTypeReplacer::replace( + $replaced_type = TemplateInferredTypeReplacer::replace( $replaced_type, $replace_template_result, $codebase @@ -1234,7 +1234,7 @@ private static function handlePossiblyMatchingByRefParam( ); if ($template_result->lower_bounds) { - TemplateInferredTypeReplacer::replace( + $original_by_ref_type = TemplateInferredTypeReplacer::replace( $original_by_ref_type, $template_result, $codebase @@ -1259,7 +1259,7 @@ private static function handlePossiblyMatchingByRefParam( ); if ($template_result->lower_bounds) { - TemplateInferredTypeReplacer::replace( + $original_by_ref_out_type = TemplateInferredTypeReplacer::replace( $original_by_ref_out_type, $template_result, $codebase @@ -1386,16 +1386,18 @@ private static function evaluateArbitraryParam( $statements_analyzer ); - foreach ($context->vars_in_scope[$var_id]->getAtomicTypes() as $type) { + $t = $context->vars_in_scope[$var_id]->getBuilder(); + foreach ($t->getAtomicTypes() as $type) { if ($type instanceof TArray && $type->isEmptyArray()) { - $context->vars_in_scope[$var_id]->removeType('array'); - $context->vars_in_scope[$var_id]->addType( + $t->removeType('array'); + $t->addType( new TArray( [Type::getArrayKey(), Type::getMixed()] ) ); } } + $context->vars_in_scope[$var_id] = $t->freeze(); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index a34f9e6d030..f3852df5154 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -266,7 +266,7 @@ public static function handleAddition( new Union([new TArray([$new_offset_type, Type::getMixed()])]) ); } elseif ($arg->unpack) { - $arg_value_type = clone $arg_value_type; + $arg_value_type = $arg_value_type->getBuilder(); foreach ($arg_value_type->getAtomicTypes() as $arg_value_atomic_type) { if ($arg_value_atomic_type instanceof TKeyedArray) { @@ -285,6 +285,7 @@ public static function handleAddition( $arg_value_type->addType($arg_value_atomic_type); } } + $arg_value_type = $arg_value_type->freeze(); $by_ref_type = Type::combineUnionTypes( $by_ref_type, @@ -508,7 +509,7 @@ public static function handleByRefArrayAdjustment( $context->removeVarFromConflictingClauses($var_id, null, $statements_analyzer); if (isset($context->vars_in_scope[$var_id])) { - $array_type = clone $context->vars_in_scope[$var_id]; + $array_type = $context->vars_in_scope[$var_id]->getBuilder(); $array_atomic_types = $array_type->getAtomicTypes(); @@ -574,6 +575,7 @@ public static function handleByRefArrayAdjustment( } } + $array_type = $array_type->freeze(); $context->removeDescendents($var_id, $array_type); $context->vars_in_scope[$var_id] = $array_type; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index ed207aded41..e5be6f5244e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -41,6 +41,7 @@ use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TString; use Psalm\Type\Union; use UnexpectedValueException; @@ -172,7 +173,7 @@ public static function fetch( null ); - TemplateInferredTypeReplacer::replace( + $return_type = TemplateInferredTypeReplacer::replace( $return_type, $template_result, $codebase @@ -501,8 +502,10 @@ private static function getReturnTypeFromCallMapWithArgs( break; case 'fgetcsv': - $string_type = Type::getString(); - $string_type->addType(new TNull); + $string_type = new Union([ + new TString, + new TNull + ]); $string_type->ignore_nullable_issues = true; $call_map_return_type = new Union([ @@ -609,10 +612,8 @@ private static function taintReturnType( $conditionally_removed_taints = []; foreach ($function_storage->conditionally_removed_taints as $conditionally_removed_taint) { - $conditionally_removed_taint = clone $conditionally_removed_taint; - - TemplateInferredTypeReplacer::replace( - $conditionally_removed_taint, + $conditionally_removed_taint = TemplateInferredTypeReplacer::replace( + clone $conditionally_removed_taint, $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php index b16fadae07a..72c0c7cbda5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -300,9 +300,11 @@ public static function analyze( if ($method_storage) { if ($method_storage->if_this_is_type) { $class_type = new Union([$lhs_type_part]); - $if_this_is_type = clone $method_storage->if_this_is_type; - - TemplateInferredTypeReplacer::replace($if_this_is_type, $template_result, $codebase); + $if_this_is_type = TemplateInferredTypeReplacer::replace( + clone $method_storage->if_this_is_type, + $template_result, + $codebase + ); if (!UnionTypeComparator::isContainedBy($codebase, $class_type, $if_this_is_type)) { IssueBuffer::maybeAdd( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php index 2ed17b4bc67..d62ee2d694f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MethodCallReturnTypeFetcher.php @@ -618,7 +618,7 @@ public static function replaceTemplateTypes( null ); - TemplateInferredTypeReplacer::replace( + $return_type_candidate = TemplateInferredTypeReplacer::replace( $return_type_candidate, $template_result, $codebase diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php index 086a8d255dd..ac2346d95ee 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php @@ -149,7 +149,7 @@ public static function handleMagicMethod( $return_type_candidate = clone $pseudo_method_storage->return_type; if ($found_generic_params) { - TemplateInferredTypeReplacer::replace( + $return_type_candidate = TemplateInferredTypeReplacer::replace( $return_type_candidate, new TemplateResult([], $found_generic_params), $codebase @@ -315,7 +315,7 @@ public static function handleMissingOrMagicMethod( $return_type_candidate = clone $pseudo_method_storage->return_type; if ($found_generic_params) { - TemplateInferredTypeReplacer::replace( + $return_type_candidate = TemplateInferredTypeReplacer::replace( $return_type_candidate, new TemplateResult([], $found_generic_params), $codebase diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index a755115db7f..db15ccafa41 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -400,7 +400,7 @@ public static function analyze( ) { $keys_to_remove = []; - $class_type = clone $class_type; + $class_type = $class_type->getBuilder(); foreach ($class_type->getAtomicTypes() as $key => $type) { if (!$type instanceof TNamedObject) { @@ -418,7 +418,7 @@ public static function analyze( $context->removeVarFromConflictingClauses($lhs_var_id, null, $statements_analyzer); - $context->vars_in_scope[$lhs_var_id] = $class_type; + $context->vars_in_scope[$lhs_var_id] = $class_type->freeze(); } return true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index 772ad322c0c..e1754c01c93 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -298,10 +298,8 @@ public static function taintReturnType( if ($method_storage && $template_result) { foreach ($method_storage->conditionally_removed_taints as $conditionally_removed_taint) { - $conditionally_removed_taint = clone $conditionally_removed_taint; - - TemplateInferredTypeReplacer::replace( - $conditionally_removed_taint, + $conditionally_removed_taint = TemplateInferredTypeReplacer::replace( + clone $conditionally_removed_taint, $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php index 619868af958..50946b5a501 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -469,13 +469,13 @@ private static function handleNamedCall( $tGenericMixin, $class_storage, $mixin_declaring_class_storage - ); + )->getBuilder(); foreach ($mixin_candidate_type->getAtomicTypes() as $type) { $new_mixin_candidate_type->addType($type); } - $mixin_candidate_type = $new_mixin_candidate_type; + $mixin_candidate_type = $new_mixin_candidate_type->freeze(); } $new_lhs_type = TypeExpander::expandUnion( @@ -720,7 +720,7 @@ private static function handleNamedCall( if (isset($context->vars_in_scope['$this']) && $method_call_type = $statements_analyzer->node_data->getType($stmt) ) { - $method_call_type = clone $method_call_type; + $method_call_type = $method_call_type->getBuilder(); foreach ($method_call_type->getAtomicTypes() as $name => $type) { if ($type instanceof TNamedObject && $type->is_static && $type->value === $fq_class_name) { @@ -730,7 +730,7 @@ private static function handleNamedCall( } } - $statements_analyzer->node_data->setType($stmt, $method_call_type); + $statements_analyzer->node_data->setType($stmt, $method_call_type->freeze()); } return true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index f7e0d7e09aa..55445238f9b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -567,7 +567,7 @@ private static function getMethodReturnType( null ); - TemplateInferredTypeReplacer::replace( + $return_type_candidate = TemplateInferredTypeReplacer::replace( $return_type_candidate, $template_result, $codebase diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index ade1205332c..b39e5579ce5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -756,9 +756,8 @@ public static function applyAssertionsToContext( $assertion_type_atomic = $assertion_rule->getAtomicType(); if ($assertion_type_atomic) { - $assertion_type = new Union([clone $assertion_type_atomic]); - TemplateInferredTypeReplacer::replace( - $assertion_type, + $assertion_type = TemplateInferredTypeReplacer::replace( + new Union([clone $assertion_type_atomic]), $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 48795d26f34..1e7624f9feb 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -78,7 +78,7 @@ public static function analyze( } if ($maybe_type->hasBool()) { - $casted_type = clone $maybe_type; + $casted_type = $maybe_type->getBuilder(); if (isset($casted_type->getAtomicTypes()['bool'])) { $casted_type->addType(new TLiteralInt(0)); $casted_type->addType(new TLiteralInt(1)); @@ -95,7 +95,7 @@ public static function analyze( $casted_type->removeType('false'); if ($casted_type->isInt()) { - $valid_int_type = $casted_type; + $valid_int_type = $casted_type->freeze(); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 57bfbdce1f4..27e89050bc4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -80,6 +80,7 @@ use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateParamClass; use Psalm\Type\Atomic\TTrue; +use Psalm\Type\MutableUnion; use Psalm\Type\Union; use UnexpectedValueException; @@ -271,7 +272,7 @@ public static function analyze( && !$const_array_key_type->hasMixed() && !$stmt_dim_type->hasMixed() ) { - $new_offset_type = clone $stmt_dim_type; + $new_offset_type = $stmt_dim_type->getBuilder(); $const_array_key_atomic_types = $const_array_key_type->getAtomicTypes(); foreach ($new_offset_type->getAtomicTypes() as $offset_key => $offset_atomic_type) { @@ -295,6 +296,8 @@ public static function analyze( $new_offset_type->removeType($offset_key); } } + + $new_offset_type = $new_offset_type->freeze(); } } } @@ -456,14 +459,17 @@ public static function taintArrayFetch( public static function getArrayAccessTypeGivenOffset( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\ArrayDimFetch $stmt, - Union $array_type, - Union $offset_type, + Union &$array_type_original, + Union &$offset_type_original, bool $in_assignment, ?string $extended_var_id, Context $context, PhpParser\Node\Expr $assign_value = null, Union $replacement_type = null ): Union { + $array_type = $array_type_original->getBuilder(); + $offset_type = $offset_type_original->getBuilder(); + $codebase = $statements_analyzer->getCodebase(); $has_array_access = false; @@ -847,6 +853,9 @@ public static function getArrayAccessTypeGivenOffset( } } + $array_type_original = $array_type->freeze(); + $offset_type_original = $offset_type->freeze(); + if ($array_access_type === null) { // shouldn’t happen, but don’t crash return Type::getMixed(); @@ -864,7 +873,7 @@ public static function getArrayAccessTypeGivenOffset( } private static function checkLiteralIntArrayOffset( - Union $offset_type, + MutableUnion $offset_type, Union $expected_offset_type, ?string $extended_var_id, PhpParser\Node\Expr\ArrayDimFetch $stmt, @@ -912,7 +921,7 @@ private static function checkLiteralIntArrayOffset( } private static function checkLiteralStringArrayOffset( - Union $offset_type, + MutableUnion $offset_type, Union $expected_offset_type, ?string $extended_var_id, PhpParser\Node\Expr\ArrayDimFetch $stmt, @@ -961,26 +970,16 @@ private static function checkLiteralStringArrayOffset( public static function replaceOffsetTypeWithInts(Union $offset_type): Union { + $offset_type = $offset_type->getBuilder(); $offset_types = $offset_type->getAtomicTypes(); - $cloned = false; - foreach ($offset_types as $key => $offset_type_part) { if ($offset_type_part instanceof TLiteralString) { if (preg_match('/^(0|[1-9][0-9]*)$/', $offset_type_part->value)) { - if (!$cloned) { - $offset_type = clone $offset_type; - $cloned = true; - } $offset_type->addType(new TLiteralInt((int) $offset_type_part->value)); $offset_type->removeType($key); } } elseif ($offset_type_part instanceof TBool) { - if (!$cloned) { - $offset_type = clone $offset_type; - $cloned = true; - } - if ($offset_type_part instanceof TFalse) { if (!$offset_type->ignore_falsable_issues) { $offset_type->addType(new TLiteralInt(0)); @@ -997,7 +996,7 @@ public static function replaceOffsetTypeWithInts(Union $offset_type): Union } } - return $offset_type; + return $offset_type->freeze(); } /** @@ -1085,11 +1084,11 @@ private static function handleArrayAccessOnArray( bool $in_assignment, Atomic &$type, array &$key_values, - Union $array_type, + MutableUnion $array_type, string $type_string, PhpParser\Node\Expr\ArrayDimFetch $stmt, ?Union $replacement_type, - Union &$offset_type, + MutableUnion $offset_type, Atomic $original_type, Codebase $codebase, ?string $extended_var_id, @@ -1143,7 +1142,7 @@ private static function handleArrayAccessOnArray( } } - $offset_type = self::replaceOffsetTypeWithInts($offset_type); + $offset_type = self::replaceOffsetTypeWithInts($offset_type->freeze())->getBuilder(); if ($type instanceof TList && (($in_assignment && $stmt->dim) @@ -1226,10 +1225,10 @@ private static function handleArrayAccessOnTArray( Codebase $codebase, Context $context, PhpParser\Node\Expr\ArrayDimFetch $stmt, - Union $array_type, + MutableUnion $array_type, ?string $extended_var_id, TArray $type, - Union $offset_type, + MutableUnion $offset_type, bool $in_assignment, array &$expected_offset_types, ?Union &$array_access_type, @@ -1241,7 +1240,7 @@ private static function handleArrayAccessOnTArray( if ($type->isEmptyArray()) { $type->type_params[0] = $offset_type->isMixed() ? Type::getArrayKey() - : $offset_type; + : $offset_type->freeze(); } } elseif (!$type->isEmptyArray()) { $expected_offset_type = $type->type_params[0]->hasMixed() @@ -1278,7 +1277,7 @@ private static function handleArrayAccessOnTArray( } else { $offset_type_contained_by_expected = UnionTypeComparator::isContainedBy( $codebase, - $offset_type, + $offset_type->freeze(), $expected_offset_type, true, $offset_type->ignore_falsable_issues, @@ -1336,7 +1335,7 @@ private static function handleArrayAccessOnTArray( if (UnionTypeComparator::canExpressionTypesBeIdentical( $codebase, - $offset_type, + $offset_type->freeze(), $expected_offset_type )) { $has_valid_offset = true; @@ -1378,7 +1377,7 @@ private static function handleArrayAccessOnTArray( private static function handleArrayAccessOnClassStringMap( Codebase $codebase, TClassStringMap $type, - Union $offset_type, + MutableUnion $offset_type, ?Union $replacement_type, ?Union &$array_access_type ): void { @@ -1440,7 +1439,7 @@ private static function handleArrayAccessOnClassStringMap( $expected_value_param_get = clone $type->value_param; - TemplateInferredTypeReplacer::replace( + $expected_value_param_get = TemplateInferredTypeReplacer::replace( $expected_value_param_get, $template_result_get, $codebase @@ -1449,7 +1448,7 @@ private static function handleArrayAccessOnClassStringMap( if ($replacement_type) { $expected_value_param_set = clone $type->value_param; - TemplateInferredTypeReplacer::replace( + $replacement_type = TemplateInferredTypeReplacer::replace( $replacement_type, $template_result_set, $codebase @@ -1483,11 +1482,11 @@ private static function handleArrayAccessOnKeyedArray( ?Union &$array_access_type, bool $in_assignment, PhpParser\Node\Expr\ArrayDimFetch $stmt, - Union $offset_type, + MutableUnion $offset_type, ?string $extended_var_id, Context $context, TKeyedArray $type, - Union $array_type, + MutableUnion $array_type, array &$expected_offset_types, string $type_string, bool &$has_valid_offset @@ -1589,7 +1588,7 @@ private static function handleArrayAccessOnKeyedArray( $is_contained = UnionTypeComparator::isContainedBy( $codebase, - $offset_type, + $offset_type->freeze(), $key_type, true, $offset_type->ignore_falsable_issues, @@ -1600,7 +1599,7 @@ private static function handleArrayAccessOnKeyedArray( $is_contained = UnionTypeComparator::isContainedBy( $codebase, $key_type, - $offset_type, + $offset_type->freeze(), true, $offset_type->ignore_falsable_issues ); @@ -1620,7 +1619,7 @@ private static function handleArrayAccessOnKeyedArray( $new_key_type = Type::combineUnionTypes( $generic_key_type, - $offset_type->isMixed() ? Type::getArrayKey() : $offset_type + $offset_type->isMixed() ? Type::getArrayKey() : $offset_type->freeze() ); $property_count = $type->sealed ? count($type->properties) : null; @@ -1682,7 +1681,7 @@ private static function handleArrayAccessOnList( Codebase $codebase, PhpParser\Node\Expr\ArrayDimFetch $stmt, TList $type, - Union $offset_type, + MutableUnion $offset_type, ?string $extended_var_id, array $key_values, Context $context, @@ -1897,7 +1896,7 @@ private static function handleArrayAccessOnString( Context $context, ?Union $replacement_type, TString $type, - Union $offset_type, + MutableUnion $offset_type, array &$expected_offset_types, ?Union &$array_access_type, bool &$has_valid_offset @@ -1960,7 +1959,7 @@ private static function handleArrayAccessOnString( if (!UnionTypeComparator::isContainedBy( $codebase, - $offset_type, + $offset_type->freeze(), $valid_offset_type, true )) { @@ -1981,7 +1980,7 @@ private static function handleArrayAccessOnString( * @param Atomic[] $offset_types */ private static function checkArrayOffsetType( - Union $offset_type, + MutableUnion $offset_type, array $offset_types, Codebase $codebase ): bool { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index d9c83bdc4ee..6136d708155 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -743,7 +743,7 @@ public static function localizePropertyType( } } - TemplateInferredTypeReplacer::replace( + $class_property_type = TemplateInferredTypeReplacer::replace( $class_property_type, new TemplateResult([], $template_types), $codebase diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php index 195bccced82..2e25c2cd7e5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/InstancePropertyFetchAnalyzer.php @@ -262,7 +262,8 @@ public static function analyze( $stmt_type = $statements_analyzer->node_data->getType($stmt); if ($stmt_var_type->isNullable() && !$context->inside_isset && $stmt_type) { - $stmt_type->addType(new TNull); + $stmt_type = $stmt_type->getBuilder()->addType(new TNull)->freeze(); + $statements_analyzer->node_data->setType($stmt, $stmt_type); if ($stmt_var_type->ignore_nullable_issues) { $stmt_type->ignore_nullable_issues = true; @@ -388,7 +389,10 @@ private static function handleScopedProperty( $statements_analyzer->getSuppressedIssues() ); - $stmt_type->addType(new TNull); + $stmt_type = $stmt_type->getBuilder()->addType(new TNull)->freeze(); + + $context->vars_in_scope[$var_id] = $stmt_type; + $statements_analyzer->node_data->setType($stmt, $stmt_type); } } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index cb0c56810f5..1549caeb5ce 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -218,7 +218,7 @@ public static function infer( return null; } - $invalidTypes = clone $stmt_expr_type; + $invalidTypes = $stmt_expr_type->getBuilder(); $invalidTypes->removeType('string'); $invalidTypes->removeType('int'); $invalidTypes->removeType('float'); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php index 4847e04599b..54692df7dde 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/YieldAnalyzer.php @@ -177,7 +177,7 @@ public static function analyze( } if ($yield_type) { - $expression_type->substitute($expression_type, $yield_type); + $expression_type = $expression_type->getBuilder()->substitute($expression_type, $yield_type)->freeze(); } $statements_analyzer->node_data->setType($stmt, $expression_type); diff --git a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php index c063431a38f..2cf90031d27 100644 --- a/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/ReturnAnalyzer.php @@ -284,10 +284,8 @@ public static function analyze( unset($found_generic_params[$template_name][$fq_class_name]); } - $local_return_type = clone $local_return_type; - - TemplateInferredTypeReplacer::replace( - $local_return_type, + $local_return_type = TemplateInferredTypeReplacer::replace( + clone $local_return_type, new TemplateResult([], $found_generic_params), $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php index 2c9a1644902..45347e0dc13 100644 --- a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php @@ -58,7 +58,7 @@ public static function analyze( ); if ($root_var_id && isset($context->vars_in_scope[$root_var_id])) { - $root_type = clone $context->vars_in_scope[$root_var_id]; + $root_type = $context->vars_in_scope[$root_var_id]->getBuilder(); foreach ($root_type->getAtomicTypes() as $atomic_root_type) { if ($atomic_root_type instanceof TKeyedArray) { @@ -126,7 +126,7 @@ public static function analyze( } } - $context->vars_in_scope[$root_var_id] = $root_type; + $context->vars_in_scope[$root_var_id] = $root_type->freeze(); $context->removeVarFromConflictingClauses( $root_var_id, diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index 9a78716c0dc..d7f760e396b 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -1460,9 +1460,9 @@ public function handleDocblockTypeInMigration( foreach ($codebase->class_transforms as $old_fq_class_name => $new_fq_class_name) { if ($type->containsClassLike($old_fq_class_name)) { - $type = clone $type; + $type = $type->getBuilder(); - $type->replaceClassLike($old_fq_class_name, $new_fq_class_name); + $type = $type->replaceClassLike($old_fq_class_name, $new_fq_class_name)->freeze(); $bounds = $type_location->getSelectionBounds(); @@ -1500,9 +1500,9 @@ public function handleDocblockTypeInMigration( $destination_class = $codebase->classes_to_move[$fq_class_name_lc]; if ($type->containsClassLike($fq_class_name_lc)) { - $type = clone $type; + $type = $type->getBuilder(); - $type->replaceClassLike($fq_class_name_lc, $destination_class); + $type = $type->replaceClassLike($fq_class_name_lc, $destination_class)->freeze(); } $this->airliftClassDefinedDocblockType( diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index ba56971366e..da7f59b6122 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -494,7 +494,7 @@ public function getMethodParams( if ($params[$i]->signature_type && $params[$i]->signature_type->isNullable() ) { - $params[$i]->type->addType(new TNull); + $params[$i]->type = $params[$i]->type->getBuilder()->addType(new TNull)->freeze(); } $params[$i]->type_location = $overridden_storage->params[$i]->type_location; @@ -520,7 +520,7 @@ public static function localizeType( return $type; } - $type = clone $type; + $type = $type->getBuilder(); foreach ($type->getAtomicTypes() as $key => $atomic_type) { if ($atomic_type instanceof TTemplateParam @@ -618,9 +618,7 @@ public static function localizeType( } } - $type->bustCache(); - - return $type; + return $type->freeze(); } /** diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 2a99fff148d..9987fe6d70f 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -1629,7 +1629,7 @@ private function visitPropertyDeclaration( if ($property_storage->signature_type->isNullable() && !$property_storage->type->isNullable() ) { - $property_storage->type->addType(new TNull()); + $property_storage->type = $property_storage->type->getBuilder()->addType(new TNull())->freeze(); } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index 084a6ce1bb7..d79bac47b01 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -846,7 +846,7 @@ private static function improveParamsFromDocblock( && !$new_param_type->isNullable() && !$new_param_type->hasTemplate() ) { - $new_param_type->addType(new TNull()); + $new_param_type = $new_param_type->getBuilder()->addType(new TNull())->freeze(); } $config = Config::getInstance(); @@ -888,7 +888,7 @@ private static function improveParamsFromDocblock( } if ($existing_param_type_nullable && !$new_param_type->isNullable()) { - $new_param_type->addType(new TNull()); + $new_param_type = $new_param_type->getBuilder()->addType(new TNull())->freeze(); } $storage_param->type = $new_param_type; @@ -1010,7 +1010,7 @@ private static function handleReturn( $storage->signature_return_type ) ) { - $storage->return_type->addType(new TNull()); + $storage->return_type = $storage->return_type->getBuilder()->addType(new TNull())->freeze(); } } } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index 5583040e62b..9f7c130bda5 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -839,7 +839,7 @@ private function getTranslatedFunctionParam( ); if ($is_nullable) { - $param_type->addType(new TNull); + $param_type = $param_type->getBuilder()->addType(new TNull)->freeze(); } else { $is_nullable = $param_type->isNullable(); } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php index 9265d3c8b15..b359c60ec2e 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php @@ -169,7 +169,7 @@ public static function resolve( } if ($is_nullable) { - $type->addType(new TNull); + $type = $type->getBuilder()->addType(new TNull)->freeze(); } return $type; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php index be8e069e7e6..5cc17218c32 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php @@ -153,11 +153,11 @@ static function ($keyed_type) use ($statements_source, $context) { } if ($key_type->getLiteralStrings()) { - $key_type->addType(new TString); + $key_type = $key_type->getBuilder()->addType(new TString)->freeze(); } if ($key_type->getLiteralInts()) { - $key_type->addType(new TInt); + $key_type = $key_type->getBuilder()->addType(new TInt)->freeze(); } if ($inner_type->isUnionEmpty()) { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php index 5ac91d639da..f89b9012e08 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPointerAdjustmentReturnTypeProvider.php @@ -86,7 +86,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if ($value_type->isNever()) { $value_type = Type::getFalse(); } elseif (($function_id !== 'reset' && $function_id !== 'end') || !$definitely_has_items) { - $value_type->addType(new TFalse); + $value_type = $value_type->getBuilder()->addType(new TFalse)->freeze(); $codebase = $statements_source->getCodebase(); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php index 0ea580949d7..64266953d8f 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayPopReturnTypeProvider.php @@ -85,7 +85,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } if ($nullable) { - $value_type->addType(new TNull); + $value_type = $value_type->getBuilder()->addType(new TNull)->freeze(); $codebase = $statements_source->getCodebase(); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php index e24f5a3e531..afa872b72ca 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php @@ -104,7 +104,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $options_array->properties['default'] ); } else { - $filter_type->addType(new TFalse); + $filter_type = $filter_type->getBuilder()->addType(new TFalse)->freeze(); } if (isset($atomic_type->properties['flags']) @@ -116,20 +116,20 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if ($filter_type->hasBool() && $filter_flag_type->value === FILTER_NULL_ON_FAILURE ) { - $filter_type->addType(new TNull); + $filter_type = $filter_type->getBuilder()->addType(new TNull)->freeze(); } } } elseif ($atomic_type instanceof TLiteralInt) { if ($atomic_type->value === FILTER_NULL_ON_FAILURE) { $filter_null = true; - $filter_type->addType(new TNull); + $filter_type = $filter_type->getBuilder()->addType(new TNull)->freeze(); } } } } if (!$has_object_like && !$filter_null && $filter_type) { - $filter_type->addType(new TFalse); + $filter_type = $filter_type->getBuilder()->addType(new TFalse)->freeze(); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php index 00f633bd858..2a3d18e8048 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FirstArgStringReturnTypeProvider.php @@ -7,6 +7,7 @@ use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; use Psalm\Type; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TString; use Psalm\Type\Union; /** @@ -34,15 +35,13 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev return Type::getMixed(); } - $return_type = Type::getString(); - if (($first_arg_type = $statements_source->node_data->getType($call_args[0]->value)) && $first_arg_type->isString() ) { - return $return_type; + return new Union([new TString]); } - $return_type->addType(new TNull); + $return_type = new Union([new TString, new TNull]); $return_type->ignore_nullable_issues = true; return $return_type; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php index 6061ddf0d34..2dfa3681691 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/StrReplaceReturnTypeProvider.php @@ -7,6 +7,7 @@ use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; use Psalm\Type; use Psalm\Type\Atomic\TNull; +use Psalm\Type\Atomic\TString; use Psalm\Type\Union; use function count; @@ -50,7 +51,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $return_type = Type::getString(); if (in_array($function_id, ['preg_replace', 'preg_replace_callback'], true)) { - $return_type->addType(new TNull()); + $return_type = new Union([new TString, new TNull()]); $codebase = $statements_source->getCodebase(); diff --git a/src/Psalm/Internal/ReferenceConstraint.php b/src/Psalm/Internal/ReferenceConstraint.php index 14bb1707b41..a77ec7ec648 100644 --- a/src/Psalm/Internal/ReferenceConstraint.php +++ b/src/Psalm/Internal/ReferenceConstraint.php @@ -18,19 +18,21 @@ class ReferenceConstraint public function __construct(?Union $type = null) { if ($type) { - $this->type = clone $type; + $type = $type->getBuilder(); - if ($this->type->getLiteralStrings()) { - $this->type->addType(new TString); + if ($type->getLiteralStrings()) { + $type->addType(new TString); } - if ($this->type->getLiteralInts()) { - $this->type->addType(new TInt); + if ($type->getLiteralInts()) { + $type->addType(new TInt); } - if ($this->type->getLiteralFloats()) { - $this->type->addType(new TFloat); + if ($type->getLiteralFloats()) { + $type->addType(new TFloat); } + + $this->type = $type->freeze(); } } } diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index 3778bbb5a45..ee5c5667fc1 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -943,6 +943,7 @@ private static function handleLiteralEquality( $can_be_equal = false; $did_remove_type = false; + $existing_var_type = $existing_var_type->getBuilder(); foreach ($existing_var_atomic_types as $atomic_key => $atomic_type) { if (get_class($atomic_type) === TNamedObject::class && $atomic_type->value === $fq_enum_name @@ -958,6 +959,7 @@ private static function handleLiteralEquality( $can_be_equal = true; } } + $existing_var_type = $existing_var_type->freeze(); if ($var_id && $code_location diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index e89269c3e0d..2670ff04d87 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -27,6 +27,7 @@ use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TTemplateParam; +use Psalm\Type\Union; use UnexpectedValueException; use function end; @@ -440,15 +441,11 @@ public static function getCallableFromAtomic( ); if ($template_result) { - $replaced_callable = clone $callable; - - TemplateInferredTypeReplacer::replace( - new Type\Union([$replaced_callable]), + $callable = TemplateInferredTypeReplacer::replace( + new Union([clone $callable]), $template_result, $codebase - ); - - $callable = $replaced_callable; + )->getSingleAtomic(); } return $callable; diff --git a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php index 2d0a6531d8b..9ceabd5bd07 100644 --- a/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/UnionTypeComparator.php @@ -174,14 +174,15 @@ public static function isContainedBy( && $atomic_comparison_result->replacement_atomic_type ) { if (!$union_comparison_result->replacement_union_type) { - $union_comparison_result->replacement_union_type = clone $input_type; + $union_comparison_result->replacement_union_type = $input_type; } - $union_comparison_result->replacement_union_type->removeType($input_type->getKey()); - - $union_comparison_result->replacement_union_type->addType( + $replacement = $union_comparison_result->replacement_union_type->getBuilder(); + $replacement->removeType($input_type->getKey()); + $replacement->addType( $atomic_comparison_result->replacement_atomic_type ); + $union_comparison_result->replacement_union_type = $replacement->freeze(); } } @@ -321,10 +322,10 @@ public static function isContainedByInPhp( return false; } - $input_type_not_null = clone $input_type; + $input_type_not_null = $input_type->getBuilder(); $input_type_not_null->removeType('null'); - $container_type_not_null = clone $container_type; + $container_type_not_null = $container_type->getBuilder(); $container_type_not_null->removeType('null'); if ($input_type_not_null->getId() === $container_type_not_null->getId()) { diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index efdf02d80c6..dfbf026a917 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -91,18 +91,20 @@ public static function reconcile( } $existing_var_atomic_types = $existing_var_type->getAtomicTypes(); + $existing_var_type = $existing_var_type->getBuilder(); if ($assertion_type instanceof TFalse && isset($existing_var_atomic_types['bool'])) { $existing_var_type->removeType('bool'); $existing_var_type->addType(new TTrue); } elseif ($assertion_type instanceof TTrue && isset($existing_var_atomic_types['bool'])) { + $existing_var_type = $existing_var_type->getBuilder(); $existing_var_type->removeType('bool'); $existing_var_type->addType(new TFalse); } else { $simple_negated_type = SimpleNegatedAssertionReconciler::reconcile( $statements_analyzer->getCodebase(), $assertion, - $existing_var_type, + $existing_var_type->freeze(), $key, $negated, $code_location, @@ -142,7 +144,7 @@ public static function reconcile( $existing_var_type->from_calculation = false; - return $existing_var_type; + return $existing_var_type->freeze(); } if (!$is_equality @@ -158,7 +160,7 @@ public static function reconcile( $existing_var_type->addType(new TNamedObject('DateTime')); } - return $existing_var_type; + return $existing_var_type->freeze(); } if (!$is_equality && $assertion_type instanceof TNamedObject) { @@ -251,6 +253,8 @@ public static function reconcile( } } + $existing_var_type = $existing_var_type->freeze(); + if ($assertion instanceof IsNotIdentical && ($key !== '$this' || !($statements_analyzer->getSource()->getSource() instanceof TraitAnalyzer)) @@ -322,6 +326,7 @@ private static function handleLiteralNegatedEquality( ?CodeLocation $code_location, array $suppressed_issues ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $existing_var_atomic_types = $existing_var_type->getAtomicTypes(); $did_remove_type = false; @@ -443,6 +448,8 @@ private static function handleLiteralNegatedEquality( } } + $existing_var_type = $existing_var_type->freeze(); + if ($key && $code_location) { if ($did_match_literal_type && (!$did_remove_type || count($existing_var_atomic_types) === 1) diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 62a0473fdb8..0ca73f5c665 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -504,6 +504,7 @@ private static function reconcileIsset( bool $is_equality, bool $inside_loop ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); // if key references an array offset @@ -554,7 +555,7 @@ private static function reconcileIsset( $existing_var_type->possibly_undefined_from_try = false; $existing_var_type->ignore_isset = false; - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -570,6 +571,7 @@ private static function reconcileNonEmptyCountable( bool $is_equality ): Union { $old_var_type_string = $existing_var_type->getId(); + $existing_var_type = $existing_var_type->getBuilder(); if ($existing_var_type->hasType('array')) { $array_atomic_type = $existing_var_type->getAtomicTypes()['array']; @@ -665,7 +667,7 @@ private static function reconcileNonEmptyCountable( } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -675,6 +677,7 @@ private static function reconcileExactlyCountable( Union $existing_var_type, int $count ): Union { + $existing_var_type = $existing_var_type->getBuilder(); if ($existing_var_type->hasType('array')) { $array_atomic_type = $existing_var_type->getAtomicTypes()['array']; @@ -701,7 +704,7 @@ private static function reconcileExactlyCountable( } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1177,6 +1180,7 @@ private static function reconcileNumeric( if ($existing_var_type->hasMixed()) { return Type::getNumeric(); } + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); @@ -1631,6 +1635,7 @@ private static function reconcileIsGreaterThan( ?CodeLocation $code_location, array $suppressed_issues ): Union { + $existing_var_type = $existing_var_type->getBuilder(); //we add 1 from the assertion value because we're on a strict operator $assertion_value = $assertion->value + 1; @@ -1720,7 +1725,7 @@ private static function reconcileIsGreaterThan( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1738,6 +1743,7 @@ private static function reconcileIsLessThan( ): Union { //we remove 1 from the assertion value because we're on a strict operator $assertion_value = $assertion->value - 1; + $existing_var_type = $existing_var_type->getBuilder(); $did_remove_type = false; @@ -1822,7 +1828,7 @@ private static function reconcileIsLessThan( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -2354,6 +2360,7 @@ private static function reconcileTruthyOrNonEmpty( int &$failed_reconciliation, bool $recursive_check ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); //empty is used a lot to check for array offset existence, so we have to silent errors a lot @@ -2412,7 +2419,7 @@ private static function reconcileTruthyOrNonEmpty( $failed_reconciliation = 1; - return $existing_var_type; + return $existing_var_type->freeze(); } $existing_var_type->possibly_undefined = false; @@ -2501,7 +2508,7 @@ private static function reconcileTruthyOrNonEmpty( } if ($existing_var_type->isSingle()) { - return $existing_var_type; + return $existing_var_type->freeze(); } } @@ -2532,7 +2539,7 @@ private static function reconcileTruthyOrNonEmpty( } assert(!$existing_var_type->isUnionEmpty()); - return $existing_var_type; + return $existing_var_type->freeze(); } /** diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index ff1b3dcdfad..e08262cea9b 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -413,6 +413,7 @@ public static function reconcile( private static function reconcileCallable( Union $existing_var_type ): Union { + $existing_var_type = $existing_var_type->getBuilder(); foreach ($existing_var_type->getAtomicTypes() as $atomic_key => $type) { if ($type instanceof TLiteralString && InternalCallMapHandler::inCallMap($type->value) @@ -425,7 +426,7 @@ private static function reconcileCallable( } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -513,6 +514,7 @@ private static function reconcileNotNonEmptyCountable( bool $is_equality, ?int $min_count ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); $existing_var_atomic_types = $existing_var_type->getAtomicTypes(); @@ -570,7 +572,7 @@ private static function reconcileNotNonEmptyCountable( } } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -587,6 +589,7 @@ private static function reconcileNull( int &$failed_reconciliation, bool $is_equality ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); $did_remove_type = false; @@ -633,7 +636,7 @@ private static function reconcileNull( } if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type; + return $existing_var_type->freeze(); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -660,6 +663,7 @@ private static function reconcileFalse( $old_var_type_string = $existing_var_type->getId(); $did_remove_type = $existing_var_type->hasScalar(); + $existing_var_type = $existing_var_type->getBuilder(); if ($existing_var_type->hasType('false')) { $did_remove_type = true; $existing_var_type->removeType('false'); @@ -703,7 +707,7 @@ private static function reconcileFalse( } if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type; + return $existing_var_type->freeze(); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -728,6 +732,7 @@ private static function reconcileFalsyOrEmpty( int &$failed_reconciliation, bool $recursive_check ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $old_var_type_string = $existing_var_type->getId(); $did_remove_type = $existing_var_type->possibly_undefined @@ -786,7 +791,7 @@ private static function reconcileFalsyOrEmpty( $failed_reconciliation = 1; - return $existing_var_type; + return $existing_var_type->freeze(); } if ($existing_var_type->hasType('bool')) { @@ -894,7 +899,7 @@ private static function reconcileFalsyOrEmpty( } assert(!$existing_var_type->isUnionEmpty()); - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1621,7 +1626,9 @@ private static function reconcileResource( if ($existing_var_type->hasType('resource')) { $did_remove_type = true; + $existing_var_type = $existing_var_type->getBuilder(); $existing_var_type->removeType('resource'); + $existing_var_type = $existing_var_type->freeze(); } foreach ($existing_var_type->getAtomicTypes() as $type) { @@ -1685,6 +1692,7 @@ private static function reconcileIsLessThanOrEqualTo( ?CodeLocation $code_location, array $suppressed_issues ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $assertion_value = $assertion->value; $did_remove_type = false; @@ -1773,7 +1781,7 @@ private static function reconcileIsLessThanOrEqualTo( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } /** @@ -1789,6 +1797,7 @@ private static function reconcileIsGreaterThanOrEqualTo( ?CodeLocation $code_location, array $suppressed_issues ): Union { + $existing_var_type = $existing_var_type->getBuilder(); $assertion_value = $assertion->value; $did_remove_type = false; @@ -1874,6 +1883,6 @@ private static function reconcileIsGreaterThanOrEqualTo( $existing_var_type->addType(new TNever()); } - return $existing_var_type; + return $existing_var_type->freeze(); } } diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index 6d4d753157b..0b0664f989b 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -47,7 +47,7 @@ public static function replace( Union $union, TemplateResult $template_result, ?Codebase $codebase - ): void { + ): Union { $keys_to_unset = []; $new_types = []; @@ -56,6 +56,7 @@ public static function replace( $inferred_lower_bounds = $template_result->lower_bounds ?: []; + $union = $union->getBuilder(); foreach ($union->getAtomicTypes() as $key => $atomic_type) { $atomic_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); @@ -214,14 +215,12 @@ public static function replace( throw new UnexpectedValueException('This array should be full'); } - $union->replaceTypes( + return $union->replaceTypes( TypeCombiner::combine( $new_types, $codebase )->getAtomicTypes() - ); - - return; + )->freeze(); } foreach ($keys_to_unset as $key) { @@ -230,12 +229,12 @@ public static function replace( $atomic_types = array_values(array_merge($union->getAtomicTypes(), $new_types)); - $union->replaceTypes( + return $union->replaceTypes( TypeCombiner::combine( $atomic_types, $codebase )->getAtomicTypes() - ); + )->freeze(); } /** @@ -261,9 +260,9 @@ private static function replaceTemplateParam( $template_type = $traversed_type; if (!$atomic_type->as->isMixed() && $template_type->isMixed()) { - $template_type = clone $atomic_type->as; + $template_type = $atomic_type->as->getBuilder(); } else { - $template_type = clone $template_type; + $template_type = $template_type->getBuilder(); } if ($atomic_type->extra_types) { @@ -289,6 +288,7 @@ private static function replaceTemplateParam( } } } + $template_type = $template_type->freeze(); } elseif ($codebase) { foreach ($inferred_lower_bounds as $template_type_map) { foreach ($template_type_map as $template_class => $_) { @@ -410,7 +410,7 @@ private static function replaceConditional( $atomic_type = clone $atomic_type; if ($template_type) { - self::replace( + $atomic_type->as_type = self::replace( $atomic_type->as_type, $template_result, $codebase @@ -478,7 +478,7 @@ private static function replaceConditional( ) ]; - self::replace( + $if_template_type = self::replace( $if_template_type, $refined_template_result, $codebase @@ -508,7 +508,7 @@ private static function replaceConditional( ) ]; - self::replace( + $else_template_type = self::replace( $else_template_type, $refined_template_result, $codebase @@ -517,13 +517,13 @@ private static function replaceConditional( } if (!$if_template_type && !$else_template_type) { - self::replace( + $atomic_type->if_type = self::replace( $atomic_type->if_type, $template_result, $codebase ); - self::replace( + $atomic_type->else_type = self::replace( $atomic_type->else_type, $template_result, $codebase diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 5b3341d5570..32c3afaa714 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -84,7 +84,7 @@ public static function replace( // when they're also in the union type, so those shared atomic // types will never be inferred as part of the generic type if ($input_type && !$input_type->isSingle()) { - $new_input_type = clone $input_type; + $new_input_type = $input_type->getBuilder(); foreach ($original_atomic_types as $key => $_) { if ($new_input_type->hasType($key)) { @@ -93,7 +93,7 @@ public static function replace( } if (!$new_input_type->isUnionEmpty()) { - $input_type = $new_input_type; + $input_type = $new_input_type->freeze(); } else { return $union_type; } @@ -766,7 +766,7 @@ private static function handleTemplateParamStandin( ) ) ) { - $generic_param = clone $input_type; + $generic_param = $input_type->getBuilder(); if ($matching_input_keys) { $generic_param_keys = array_keys($generic_param->getAtomicTypes()); @@ -777,6 +777,7 @@ private static function handleTemplateParamStandin( } } } + $generic_param = $generic_param->freeze(); if ($add_lower_bound) { return array_values($generic_param->getAtomicTypes()); @@ -858,7 +859,7 @@ private static function handleTemplateParamStandin( $matching_input_keys ) ) { - $generic_param = clone $input_type; + $generic_param = $input_type->getBuilder(); if ($matching_input_keys) { $generic_param_keys = array_keys($generic_param->getAtomicTypes()); @@ -869,6 +870,7 @@ private static function handleTemplateParamStandin( } } } + $generic_param = $generic_param->freeze(); $upper_bound = $template_result->upper_bounds [$param_name_key] @@ -1258,7 +1260,7 @@ public static function getMappedGenericTypeParams( $new_input_param = clone $new_input_param; - TemplateInferredTypeReplacer::replace( + $new_input_param = TemplateInferredTypeReplacer::replace( $new_input_param, new TemplateResult([], $replacement_templates), $codebase diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 71a2ddd1e58..a77c77093d5 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -255,7 +255,7 @@ public static function getTypeFromTree( ); if ($non_nullable_type instanceof Union) { - $non_nullable_type->addType(new TNull); + $non_nullable_type = $non_nullable_type->getBuilder()->addType(new TNull)->freeze(); return $non_nullable_type; } @@ -396,7 +396,7 @@ public static function getTypeFromTree( private static function getGenericParamClass( string $param_name, - Union $as, + Union &$as, string $defining_class ): TTemplateParamClass { if ($as->hasMixed()) { @@ -430,7 +430,7 @@ private static function getGenericParamClass( $t->type_params ); - $as->substitute(new Union([$t]), new Union([$traversable])); + $as = $as->getBuilder()->substitute(new Union([$t]), new Union([$traversable]))->freeze(); return new TTemplateParamClass( $param_name, diff --git a/src/Psalm/Storage/Possibilities.php b/src/Psalm/Storage/Possibilities.php index c526881800f..eb885c9e652 100644 --- a/src/Psalm/Storage/Possibilities.php +++ b/src/Psalm/Storage/Possibilities.php @@ -45,7 +45,7 @@ public function getUntemplatedCopy( if ($assertion_type) { $union = new Union([clone $assertion_type]); - TemplateInferredTypeReplacer::replace( + $union = TemplateInferredTypeReplacer::replace( $union, $template_result, $codebase diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 8508caa2c15..42db8658e2d 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -45,6 +45,7 @@ use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTrue; use Psalm\Type\Atomic\TVoid; +use Psalm\Type\MutableUnion; use Psalm\Type\Union; use UnexpectedValueException; @@ -601,13 +602,16 @@ public static function intersectUnionTypes( if (null !== $intersection_atomic) { if (null === $combined_type) { - $combined_type = new Union([$intersection_atomic]); + $combined_type = new MutableUnion([$intersection_atomic]); } else { $combined_type->addType($intersection_atomic); } } } } + if ($combined_type) { + $combined_type = $combined_type->freeze(); + } } //if a type is contained by the other, the intersection is the narrowest type diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 126ac673269..f76cd62e037 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -549,7 +549,7 @@ public function replaceClassLike(string $old, string $new): void } if ($this instanceof TTemplateParam) { - $this->as->replaceClassLike($old, $new); + $this->as = $this->as->getBuilder()->replaceClassLike($old, $new)->freeze(); } if ($this instanceof TLiteralClassString) { @@ -562,14 +562,14 @@ public function replaceClassLike(string $old, string $new): void || $this instanceof TGenericObject || $this instanceof TIterable ) { - foreach ($this->type_params as $type_param) { - $type_param->replaceClassLike($old, $new); + foreach ($this->type_params as &$type_param) { + $type_param = $type_param->getBuilder()->replaceClassLike($old, $new)->freeze(); } } if ($this instanceof TKeyedArray) { - foreach ($this->properties as $property_type) { - $property_type->replaceClassLike($old, $new); + foreach ($this->properties as &$property_type) { + $property_type = $property_type->getBuilder()->replaceClassLike($old, $new)->freeze(); } } @@ -579,13 +579,13 @@ public function replaceClassLike(string $old, string $new): void if ($this->params) { foreach ($this->params as $param) { if ($param->type) { - $param->type->replaceClassLike($old, $new); + $param->type = $param->type->getBuilder()->replaceClassLike($old, $new)->freeze(); } } } if ($this->return_type) { - $this->return_type->replaceClassLike($old, $new); + $this->return_type = $this->return_type->getBuilder()->replaceClassLike($old, $new)->freeze(); } } } diff --git a/src/Psalm/Type/Atomic/CallableTrait.php b/src/Psalm/Type/Atomic/CallableTrait.php index 8e8e1e633e8..072c21957ec 100644 --- a/src/Psalm/Type/Atomic/CallableTrait.php +++ b/src/Psalm/Type/Atomic/CallableTrait.php @@ -262,7 +262,7 @@ public function replaceTemplateTypesWithArgTypes( continue; } - TemplateInferredTypeReplacer::replace( + $param->type = TemplateInferredTypeReplacer::replace( $param->type, $template_result, $codebase @@ -271,7 +271,7 @@ public function replaceTemplateTypesWithArgTypes( } if ($this->return_type) { - TemplateInferredTypeReplacer::replace( + $this->return_type = TemplateInferredTypeReplacer::replace( $this->return_type, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/GenericTrait.php b/src/Psalm/Type/Atomic/GenericTrait.php index ba4df1e9873..78578107a27 100644 --- a/src/Psalm/Type/Atomic/GenericTrait.php +++ b/src/Psalm/Type/Atomic/GenericTrait.php @@ -234,8 +234,8 @@ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase ): void { - foreach ($this->type_params as $offset => $type_param) { - TemplateInferredTypeReplacer::replace( + foreach ($this->type_params as $offset => &$type_param) { + $type_param = TemplateInferredTypeReplacer::replace( $type_param, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index aadcad0b7ce..79dea6b4996 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -182,7 +182,7 @@ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase ): void { - TemplateInferredTypeReplacer::replace( + $this->value_param = TemplateInferredTypeReplacer::replace( $this->value_param, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/TConditional.php b/src/Psalm/Type/Atomic/TConditional.php index 070f7ac2d71..9b23b966450 100644 --- a/src/Psalm/Type/Atomic/TConditional.php +++ b/src/Psalm/Type/Atomic/TConditional.php @@ -128,7 +128,7 @@ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase ): void { - TemplateInferredTypeReplacer::replace( + $this->conditional_type = TemplateInferredTypeReplacer::replace( $this->conditional_type, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index b92926ffb1b..45bb4eac01f 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -322,8 +322,8 @@ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase ): void { - foreach ($this->properties as $property) { - TemplateInferredTypeReplacer::replace( + foreach ($this->properties as &$property) { + $property = TemplateInferredTypeReplacer::replace( $property, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/TList.php b/src/Psalm/Type/Atomic/TList.php index 44d6578ee41..4b33028560b 100644 --- a/src/Psalm/Type/Atomic/TList.php +++ b/src/Psalm/Type/Atomic/TList.php @@ -165,7 +165,7 @@ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase ): void { - TemplateInferredTypeReplacer::replace( + $this->type_param = TemplateInferredTypeReplacer::replace( $this->type_param, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index 81d3fc76f66..0338bd8fc69 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -216,8 +216,8 @@ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase ): void { - foreach ($this->properties as $property) { - TemplateInferredTypeReplacer::replace( + foreach ($this->properties as &$property) { + $property = TemplateInferredTypeReplacer::replace( $property, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/TTemplateKeyOf.php b/src/Psalm/Type/Atomic/TTemplateKeyOf.php index bb2f5f947e0..754063833a7 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -85,7 +85,7 @@ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase ): void { - TemplateInferredTypeReplacer::replace( + $this->as = TemplateInferredTypeReplacer::replace( $this->as, $template_result, $codebase diff --git a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php index 5a1ac50e5b8..5dd0f0d52c1 100644 --- a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php +++ b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php @@ -80,10 +80,10 @@ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase ): void { - TemplateInferredTypeReplacer::replace( + $this->as = TemplateInferredTypeReplacer::replace( new Union([$this->as]), $template_result, $codebase - ); + )->getSingleAtomic(); } } diff --git a/src/Psalm/Type/Atomic/TTemplateValueOf.php b/src/Psalm/Type/Atomic/TTemplateValueOf.php index d00cb597484..23fe4d0711d 100644 --- a/src/Psalm/Type/Atomic/TTemplateValueOf.php +++ b/src/Psalm/Type/Atomic/TTemplateValueOf.php @@ -85,7 +85,7 @@ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase ): void { - TemplateInferredTypeReplacer::replace( + $this->as = TemplateInferredTypeReplacer::replace( $this->as, $template_result, $codebase diff --git a/src/Psalm/Type/MutableUnion.php b/src/Psalm/Type/MutableUnion.php new file mode 100644 index 00000000000..9703ea3b100 --- /dev/null +++ b/src/Psalm/Type/MutableUnion.php @@ -0,0 +1,455 @@ + + */ + private $types; + + /** + * Whether the type originated in a docblock + * + * @var bool + */ + public $from_docblock = false; + + /** + * Whether the type originated from integer calculation + * + * @var bool + */ + public $from_calculation = false; + + /** + * Whether the type originated from a property + * + * This helps turn isset($foo->bar) into a different sort of issue + * + * @var bool + */ + public $from_property = false; + + /** + * Whether the type originated from *static* property + * + * Unlike non-static properties, static properties have no prescribed place + * like __construct() to be initialized in + * + * @var bool + */ + public $from_static_property = false; + + /** + * Whether the property that this type has been derived from has been initialized in a constructor + * + * @var bool + */ + public $initialized = true; + + /** + * Which class the type was initialised in + * + * @var ?string + */ + public $initialized_class; + + /** + * Whether or not the type has been checked yet + * + * @var bool + */ + public $checked = false; + + /** + * @var bool + */ + public $failed_reconciliation = false; + + /** + * Whether or not to ignore issues with possibly-null values + * + * @var bool + */ + public $ignore_nullable_issues = false; + + /** + * Whether or not to ignore issues with possibly-false values + * + * @var bool + */ + public $ignore_falsable_issues = false; + + /** + * Whether or not to ignore issues with isset on this type + * + * @var bool + */ + public $ignore_isset = false; + + /** + * Whether or not this variable is possibly undefined + * + * @var bool + */ + public $possibly_undefined = false; + + /** + * Whether or not this variable is possibly undefined + * + * @var bool + */ + public $possibly_undefined_from_try = false; + + /** + * Whether or not this union had a template, since replaced + * + * @var bool + */ + public $had_template = false; + + /** + * Whether or not this union comes from a template "as" default + * + * @var bool + */ + public $from_template_default = false; + + /** + * @var array + */ + private $literal_string_types = []; + + /** + * @var array + */ + private $typed_class_strings = []; + + /** + * @var array + */ + private $literal_int_types = []; + + /** + * @var array + */ + private $literal_float_types = []; + + /** + * True if the type was passed or returned by reference, or if the type refers to an object's + * property or an item in an array. Note that this is not true for locally created references + * that don't refer to properties or array items (see Context::$references_in_scope). + * + * @var bool + */ + public $by_ref = false; + + /** + * @var bool + */ + public $reference_free = false; + + /** + * @var bool + */ + public $allow_mutations = true; + + /** + * @var bool + */ + public $has_mutations = true; + + /** + * This is a cache of getId on non-exact mode + * @var null|string + */ + private $id; + + /** + * This is a cache of getId on exact mode + * @var null|string + */ + private $exact_id; + + + /** + * @var array + */ + public $parent_nodes = []; + + /** + * @var bool + */ + public $different = false; + + /** + * @param non-empty-array $types + */ + public function replaceTypes(array $types): self + { + $this->types = $types; + return $this; + } + + public function addType(Atomic $type): self + { + $this->types[$type->getKey()] = $type; + + if ($type instanceof TLiteralString) { + $this->literal_string_types[$type->getKey()] = $type; + } elseif ($type instanceof TLiteralInt) { + $this->literal_int_types[$type->getKey()] = $type; + } elseif ($type instanceof TLiteralFloat) { + $this->literal_float_types[$type->getKey()] = $type; + } elseif ($type instanceof TString && $this->literal_string_types) { + foreach ($this->literal_string_types as $key => $_) { + unset($this->literal_string_types[$key], $this->types[$key]); + } + if (!$type instanceof TClassString + || (!$type->as_type && !$type instanceof TTemplateParamClass) + ) { + foreach ($this->typed_class_strings as $key => $_) { + unset($this->typed_class_strings[$key], $this->types[$key]); + } + } + } elseif ($type instanceof TInt && $this->literal_int_types) { + //we remove any literal that is already included in a wider type + $int_type_in_range = TIntRange::convertToIntRange($type); + foreach ($this->literal_int_types as $key => $literal_int_type) { + if ($int_type_in_range->contains($literal_int_type->value)) { + unset($this->literal_int_types[$key], $this->types[$key]); + } + } + } elseif ($type instanceof TFloat && $this->literal_float_types) { + foreach ($this->literal_float_types as $key => $_) { + unset($this->literal_float_types[$key], $this->types[$key]); + } + } + + $this->bustCache(); + + return $this; + } + + public function removeType(string $type_string): bool + { + if (isset($this->types[$type_string])) { + unset($this->types[$type_string]); + + if (strpos($type_string, '(')) { + unset( + $this->literal_string_types[$type_string], + $this->literal_int_types[$type_string], + $this->literal_float_types[$type_string] + ); + } + + $this->bustCache(); + + return true; + } + + if ($type_string === 'string') { + if ($this->literal_string_types) { + foreach ($this->literal_string_types as $literal_key => $_) { + unset($this->types[$literal_key]); + } + $this->literal_string_types = []; + } + + if ($this->typed_class_strings) { + foreach ($this->typed_class_strings as $typed_class_key => $_) { + unset($this->types[$typed_class_key]); + } + $this->typed_class_strings = []; + } + + unset($this->types['class-string'], $this->types['trait-string']); + } elseif ($type_string === 'int' && $this->literal_int_types) { + foreach ($this->literal_int_types as $literal_key => $_) { + unset($this->types[$literal_key]); + } + $this->literal_int_types = []; + } elseif ($type_string === 'float' && $this->literal_float_types) { + foreach ($this->literal_float_types as $literal_key => $_) { + unset($this->types[$literal_key]); + } + $this->literal_float_types = []; + } + + return false; + } + + public function bustCache(): void + { + $this->id = null; + $this->exact_id = null; + } + + /** + * @param Union|MutableUnion $old_type + * @param Union|MutableUnion|null $new_type + */ + public function substitute($old_type, $new_type = null): self + { + if ($this->hasMixed() && !$this->isEmptyMixed()) { + return $this; + } + $old_type = $old_type->getBuilder(); + if ($new_type) { + $new_type = $new_type->getBuilder(); + } + + if ($new_type && $new_type->ignore_nullable_issues) { + $this->ignore_nullable_issues = true; + } + + if ($new_type && $new_type->ignore_falsable_issues) { + $this->ignore_falsable_issues = true; + } + + foreach ($old_type->types as $old_type_part) { + $had = isset($this->types[$old_type_part->getKey()]); + $this->removeType($old_type_part->getKey()); + if (!$had) { + if ($old_type_part instanceof TFalse + && isset($this->types['bool']) + && !isset($this->types['true']) + ) { + $this->removeType('bool'); + $this->types['true'] = new TTrue; + } elseif ($old_type_part instanceof TTrue + && isset($this->types['bool']) + && !isset($this->types['false']) + ) { + $this->removeType('bool'); + $this->types['false'] = new TFalse; + } elseif (isset($this->types['iterable'])) { + if ($old_type_part instanceof TNamedObject + && $old_type_part->value === 'Traversable' + && !isset($this->types['array']) + ) { + $this->removeType('iterable'); + $this->types['array'] = new TArray([Type::getArrayKey(), Type::getMixed()]); + } + + if ($old_type_part instanceof TArray + && !isset($this->types['traversable']) + ) { + $this->removeType('iterable'); + $this->types['traversable'] = new TNamedObject('Traversable'); + } + } elseif (isset($this->types['array-key'])) { + if ($old_type_part instanceof TString + && !isset($this->types['int']) + ) { + $this->removeType('array-key'); + $this->types['int'] = new TInt(); + } + + if ($old_type_part instanceof TInt + && !isset($this->types['string']) + ) { + $this->removeType('array-key'); + $this->types['string'] = new TString(); + } + } + } + } + + if ($new_type) { + foreach ($new_type->types as $key => $new_type_part) { + if (!isset($this->types[$key]) + || ($new_type_part instanceof Scalar + && get_class($new_type_part) === get_class($this->types[$key])) + ) { + $this->types[$key] = $new_type_part; + } else { + $this->types[$key] = TypeCombiner::combine([$new_type_part, $this->types[$key]])->getSingleAtomic(); + } + } + } elseif (count($this->types) === 0) { + $this->types['mixed'] = new TMixed(); + } + + $this->bustCache(); + + return $this; + } + + + public function replaceClassLike(string $old, string $new): self + { + foreach ($this->types as $key => $atomic_type) { + $atomic_type->replaceClassLike($old, $new); + + $this->removeType($key); + $this->addType($atomic_type); + } + return $this; + } + + public function getBuilder(): self + { + return $this; + } + + public function freeze(): Union + { + $union = new Union($this->getAtomicTypes()); + foreach (get_object_vars($this) as $key => $value) { + if ($key === 'types') { + continue; + } + if ($key === 'id') { + continue; + } + if ($key === 'exact_id') { + continue; + } + if ($key === 'literal_string_types') { + continue; + } + if ($key === 'typed_class_strings') { + continue; + } + if ($key === 'literal_int_types') { + continue; + } + if ($key === 'literal_float_types') { + continue; + } + $union->{$key} = $value; + } + return $union; + } +} diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 19b706bcb62..7fd5fb055d2 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -283,7 +283,7 @@ public static function reconcileKeyedTypes( ); if ($result_type_candidate->isUnionEmpty()) { - $result_type_candidate->addType(new TNever); + $result_type_candidate = $result_type_candidate->getBuilder()->addType(new TNever)->freeze(); } $orred_type = Type::combineUnionTypes( @@ -715,7 +715,9 @@ private static function getValueForKey( if (($has_isset || $has_inverted_isset) && isset($new_assertions[$new_base_key])) { if ($has_inverted_isset && $new_base_key === $key) { + $new_base_type_candidate = $new_base_type_candidate->getBuilder(); $new_base_type_candidate->addType(new TNull); + $new_base_type_candidate = $new_base_type_candidate->freeze(); } $new_base_type_candidate->possibly_undefined = true; @@ -729,7 +731,10 @@ private static function getValueForKey( if (($has_isset || $has_inverted_isset) && isset($new_assertions[$new_base_key])) { if ($has_inverted_isset && $new_base_key === $key) { - $new_base_type_candidate->addType(new TNull); + $new_base_type_candidate = $new_base_type_candidate + ->getBuilder() + ->addType(new TNull) + ->freeze(); } $new_base_type_candidate->possibly_undefined = true; @@ -963,11 +968,11 @@ private static function getPropertyType( } /** + * @param Union|MutableUnion $existing_var_type * @param string[] $suppressed_issues - * */ protected static function triggerIssueForImpossible( - Union $existing_var_type, + $existing_var_type, string $old_var_type_string, string $key, Assertion $assertion, @@ -1161,7 +1166,7 @@ private static function adjustTKeyedArrayType( $base_atomic_type->properties[$array_key_offset] = clone $result_type; } - $new_base_type->addType($base_atomic_type); + $new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze(); $changed_var_ids[$base_key . '[' . $array_key . ']'] = true; @@ -1181,8 +1186,9 @@ private static function adjustTKeyedArrayType( } } - protected static function refineArrayKey(Union $key_type): void + protected static function refineArrayKey(Union &$key_type): void { + $key_type = $key_type->getBuilder(); foreach ($key_type->getAtomicTypes() as $key => $cat) { if ($cat instanceof TTemplateParam) { self::refineArrayKey($cat->as); @@ -1200,5 +1206,6 @@ protected static function refineArrayKey(Union $key_type): void // this should ideally prompt some sort of error $key_type->addType(new TArrayKey()); } + $key_type = $key_type->freeze(); } } diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 9d004d386ce..146ce582a20 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -2,60 +2,21 @@ namespace Psalm\Type; -use InvalidArgumentException; -use Psalm\CodeLocation; -use Psalm\Codebase; use Psalm\Internal\DataFlow\DataFlowNode; -use Psalm\Internal\Type\TypeCombiner; -use Psalm\Internal\TypeVisitor\ContainsClassLikeVisitor; -use Psalm\Internal\TypeVisitor\ContainsLiteralVisitor; -use Psalm\Internal\TypeVisitor\FromDocblockSetter; -use Psalm\Internal\TypeVisitor\TemplateTypeCollector; -use Psalm\Internal\TypeVisitor\TypeChecker; -use Psalm\Internal\TypeVisitor\TypeScanner; -use Psalm\StatementsSource; -use Psalm\Storage\FileStorage; -use Psalm\Type; -use Psalm\Type\Atomic\Scalar; -use Psalm\Type\Atomic\TArray; -use Psalm\Type\Atomic\TCallable; use Psalm\Type\Atomic\TClassString; -use Psalm\Type\Atomic\TClassStringMap; -use Psalm\Type\Atomic\TClosure; -use Psalm\Type\Atomic\TConditional; -use Psalm\Type\Atomic\TEmptyMixed; -use Psalm\Type\Atomic\TFalse; -use Psalm\Type\Atomic\TFloat; -use Psalm\Type\Atomic\TInt; -use Psalm\Type\Atomic\TIntRange; -use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; -use Psalm\Type\Atomic\TLowercaseString; -use Psalm\Type\Atomic\TMixed; -use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\Atomic\TNonEmptyLowercaseString; -use Psalm\Type\Atomic\TNonspecificLiteralInt; -use Psalm\Type\Atomic\TNonspecificLiteralString; -use Psalm\Type\Atomic\TString; -use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\Atomic\TTemplateParamClass; -use Psalm\Type\Atomic\TTrue; +use Stringable; -use function array_filter; -use function array_unique; -use function count; -use function get_class; -use function implode; -use function ksort; -use function reset; -use function sort; -use function strpos; +use function get_object_vars; -final class Union implements TypeNode +final class Union implements TypeNode, Stringable { + use UnionTrait; + /** + * @psalm-readonly * @var non-empty-array */ private $types; @@ -235,1424 +196,37 @@ final class Union implements TypeNode */ public $different = false; - /** - * Constructs an Union instance - * - * @param non-empty-array $types - */ - public function __construct(array $types) - { - $from_docblock = false; - - $keyed_types = []; - - foreach ($types as $type) { - $key = $type->getKey(); - $keyed_types[$key] = $type; - - if ($type instanceof TLiteralInt) { - $this->literal_int_types[$key] = $type; - } elseif ($type instanceof TLiteralString) { - $this->literal_string_types[$key] = $type; - } elseif ($type instanceof TLiteralFloat) { - $this->literal_float_types[$key] = $type; - } elseif ($type instanceof TClassString - && ($type->as_type || $type instanceof TTemplateParamClass) - ) { - $this->typed_class_strings[$key] = $type; - } - - $from_docblock = $from_docblock || $type->from_docblock; - } - - $this->types = $keyed_types; - - $this->from_docblock = $from_docblock; - } - - /** - * @param non-empty-array $types - */ - public function replaceTypes(array $types): void - { - $this->types = $types; - } - - /** - * @psalm-mutation-free - * @return non-empty-array - */ - public function getAtomicTypes(): array - { - return $this->types; - } - - public function addType(Atomic $type): void - { - $this->types[$type->getKey()] = $type; - - if ($type instanceof TLiteralString) { - $this->literal_string_types[$type->getKey()] = $type; - } elseif ($type instanceof TLiteralInt) { - $this->literal_int_types[$type->getKey()] = $type; - } elseif ($type instanceof TLiteralFloat) { - $this->literal_float_types[$type->getKey()] = $type; - } elseif ($type instanceof TString && $this->literal_string_types) { - foreach ($this->literal_string_types as $key => $_) { - unset($this->literal_string_types[$key], $this->types[$key]); - } - if (!$type instanceof TClassString - || (!$type->as_type && !$type instanceof TTemplateParamClass) - ) { - foreach ($this->typed_class_strings as $key => $_) { - unset($this->typed_class_strings[$key], $this->types[$key]); - } - } - } elseif ($type instanceof TInt && $this->literal_int_types) { - //we remove any literal that is already included in a wider type - $int_type_in_range = TIntRange::convertToIntRange($type); - foreach ($this->literal_int_types as $key => $literal_int_type) { - if ($int_type_in_range->contains($literal_int_type->value)) { - unset($this->literal_int_types[$key], $this->types[$key]); - } - } - } elseif ($type instanceof TFloat && $this->literal_float_types) { - foreach ($this->literal_float_types as $key => $_) { - unset($this->literal_float_types[$key], $this->types[$key]); - } - } - - $this->bustCache(); - } - - public function __clone() - { - $this->literal_string_types = []; - $this->literal_int_types = []; - $this->literal_float_types = []; - $this->typed_class_strings = []; - - foreach ($this->types as $key => &$type) { - $type = clone $type; - - if ($type instanceof TLiteralInt) { - $this->literal_int_types[$key] = $type; - } elseif ($type instanceof TLiteralString) { - $this->literal_string_types[$key] = $type; - } elseif ($type instanceof TLiteralFloat) { - $this->literal_float_types[$key] = $type; - } elseif ($type instanceof TClassString - && ($type->as_type || $type instanceof TTemplateParamClass) - ) { - $this->typed_class_strings[$key] = $type; - } - } - } - - public function __toString(): string - { - $types = []; - - $printed_int = false; - $printed_float = false; - $printed_string = false; - - foreach ($this->types as $type) { - if ($type instanceof TLiteralFloat) { - if ($printed_float) { - continue; - } - - $printed_float = true; - } elseif ($type instanceof TLiteralString) { - if ($printed_string) { - continue; - } - - $printed_string = true; - } elseif ($type instanceof TLiteralInt) { - if ($printed_int) { - continue; - } - - $printed_int = true; - } - - $types[] = $type->getId(false); - } - - sort($types); - return implode('|', $types); - } - - public function getKey(): string - { - $types = []; - - $printed_int = false; - $printed_float = false; - $printed_string = false; - - foreach ($this->types as $type) { - if ($type instanceof TLiteralFloat) { - if ($printed_float) { - continue; - } - - $types[] = 'float'; - $printed_float = true; - } elseif ($type instanceof TLiteralString) { - if ($printed_string) { - continue; - } - - $types[] = 'string'; - $printed_string = true; - } elseif ($type instanceof TLiteralInt) { - if ($printed_int) { - continue; - } - - $types[] = 'int'; - $printed_int = true; - } else { - $types[] = $type->getKey(); - } - } - - sort($types); - return implode('|', $types); - } - - public function getId(bool $exact = true): string + public function getBuilder(): MutableUnion { - if ($exact && $this->exact_id) { - return $this->exact_id; - } elseif (!$exact && $this->id) { - return $this->id; - } - $types = []; - foreach ($this->types as $type) { - $types[] = $type->getId($exact); - } - $types = array_unique($types); - sort($types); - - if (count($types) > 1) { - foreach ($types as $i => $type) { - if (strpos($type, ' as ') && strpos($type, '(') === false) { - $types[$i] = '(' . $type . ')'; - } - } - } - - $id = implode('|', $types); - - if ($exact) { - $this->exact_id = $id; - } else { - $this->id = $id; - } - - return $id; - } - - /** - * @param array $aliased_classes - * - */ - public function toNamespacedString( - ?string $namespace, - array $aliased_classes, - ?string $this_class, - bool $use_phpdoc_format - ): string { - $other_types = []; - - $literal_ints = []; - $literal_strings = []; - - $has_non_literal_int = false; - $has_non_literal_string = false; - - foreach ($this->types as $type) { - $type_string = $type->toNamespacedString($namespace, $aliased_classes, $this_class, $use_phpdoc_format); - if ($type instanceof TLiteralInt) { - $literal_ints[] = $type_string; - } elseif ($type instanceof TLiteralString) { - $literal_strings[] = $type_string; - } else { - if (get_class($type) === TString::class) { - $has_non_literal_string = true; - } elseif (get_class($type) === TInt::class) { - $has_non_literal_int = true; - } - $other_types[] = $type_string; - } - } - - if (count($literal_ints) <= 3 && !$has_non_literal_int) { - $other_types = [...$other_types, ...$literal_ints]; - } else { - $other_types[] = 'int'; - } - - if (count($literal_strings) <= 3 && !$has_non_literal_string) { - $other_types = [...$other_types, ...$literal_strings]; - } else { - $other_types[] = 'string'; - } - - sort($other_types); - return implode('|', array_unique($other_types)); - } - - /** - * @param array $aliased_classes - */ - public function toPhpString( - ?string $namespace, - array $aliased_classes, - ?string $this_class, - int $analysis_php_version_id - ): ?string { - if (!$this->isSingleAndMaybeNullable()) { - if ($analysis_php_version_id < 8_00_00) { - return null; - } - } elseif ($analysis_php_version_id < 7_00_00 - || (isset($this->types['null']) && $analysis_php_version_id < 7_01_00) - ) { - return null; - } - - $types = $this->types; - - $nullable = false; - - if (isset($types['null']) && count($types) > 1) { - unset($types['null']); - - $nullable = true; - } - - $falsable = false; - - if (isset($types['false']) && count($types) > 1) { - unset($types['false']); - - $falsable = true; - } - - $php_types = []; - - foreach ($types as $atomic_type) { - $php_type = $atomic_type->toPhpString( - $namespace, - $aliased_classes, - $this_class, - $analysis_php_version_id - ); - - if (!$php_type) { - return null; - } - - $php_types[] = $php_type; - } - - if ($falsable) { - if ($nullable) { - $php_types['null'] = 'null'; - } - $php_types['false'] = 'false'; - ksort($php_types); - return implode('|', array_unique($php_types)); - } - - if ($analysis_php_version_id < 8_00_00) { - return ($nullable ? '?' : '') . implode('|', array_unique($php_types)); - } - if ($nullable) { - $php_types['null'] = 'null'; - } - return implode('|', array_unique($php_types)); - } - - public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool - { - if (!$this->isSingleAndMaybeNullable() && $analysis_php_version_id < 8_00_00) { - return false; - } - - $types = $this->types; - - if (isset($types['null'])) { - if (count($types) > 1) { - unset($types['null']); - } else { - return false; - } - } - - return !array_filter( - $types, - static fn($atomic_type): bool => !$atomic_type->canBeFullyExpressedInPhp($analysis_php_version_id) - ); - } - - public function removeType(string $type_string): bool - { - if (isset($this->types[$type_string])) { - unset($this->types[$type_string]); - - if (strpos($type_string, '(')) { - unset( - $this->literal_string_types[$type_string], - $this->literal_int_types[$type_string], - $this->literal_float_types[$type_string] - ); - } - - $this->bustCache(); - - return true; - } - - if ($type_string === 'string') { - if ($this->literal_string_types) { - foreach ($this->literal_string_types as $literal_key => $_) { - unset($this->types[$literal_key]); - } - $this->literal_string_types = []; - } - - if ($this->typed_class_strings) { - foreach ($this->typed_class_strings as $typed_class_key => $_) { - unset($this->types[$typed_class_key]); - } - $this->typed_class_strings = []; - } - - unset($this->types['class-string'], $this->types['trait-string']); - } elseif ($type_string === 'int' && $this->literal_int_types) { - foreach ($this->literal_int_types as $literal_key => $_) { - unset($this->types[$literal_key]); - } - $this->literal_int_types = []; - } elseif ($type_string === 'float' && $this->literal_float_types) { - foreach ($this->literal_float_types as $literal_key => $_) { - unset($this->types[$literal_key]); - } - $this->literal_float_types = []; + foreach ($this->getAtomicTypes() as $type) { + $types []= clone $type; } - - return false; - } - - public function bustCache(): void - { - $this->id = null; - $this->exact_id = null; - } - - public function hasType(string $type_string): bool - { - return isset($this->types[$type_string]); - } - - public function hasArray(): bool - { - return isset($this->types['array']); - } - - public function hasIterable(): bool - { - return isset($this->types['iterable']); - } - - public function hasList(): bool - { - return isset($this->types['array']) && $this->types['array'] instanceof TList; - } - - public function hasClassStringMap(): bool - { - return isset($this->types['array']) && $this->types['array'] instanceof TClassStringMap; - } - - public function isTemplatedClassString(): bool - { - return $this->isSingle() - && count( - array_filter( - $this->types, - static fn($type): bool => $type instanceof TTemplateParamClass - ) - ) === 1; - } - - public function hasArrayAccessInterface(Codebase $codebase): bool - { - return (bool)array_filter( - $this->types, - static fn($type): bool => $type->hasArrayAccessInterface($codebase) - ); - } - - public function hasCallableType(): bool - { - return $this->getCallableTypes() || $this->getClosureTypes(); - } - - /** - * @return array - */ - public function getCallableTypes(): array - { - return array_filter( - $this->types, - static fn($type): bool => $type instanceof TCallable - ); - } - - /** - * @return array - */ - public function getClosureTypes(): array - { - return array_filter( - $this->types, - static fn($type): bool => $type instanceof TClosure - ); - } - - public function hasObject(): bool - { - return isset($this->types['object']); - } - - public function hasObjectType(): bool - { - foreach ($this->types as $type) { - if ($type->isObjectType()) { - return true; + $union = new MutableUnion($types); + foreach (get_object_vars($this) as $key => $value) { + if ($key === 'types') { + continue; } - } - - return false; - } - - public function isObjectType(): bool - { - foreach ($this->types as $type) { - if (!$type->isObjectType()) { - return false; + if ($key === 'id') { + continue; } - } - - return true; - } - - public function hasNamedObjectType(): bool - { - foreach ($this->types as $type) { - if ($type->isNamedObjectType()) { - return true; + if ($key === 'exact_id') { + continue; } - } - - return false; - } - - public function isStaticObject(): bool - { - foreach ($this->types as $type) { - if (!$type instanceof TNamedObject - || !$type->is_static - ) { - return false; + if ($key === 'literal_string_types') { + continue; } - } - - return true; - } - - public function hasStaticObject(): bool - { - foreach ($this->types as $type) { - if ($type instanceof TNamedObject - && $type->is_static - ) { - return true; + if ($key === 'typed_class_strings') { + continue; } - } - - return false; - } - - public function isNullable(): bool - { - if (isset($this->types['null'])) { - return true; - } - - foreach ($this->types as $type) { - if ($type instanceof TTemplateParam && $type->as->isNullable()) { - return true; + if ($key === 'literal_int_types') { + continue; } - } - - return false; - } - - public function isFalsable(): bool - { - if (isset($this->types['false'])) { - return true; - } - - foreach ($this->types as $type) { - if ($type instanceof TTemplateParam && $type->as->isFalsable()) { - return true; + if ($key === 'literal_float_types') { + continue; } + $union->{$key} = $value; } - - return false; - } - - public function hasBool(): bool - { - return isset($this->types['bool']) || isset($this->types['false']) || isset($this->types['true']); - } - - public function hasString(): bool - { - return isset($this->types['string']) - || isset($this->types['class-string']) - || isset($this->types['trait-string']) - || isset($this->types['numeric-string']) - || isset($this->types['callable-string']) - || isset($this->types['array-key']) - || $this->literal_string_types - || $this->typed_class_strings; - } - - public function hasLowercaseString(): bool - { - return isset($this->types['string']) - && ($this->types['string'] instanceof TLowercaseString - || $this->types['string'] instanceof TNonEmptyLowercaseString); - } - - public function hasLiteralClassString(): bool - { - return count($this->typed_class_strings) > 0; - } - - public function hasInt(): bool - { - return isset($this->types['int']) || isset($this->types['array-key']) || $this->literal_int_types - || array_filter($this->types, static fn(Atomic $type): bool => $type instanceof TIntRange); - } - - public function hasArrayKey(): bool - { - return isset($this->types['array-key']); - } - - public function hasFloat(): bool - { - return isset($this->types['float']) || $this->literal_float_types; - } - - public function hasScalar(): bool - { - return isset($this->types['scalar']); - } - - public function hasNumeric(): bool - { - return isset($this->types['numeric']); - } - - public function hasScalarType(): bool - { - return isset($this->types['int']) - || isset($this->types['float']) - || isset($this->types['string']) - || isset($this->types['class-string']) - || isset($this->types['trait-string']) - || isset($this->types['bool']) - || isset($this->types['false']) - || isset($this->types['true']) - || isset($this->types['numeric']) - || isset($this->types['numeric-string']) - || $this->literal_int_types - || $this->literal_float_types - || $this->literal_string_types - || $this->typed_class_strings; - } - - public function hasTemplate(): bool - { - return (bool) array_filter( - $this->types, - static fn(Atomic $type): bool => $type instanceof TTemplateParam - || ($type instanceof TNamedObject - && $type->extra_types - && array_filter( - $type->extra_types, - static fn($t): bool => $t instanceof TTemplateParam - ) - ) - ); - } - - public function hasConditional(): bool - { - return (bool) array_filter( - $this->types, - static fn(Atomic $type): bool => $type instanceof TConditional - ); - } - - public function hasTemplateOrStatic(): bool - { - return (bool) array_filter( - $this->types, - static fn(Atomic $type): bool => $type instanceof TTemplateParam - || ($type instanceof TNamedObject - && ($type->is_static - || ($type->extra_types - && array_filter( - $type->extra_types, - static fn($t): bool => $t instanceof TTemplateParam - ) - ) - ) - ) - ); - } - - public function hasMixed(): bool - { - return isset($this->types['mixed']); - } - - public function isMixed(): bool - { - return isset($this->types['mixed']) && count($this->types) === 1; - } - - public function isEmptyMixed(): bool - { - return isset($this->types['mixed']) - && $this->types['mixed'] instanceof TEmptyMixed - && count($this->types) === 1; - } - - public function isVanillaMixed(): bool - { - return isset($this->types['mixed']) - && get_class($this->types['mixed']) === TMixed::class - && !$this->types['mixed']->from_loop_isset - && count($this->types) === 1; - } - - public function isArrayKey(): bool - { - return isset($this->types['array-key']) && count($this->types) === 1; - } - - public function isNull(): bool - { - return count($this->types) === 1 && isset($this->types['null']); - } - - public function isFalse(): bool - { - return count($this->types) === 1 && isset($this->types['false']); - } - - public function isAlwaysFalsy(): bool - { - foreach ($this->getAtomicTypes() as $atomic_type) { - if (!$atomic_type->isFalsy()) { - return false; - } - } - - return true; - } - - public function isTrue(): bool - { - return count($this->types) === 1 && isset($this->types['true']); - } - - public function isAlwaysTruthy(): bool - { - if ($this->possibly_undefined || $this->possibly_undefined_from_try) { - return false; - } - - foreach ($this->getAtomicTypes() as $atomic_type) { - if (!$atomic_type->isTruthy()) { - return false; - } - } - - return true; - } - - public function isVoid(): bool - { - return isset($this->types['void']) && count($this->types) === 1; - } - - public function isNever(): bool - { - return isset($this->types['never']) && count($this->types) === 1; - } - - public function isGenerator(): bool - { - return count($this->types) === 1 - && (($single_type = reset($this->types)) instanceof TNamedObject) - && ($single_type->value === 'Generator'); - } - - public function substitute(Union $old_type, ?Union $new_type = null): void - { - if ($this->hasMixed() && !$this->isEmptyMixed()) { - return; - } - - if ($new_type && $new_type->ignore_nullable_issues) { - $this->ignore_nullable_issues = true; - } - - if ($new_type && $new_type->ignore_falsable_issues) { - $this->ignore_falsable_issues = true; - } - - foreach ($old_type->types as $old_type_part) { - if (!$this->removeType($old_type_part->getKey())) { - if ($old_type_part instanceof TFalse - && isset($this->types['bool']) - && !isset($this->types['true']) - ) { - $this->removeType('bool'); - $this->types['true'] = new TTrue; - } elseif ($old_type_part instanceof TTrue - && isset($this->types['bool']) - && !isset($this->types['false']) - ) { - $this->removeType('bool'); - $this->types['false'] = new TFalse; - } elseif (isset($this->types['iterable'])) { - if ($old_type_part instanceof TNamedObject - && $old_type_part->value === 'Traversable' - && !isset($this->types['array']) - ) { - $this->removeType('iterable'); - $this->types['array'] = new TArray([Type::getArrayKey(), Type::getMixed()]); - } - - if ($old_type_part instanceof TArray - && !isset($this->types['traversable']) - ) { - $this->removeType('iterable'); - $this->types['traversable'] = new TNamedObject('Traversable'); - } - } elseif (isset($this->types['array-key'])) { - if ($old_type_part instanceof TString - && !isset($this->types['int']) - ) { - $this->removeType('array-key'); - $this->types['int'] = new TInt(); - } - - if ($old_type_part instanceof TInt - && !isset($this->types['string']) - ) { - $this->removeType('array-key'); - $this->types['string'] = new TString(); - } - } - } - } - - if ($new_type) { - foreach ($new_type->types as $key => $new_type_part) { - if (!isset($this->types[$key]) - || ($new_type_part instanceof Scalar - && get_class($new_type_part) === get_class($this->types[$key])) - ) { - $this->types[$key] = $new_type_part; - } else { - $this->types[$key] = TypeCombiner::combine([$new_type_part, $this->types[$key]])->getSingleAtomic(); - } - } - } elseif (count($this->types) === 0) { - $this->types['mixed'] = new TMixed(); - } - - $this->bustCache(); - } - - public function isSingle(): bool - { - $type_count = count($this->types); - - $int_literal_count = count($this->literal_int_types); - $string_literal_count = count($this->literal_string_types); - $float_literal_count = count($this->literal_float_types); - - if (($int_literal_count && $string_literal_count) - || ($int_literal_count && $float_literal_count) - || ($string_literal_count && $float_literal_count) - ) { - return false; - } - - if ($int_literal_count || $string_literal_count || $float_literal_count) { - $type_count -= $int_literal_count + $string_literal_count + $float_literal_count - 1; - } - - return $type_count === 1; - } - - public function isSingleAndMaybeNullable(): bool - { - $is_nullable = isset($this->types['null']); - - $type_count = count($this->types); - - if ($type_count === 1 && $is_nullable) { - return false; - } - - $int_literal_count = count($this->literal_int_types); - $string_literal_count = count($this->literal_string_types); - $float_literal_count = count($this->literal_float_types); - - if (($int_literal_count && $string_literal_count) - || ($int_literal_count && $float_literal_count) - || ($string_literal_count && $float_literal_count) - ) { - return false; - } - - if ($int_literal_count || $string_literal_count || $float_literal_count) { - $type_count -= $int_literal_count + $string_literal_count + $float_literal_count - 1; - } - - return ($type_count - (int) $is_nullable) === 1; - } - - /** - * @return bool true if this is an int - */ - public function isInt(bool $check_templates = false): bool - { - return count( - array_filter( - $this->types, - static fn($type): bool => $type instanceof TInt - || ($check_templates - && $type instanceof TTemplateParam - && $type->as->isInt() - ) - ) - ) === count($this->types); - } - - /** - * @return bool true if this is a float - */ - public function isFloat(): bool - { - if (!$this->isSingle()) { - return false; - } - - return isset($this->types['float']) || $this->literal_float_types; - } - - /** - * @return bool true if this is a string - */ - public function isString(bool $check_templates = false): bool - { - return count( - array_filter( - $this->types, - static fn($type): bool => $type instanceof TString - || ($check_templates - && $type instanceof TTemplateParam - && $type->as->isString() - ) - ) - ) === count($this->types); - } - - /** - * @return bool true if this is a boolean - */ - public function isBool(): bool - { - if (!$this->isSingle()) { - return false; - } - - return isset($this->types['bool']); - } - - /** - * @return bool true if this is an array - */ - public function isArray(): bool - { - if (!$this->isSingle()) { - return false; - } - - return isset($this->types['array']); - } - - /** - * @return bool true if this is a string literal with only one possible value - */ - public function isSingleStringLiteral(): bool - { - return count($this->types) === 1 && count($this->literal_string_types) === 1; - } - - /** - * @throws InvalidArgumentException if isSingleStringLiteral is false - * - * @return TLiteralString the only string literal represented by this union type - */ - public function getSingleStringLiteral(): TLiteralString - { - if (count($this->types) !== 1 || count($this->literal_string_types) !== 1) { - throw new InvalidArgumentException('Not a string literal'); - } - - return reset($this->literal_string_types); - } - - public function allStringLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralString) { - return false; - } - } - - return true; - } - - public function allIntLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralInt) { - return false; - } - } - - return true; - } - - /** - * @psalm-suppress PossiblyUnusedMethod Public API - */ - public function allFloatLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralFloat) { - return false; - } - } - - return true; - } - - /** - * @psalm-assert-if-true array< - * array-key, - * TLiteralString|TLiteralInt|TLiteralFloat|TFalse|TTrue - * > $this->getAtomicTypes() - */ - public function allSpecificLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralString - && !$atomic_key_type instanceof TLiteralInt - && !$atomic_key_type instanceof TLiteralFloat - && !$atomic_key_type instanceof TFalse - && !$atomic_key_type instanceof TTrue - ) { - return false; - } - } - - return true; - } - - /** - * @psalm-assert-if-true array< - * array-key, - * TLiteralString|TLiteralInt|TLiteralFloat|TNonspecificLiteralString|TNonSpecificLiteralInt|TFalse|TTrue - * > $this->getAtomicTypes() - */ - public function allLiterals(): bool - { - foreach ($this->types as $atomic_key_type) { - if (!$atomic_key_type instanceof TLiteralString - && !$atomic_key_type instanceof TLiteralInt - && !$atomic_key_type instanceof TLiteralFloat - && !$atomic_key_type instanceof TNonspecificLiteralString - && !$atomic_key_type instanceof TNonspecificLiteralInt - && !$atomic_key_type instanceof TFalse - && !$atomic_key_type instanceof TTrue - ) { - return false; - } - } - - return true; - } - - public function hasLiteralValue(): bool - { - return $this->literal_int_types - || $this->literal_string_types - || $this->literal_float_types - || isset($this->types['false']) - || isset($this->types['true']); - } - - public function isSingleLiteral(): bool - { - return count($this->types) === 1 - && count($this->literal_int_types) - + count($this->literal_string_types) - + count($this->literal_float_types) === 1 - ; - } - - /** - * @return TLiteralInt|TLiteralString|TLiteralFloat - */ - public function getSingleLiteral() - { - if (!$this->isSingleLiteral()) { - throw new InvalidArgumentException("Not a single literal"); - } - - return ($literal = reset($this->literal_int_types)) !== false - ? $literal - : (($literal = reset($this->literal_string_types)) !== false - ? $literal - : reset($this->literal_float_types)) - ; - } - - public function hasLiteralString(): bool - { - return count($this->literal_string_types) > 0; - } - - public function hasLiteralInt(): bool - { - return count($this->literal_int_types) > 0; - } - - /** - * @return bool true if this is a int literal with only one possible value - */ - public function isSingleIntLiteral(): bool - { - return count($this->types) === 1 && count($this->literal_int_types) === 1; - } - - /** - * @throws InvalidArgumentException if isSingleIntLiteral is false - * - * @return TLiteralInt the only int literal represented by this union type - */ - public function getSingleIntLiteral(): TLiteralInt - { - if (count($this->types) !== 1 || count($this->literal_int_types) !== 1) { - throw new InvalidArgumentException('Not an int literal'); - } - - return reset($this->literal_int_types); - } - - /** - * @param array $suppressed_issues - * @param array $phantom_classes - * - */ - public function check( - StatementsSource $source, - CodeLocation $code_location, - array $suppressed_issues, - array $phantom_classes = [], - bool $inferred = true, - bool $inherited = false, - bool $prevent_template_covariance = false, - ?string $calling_method_id = null - ): bool { - if ($this->checked) { - return true; - } - - $checker = new TypeChecker( - $source, - $code_location, - $suppressed_issues, - $phantom_classes, - $inferred, - $inherited, - $prevent_template_covariance, - $calling_method_id - ); - - $checker->traverseArray($this->types); - - $this->checked = true; - - return !$checker->hasErrors(); - } - - /** - * @param array $phantom_classes - * - */ - public function queueClassLikesForScanning( - Codebase $codebase, - ?FileStorage $file_storage = null, - array $phantom_classes = [] - ): void { - $scanner_visitor = new TypeScanner( - $codebase->scanner, - $file_storage, - $phantom_classes - ); - - $scanner_visitor->traverseArray($this->types); - } - - /** - * @param lowercase-string $fq_class_like_name - */ - public function containsClassLike(string $fq_class_like_name): bool - { - $classlike_visitor = new ContainsClassLikeVisitor($fq_class_like_name); - - $classlike_visitor->traverseArray($this->types); - - return $classlike_visitor->matches(); - } - - public function containsAnyLiteral(): bool - { - $literal_visitor = new ContainsLiteralVisitor(); - - $literal_visitor->traverseArray($this->types); - - return $literal_visitor->matches(); - } - - /** - * @return list - */ - public function getTemplateTypes(): array - { - $template_type_collector = new TemplateTypeCollector(); - - $template_type_collector->traverseArray($this->types); - - return $template_type_collector->getTemplateTypes(); - } - - public function setFromDocblock(): void - { - $this->from_docblock = true; - - (new FromDocblockSetter())->traverseArray($this->types); - } - - public function replaceClassLike(string $old, string $new): void - { - foreach ($this->types as $key => $atomic_type) { - $atomic_type->replaceClassLike($old, $new); - - $this->removeType($key); - $this->addType($atomic_type); - } - } - - public function equals(Union $other_type, bool $ensure_source_equality = true): bool - { - if ($other_type === $this) { - return true; - } - - if ($other_type->id && $this->id && $other_type->id !== $this->id) { - return false; - } - - if ($other_type->exact_id && $this->exact_id && $other_type->exact_id !== $this->exact_id) { - return false; - } - - if ($this->possibly_undefined !== $other_type->possibly_undefined) { - return false; - } - - if ($this->had_template !== $other_type->had_template) { - return false; - } - - if ($this->possibly_undefined_from_try !== $other_type->possibly_undefined_from_try) { - return false; - } - - if ($this->from_calculation !== $other_type->from_calculation) { - return false; - } - - if ($this->initialized !== $other_type->initialized) { - return false; - } - - if ($ensure_source_equality && $this->from_docblock !== $other_type->from_docblock) { - return false; - } - - if (count($this->types) !== count($other_type->types)) { - return false; - } - - if ($this->parent_nodes !== $other_type->parent_nodes) { - return false; - } - - if ($this->different || $other_type->different) { - return false; - } - - $other_atomic_types = $other_type->types; - - foreach ($this->types as $key => $atomic_type) { - if (!isset($other_atomic_types[$key])) { - return false; - } - - if (!$atomic_type->equals($other_atomic_types[$key], $ensure_source_equality)) { - return false; - } - } - - return true; - } - - /** - * @return array - */ - public function getLiteralStrings(): array - { - return $this->literal_string_types; - } - - /** - * @return array - */ - public function getLiteralInts(): array - { - return $this->literal_int_types; - } - - /** - * @return array - */ - public function getRangeInts(): array - { - $ranges = []; - foreach ($this->getAtomicTypes() as $atomic) { - if ($atomic instanceof TIntRange) { - $ranges[$atomic->getKey()] = $atomic; - } - } - - return $ranges; - } - - /** - * @return array - */ - public function getLiteralFloats(): array - { - return $this->literal_float_types; - } - - /** - * @return array - */ - public function getChildNodes(): array - { - return $this->types; - } - - /** - * @return bool true if this is a float literal with only one possible value - */ - public function isSingleFloatLiteral(): bool - { - return count($this->types) === 1 && count($this->literal_float_types) === 1; - } - - /** - * @throws InvalidArgumentException if isSingleFloatLiteral is false - * - * @return TLiteralFloat the only float literal represented by this union type - */ - public function getSingleFloatLiteral(): TLiteralFloat - { - if (count($this->types) !== 1 || count($this->literal_float_types) !== 1) { - throw new InvalidArgumentException('Not a float literal'); - } - - return reset($this->literal_float_types); - } - - public function hasLiteralFloat(): bool - { - return count($this->literal_float_types) > 0; - } - - public function getSingleAtomic(): Atomic - { - return reset($this->types); - } - - public function isEmptyArray(): bool - { - return count($this->types) === 1 - && isset($this->types['array']) - && $this->types['array'] instanceof TArray - && $this->types['array']->isEmptyArray(); - } - - public function isUnionEmpty(): bool - { - return $this->types === []; + return $union; } } diff --git a/src/Psalm/Type/UnionTrait.php b/src/Psalm/Type/UnionTrait.php new file mode 100644 index 00000000000..cbf55c95dd7 --- /dev/null +++ b/src/Psalm/Type/UnionTrait.php @@ -0,0 +1,1287 @@ + $types + */ + public function __construct(array $types) + { + $from_docblock = false; + + $keyed_types = []; + + foreach ($types as $type) { + $key = $type->getKey(); + $keyed_types[$key] = $type; + + if ($type instanceof TLiteralInt) { + $this->literal_int_types[$key] = $type; + } elseif ($type instanceof TLiteralString) { + $this->literal_string_types[$key] = $type; + } elseif ($type instanceof TLiteralFloat) { + $this->literal_float_types[$key] = $type; + } elseif ($type instanceof TClassString + && ($type->as_type || $type instanceof TTemplateParamClass) + ) { + $this->typed_class_strings[$key] = $type; + } + + $from_docblock = $from_docblock || $type->from_docblock; + } + + $this->types = $keyed_types; + + $this->from_docblock = $from_docblock; + } + + public function __clone() + { + $this->literal_string_types = []; + $this->literal_int_types = []; + $this->literal_float_types = []; + $this->typed_class_strings = []; + + foreach ($this->types as $key => &$type) { + $type = clone $type; + + if ($type instanceof TLiteralInt) { + $this->literal_int_types[$key] = $type; + } elseif ($type instanceof TLiteralString) { + $this->literal_string_types[$key] = $type; + } elseif ($type instanceof TLiteralFloat) { + $this->literal_float_types[$key] = $type; + } elseif ($type instanceof TClassString + && ($type->as_type || $type instanceof TTemplateParamClass) + ) { + $this->typed_class_strings[$key] = $type; + } + } + } + + /** + * @psalm-mutation-free + * @return non-empty-array + */ + public function getAtomicTypes(): array + { + return $this->types; + } + + public function __toString(): string + { + $types = []; + + $printed_int = false; + $printed_float = false; + $printed_string = false; + + foreach ($this->types as $type) { + if ($type instanceof TLiteralFloat) { + if ($printed_float) { + continue; + } + + $printed_float = true; + } elseif ($type instanceof TLiteralString) { + if ($printed_string) { + continue; + } + + $printed_string = true; + } elseif ($type instanceof TLiteralInt) { + if ($printed_int) { + continue; + } + + $printed_int = true; + } + + $types[] = $type->getId(false); + } + + sort($types); + return implode('|', $types); + } + + public function getKey(): string + { + $types = []; + + $printed_int = false; + $printed_float = false; + $printed_string = false; + + foreach ($this->types as $type) { + if ($type instanceof TLiteralFloat) { + if ($printed_float) { + continue; + } + + $types[] = 'float'; + $printed_float = true; + } elseif ($type instanceof TLiteralString) { + if ($printed_string) { + continue; + } + + $types[] = 'string'; + $printed_string = true; + } elseif ($type instanceof TLiteralInt) { + if ($printed_int) { + continue; + } + + $types[] = 'int'; + $printed_int = true; + } else { + $types[] = $type->getKey(); + } + } + + sort($types); + return implode('|', $types); + } + + public function bustCache(): void + { + $this->id = null; + $this->exact_id = null; + } + + public function getId(bool $exact = true): string + { + if ($exact && $this->exact_id) { + return $this->exact_id; + } elseif (!$exact && $this->id) { + return $this->id; + } + + $types = []; + foreach ($this->types as $type) { + $types[] = $type->getId($exact); + } + $types = array_unique($types); + sort($types); + + if (count($types) > 1) { + foreach ($types as $i => $type) { + if (strpos($type, ' as ') && strpos($type, '(') === false) { + $types[$i] = '(' . $type . ')'; + } + } + } + + $id = implode('|', $types); + + if ($exact) { + $this->exact_id = $id; + } else { + $this->id = $id; + } + + return $id; + } + + /** + * @param array $aliased_classes + * + */ + public function toNamespacedString( + ?string $namespace, + array $aliased_classes, + ?string $this_class, + bool $use_phpdoc_format + ): string { + $other_types = []; + + $literal_ints = []; + $literal_strings = []; + + $has_non_literal_int = false; + $has_non_literal_string = false; + + foreach ($this->types as $type) { + $type_string = $type->toNamespacedString($namespace, $aliased_classes, $this_class, $use_phpdoc_format); + if ($type instanceof TLiteralInt) { + $literal_ints[] = $type_string; + } elseif ($type instanceof TLiteralString) { + $literal_strings[] = $type_string; + } else { + if (get_class($type) === TString::class) { + $has_non_literal_string = true; + } elseif (get_class($type) === TInt::class) { + $has_non_literal_int = true; + } + $other_types[] = $type_string; + } + } + + if (count($literal_ints) <= 3 && !$has_non_literal_int) { + $other_types = array_merge($other_types, $literal_ints); + } else { + $other_types[] = 'int'; + } + + if (count($literal_strings) <= 3 && !$has_non_literal_string) { + $other_types = array_merge($other_types, $literal_strings); + } else { + $other_types[] = 'string'; + } + + sort($other_types); + return implode('|', array_unique($other_types)); + } + + /** + * @param array $aliased_classes + */ + public function toPhpString( + ?string $namespace, + array $aliased_classes, + ?string $this_class, + int $analysis_php_version_id + ): ?string { + if (!$this->isSingleAndMaybeNullable()) { + if ($analysis_php_version_id < 8_00_00) { + return null; + } + } elseif ($analysis_php_version_id < 7_00_00 + || (isset($this->types['null']) && $analysis_php_version_id < 7_01_00) + ) { + return null; + } + + $types = $this->types; + + $nullable = false; + + if (isset($types['null']) && count($types) > 1) { + unset($types['null']); + + $nullable = true; + } + + $falsable = false; + + if (isset($types['false']) && count($types) > 1) { + unset($types['false']); + + $falsable = true; + } + + $php_types = []; + + foreach ($types as $atomic_type) { + $php_type = $atomic_type->toPhpString( + $namespace, + $aliased_classes, + $this_class, + $analysis_php_version_id + ); + + if (!$php_type) { + return null; + } + + $php_types[] = $php_type; + } + + if ($falsable) { + if ($nullable) { + $php_types['null'] = 'null'; + } + $php_types['false'] = 'false'; + ksort($php_types); + return implode('|', array_unique($php_types)); + } + + if ($analysis_php_version_id < 8_00_00) { + return ($nullable ? '?' : '') . implode('|', array_unique($php_types)); + } + if ($nullable) { + $php_types['null'] = 'null'; + } + return implode('|', array_unique($php_types)); + } + + public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool + { + if (!$this->isSingleAndMaybeNullable() && $analysis_php_version_id < 8_00_00) { + return false; + } + + $types = $this->types; + + if (isset($types['null'])) { + if (count($types) > 1) { + unset($types['null']); + } else { + return false; + } + } + + return !array_filter( + $types, + static fn($atomic_type): bool => !$atomic_type->canBeFullyExpressedInPhp($analysis_php_version_id) + ); + } + + public function hasType(string $type_string): bool + { + return isset($this->types[$type_string]); + } + + public function hasArray(): bool + { + return isset($this->types['array']); + } + + public function hasIterable(): bool + { + return isset($this->types['iterable']); + } + + public function hasList(): bool + { + return isset($this->types['array']) && $this->types['array'] instanceof TList; + } + + public function hasClassStringMap(): bool + { + return isset($this->types['array']) && $this->types['array'] instanceof TClassStringMap; + } + + public function isTemplatedClassString(): bool + { + return $this->isSingle() + && count( + array_filter( + $this->types, + static fn($type): bool => $type instanceof TTemplateParamClass + ) + ) === 1; + } + + public function hasArrayAccessInterface(Codebase $codebase): bool + { + return (bool)array_filter( + $this->types, + static fn($type): bool => $type->hasArrayAccessInterface($codebase) + ); + } + + public function hasCallableType(): bool + { + return $this->getCallableTypes() || $this->getClosureTypes(); + } + + /** + * @return array + */ + public function getCallableTypes(): array + { + return array_filter( + $this->types, + static fn($type): bool => $type instanceof TCallable + ); + } + + /** + * @return array + */ + public function getClosureTypes(): array + { + return array_filter( + $this->types, + static fn($type): bool => $type instanceof TClosure + ); + } + + public function hasObject(): bool + { + return isset($this->types['object']); + } + + public function hasObjectType(): bool + { + foreach ($this->types as $type) { + if ($type->isObjectType()) { + return true; + } + } + + return false; + } + + public function isObjectType(): bool + { + foreach ($this->types as $type) { + if (!$type->isObjectType()) { + return false; + } + } + + return true; + } + + public function hasNamedObjectType(): bool + { + foreach ($this->types as $type) { + if ($type->isNamedObjectType()) { + return true; + } + } + + return false; + } + + public function isStaticObject(): bool + { + foreach ($this->types as $type) { + if (!$type instanceof TNamedObject + || !$type->is_static + ) { + return false; + } + } + + return true; + } + + public function hasStaticObject(): bool + { + foreach ($this->types as $type) { + if ($type instanceof TNamedObject + && $type->is_static + ) { + return true; + } + } + + return false; + } + + public function isNullable(): bool + { + if (isset($this->types['null'])) { + return true; + } + + foreach ($this->types as $type) { + if ($type instanceof TTemplateParam && $type->as->isNullable()) { + return true; + } + } + + return false; + } + + public function isFalsable(): bool + { + if (isset($this->types['false'])) { + return true; + } + + foreach ($this->types as $type) { + if ($type instanceof TTemplateParam && $type->as->isFalsable()) { + return true; + } + } + + return false; + } + + public function hasBool(): bool + { + return isset($this->types['bool']) || isset($this->types['false']) || isset($this->types['true']); + } + + public function hasString(): bool + { + return isset($this->types['string']) + || isset($this->types['class-string']) + || isset($this->types['trait-string']) + || isset($this->types['numeric-string']) + || isset($this->types['callable-string']) + || isset($this->types['array-key']) + || $this->literal_string_types + || $this->typed_class_strings; + } + + public function hasLowercaseString(): bool + { + return isset($this->types['string']) + && ($this->types['string'] instanceof TLowercaseString + || $this->types['string'] instanceof TNonEmptyLowercaseString); + } + + public function hasLiteralClassString(): bool + { + return count($this->typed_class_strings) > 0; + } + + public function hasInt(): bool + { + return isset($this->types['int']) || isset($this->types['array-key']) || $this->literal_int_types + || array_filter($this->types, static fn(Atomic $type): bool => $type instanceof TIntRange); + } + + public function hasArrayKey(): bool + { + return isset($this->types['array-key']); + } + + public function hasFloat(): bool + { + return isset($this->types['float']) || $this->literal_float_types; + } + + public function hasScalar(): bool + { + return isset($this->types['scalar']); + } + + public function hasNumeric(): bool + { + return isset($this->types['numeric']); + } + + public function hasScalarType(): bool + { + return isset($this->types['int']) + || isset($this->types['float']) + || isset($this->types['string']) + || isset($this->types['class-string']) + || isset($this->types['trait-string']) + || isset($this->types['bool']) + || isset($this->types['false']) + || isset($this->types['true']) + || isset($this->types['numeric']) + || isset($this->types['numeric-string']) + || $this->literal_int_types + || $this->literal_float_types + || $this->literal_string_types + || $this->typed_class_strings; + } + + public function hasTemplate(): bool + { + return (bool) array_filter( + $this->types, + static fn(Atomic $type): bool => $type instanceof TTemplateParam + || ($type instanceof TNamedObject + && $type->extra_types + && array_filter( + $type->extra_types, + static fn($t): bool => $t instanceof TTemplateParam + ) + ) + ); + } + + public function hasConditional(): bool + { + return (bool) array_filter( + $this->types, + static fn(Atomic $type): bool => $type instanceof TConditional + ); + } + + public function hasTemplateOrStatic(): bool + { + return (bool) array_filter( + $this->types, + static fn(Atomic $type): bool => $type instanceof TTemplateParam + || ($type instanceof TNamedObject + && ($type->is_static + || ($type->extra_types + && array_filter( + $type->extra_types, + static fn($t): bool => $t instanceof TTemplateParam + ) + ) + ) + ) + ); + } + + public function hasMixed(): bool + { + return isset($this->types['mixed']); + } + + public function isMixed(): bool + { + return isset($this->types['mixed']) && count($this->types) === 1; + } + + public function isEmptyMixed(): bool + { + return isset($this->types['mixed']) + && $this->types['mixed'] instanceof TEmptyMixed + && count($this->types) === 1; + } + + public function isVanillaMixed(): bool + { + return isset($this->types['mixed']) + && get_class($this->types['mixed']) === TMixed::class + && !$this->types['mixed']->from_loop_isset + && count($this->types) === 1; + } + + public function isArrayKey(): bool + { + return isset($this->types['array-key']) && count($this->types) === 1; + } + + public function isNull(): bool + { + return count($this->types) === 1 && isset($this->types['null']); + } + + public function isFalse(): bool + { + return count($this->types) === 1 && isset($this->types['false']); + } + + public function isAlwaysFalsy(): bool + { + foreach ($this->getAtomicTypes() as $atomic_type) { + if (!$atomic_type->isFalsy()) { + return false; + } + } + + return true; + } + + public function isTrue(): bool + { + return count($this->types) === 1 && isset($this->types['true']); + } + + public function isAlwaysTruthy(): bool + { + if ($this->possibly_undefined || $this->possibly_undefined_from_try) { + return false; + } + + foreach ($this->getAtomicTypes() as $atomic_type) { + if (!$atomic_type->isTruthy()) { + return false; + } + } + + return true; + } + + public function isVoid(): bool + { + return isset($this->types['void']) && count($this->types) === 1; + } + + public function isNever(): bool + { + return isset($this->types['never']) && count($this->types) === 1; + } + + public function isGenerator(): bool + { + return count($this->types) === 1 + && (($single_type = reset($this->types)) instanceof TNamedObject) + && ($single_type->value === 'Generator'); + } + + public function isSingle(): bool + { + $type_count = count($this->types); + + $int_literal_count = count($this->literal_int_types); + $string_literal_count = count($this->literal_string_types); + $float_literal_count = count($this->literal_float_types); + + if (($int_literal_count && $string_literal_count) + || ($int_literal_count && $float_literal_count) + || ($string_literal_count && $float_literal_count) + ) { + return false; + } + + if ($int_literal_count || $string_literal_count || $float_literal_count) { + $type_count -= $int_literal_count + $string_literal_count + $float_literal_count - 1; + } + + return $type_count === 1; + } + + public function isSingleAndMaybeNullable(): bool + { + $is_nullable = isset($this->types['null']); + + $type_count = count($this->types); + + if ($type_count === 1 && $is_nullable) { + return false; + } + + $int_literal_count = count($this->literal_int_types); + $string_literal_count = count($this->literal_string_types); + $float_literal_count = count($this->literal_float_types); + + if (($int_literal_count && $string_literal_count) + || ($int_literal_count && $float_literal_count) + || ($string_literal_count && $float_literal_count) + ) { + return false; + } + + if ($int_literal_count || $string_literal_count || $float_literal_count) { + $type_count -= $int_literal_count + $string_literal_count + $float_literal_count - 1; + } + + return ($type_count - (int) $is_nullable) === 1; + } + + /** + * @return bool true if this is an int + */ + public function isInt(bool $check_templates = false): bool + { + return count( + array_filter( + $this->types, + static fn($type): bool => $type instanceof TInt + || ($check_templates + && $type instanceof TTemplateParam + && $type->as->isInt() + ) + ) + ) === count($this->types); + } + + /** + * @return bool true if this is a float + */ + public function isFloat(): bool + { + if (!$this->isSingle()) { + return false; + } + + return isset($this->types['float']) || $this->literal_float_types; + } + + /** + * @return bool true if this is a string + */ + public function isString(bool $check_templates = false): bool + { + return count( + array_filter( + $this->types, + static fn($type): bool => $type instanceof TString + || ($check_templates + && $type instanceof TTemplateParam + && $type->as->isString() + ) + ) + ) === count($this->types); + } + + /** + * @return bool true if this is a boolean + */ + public function isBool(): bool + { + if (!$this->isSingle()) { + return false; + } + + return isset($this->types['bool']); + } + + /** + * @return bool true if this is an array + */ + public function isArray(): bool + { + if (!$this->isSingle()) { + return false; + } + + return isset($this->types['array']); + } + + /** + * @return bool true if this is a string literal with only one possible value + */ + public function isSingleStringLiteral(): bool + { + return count($this->types) === 1 && count($this->literal_string_types) === 1; + } + + /** + * @throws InvalidArgumentException if isSingleStringLiteral is false + * + * @return TLiteralString the only string literal represented by this union type + */ + public function getSingleStringLiteral(): TLiteralString + { + if (count($this->types) !== 1 || count($this->literal_string_types) !== 1) { + throw new InvalidArgumentException('Not a string literal'); + } + + return reset($this->literal_string_types); + } + + public function allStringLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralString) { + return false; + } + } + + return true; + } + + public function allIntLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralInt) { + return false; + } + } + + return true; + } + + public function allFloatLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralFloat) { + return false; + } + } + + return true; + } + + /** + * @psalm-assert-if-true array< + * array-key, + * TLiteralString|TLiteralInt|TLiteralFloat|TFalse|TTrue + * > $this->getAtomicTypes() + */ + public function allSpecificLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralString + && !$atomic_key_type instanceof TLiteralInt + && !$atomic_key_type instanceof TLiteralFloat + && !$atomic_key_type instanceof TFalse + && !$atomic_key_type instanceof TTrue + ) { + return false; + } + } + + return true; + } + + /** + * @psalm-assert-if-true array< + * array-key, + * TLiteralString|TLiteralInt|TLiteralFloat|TNonspecificLiteralString|TNonSpecificLiteralInt|TFalse|TTrue + * > $this->getAtomicTypes() + */ + public function allLiterals(): bool + { + foreach ($this->types as $atomic_key_type) { + if (!$atomic_key_type instanceof TLiteralString + && !$atomic_key_type instanceof TLiteralInt + && !$atomic_key_type instanceof TLiteralFloat + && !$atomic_key_type instanceof TNonspecificLiteralString + && !$atomic_key_type instanceof TNonspecificLiteralInt + && !$atomic_key_type instanceof TFalse + && !$atomic_key_type instanceof TTrue + ) { + return false; + } + } + + return true; + } + + public function hasLiteralValue(): bool + { + return $this->literal_int_types + || $this->literal_string_types + || $this->literal_float_types + || isset($this->types['false']) + || isset($this->types['true']); + } + + public function isSingleLiteral(): bool + { + return count($this->types) === 1 + && count($this->literal_int_types) + + count($this->literal_string_types) + + count($this->literal_float_types) === 1 + ; + } + + /** + * @return TLiteralInt|TLiteralString|TLiteralFloat + */ + public function getSingleLiteral() + { + if (!$this->isSingleLiteral()) { + throw new InvalidArgumentException("Not a single literal"); + } + + return ($literal = reset($this->literal_int_types)) !== false + ? $literal + : (($literal = reset($this->literal_string_types)) !== false + ? $literal + : reset($this->literal_float_types)) + ; + } + + public function hasLiteralString(): bool + { + return count($this->literal_string_types) > 0; + } + + public function hasLiteralInt(): bool + { + return count($this->literal_int_types) > 0; + } + + /** + * @return bool true if this is a int literal with only one possible value + */ + public function isSingleIntLiteral(): bool + { + return count($this->types) === 1 && count($this->literal_int_types) === 1; + } + + /** + * @throws InvalidArgumentException if isSingleIntLiteral is false + * + * @return TLiteralInt the only int literal represented by this union type + */ + public function getSingleIntLiteral(): TLiteralInt + { + if (count($this->types) !== 1 || count($this->literal_int_types) !== 1) { + throw new InvalidArgumentException('Not an int literal'); + } + + return reset($this->literal_int_types); + } + + /** + * @param array $suppressed_issues + * @param array $phantom_classes + * + */ + public function check( + StatementsSource $source, + CodeLocation $code_location, + array $suppressed_issues, + array $phantom_classes = [], + bool $inferred = true, + bool $inherited = false, + bool $prevent_template_covariance = false, + ?string $calling_method_id = null + ): bool { + if ($this->checked) { + return true; + } + + $checker = new TypeChecker( + $source, + $code_location, + $suppressed_issues, + $phantom_classes, + $inferred, + $inherited, + $prevent_template_covariance, + $calling_method_id + ); + + $checker->traverseArray($this->types); + + $this->checked = true; + + return !$checker->hasErrors(); + } + + /** + * @param array $phantom_classes + * + */ + public function queueClassLikesForScanning( + Codebase $codebase, + ?FileStorage $file_storage = null, + array $phantom_classes = [] + ): void { + $scanner_visitor = new TypeScanner( + $codebase->scanner, + $file_storage, + $phantom_classes + ); + + $scanner_visitor->traverseArray($this->types); + } + + /** + * @param lowercase-string $fq_class_like_name + */ + public function containsClassLike(string $fq_class_like_name): bool + { + $classlike_visitor = new ContainsClassLikeVisitor($fq_class_like_name); + + $classlike_visitor->traverseArray($this->types); + + return $classlike_visitor->matches(); + } + + public function containsAnyLiteral(): bool + { + $literal_visitor = new ContainsLiteralVisitor(); + + $literal_visitor->traverseArray($this->types); + + return $literal_visitor->matches(); + } + + /** + * @return list + */ + public function getTemplateTypes(): array + { + $template_type_collector = new TemplateTypeCollector(); + + $template_type_collector->traverseArray($this->types); + + return $template_type_collector->getTemplateTypes(); + } + + public function setFromDocblock(): void + { + $this->from_docblock = true; + + (new FromDocblockSetter())->traverseArray($this->types); + } + + public function equals(self $other_type, bool $ensure_source_equality = true): bool + { + if ($other_type === $this) { + return true; + } + + if ($other_type->id && $this->id && $other_type->id !== $this->id) { + return false; + } + + if ($other_type->exact_id && $this->exact_id && $other_type->exact_id !== $this->exact_id) { + return false; + } + + if ($this->possibly_undefined !== $other_type->possibly_undefined) { + return false; + } + + if ($this->had_template !== $other_type->had_template) { + return false; + } + + if ($this->possibly_undefined_from_try !== $other_type->possibly_undefined_from_try) { + return false; + } + + if ($this->from_calculation !== $other_type->from_calculation) { + return false; + } + + if ($this->initialized !== $other_type->initialized) { + return false; + } + + if ($ensure_source_equality && $this->from_docblock !== $other_type->from_docblock) { + return false; + } + + if (count($this->types) !== count($other_type->types)) { + return false; + } + + if ($this->parent_nodes !== $other_type->parent_nodes) { + return false; + } + + if ($this->different || $other_type->different) { + return false; + } + + $other_atomic_types = $other_type->types; + + foreach ($this->types as $key => $atomic_type) { + if (!isset($other_atomic_types[$key])) { + return false; + } + + if (!$atomic_type->equals($other_atomic_types[$key], $ensure_source_equality)) { + return false; + } + } + + return true; + } + + /** + * @return array + */ + public function getLiteralStrings(): array + { + return $this->literal_string_types; + } + + /** + * @return array + */ + public function getLiteralInts(): array + { + return $this->literal_int_types; + } + + /** + * @return array + */ + public function getRangeInts(): array + { + $ranges = []; + foreach ($this->getAtomicTypes() as $atomic) { + if ($atomic instanceof TIntRange) { + $ranges[$atomic->getKey()] = $atomic; + } + } + + return $ranges; + } + + /** + * @return array + */ + public function getLiteralFloats(): array + { + return $this->literal_float_types; + } + + /** + * @return array + */ + public function getChildNodes(): array + { + return $this->types; + } + + /** + * @return bool true if this is a float literal with only one possible value + */ + public function isSingleFloatLiteral(): bool + { + return count($this->types) === 1 && count($this->literal_float_types) === 1; + } + + /** + * @throws InvalidArgumentException if isSingleFloatLiteral is false + * + * @return TLiteralFloat the only float literal represented by this union type + */ + public function getSingleFloatLiteral(): TLiteralFloat + { + if (count($this->types) !== 1 || count($this->literal_float_types) !== 1) { + throw new InvalidArgumentException('Not a float literal'); + } + + return reset($this->literal_float_types); + } + + public function hasLiteralFloat(): bool + { + return count($this->literal_float_types) > 0; + } + + public function getSingleAtomic(): Atomic + { + return reset($this->types); + } + + public function isEmptyArray(): bool + { + return count($this->types) === 1 + && isset($this->types['array']) + && $this->types['array'] instanceof TArray + && $this->types['array']->isEmptyArray(); + } + + public function isUnionEmpty(): bool + { + return $this->types === []; + } +} diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index 76003499ced..f1d3e16bb63 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -1098,7 +1098,7 @@ function foo(?array $arr, string $s) : void { $_arr2[$index] = 5;', 'assertions' => [ '$_arr1===' => 'non-empty-array<1, 5>', - '$_arr2===' => 'non-empty-array<1, 5>', + '$_arr2===' => 'array{1: 5}', ] ], 'accessArrayWithSingleStringLiteralOffset' => [ diff --git a/tests/Template/ClassTemplateTest.php b/tests/Template/ClassTemplateTest.php index 6dc1a02a118..0068e7cc06c 100644 --- a/tests/Template/ClassTemplateTest.php +++ b/tests/Template/ClassTemplateTest.php @@ -4457,7 +4457,7 @@ public function setCallback(Closure $callback): void { } $a = new A(function() { return "a";}); $a->setCallback(function() { return "b";});', - 'error_message' => 'InvalidScalarArgument', + 'error_message' => 'InvalidArgument', ], 'preventBoundsMismatchDifferentContainers' => [ 'code' => ' Date: Mon, 3 Oct 2022 11:28:01 +0200 Subject: [PATCH 137/194] Fix properties-of on generics&intersections --- psalm-baseline.xml | 13 +- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 2 +- .../Analyzer/FunctionLikeAnalyzer.php | 2 +- .../Analyzer/Statements/Block/TryAnalyzer.php | 2 +- .../Expression/Call/ArgumentsAnalyzer.php | 38 +++-- .../Call/ArrayFunctionArgumentsAnalyzer.php | 18 ++- .../Call/ClassTemplateParamCollector.php | 3 +- .../Call/Method/AtomicMethodCallAnalyzer.php | 4 +- .../ExistingAtomicMethodCallAnalyzer.php | 2 +- src/Psalm/Internal/Codebase/Methods.php | 15 +- .../Internal/Type/AssertionReconciler.php | 9 +- .../Type/Comparator/AtomicTypeComparator.php | 4 +- .../Comparator/CallableTypeComparator.php | 2 +- .../Type/Comparator/ObjectComparator.php | 2 +- .../Type/TemplateInferredTypeReplacer.php | 62 ++++----- .../Type/TemplateStandinTypeReplacer.php | 34 ++++- src/Psalm/Internal/Type/TypeCombination.php | 4 +- src/Psalm/Internal/Type/TypeCombiner.php | 2 +- src/Psalm/Internal/Type/TypeExpander.php | 89 ++++++------ src/Psalm/Internal/Type/TypeParser.php | 7 +- src/Psalm/Storage/FunctionLikeParameter.php | 10 ++ src/Psalm/Type.php | 8 +- src/Psalm/Type/Atomic.php | 108 +++++---------- src/Psalm/Type/Atomic/CallableTrait.php | 114 ++++++++++----- src/Psalm/Type/Atomic/GenericTrait.php | 71 +++++++--- .../Type/Atomic/HasIntersectionTrait.php | 101 ++++++++++++-- .../Type/Atomic/TAnonymousClassInstance.php | 11 +- src/Psalm/Type/Atomic/TArray.php | 80 ++++++++++- src/Psalm/Type/Atomic/TCallable.php | 78 +++++++++++ src/Psalm/Type/Atomic/TCallableString.php | 1 + src/Psalm/Type/Atomic/TClassConstant.php | 16 +++ src/Psalm/Type/Atomic/TClassString.php | 52 +++++-- src/Psalm/Type/Atomic/TClassStringMap.php | 33 +++-- src/Psalm/Type/Atomic/TClosure.php | 131 ++++++++++++++++++ src/Psalm/Type/Atomic/TConditional.php | 18 ++- src/Psalm/Type/Atomic/TDependentGetClass.php | 3 +- .../Type/Atomic/TDependentGetDebugType.php | 4 +- src/Psalm/Type/Atomic/TDependentListKey.php | 4 +- src/Psalm/Type/Atomic/TGenericObject.php | 120 +++++++++++++++- src/Psalm/Type/Atomic/TIterable.php | 108 +++++++++++++-- src/Psalm/Type/Atomic/TKeyedArray.php | 73 ++++++++-- src/Psalm/Type/Atomic/TList.php | 39 ++++-- src/Psalm/Type/Atomic/TLiteralClassString.php | 11 ++ src/Psalm/Type/Atomic/TLowercaseString.php | 2 + src/Psalm/Type/Atomic/TMixed.php | 1 + src/Psalm/Type/Atomic/TNamedObject.php | 79 ++++++++++- src/Psalm/Type/Atomic/TNonEmptyArray.php | 19 +++ src/Psalm/Type/Atomic/TNonEmptyList.php | 15 ++ .../Type/Atomic/TObjectWithProperties.php | 69 ++++++++- src/Psalm/Type/Atomic/TPropertiesOf.php | 38 +++-- .../Type/Atomic/TTemplateIndexedAccess.php | 2 + src/Psalm/Type/Atomic/TTemplateKeyOf.php | 15 +- src/Psalm/Type/Atomic/TTemplateParam.php | 60 +++++++- .../Type/Atomic/TTemplatePropertiesOf.php | 28 +++- src/Psalm/Type/Atomic/TTemplateValueOf.php | 15 +- src/Psalm/Type/Atomic/TTypeAlias.php | 2 + src/Psalm/Type/MutableUnion.php | 38 ++++- src/Psalm/Type/Union.php | 9 ++ .../Codebase/InternalCallMapHandlerTest.php | 5 +- tests/PropertiesOfTest.php | 38 +++++ tests/Template/ClassTemplateTest.php | 43 ++++++ tests/Template/PropertiesOfTemplateTest.php | 31 +++++ 62 files changed, 1622 insertions(+), 395 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index e140ba1a087..09f1f3a3587 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + $comment_block->tags['variablesfrom'][0] @@ -331,9 +331,16 @@ $this->type_params[1] + + replaceTypeParams + replaceTypeParams + replaceTypeParams + - - + + + replaceAs + diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index d585d398584..f8148f1c183 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -758,7 +758,7 @@ public static function addContextProperties( // Get actual types used for templates (to support @template-covariant) $template_standins = new TemplateResult($lower_bounds, []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( $guide_property_type, $template_standins, $codebase, diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 735dc7fef0a..b45ef64e430 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -1837,7 +1837,7 @@ private function getFunctionInformation( if ($this->storage instanceof MethodStorage && $this->storage->if_this_is_type) { $template_result = new TemplateResult($this->getTemplateTypeMap() ?? [], []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( new Union([$this_object_type]), $template_result, $codebase, diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php index 1160f615394..68fc8398a9b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/TryAnalyzer.php @@ -273,7 +273,7 @@ static function (string $fq_catch_class) use ($codebase): TNamedObject { && $codebase->interfaceExists($fq_catch_class) && !$codebase->interfaceExtends($fq_catch_class, 'Throwable') ) { - $catch_class_type->addIntersectionType(new TNamedObject('Throwable')); + return $catch_class_type->addIntersectionType(new TNamedObject('Throwable')); } return $catch_class_type; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 424c748f36b..533cda89821 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -268,7 +268,7 @@ public static function analyze( if (null !== $inferred_arg_type && null !== $template_result && null !== $param && null !== $param->type) { $codebase = $statements_analyzer->getCodebase(); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $param->type, $template_result, $codebase, @@ -308,19 +308,6 @@ private static function handleArrayMapFilterArrayArg( ): void { $codebase = $statements_analyzer->getCodebase(); - $generic_param_type = new Union([ - new TArray([ - Type::getArrayKey(), - new Union([ - new TTemplateParam( - 'ArrayValue' . $argument_offset, - Type::getMixed(), - $method_id - ) - ]) - ]) - ]); - $template_types = ['ArrayValue' . $argument_offset => [$method_id => Type::getMixed()]]; $replace_template_result = new TemplateResult( @@ -330,8 +317,19 @@ private static function handleArrayMapFilterArrayArg( $existing_type = $statements_analyzer->node_data->getType($arg->value); - TemplateStandinTypeReplacer::replace( - $generic_param_type, + TemplateStandinTypeReplacer::fillTemplateResult( + new Union([ + new TArray([ + Type::getArrayKey(), + new Union([ + new TTemplateParam( + 'ArrayValue' . $argument_offset, + Type::getMixed(), + $method_id + ) + ]) + ]) + ]), $replace_template_result, $codebase, $statements_analyzer, @@ -515,7 +513,7 @@ private static function handleHighOrderFuncCallArg( $actual_func_param->type->getTemplateTypes() && isset($container_hof_atomic->params[$offset]) ) { - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $actual_func_param->type, $high_order_template_result, $codebase, @@ -769,7 +767,7 @@ public static function checkArgumentsMatch( } } - if ($function_params) { + if ($function_params && !$is_variadic) { foreach ($function_params as $function_param) { $is_variadic = $is_variadic || $function_param->is_variadic; } @@ -1616,7 +1614,7 @@ private static function getProvisionalTemplateResultForFunctionLike( $calling_class_storage->final ?? false ); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( $fleshed_out_param_type, $template_result, $codebase, @@ -1796,7 +1794,7 @@ private static function checkArgCount( $default_type = new Union([$default_type_atomic]); } - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $param->type, $template_result, $codebase, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index f3852df5154..88b6df7a3a6 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -115,6 +115,7 @@ public static function checkArgumentsMatch( $max_closure_param_count = count($args) > 2 ? 2 : 1; } + $new = []; foreach ($closure_arg_type->getAtomicTypes() as $closure_type) { self::checkClosureType( $statements_analyzer, @@ -127,7 +128,13 @@ public static function checkArgumentsMatch( $array_arg_types, $check_functions ); + $new []= $closure_type; } + + $statements_analyzer->node_data->setType( + $closure_arg->value, + $closure_arg_type->getBuilder()->setTypes($new)->freeze() + ); } } @@ -584,13 +591,12 @@ public static function handleByRefArrayAdjustment( /** * @param (TArray|null)[] $array_arg_types - * */ private static function checkClosureType( StatementsAnalyzer $statements_analyzer, Context $context, string $method_id, - Atomic $closure_type, + Atomic &$closure_type, PhpParser\Node\Arg $closure_arg, int $min_closure_param_count, int $max_closure_param_count, @@ -726,10 +732,10 @@ private static function checkClosureType( } } } else { - $closure_types = [$closure_type]; + $closure_types = [&$closure_type]; } - foreach ($closure_types as $closure_type) { + foreach ($closure_types as &$closure_type) { if ($closure_type->params === null) { continue; } @@ -755,7 +761,7 @@ private static function checkClosureTypeArgs( StatementsAnalyzer $statements_analyzer, Context $context, string $method_id, - Atomic $closure_type, + Atomic &$closure_type, PhpParser\Node\Arg $closure_arg, int $min_closure_param_count, int $max_closure_param_count, @@ -863,7 +869,7 @@ private static function checkClosureTypeArgs( $context->calling_method_id ?: $context->calling_function_id ); - $closure_type->replaceTemplateTypesWithArgTypes( + $closure_type = $closure_type->replaceTemplateTypesWithArgTypes( $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php index d498beff791..30022eb2a82 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ClassTemplateParamCollector.php @@ -240,8 +240,7 @@ private static function resolveTemplateParam( } } else { if ($template_result !== null) { - $type_extends_atomic = clone $type_extends_atomic; - $type_extends_atomic->replaceTemplateTypesWithArgTypes( + $type_extends_atomic = $type_extends_atomic->replaceTemplateTypesWithArgTypes( $template_result, $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php index 1c9edaf7607..9d6401c66c0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -849,9 +849,7 @@ private static function handleRegularMixins( $lhs_var_id === '$this' ); - $lhs_type_part = clone $mixin; - - $lhs_type_part->replaceTemplateTypesWithArgTypes( + $lhs_type_part = $mixin->replaceTemplateTypesWithArgTypes( new TemplateResult([], $mixin_class_template_params ?: []), $codebase ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php index 72c0c7cbda5..795b6b1c02c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -207,7 +207,7 @@ public static function analyze( if ($method_storage && $method_storage->if_this_is_type) { $method_template_result = new TemplateResult($method_storage->template_types ?: [], []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( clone $method_storage->if_this_is_type, $method_template_result, $codebase, diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index da7f59b6122..262101a74f1 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -887,12 +887,17 @@ public function getMethodReturnType( if ((!$old_contained_by_new && !$new_contained_by_old) || ($old_contained_by_new && $new_contained_by_old) ) { + $attempted_intersection = null; if ($old_contained_by_new) { //implicitly $new_contained_by_old as well - $attempted_intersection = Type::intersectUnionTypes( - $candidate_type, - $overridden_storage->return_type, - $source_analyzer->getCodebase() - ); + try { + $attempted_intersection = Type::intersectUnionTypes( + $candidate_type, + $overridden_storage->return_type, + $source_analyzer->getCodebase() + ); + } catch (InvalidArgumentException $e) { + // TODO: fix + } } else { $attempted_intersection = Type::intersectUnionTypes( $overridden_storage->return_type, diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index ee5c5667fc1..e62b04d849f 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -402,14 +402,12 @@ private static function refine( && ($codebase->classExists($existing_var_type_part->value) || $codebase->interfaceExists($existing_var_type_part->value)) ) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->addIntersectionType($new_type_part); + $existing_var_type_part = $existing_var_type_part->addIntersectionType($new_type_part); $acceptable_atomic_types[] = $existing_var_type_part; } if ($existing_var_type_part instanceof TTemplateParam) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->addIntersectionType($new_type_part); + $existing_var_type_part = $existing_var_type_part->addIntersectionType($new_type_part); $acceptable_atomic_types[] = $existing_var_type_part; } } @@ -1617,8 +1615,7 @@ private static function handleIsA( if ($codebase->classExists($existing_var_type_part->value) || $codebase->interfaceExists($existing_var_type_part->value) ) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->addIntersectionType($new_type_part); + $existing_var_type_part = $existing_var_type_part->addIntersectionType($new_type_part); $acceptable_atomic_types[] = $existing_var_type_part; } } diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index 28064420305..870026ff6fa 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -63,10 +63,10 @@ public static function isContainedBy( if (($container_type_part instanceof TTemplateParam || ($container_type_part instanceof TNamedObject - && isset($container_type_part->extra_types))) + && $container_type_part->extra_types)) && ($input_type_part instanceof TTemplateParam || ($input_type_part instanceof TNamedObject - && isset($input_type_part->extra_types))) + && $input_type_part->extra_types)) ) { return ObjectComparator::isShallowlyContainedBy( $codebase, diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 2670ff04d87..d3a93bd60eb 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -409,7 +409,7 @@ public static function getCallableFromAtomic( $input_with_templates = new Atomic\TGenericObject($input_type_part->value, $type_params); $template_result = new TemplateResult($invokable_storage->template_types ?? [], []); - TemplateStandinTypeReplacer::replace( + TemplateStandinTypeReplacer::fillTemplateResult( new Type\Union([$input_with_templates]), $template_result, $codebase, diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index 5e3aff80828..a2423abf3b3 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -115,7 +115,7 @@ private static function getIntersectionTypes(Atomic $type_part): array $type_part = clone $type_part; $extra_types = $type_part->extra_types; - $type_part->extra_types = null; + $type_part->extra_types = []; $extra_types[$type_part->getKey()] = $type_part; diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index 0b0664f989b..d293baecdd6 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -48,17 +48,17 @@ public static function replace( TemplateResult $template_result, ?Codebase $codebase ): Union { - $keys_to_unset = []; - $new_types = []; $is_mixed = false; $inferred_lower_bounds = $template_result->lower_bounds ?: []; - $union = $union->getBuilder(); + $types = []; + foreach ($union->getAtomicTypes() as $key => $atomic_type) { - $atomic_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); + $should_set = true; + $atomic_type = $atomic_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); if ($atomic_type instanceof TTemplateParam) { $template_type = self::replaceTemplateParam( @@ -69,7 +69,7 @@ public static function replace( ); if ($template_type) { - $keys_to_unset[] = $key; + $should_set = false; foreach ($template_type->getAtomicTypes() as $template_type_part) { if ($template_type_part instanceof TMixed) { @@ -114,11 +114,11 @@ public static function replace( } if ($class_template_type) { - $keys_to_unset[] = $key; + $should_set = false; $new_types[] = $class_template_type; } } elseif ($atomic_type instanceof TTemplateIndexedAccess) { - $keys_to_unset[] = $key; + $should_set = false; $template_type = null; @@ -176,7 +176,7 @@ public static function replace( ); if ($new_type) { - $keys_to_unset[] = $key; + $should_set = false; $new_types[] = $new_type; } } elseif ($atomic_type instanceof TTemplatePropertiesOf) { @@ -187,7 +187,7 @@ public static function replace( ); if ($new_type) { - $keys_to_unset[] = $key; + $should_set = false; $new_types[] = $new_type; } } elseif ($atomic_type instanceof TConditional @@ -200,22 +200,24 @@ public static function replace( $inferred_lower_bounds ); - $keys_to_unset[] = $key; + $should_set = false; foreach ($class_template_type->getAtomicTypes() as $class_template_atomic_type) { $new_types[] = $class_template_atomic_type; } } - } - $union->bustCache(); + if ($should_set) { + $types []= $atomic_type; + } + } if ($is_mixed) { if (!$new_types) { throw new UnexpectedValueException('This array should be full'); } - return $union->replaceTypes( + return $union->getBuilder()->setTypes( TypeCombiner::combine( $new_types, $codebase @@ -223,13 +225,12 @@ public static function replace( )->freeze(); } - foreach ($keys_to_unset as $key) { - $union->removeType($key); + $atomic_types = array_merge($types, $new_types); + if (!$atomic_types) { + throw new UnexpectedValueException('This array should be full'); } - $atomic_types = array_values(array_merge($union->getAtomicTypes(), $new_types)); - - return $union->replaceTypes( + return $union->getBuilder()->setTypes( TypeCombiner::combine( $atomic_types, $codebase @@ -259,36 +260,36 @@ private static function replaceTemplateParam( if ($traversed_type) { $template_type = $traversed_type; - if (!$atomic_type->as->isMixed() && $template_type->isMixed()) { - $template_type = $atomic_type->as->getBuilder(); - } else { - $template_type = $template_type->getBuilder(); + if ($template_type->isMixed() && !$atomic_type->as->isMixed()) { + $template_type = $atomic_type->as; } if ($atomic_type->extra_types) { - foreach ($template_type->getAtomicTypes() as $template_type_key => $atomic_template_type) { + $types = []; + foreach ($template_type->getAtomicTypes() as $atomic_template_type) { if ($atomic_template_type instanceof TNamedObject || $atomic_template_type instanceof TTemplateParam || $atomic_template_type instanceof TIterable || $atomic_template_type instanceof TObjectWithProperties ) { - $atomic_template_type->extra_types = array_merge( + $types []= $atomic_template_type->setIntersectionTypes(array_merge( $atomic_type->extra_types, - $atomic_template_type->extra_types ?: [] - ); + $atomic_template_type->extra_types + )); } elseif ($atomic_template_type instanceof TObject) { $first_atomic_type = array_shift($atomic_type->extra_types); if ($atomic_type->extra_types) { - $first_atomic_type->extra_types = $atomic_type->extra_types; + $first_atomic_type = $first_atomic_type->setIntersectionTypes($atomic_type->extra_types); } - $template_type->removeType($template_type_key); - $template_type->addType($first_atomic_type); + $types []= $first_atomic_type; + } else { + $types []= $atomic_template_type; } } + $template_type = $template_type->getBuilder()->setTypes($types)->freeze(); } - $template_type = $template_type->freeze(); } elseif ($codebase) { foreach ($inferred_lower_bounds as $template_type_map) { foreach ($template_type_map as $template_class => $_) { @@ -382,7 +383,6 @@ private static function replaceTemplatePropertiesOf( } return new TPropertiesOf( - (string) $classlike_type, clone $classlike_type, $atomic_type->visibility_filter ); diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 32c3afaa714..8ed8450b820 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -54,6 +54,39 @@ */ class TemplateStandinTypeReplacer { + /** + * This method fills in the values in $template_result based on how the various atomic types + * of $union_type match up to the types inside $input_type. + */ + public static function fillTemplateResult( + Union $union_type, + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer, + ?Union $input_type, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + ?string $bound_equality_classlike = null, + int $depth = 1 + ): void { + self::replace( + $union_type, + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $bound_equality_classlike, + $depth + ); + } /** * This replaces template types in unions with standins (normally the template as type) * @@ -341,7 +374,6 @@ private static function handleAtomicStandin( } $atomic_type = new TPropertiesOf( - (string) $classlike_type, clone $classlike_type, $atomic_type->visibility_filter ); diff --git a/src/Psalm/Internal/Type/TypeCombination.php b/src/Psalm/Internal/Type/TypeCombination.php index c7770a32c26..2d7c74b4af5 100644 --- a/src/Psalm/Internal/Type/TypeCombination.php +++ b/src/Psalm/Internal/Type/TypeCombination.php @@ -81,9 +81,9 @@ class TypeCombination public $class_string_types = []; /** - * @var array|null + * @var array */ - public $extra_types; + public $extra_types = []; /** @var ?bool */ public $all_arrays_lists; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 6d25b1f40c2..b8e3f18ca0f 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -511,7 +511,7 @@ private static function scrapeTypeProperties( ) { if ($type->extra_types) { $combination->extra_types = array_merge( - $combination->extra_types ?: [], + $combination->extra_types, $type->extra_types ); } diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index babaf30aa77..5d831be2cef 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -5,11 +5,11 @@ use Psalm\Codebase; use Psalm\Exception\CircularReferenceException; use Psalm\Exception\UnresolvableConstantException; +use Psalm\Internal\Analyzer\Statements\Expression\Fetch\AtomicPropertyFetchAnalyzer; use Psalm\Internal\Type\SimpleAssertionReconciler; use Psalm\Internal\Type\SimpleNegatedAssertionReconciler; use Psalm\Internal\Type\TypeParser; use Psalm\Storage\Assertion\IsType; -use Psalm\Storage\PropertyStorage; use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; @@ -130,6 +130,7 @@ public static function expandUnion( * * @return non-empty-list * + * @psalm-suppress ConflictingReferenceConstraint Ultimately, the output type is always an Atomic * @psalm-suppress ComplexMethod */ public static function expandAtomic( @@ -644,9 +645,8 @@ private static function expandNamedObject( || $static_class_type instanceof TTemplateParam) ) { $return_type = clone $return_type; - $cloned_static = clone $static_class_type; - $extra_static = $cloned_static->extra_types ?: []; - $cloned_static->extra_types = null; + $extra_static = $static_class_type->extra_types; + $cloned_static = $static_class_type->setIntersectionTypes([]); if ($cloned_static->getKey(false) !== $return_type->getKey(false)) { $return_type->extra_types[$static_class_type->getKey()] = clone $cloned_static; @@ -892,62 +892,65 @@ private static function expandConditional( */ private static function expandPropertiesOf( Codebase $codebase, - TPropertiesOf $return_type, + TPropertiesOf &$return_type, ?string $self_class, $static_class_type ): array { - if ($return_type->fq_classlike_name === 'self' && $self_class) { - $return_type->fq_classlike_name = $self_class; + if ($self_class) { + $return_type = $return_type->replaceClassLike('self', $self_class); + $return_type = $return_type->replaceClassLike( + 'static', + is_string($static_class_type) ? $static_class_type : $self_class + ); } - if ($return_type->fq_classlike_name === 'static' && $self_class) { - $return_type->fq_classlike_name = is_string($static_class_type) ? $static_class_type : $self_class; + $class_storage = null; + if ($codebase->classExists($return_type->classlike_type->value)) { + $class_storage = $codebase->classlike_storage_provider->get($return_type->classlike_type->value); + } else { + foreach ($return_type->classlike_type->extra_types as $type) { + if ($type instanceof TNamedObject && $codebase->classExists($type->value)) { + $class_storage = $codebase->classlike_storage_provider->get($type->value); + break; + } + } } - if (!$codebase->classExists($return_type->fq_classlike_name)) { + if (!$class_storage) { return [$return_type]; } - // Get and merge all properties from parent classes - $class_storage = $codebase->classlike_storage_provider->get($return_type->fq_classlike_name); - $properties_types = $class_storage->properties; - foreach ($class_storage->parent_classes as $parent_class) { - if (!$codebase->classOrInterfaceExists($parent_class)) { + $properties = []; + foreach ([$class_storage->name, ...array_values($class_storage->parent_classes)] as $class) { + if (!$codebase->classExists($class)) { continue; } - $parent_class_storage = $codebase->classlike_storage_provider->get($parent_class); - $properties_types = array_merge( - $properties_types, - $parent_class_storage->properties - ); - } - - // Filter only non-static properties, and check visibility filter - $properties_types = array_filter( - $properties_types, - function (PropertyStorage $property) use ($return_type): bool { + $storage = $codebase->classlike_storage_provider->get($class); + foreach ($storage->properties as $key => $property) { + if (isset($properties[$key])) { + continue; + } if ($return_type->visibility_filter !== null && $property->visibility !== $return_type->visibility_filter ) { - return false; + continue; } - return !$property->is_static; - } - ); - - // Return property names as literal string - $properties = array_map( - function (PropertyStorage $property): ?Union { - return $property->type; - }, - $properties_types - ); - $properties = array_filter( - $properties, - function (?Union $property_type): bool { - return $property_type !== null; + if ($property->is_static || !$property->type) { + continue; + } + $type = $return_type->classlike_type instanceof TGenericObject + ? AtomicPropertyFetchAnalyzer::localizePropertyType( + $codebase, + $property->type, + $return_type->classlike_type, + $storage, + $storage + ) + : $property->type + ; + $properties[$key] = $type; } - ); + } if ($properties === []) { return [$return_type]; diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index a77c77093d5..711375ba150 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -703,6 +703,12 @@ private static function getTypeFromGenericTree( $generic_type_value . '<' . $param_name . '> must be a TTemplateParam.' ); } + if ($template_param->getIntersectionTypes()) { + throw new TypeParseTreeException( + $generic_type_value . '<' . $param_name . '> must be a TTemplateParam' + . ' with no intersection types.' + ); + } return new TTemplatePropertiesOf( $param_name, @@ -723,7 +729,6 @@ private static function getTypeFromGenericTree( } return new TPropertiesOf( - $param_name, $param_union_types[0], TPropertiesOf::filterForTokenName($generic_type_value) ); diff --git a/src/Psalm/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php index 4a5e0e7b2ce..f333dbeba93 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -144,6 +144,16 @@ public function getId(): string . ($this->is_optional ? '=' : ''); } + public function replaceType(Union $type): self + { + if ($this->type === $type) { + return $this; + } + $cloned = clone $this; + $cloned->type = $type; + return $cloned; + } + public function __clone() { if ($this->type) { diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 42db8658e2d..2e9670fd6af 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -787,11 +787,9 @@ private static function intersectAtomicTypes( $wider_type_intersection_types = $wider_type->getIntersectionTypes(); - if ($wider_type_intersection_types !== null) { - foreach ($wider_type_intersection_types as $wider_type_intersection_type) { - $intersection_atomic->extra_types[$wider_type_intersection_type->getKey()] - = clone $wider_type_intersection_type; - } + foreach ($wider_type_intersection_types as $wider_type_intersection_type) { + $intersection_atomic->extra_types[$wider_type_intersection_type->getKey()] + = clone $wider_type_intersection_type; } } diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index f76cd62e037..b95b789f0cb 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -20,7 +20,6 @@ use Psalm\Type\Atomic\TCallableList; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; -use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TClassStringMap; use Psalm\Type\Atomic\TClosedResource; @@ -113,6 +112,26 @@ abstract class Atomic implements TypeNode * @param array $type_aliases */ public static function create( + string $value, + ?int $analysis_php_version_id = null, + array $template_type_map = [], + array $type_aliases = [], + ?int $offset_start = null, + ?int $offset_end = null, + ?string $text = null + ): Atomic { + $result = self::createInner($value, $analysis_php_version_id, $template_type_map, $type_aliases); + $result->offset_start = $offset_start; + $result->offset_end = $offset_end; + $result->text = $text; + return $result; + } + /** + * @param int $analysis_php_version_id contains php version when the type comes from signature + * @param array> $template_type_map + * @param array $type_aliases + */ + private static function createInner( string $value, ?int $analysis_php_version_id = null, array $template_type_map = [], @@ -354,7 +373,7 @@ public function isNamedObjectType(): bool || ($this instanceof TTemplateParam && ($this->as->hasNamedObjectType() || array_filter( - $this->extra_types ?: [], + $this->extra_types, static fn($extra_type): bool => $extra_type->isNamedObjectType() ) ) @@ -517,77 +536,12 @@ public function getChildNodes(): array return []; } - public function replaceClassLike(string $old, string $new): void + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self { - if ($this instanceof TNamedObject) { - if (strtolower($this->value) === $old) { - $this->value = $new; - } - } - - if ($this instanceof TNamedObject - || $this instanceof TIterable - || $this instanceof TTemplateParam - ) { - if ($this->extra_types) { - foreach ($this->extra_types as $extra_type) { - $extra_type->replaceClassLike($old, $new); - } - } - } - - if ($this instanceof TClassConstant) { - if (strtolower($this->fq_classlike_name) === $old) { - $this->fq_classlike_name = $new; - } - } - - if ($this instanceof TClassString && $this->as !== 'object') { - if (strtolower($this->as) === $old) { - $this->as = $new; - } - } - - if ($this instanceof TTemplateParam) { - $this->as = $this->as->getBuilder()->replaceClassLike($old, $new)->freeze(); - } - - if ($this instanceof TLiteralClassString) { - if (strtolower($this->value) === $old) { - $this->value = $new; - } - } - - if ($this instanceof TArray - || $this instanceof TGenericObject - || $this instanceof TIterable - ) { - foreach ($this->type_params as &$type_param) { - $type_param = $type_param->getBuilder()->replaceClassLike($old, $new)->freeze(); - } - } - - if ($this instanceof TKeyedArray) { - foreach ($this->properties as &$property_type) { - $property_type = $property_type->getBuilder()->replaceClassLike($old, $new)->freeze(); - } - } - - if ($this instanceof TClosure - || $this instanceof TCallable - ) { - if ($this->params) { - foreach ($this->params as $param) { - if ($param->type) { - $param->type = $param->type->getBuilder()->replaceClassLike($old, $new)->freeze(); - } - } - } - - if ($this->return_type) { - $this->return_type = $this->return_type->getBuilder()->replaceClassLike($old, $new)->freeze(); - } - } + return $this; } final public function __toString(): string @@ -660,6 +614,9 @@ abstract public function toPhpString( abstract public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool; + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -672,14 +629,19 @@ public function replaceTemplateTypesWithStandins( bool $add_lower_bound = false, int $depth = 0 ): self { + // do nothing return $this; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { + ): self { // do nothing + return $this; } public function equals(Atomic $other_type, bool $ensure_source_equality): bool diff --git a/src/Psalm/Type/Atomic/CallableTrait.php b/src/Psalm/Type/Atomic/CallableTrait.php index 072c21957ec..328c4a01ce2 100644 --- a/src/Psalm/Type/Atomic/CallableTrait.php +++ b/src/Psalm/Type/Atomic/CallableTrait.php @@ -187,7 +187,10 @@ public function getId(bool $exact = true, bool $nested = false): string . $this->value . $param_string . $return_type_string; } - public function replaceTemplateTypesWithStandins( + /** + * @return array{list|null, Union|null}|null + */ + protected function replaceCallableTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, ?StatementsAnalyzer $statements_analyzer = null, @@ -198,11 +201,15 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $callable = clone $this; + ): ?array { + $replaced = false; + $params = $this->params; + if ($params) { + foreach ($params as $offset => &$param) { + if (!$param->type) { + continue; + } - if ($callable->params) { - foreach ($callable->params as $offset => $param) { $input_param_type = null; if (($input_type instanceof TClosure || $input_type instanceof TCallable) @@ -211,11 +218,7 @@ public function replaceTemplateTypesWithStandins( $input_param_type = $input_type->params[$offset]->type; } - if (!$param->type) { - continue; - } - - $param->type = TemplateStandinTypeReplacer::replace( + $new_param = $param->replaceType(TemplateStandinTypeReplacer::replace( $param->type, $template_result, $codebase, @@ -228,13 +231,16 @@ public function replaceTemplateTypesWithStandins( !$add_lower_bound, null, $depth - ); + )); + $replaced = $replaced || $new_param !== $param; + $param = $new_param; } } - if ($callable->return_type) { - $callable->return_type = TemplateStandinTypeReplacer::replace( - $callable->return_type, + $return_type = $this->return_type; + if ($return_type) { + $return_type = TemplateStandinTypeReplacer::replace( + $return_type, $template_result, $codebase, $statements_analyzer, @@ -247,42 +253,88 @@ public function replaceTemplateTypesWithStandins( $replace, $add_lower_bound ); + $replaced = $replaced || $this->return_type !== $return_type; } - return $callable; + if ($replaced) { + return [$params, $return_type]; + } + return null; } - public function replaceTemplateTypesWithArgTypes( + + /** + * @return array{list|null, Union|null}|null + */ + protected function replaceCallableTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - if ($this->params) { - foreach ($this->params as $param) { - if (!$param->type) { - continue; - } + ): ?array { + $replaced = false; - $param->type = TemplateInferredTypeReplacer::replace( - $param->type, - $template_result, - $codebase - ); + $params = $this->params; + if ($params) { + foreach ($params as &$param) { + if ($param->type) { + $new_param = $param->replaceType(TemplateInferredTypeReplacer::replace( + $param->type, + $template_result, + $codebase + )); + $replaced = $replaced || $new_param !== $param; + $param = $new_param; + } } } - if ($this->return_type) { - $this->return_type = TemplateInferredTypeReplacer::replace( - $this->return_type, + $return_type = $this->return_type; + if ($return_type) { + $return_type = TemplateInferredTypeReplacer::replace( + $return_type, $template_result, $codebase ); + $replaced = $replaced || $return_type !== $this->return_type; + } + if ($replaced) { + return [$params, $return_type]; + } + return null; + } + + /** + * @return array{list|null, Union|null}|null + */ + protected function replaceCallableClassLike(string $old, string $new): ?array + { + $replaced = false; + + $params = $this->params; + if ($params) { + foreach ($params as &$param) { + if ($param->type) { + $new_param = $param->replaceType($param->type->replaceClassLike($old, $new)); + $replaced = $replaced || $new_param !== $param; + $param = $new_param; + } + } + } + + $return_type = $this->return_type; + if ($return_type) { + $return_type = $return_type->replaceClassLike($old, $new); + $replaced = $replaced || $return_type !== $this->return_type; + } + if ($replaced) { + return [$params, $return_type]; } + return null; } /** * @return list */ - public function getChildNodes(): array + protected function getCallableChildNodes(): array { $child_nodes = []; diff --git a/src/Psalm/Type/Atomic/GenericTrait.php b/src/Psalm/Type/Atomic/GenericTrait.php index 78578107a27..d92861d1355 100644 --- a/src/Psalm/Type/Atomic/GenericTrait.php +++ b/src/Psalm/Type/Atomic/GenericTrait.php @@ -9,7 +9,6 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Type; use Psalm\Type\Atomic; -use Psalm\Type\TypeNode; use Psalm\Type\Union; use function array_map; @@ -19,8 +18,31 @@ use function strpos; use function substr; +/** + * @template TTypeParams as array + */ trait GenericTrait { + /** + * @var TTypeParams + */ + public array $type_params; + + /** + * @param TTypeParams $type_params + * + * @return static + */ + public function replaceTypeParams(array $type_params): self + { + if ($this->type_params === $type_params) { + return $this; + } + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + public function getId(bool $exact = true, bool $nested = false): string { $s = ''; @@ -147,14 +169,9 @@ public function __clone() } /** - * @return array + * @return TTypeParams|null */ - public function getChildNodes(): array - { - return $this->type_params; - } - - public function replaceTemplateTypesWithStandins( + protected function replaceTypeParamsTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, ?StatementsAnalyzer $statements_analyzer = null, @@ -165,7 +182,7 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { + ): ?array { if ($input_type instanceof TList) { $input_type = new TArray([Type::getInt(), $input_type->type_param]); } @@ -185,9 +202,9 @@ public function replaceTemplateTypesWithStandins( ); } - $atomic = clone $this; + $type_params = $this->type_params; - foreach ($atomic->type_params as $offset => $type_param) { + foreach ($type_params as $offset => $type_param) { $input_type_param = null; if (($input_type instanceof TIterable @@ -208,7 +225,7 @@ public function replaceTemplateTypesWithStandins( $input_type_param = $input_object_type_params[$offset]; } - $atomic->type_params[$offset] = TemplateStandinTypeReplacer::replace( + $type_params[$offset] = TemplateStandinTypeReplacer::replace( $type_param, $template_result, $codebase, @@ -227,14 +244,18 @@ public function replaceTemplateTypesWithStandins( ); } - return $atomic; + return $type_params === $this->type_params ? null : $type_params; } - public function replaceTemplateTypesWithArgTypes( + /** + * @return TTypeParams|null + */ + protected function replaceTypeParamsTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - foreach ($this->type_params as $offset => &$type_param) { + ): ?array { + $type_params = $this->type_params; + foreach ($type_params as $offset => &$type_param) { $type_param = TemplateInferredTypeReplacer::replace( $type_param, $template_result, @@ -242,16 +263,22 @@ public function replaceTemplateTypesWithArgTypes( ); if ($this instanceof TArray && $offset === 0 && $type_param->isMixed()) { - $this->type_params[0] = Type::getArrayKey(); + $type_param = Type::getArrayKey(); } } - if ($this instanceof TGenericObject) { - $this->remapped_params = true; - } + return $type_params === $this->type_params ? null : $type_params; + } - if ($this instanceof TGenericObject || $this instanceof TIterable) { - $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + /** + * @return TTypeParams|null + */ + protected function replaceTypeParamsClassLike(string $old, string $new): ?array + { + $type_params = $this->type_params; + foreach ($type_params as &$type_param) { + $type_param = $type_param->replaceClassLike($old, $new); } + return $type_params === $this->type_params ? null : $type_params; } } diff --git a/src/Psalm/Type/Atomic/HasIntersectionTrait.php b/src/Psalm/Type/Atomic/HasIntersectionTrait.php index 720379d9295..3f19aa9b9bd 100644 --- a/src/Psalm/Type/Atomic/HasIntersectionTrait.php +++ b/src/Psalm/Type/Atomic/HasIntersectionTrait.php @@ -3,19 +3,21 @@ namespace Psalm\Type\Atomic; use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Type\Atomic; use function array_map; +use function array_merge; use function implode; trait HasIntersectionTrait { /** - * @var array|null + * @var array */ - public $extra_types; + public array $extra_types = []; /** * @param array $aliased_classes @@ -49,26 +51,49 @@ private function getNamespacedIntersectionTypes( /** * @param TNamedObject|TTemplateParam|TIterable|TObjectWithProperties $type + * + * @return static */ - public function addIntersectionType(Atomic $type): void + public function addIntersectionType(Atomic $type): self { - $this->extra_types[$type->getKey()] = $type; + return $this->setIntersectionTypes(array_merge( + $this->extra_types, + [$type->getKey() => $type] + )); } /** - * @return array|null + * @param array $types + * + * @return static + */ + public function setIntersectionTypes(array $types): self + { + if ($types === $this->extra_types) { + return $this; + } + $cloned = clone $this; + $cloned->extra_types = $types; + return $cloned; + } + + /** + * @return array */ - public function getIntersectionTypes(): ?array + public function getIntersectionTypes(): array { return $this->extra_types; } - public function replaceIntersectionTemplateTypesWithArgTypes( + /** + * @return array|null + */ + protected function replaceIntersectionTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { + ): ?array { if (!$this->extra_types) { - return; + return null; } $new_types = []; @@ -90,11 +115,65 @@ public function replaceIntersectionTemplateTypesWithArgTypes( } } } else { - $extra_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); + $extra_type = $extra_type->replaceTemplateTypesWithArgTypes($template_result, $codebase); $new_types[$extra_type->getKey()] = $extra_type; } } - $this->extra_types = $new_types; + return $new_types === $this->extra_types ? null : $new_types; + } + + /** + * @return array|null + */ + protected function replaceIntersectionTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): ?array { + if (!$this->extra_types) { + return null; + } + $new_types = []; + foreach ($this->extra_types as $type) { + $type = $type->replaceTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $new_types[$type->getKey()] = $type; + } + + return $new_types === $this->extra_types ? null : $new_types; + } + + /** + * @return array|null + */ + protected function replaceIntersectionClassLike(string $old, string $new): ?array + { + if (!$this->extra_types) { + return null; + } + $new_types = []; + foreach ($this->extra_types as $extra_type) { + $extra_type = $extra_type->replaceClassLike($old, $new); + $new_types[$extra_type->getKey()] = $extra_type; + } + return $new_types === $this->extra_types ? null : $new_types; } } diff --git a/src/Psalm/Type/Atomic/TAnonymousClassInstance.php b/src/Psalm/Type/Atomic/TAnonymousClassInstance.php index 1a29e4d27a4..1d77db1e5ef 100644 --- a/src/Psalm/Type/Atomic/TAnonymousClassInstance.php +++ b/src/Psalm/Type/Atomic/TAnonymousClassInstance.php @@ -14,10 +14,15 @@ final class TAnonymousClassInstance extends TNamedObject /** * @param string $value the name of the object + * @param array $extra_types */ - public function __construct(string $value, bool $is_static = false, ?string $extends = null) - { - parent::__construct($value, $is_static); + public function __construct( + string $value, + bool $is_static = false, + ?string $extends = null, + array $extra_types = [] + ) { + parent::__construct($value, $is_static, false, $extra_types); $this->extends = $extends; } diff --git a/src/Psalm/Type/Atomic/TArray.php b/src/Psalm/Type/Atomic/TArray.php index 88041e209a8..208ab12da29 100644 --- a/src/Psalm/Type/Atomic/TArray.php +++ b/src/Psalm/Type/Atomic/TArray.php @@ -2,6 +2,9 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -13,12 +16,10 @@ */ class TArray extends Atomic { - use GenericTrait; - /** - * @var array{Union, Union} + * @use GenericTrait */ - public $type_params; + use GenericTrait; /** * @var string @@ -96,4 +97,75 @@ public function isEmptyArray(): bool { return $this->type_params[1]->isNever(); } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type_params = $this->replaceTypeParamsClassLike($old, $new); + if ($type_params) { + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + return $this; + } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $type_params = $this->replaceTypeParamsTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if ($type_params) { + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + return $this; + } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self + { + $type_params = $this->replaceTypeParamsTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + if ($type_params) { + $cloned = clone $this; + $cloned->type_params = $type_params; + return $cloned; + } + return $this; + } + + public function getChildNodes(): array + { + return $this->type_params; + } } diff --git a/src/Psalm/Type/Atomic/TCallable.php b/src/Psalm/Type/Atomic/TCallable.php index c0dc900af9e..8f4420bd1b6 100644 --- a/src/Psalm/Type/Atomic/TCallable.php +++ b/src/Psalm/Type/Atomic/TCallable.php @@ -2,6 +2,9 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type\Atomic; /** @@ -32,4 +35,79 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { return $this->params === null && $this->return_type === null; } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self + { + $replaced = $this->replaceCallableTemplateTypesWithArgTypes($template_result, $codebase); + if (!$replaced) { + return $this; + } + return new static( + $this->value, + $replaced[0], + $replaced[1], + $this->is_pure + ); + } + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $replaced = $this->replaceCallableTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$replaced) { + return $this; + } + return new static( + $this->value, + $replaced[0], + $replaced[1], + $this->is_pure + ); + } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $replaced = $this->replaceCallableClassLike($old, $new); + if (!$replaced) { + return $this; + } + return new static( + $this->value, + $replaced[0], + $replaced[1], + $this->is_pure + ); + } + + public function getChildNodes(): array + { + return $this->getCallableChildNodes(); + } } diff --git a/src/Psalm/Type/Atomic/TCallableString.php b/src/Psalm/Type/Atomic/TCallableString.php index 1f52a5da8a5..a3dc8036333 100644 --- a/src/Psalm/Type/Atomic/TCallableString.php +++ b/src/Psalm/Type/Atomic/TCallableString.php @@ -4,6 +4,7 @@ /** * Denotes the `callable-string` type, used to represent an unknown string that is also `callable`. + * */ final class TCallableString extends TNonFalsyString { diff --git a/src/Psalm/Type/Atomic/TClassConstant.php b/src/Psalm/Type/Atomic/TClassConstant.php index 79bd6497246..d85492f69a9 100644 --- a/src/Psalm/Type/Atomic/TClassConstant.php +++ b/src/Psalm/Type/Atomic/TClassConstant.php @@ -5,6 +5,8 @@ use Psalm\Type; use Psalm\Type\Atomic; +use function strtolower; + /** * Denotes a class constant whose value might not yet be known. */ @@ -22,6 +24,20 @@ public function __construct(string $fq_classlike_name, string $const_name) $this->const_name = $const_name; } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + if (strtolower($this->fq_classlike_name) === $old) { + return new TClassConstant( + $new, + $this->const_name + ); + } + return $this; + } + public function getKey(bool $include_extra = true): string { return 'class-constant(' . $this->fq_classlike_name . '::' . $this->const_name . ')'; diff --git a/src/Psalm/Type/Atomic/TClassString.php b/src/Psalm/Type/Atomic/TClassString.php index 1124c9e6a78..e040bf4a54e 100644 --- a/src/Psalm/Type/Atomic/TClassString.php +++ b/src/Psalm/Type/Atomic/TClassString.php @@ -42,12 +42,31 @@ class TClassString extends TString /** @var bool */ public $is_enum = false; - public function __construct(string $as = 'object', ?TNamedObject $as_type = null) - { + public function __construct( + string $as = 'object', + ?TNamedObject $as_type = null, + bool $is_loaded = false, + bool $is_interface = false, + bool $is_enum = false + ) { $this->as = $as; $this->as_type = $as_type; + $this->is_loaded = $is_loaded; + $this->is_interface = $is_interface; + $this->is_enum = $is_enum; + } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + if ($this->as !== 'object' && strtolower($this->as) === $old) { + $cloned = clone $this; + $cloned->as = $new; + return $cloned; + } + return $this; } - public function getKey(bool $include_extra = true): string { if ($this->is_interface) { @@ -133,6 +152,9 @@ public function getChildNodes(): array return $this->as_type ? [$this->as_type] : []; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -144,11 +166,9 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $class_string = clone $this; - - if (!$class_string->as_type) { - return $class_string; + ): self { + if (!$this->as_type) { + return $this; } if ($input_type instanceof TLiteralClassString) { @@ -160,7 +180,7 @@ public function replaceTemplateTypesWithStandins( } $as_type = TemplateStandinTypeReplacer::replace( - new Union([$class_string->as_type]), + new Union([$this->as_type]), $template_result, $codebase, $statements_analyzer, @@ -176,15 +196,19 @@ public function replaceTemplateTypesWithStandins( $as_type_types = array_values($as_type->getAtomicTypes()); - $class_string->as_type = count($as_type_types) === 1 + $as_type = count($as_type_types) === 1 && $as_type_types[0] instanceof TNamedObject ? $as_type_types[0] : null; - if (!$class_string->as_type) { - $class_string->as = 'object'; + if ($this->as_type === $as_type) { + return $this; } - - return $class_string; + $cloned = clone $this; + $cloned->as_type = $as_type; + if (!$cloned->as_type) { + $cloned->as = 'object'; + } + return $cloned; } } diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index 79dea6b4996..63e4faa96a1 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -39,9 +39,9 @@ final class TClassStringMap extends Atomic */ public function __construct(string $param_name, ?TNamedObject $as_type, Union $value_param) { - $this->value_param = $value_param; $this->param_name = $param_name; $this->as_type = $as_type; + $this->value_param = $value_param; } public function getId(bool $exact = true, bool $nested = false): string @@ -117,6 +117,9 @@ public function getKey(bool $include_extra = true): string return 'array'; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -128,10 +131,10 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $map = clone $this; + ): self { + $cloned = null; - foreach ([Type::getString(), $map->value_param] as $offset => $type_param) { + foreach ([Type::getString(), $this->value_param] as $offset => $type_param) { $input_type_param = null; if (($input_type instanceof TGenericObject @@ -170,23 +173,35 @@ public function replaceTemplateTypesWithStandins( $depth + 1 ); - if ($offset === 1) { - $map->value_param = $value_param; + if ($offset === 1 && ($cloned || $this->value_param !== $value_param)) { + $cloned ??= clone $this; + $cloned->value_param = $value_param; } } - return $map; + return $cloned ?? $this; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->value_param = TemplateInferredTypeReplacer::replace( + ): self { + $value_param = TemplateInferredTypeReplacer::replace( $this->value_param, $template_result, $codebase ); + if ($value_param === $this->value_param) { + return $this; + } + return new static( + $this->param_name, + $this->as_type, + $value_param + ); } public function getChildNodes(): array diff --git a/src/Psalm/Type/Atomic/TClosure.php b/src/Psalm/Type/Atomic/TClosure.php index fda5f17726e..f21afe32c09 100644 --- a/src/Psalm/Type/Atomic/TClosure.php +++ b/src/Psalm/Type/Atomic/TClosure.php @@ -2,6 +2,16 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; +use Psalm\Storage\FunctionLikeParameter; +use Psalm\Type\Atomic; +use Psalm\Type\Union; + +use function array_merge; +use function strtolower; + /** * Represents a closure where we know the return type and params */ @@ -12,8 +22,129 @@ final class TClosure extends TNamedObject /** @var array */ public $byref_uses = []; + /** + * @param list $params + * @param array $byref_uses + * @param array $extra_types + */ + public function __construct( + string $value = 'callable', + ?array $params = null, + ?Union $return_type = null, + ?bool $is_pure = null, + array $byref_uses = [], + array $extra_types = [] + ) { + $this->value = $value; + $this->params = $params; + $this->return_type = $return_type; + $this->is_pure = $is_pure; + $this->byref_uses = $byref_uses; + $this->extra_types = $extra_types; + } + public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { return false; } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $replaced = $this->replaceCallableClassLike($old, $new); + $intersection = $this->replaceIntersectionClassLike($old, $new); + if (!$replaced && !$intersection) { + return $this; + } + return new static( + strtolower($this->value) === $old ? $new : $this->value, + $replaced[0] ?? $this->params, + $replaced[1] ?? $this->return_type, + $this->is_pure, + $this->byref_uses, + $intersection ?? $this->extra_types + ); + } + + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes( + TemplateResult $template_result, + ?Codebase $codebase + ): self { + $replaced = $this->replaceCallableTemplateTypesWithArgTypes($template_result, $codebase); + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + if (!$replaced && !$intersection) { + return $this; + } + return new static( + $this->value, + $replaced[0] ?? $this->params, + $replaced[1] ?? $this->return_type, + $this->is_pure, + $this->byref_uses, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $replaced = $this->replaceCallableTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$replaced && !$intersection) { + return $this; + } + return new static( + $this->value, + $replaced[0] ?? $this->params, + $replaced[1] ?? $this->return_type, + $this->is_pure, + $this->byref_uses, + $intersection ?? $this->extra_types + ); + } + + public function getChildNodes(): array + { + return array_merge(parent::getChildNodes(), $this->getCallableChildNodes()); + } } diff --git a/src/Psalm/Type/Atomic/TConditional.php b/src/Psalm/Type/Atomic/TConditional.php index 9b23b966450..e9caef03ddc 100644 --- a/src/Psalm/Type/Atomic/TConditional.php +++ b/src/Psalm/Type/Atomic/TConditional.php @@ -124,14 +124,28 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->conditional_type = TemplateInferredTypeReplacer::replace( + ): self { + $conditional = TemplateInferredTypeReplacer::replace( $this->conditional_type, $template_result, $codebase ); + if ($conditional === $this->conditional_type) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $this->as_type, + $conditional, + $this->if_type, + $this->else_type + ); } } diff --git a/src/Psalm/Type/Atomic/TDependentGetClass.php b/src/Psalm/Type/Atomic/TDependentGetClass.php index b1aacc2ceb0..c1b1b91fd41 100644 --- a/src/Psalm/Type/Atomic/TDependentGetClass.php +++ b/src/Psalm/Type/Atomic/TDependentGetClass.php @@ -2,7 +2,6 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; use Psalm\Type\Union; /** @@ -51,7 +50,7 @@ public function getVarId(): string return $this->typeof; } - public function getReplacement(): Atomic + public function getReplacement(): TClassString { return new TClassString(); } diff --git a/src/Psalm/Type/Atomic/TDependentGetDebugType.php b/src/Psalm/Type/Atomic/TDependentGetDebugType.php index aa51ceb6f84..e732cd3dbbe 100644 --- a/src/Psalm/Type/Atomic/TDependentGetDebugType.php +++ b/src/Psalm/Type/Atomic/TDependentGetDebugType.php @@ -2,8 +2,6 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; - /** * Represents a string whose value is that of a type found by get_debug_type($var) */ @@ -34,7 +32,7 @@ public function getVarId(): string return $this->typeof; } - public function getReplacement(): Atomic + public function getReplacement(): TString { return new TString(); } diff --git a/src/Psalm/Type/Atomic/TDependentListKey.php b/src/Psalm/Type/Atomic/TDependentListKey.php index 22f2e1c95bc..076217fe8c0 100644 --- a/src/Psalm/Type/Atomic/TDependentListKey.php +++ b/src/Psalm/Type/Atomic/TDependentListKey.php @@ -2,8 +2,6 @@ namespace Psalm\Type\Atomic; -use Psalm\Type\Atomic; - /** * Represents a list key created from foreach ($list as $key => $value) */ @@ -39,7 +37,7 @@ public function getAssertionString(): string return 'int'; } - public function getReplacement(): Atomic + public function getReplacement(): TInt { return new TInt(); } diff --git a/src/Psalm/Type/Atomic/TGenericObject.php b/src/Psalm/Type/Atomic/TGenericObject.php index 44f885e0757..ea0d00d343f 100644 --- a/src/Psalm/Type/Atomic/TGenericObject.php +++ b/src/Psalm/Type/Atomic/TGenericObject.php @@ -2,6 +2,9 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type\Atomic; use Psalm\Type\Union; @@ -9,6 +12,7 @@ use function count; use function implode; use function strrpos; +use function strtolower; use function substr; /** @@ -16,12 +20,10 @@ */ final class TGenericObject extends TNamedObject { - use GenericTrait; - /** - * @var non-empty-list + * @use GenericTrait> */ - public $type_params; + use GenericTrait; /** @var bool if the parameters have been remapped to another class */ public $remapped_params = false; @@ -29,15 +31,24 @@ final class TGenericObject extends TNamedObject /** * @param string $value the name of the object * @param non-empty-list $type_params + * @param array $extra_types */ - public function __construct(string $value, array $type_params) - { + public function __construct( + string $value, + array $type_params, + bool $remapped_params = false, + bool $is_static = false, + array $extra_types = [] + ) { if ($value[0] === '\\') { $value = substr($value, 1); } $this->value = $value; $this->type_params = $type_params; + $this->remapped_params = $remapped_params; + $this->is_static = $is_static; + $this->extra_types = $extra_types; } public function getKey(bool $include_extra = true): string @@ -105,6 +116,101 @@ public function getAssertionString(): string public function getChildNodes(): array { - return array_merge($this->type_params, $this->extra_types ?? []); + return array_merge(parent::getChildNodes(), $this->type_params); + } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type_params = $this->replaceTypeParamsClassLike($old, $new); + $intersection = $this->replaceIntersectionClassLike($old, $new); + if (!$type_params && !$intersection) { + return $this; + } + return new static( + strtolower($this->value) === $old ? $new : $this->value, + $type_params ?? $this->type_params, + $this->remapped_params, + $this->is_static, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $types = $this->replaceTypeParamsTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$types && !$intersection) { + return $this; + } + return new static( + $this->value, + $types ?? $this->type_params, + $this->remapped_params, + $this->is_static, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self + { + $type_params = $this->replaceTypeParamsTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + if (!$type_params && !$intersection) { + return $this; + } + return new static( + $this->value, + $type_params ?? $this->type_params, + true, + $this->is_static, + $intersection ?? $this->extra_types + ); } } diff --git a/src/Psalm/Type/Atomic/TIterable.php b/src/Psalm/Type/Atomic/TIterable.php index ed8462a5924..eca66a1b89f 100644 --- a/src/Psalm/Type/Atomic/TIterable.php +++ b/src/Psalm/Type/Atomic/TIterable.php @@ -2,11 +2,15 @@ namespace Psalm\Type\Atomic; +use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Type; use Psalm\Type\Atomic; use Psalm\Type\Union; use function array_merge; +use function array_values; use function count; use function implode; use function substr; @@ -17,12 +21,10 @@ final class TIterable extends Atomic { use HasIntersectionTrait; - use GenericTrait; - /** - * @var array{Union, Union} + * @use GenericTrait */ - public $type_params; + use GenericTrait; /** * @var string @@ -35,16 +37,18 @@ final class TIterable extends Atomic public $has_docblock_params = false; /** - * @param list $type_params + * @param array{Union, Union}|array $type_params + * @param array $extra_types */ - public function __construct(array $type_params = []) + public function __construct(array $type_params = [], array $extra_types = []) { - if (count($type_params) === 2) { + if (isset($type_params[0], $type_params[1])) { $this->has_docblock_params = true; $this->type_params = $type_params; } else { $this->type_params = [Type::getMixed(), Type::getMixed()]; } + $this->extra_types = $extra_types; } public function getKey(bool $include_extra = true): string @@ -115,6 +119,94 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool public function getChildNodes(): array { - return array_merge($this->type_params, $this->extra_types ?? []); + return array_merge($this->type_params, array_values($this->extra_types)); + } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type_params = $this->replaceTypeParamsClassLike( + $old, + $new + ); + $intersection = $this->replaceIntersectionClassLike( + $old, + $new + ); + if (!$type_params && !$intersection) { + return $this; + } + return new static( + $type_params ?? $this->type_params, + $intersection ?? $this->extra_types + ); + } + /** + * @return static + */ + public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result, ?Codebase $codebase): self + { + $type_params = $this->replaceTypeParamsTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + return new static( + $type_params ?? $this->type_params, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $types = $this->replaceTypeParamsTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if (!$types && !$intersection) { + return $this; + } + return new static( + $types ?? $this->type_params, + $intersection ?? $this->extra_types + ); } } diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 45bb4eac01f..0235fba89b3 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -77,10 +77,47 @@ class TKeyedArray extends Atomic * @param non-empty-array $properties * @param array $class_strings */ - public function __construct(array $properties, ?array $class_strings = null) - { + public function __construct( + array $properties, + ?array $class_strings = null, + bool $sealed = false, + ?Union $previous_key_type = null, + ?Union $previous_value_type = null, + bool $is_list = false + ) { $this->properties = $properties; $this->class_strings = $class_strings; + $this->sealed = $sealed; + $this->previous_key_type = $previous_key_type; + $this->previous_value_type = $previous_value_type; + $this->is_list = $is_list; + } + + /** + * @param non-empty-array $properties + * + * @return static + */ + public function setProperties(array $properties): self + { + if ($properties === $this->properties) { + return $this; + } + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; + } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $properties = $this->properties; + foreach ($properties as &$property_type) { + $property_type = $property_type->replaceClassLike($old, $new); + } + return $this->setProperties($properties); } public function getId(bool $exact = true, bool $nested = false): string @@ -276,6 +313,9 @@ public function getKey(bool $include_extra = true): string return static::KEY; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -287,10 +327,10 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $object_like = clone $this; + ): self { + $properties = $this->properties; - foreach ($this->properties as $offset => $property) { + foreach ($properties as $offset => &$property) { $input_type_param = null; if ($input_type instanceof TKeyedArray @@ -299,7 +339,7 @@ public function replaceTemplateTypesWithStandins( $input_type_param = $input_type->properties[$offset]; } - $object_like->properties[$offset] = TemplateStandinTypeReplacer::replace( + $property = TemplateStandinTypeReplacer::replace( $property, $template_result, $codebase, @@ -315,20 +355,35 @@ public function replaceTemplateTypesWithStandins( ); } - return $object_like; + if ($properties === $this->properties) { + return $this; + } + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - foreach ($this->properties as &$property) { + ): self { + $properties = $this->properties; + foreach ($properties as &$property) { $property = TemplateInferredTypeReplacer::replace( $property, $template_result, $codebase ); } + if ($properties !== $this->properties) { + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; + } + return $this; } public function getChildNodes(): array diff --git a/src/Psalm/Type/Atomic/TList.php b/src/Psalm/Type/Atomic/TList.php index 4b33028560b..63c334cf6a1 100644 --- a/src/Psalm/Type/Atomic/TList.php +++ b/src/Psalm/Type/Atomic/TList.php @@ -18,6 +18,7 @@ * - its keys are integers * - they start at 0 * - they are consecutive and go upwards (no negative int) + * */ class TList extends Atomic { @@ -37,6 +38,19 @@ public function __construct(Union $type_param) $this->type_param = $type_param; } + /** + * @return static + */ + public function replaceTypeParam(Union $type_param): self + { + if ($type_param === $this->type_param) { + return $this; + } + $cloned = clone $this; + $cloned->type_param = $type_param; + return $cloned; + } + public function getId(bool $exact = true, bool $nested = false): string { return static::KEY . '<' . $this->type_param->getId($exact) . '>'; @@ -100,6 +114,9 @@ public function getKey(bool $include_extra = true): string return 'array'; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -111,10 +128,10 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $list = clone $this; + ): self { + $cloned = null; - foreach ([Type::getInt(), $list->type_param] as $offset => $type_param) { + foreach ([Type::getInt(), $this->type_param] as $offset => $type_param) { $input_type_param = null; if (($input_type instanceof TGenericObject @@ -153,23 +170,27 @@ public function replaceTemplateTypesWithStandins( $depth + 1 ); - if ($offset === 1) { - $list->type_param = $type_param; + if ($offset === 1 && ($cloned || $this->type_param !== $type_param)) { + $cloned ??= clone $this; + $cloned->type_param = $type_param; } } - return $list; + return $cloned ?? $this; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->type_param = TemplateInferredTypeReplacer::replace( + ): self { + return $this->replaceTypeParam(TemplateInferredTypeReplacer::replace( $this->type_param, $template_result, $codebase - ); + )); } public function equals(Atomic $other_type, bool $ensure_source_equality): bool diff --git a/src/Psalm/Type/Atomic/TLiteralClassString.php b/src/Psalm/Type/Atomic/TLiteralClassString.php index 6339a5757b3..735d45ba3ac 100644 --- a/src/Psalm/Type/Atomic/TLiteralClassString.php +++ b/src/Psalm/Type/Atomic/TLiteralClassString.php @@ -61,6 +61,17 @@ public function getAssertionString(): string return $this->getKey(); } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + if (strtolower($this->value) === $old) { + return new static($new, $this->definite_class); + } + return $this; + } + /** * @param array $aliased_classes */ diff --git a/src/Psalm/Type/Atomic/TLowercaseString.php b/src/Psalm/Type/Atomic/TLowercaseString.php index a9eecb9f362..92afd724299 100644 --- a/src/Psalm/Type/Atomic/TLowercaseString.php +++ b/src/Psalm/Type/Atomic/TLowercaseString.php @@ -2,6 +2,8 @@ namespace Psalm\Type\Atomic; +/** + */ final class TLowercaseString extends TString { public function getId(bool $exact = true, bool $nested = false): string diff --git a/src/Psalm/Type/Atomic/TMixed.php b/src/Psalm/Type/Atomic/TMixed.php index 0c029645a47..1fb883003b1 100644 --- a/src/Psalm/Type/Atomic/TMixed.php +++ b/src/Psalm/Type/Atomic/TMixed.php @@ -6,6 +6,7 @@ /** * Denotes the `mixed` type, used when you don’t know the type of an expression. + * */ class TMixed extends Atomic { diff --git a/src/Psalm/Type/Atomic/TNamedObject.php b/src/Psalm/Type/Atomic/TNamedObject.php index 88643acd73f..77707a699ab 100644 --- a/src/Psalm/Type/Atomic/TNamedObject.php +++ b/src/Psalm/Type/Atomic/TNamedObject.php @@ -3,13 +3,16 @@ namespace Psalm\Type\Atomic; use Psalm\Codebase; +use Psalm\Internal\Analyzer\StatementsAnalyzer; use Psalm\Internal\Type\TemplateResult; use Psalm\Type; use Psalm\Type\Atomic; use function array_map; +use function array_values; use function implode; use function strrpos; +use function strtolower; use function substr; /** @@ -37,9 +40,14 @@ class TNamedObject extends Atomic /** * @param string $value the name of the object + * @param array $extra_types */ - public function __construct(string $value, bool $is_static = false, bool $definite_class = false) - { + public function __construct( + string $value, + bool $is_static = false, + bool $definite_class = false, + array $extra_types = [] + ) { if ($value[0] === '\\') { $value = substr($value, 1); } @@ -47,6 +55,7 @@ public function __construct(string $value, bool $is_static = false, bool $defini $this->value = $value; $this->is_static = $is_static; $this->definite_class = $definite_class; + $this->extra_types = $extra_types; } public function getKey(bool $include_extra = true): string @@ -134,15 +143,75 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return ($this->value !== 'static' && $this->is_static === false) || $analysis_php_version_id >= 8_00_00; } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $intersection = $this->replaceIntersectionClassLike($old, $new); + if (!$intersection && strtolower($this->value) !== $old) { + return $this; + } + $cloned = clone $this; + if (strtolower($cloned->value) === $old) { + $cloned->value = $new; + } + $cloned->extra_types = $intersection ?? $this->extra_types; + return $cloned; + } + + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + ): self { + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + if (!$intersection) { + return $this; + } + $cloned = clone $this; + $cloned->extra_types = $intersection; + return $cloned; } + /** + * @return static + */ + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if ($intersection) { + $cloned = clone $this; + $cloned->extra_types = $intersection; + return $cloned; + } + return $this; + } public function getChildNodes(): array { - return $this->extra_types ?? []; + return array_values($this->extra_types); } } diff --git a/src/Psalm/Type/Atomic/TNonEmptyArray.php b/src/Psalm/Type/Atomic/TNonEmptyArray.php index d81b5dfe9c8..ed24494a74d 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyArray.php +++ b/src/Psalm/Type/Atomic/TNonEmptyArray.php @@ -2,6 +2,8 @@ namespace Psalm\Type\Atomic; +use Psalm\Type\Union; + /** * Denotes array known to be non-empty of the form `non-empty-array`. * It expects an array with two elements, both union types. @@ -22,4 +24,21 @@ class TNonEmptyArray extends TArray * @var string */ public $value = 'non-empty-array'; + + /** + * @param array{Union, Union} $type_params + * @param positive-int|null $count + * @param positive-int|null $min_count + */ + public function __construct( + array $type_params, + ?int $count = null, + ?int $min_count = null, + string $value = 'non-empty-array' + ) { + $this->type_params = $type_params; + $this->count = $count; + $this->min_count = $min_count; + $this->value = $value; + } } diff --git a/src/Psalm/Type/Atomic/TNonEmptyList.php b/src/Psalm/Type/Atomic/TNonEmptyList.php index 9e6892ba855..3a73f9790f2 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyList.php +++ b/src/Psalm/Type/Atomic/TNonEmptyList.php @@ -2,6 +2,8 @@ namespace Psalm\Type\Atomic; +use Psalm\Type\Union; + /** * Represents a non-empty list */ @@ -20,6 +22,19 @@ class TNonEmptyList extends TList /** @var non-empty-lowercase-string */ public const KEY = 'non-empty-list'; + /** + * Constructs a new instance of a list + * + * @param positive-int|null $count + * @param positive-int|null $min_count + */ + public function __construct(Union $type_param, ?int $count = null, ?int $min_count = null) + { + $this->type_param = $type_param; + $this->count = $count; + $this->min_count = $min_count; + } + public function getAssertionString(): string { return 'non-empty-list'; diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index 0338bd8fc69..032029bec6f 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -39,11 +39,13 @@ final class TObjectWithProperties extends TObject * * @param array $properties * @param array $methods + * @param array $extra_types */ - public function __construct(array $properties, array $methods = []) + public function __construct(array $properties, array $methods = [], array $extra_types = []) { $this->properties = $properties; $this->methods = $methods; + $this->extra_types = $extra_types; } public function getId(bool $exact = true, bool $nested = false): string @@ -170,6 +172,9 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return true; } + /** + * @return static + */ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, @@ -181,8 +186,8 @@ public function replaceTemplateTypesWithStandins( bool $replace = true, bool $add_lower_bound = false, int $depth = 0 - ): Atomic { - $object_like = clone $this; + ): self { + $properties = []; foreach ($this->properties as $offset => $property) { $input_type_param = null; @@ -193,7 +198,7 @@ public function replaceTemplateTypesWithStandins( $input_type_param = $input_type->properties[$offset]; } - $object_like->properties[$offset] = TemplateStandinTypeReplacer::replace( + $properties[$offset] = TemplateStandinTypeReplacer::replace( $property, $template_result, $codebase, @@ -209,13 +214,51 @@ public function replaceTemplateTypesWithStandins( ); } - return $object_like; + $intersection = $this->replaceIntersectionTemplateTypesWithStandins( + $template_result, + $codebase, + $statements_analyzer, + $input_type, + $input_arg_offset, + $calling_class, + $calling_function, + $replace, + $add_lower_bound, + $depth + ); + if ($properties === $this->properties && !$intersection) { + return $this; + } + return new static($properties, $this->methods, $intersection ?? $this->extra_types); } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $properties = $this->properties; + foreach ($properties as &$property) { + $property = $property->replaceClassLike($old, $new); + } + $intersection = $this->replaceIntersectionClassLike($old, $new); + if (!$intersection && $properties === $this->properties) { + return $this; + } + return new static( + $properties, + $this->methods, + $intersection ?? $this->extra_types + ); + } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { + ): self { + $properties = $this->properties; foreach ($this->properties as &$property) { $property = TemplateInferredTypeReplacer::replace( $property, @@ -223,11 +266,23 @@ public function replaceTemplateTypesWithArgTypes( $codebase ); } + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes( + $template_result, + $codebase + ); + if ($properties === $this->properties && !$intersection) { + return $this; + } + return new static( + $properties, + $this->methods, + $intersection ?? $this->extra_types + ); } public function getChildNodes(): array { - return array_merge($this->properties, $this->extra_types !== null ? array_values($this->extra_types) : []); + return array_merge($this->properties, array_values($this->extra_types)); } public function getAssertionString(): string diff --git a/src/Psalm/Type/Atomic/TPropertiesOf.php b/src/Psalm/Type/Atomic/TPropertiesOf.php index b3e009c6c92..7cbfbdee376 100644 --- a/src/Psalm/Type/Atomic/TPropertiesOf.php +++ b/src/Psalm/Type/Atomic/TPropertiesOf.php @@ -9,8 +9,9 @@ * their apropriate types as values. * * @psalm-type TokenName = 'properties-of'|'public-properties-of'|'protected-properties-of'|'private-properties-of' + * */ -class TPropertiesOf extends Atomic +final class TPropertiesOf extends Atomic { // These should match the values of // `Psalm\Internal\Analyzer\ClassLikeAnalyzer::VISIBILITY_*`, as they are @@ -19,10 +20,6 @@ class TPropertiesOf extends Atomic public const VISIBILITY_PROTECTED = 2; public const VISIBILITY_PRIVATE = 3; - /** - * @var string - */ - public $fq_classlike_name; /** * @var TNamedObject */ @@ -45,6 +42,17 @@ public static function tokenNames(): array ]; } + /** + * @param self::VISIBILITY_*|null $visibility_filter + */ + public function __construct( + TNamedObject $classlike_type, + ?int $visibility_filter + ) { + $this->classlike_type = $classlike_type; + $this->visibility_filter = $visibility_filter; + } + /** * @param TokenName $tokenName * @return self::VISIBILITY_*|null @@ -81,16 +89,18 @@ public static function tokenNameForFilter(?int $visibility_filter): string } /** - * @param self::VISIBILITY_*|null $visibility_filter + * @return static */ - public function __construct( - string $fq_classlike_name, - TNamedObject $classlike_type, - ?int $visibility_filter - ) { - $this->fq_classlike_name = $fq_classlike_name; - $this->classlike_type = $classlike_type; - $this->visibility_filter = $visibility_filter; + public function replaceClassLike(string $old, string $new): self + { + $replaced = $this->classlike_type->replaceClassLike($old, $new); + if ($replaced === $this->classlike_type) { + return $this; + } + return new static( + $replaced, + $this->visibility_filter + ); } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php index ac3edfd4717..f408ba22b94 100644 --- a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php +++ b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php @@ -4,6 +4,8 @@ use Psalm\Type\Atomic; +/** + */ final class TTemplateIndexedAccess extends Atomic { /** diff --git a/src/Psalm/Type/Atomic/TTemplateKeyOf.php b/src/Psalm/Type/Atomic/TTemplateKeyOf.php index 754063833a7..26ef36b955b 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -81,14 +81,25 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->as = TemplateInferredTypeReplacer::replace( + ): self { + $as = TemplateInferredTypeReplacer::replace( $this->as, $template_result, $codebase ); + if ($as === $this->as) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $as + ); } } diff --git a/src/Psalm/Type/Atomic/TTemplateParam.php b/src/Psalm/Type/Atomic/TTemplateParam.php index b34d9e4d49f..eca94f16c16 100644 --- a/src/Psalm/Type/Atomic/TTemplateParam.php +++ b/src/Psalm/Type/Atomic/TTemplateParam.php @@ -8,6 +8,8 @@ use Psalm\Type\Union; use function array_map; +use function array_merge; +use function array_values; use function implode; /** @@ -32,11 +34,31 @@ final class TTemplateParam extends Atomic */ public $defining_class; - public function __construct(string $param_name, Union $extends, string $defining_class) + /** + * @param array $extra_types + */ + public function __construct(string $param_name, Union $extends, string $defining_class, array $extra_types = []) { $this->param_name = $param_name; $this->as = $extends; $this->defining_class = $defining_class; + $this->extra_types = $extra_types; + } + + /** + * @return static + */ + public function replaceAs(Union $as): self + { + if ($as === $this->as) { + return $this; + } + return new static( + $this->param_name, + $as, + $this->defining_class, + $this->extra_types + ); } public function getKey(bool $include_extra = true): string @@ -115,7 +137,7 @@ public function toNamespacedString( public function getChildNodes(): array { - return [$this->as]; + return array_merge([$this->as], array_values($this->extra_types)); } public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool @@ -123,10 +145,40 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $intersection = $this->replaceIntersectionClassLike($old, $new); + $replaced = $this->as->replaceClassLike($old, $new); + if (!$intersection && $replaced === $this->as) { + return $this; + } + return new static( + $this->param_name, + $replaced, + $this->defining_class, + $intersection ?? $this->extra_types + ); + } + + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + ): self { + $intersection = $this->replaceIntersectionTemplateTypesWithArgTypes($template_result, $codebase); + if (!$intersection) { + return $this; + } + return new static( + $this->param_name, + $this->as, + $this->defining_class, + $intersection + ); } } diff --git a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php index 5dd0f0d52c1..8caf397ee0a 100644 --- a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php +++ b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php @@ -76,14 +76,30 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->as = TemplateInferredTypeReplacer::replace( - new Union([$this->as]), - $template_result, - $codebase - )->getSingleAtomic(); + ): self { + $param = new TTemplateParam( + $this->as->param_name, + TemplateInferredTypeReplacer::replace( + new Union([$this->as]), + $template_result, + $codebase, + ), + $this->as->defining_class + ); + if ($param->as === $this->as->as) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $param, + $this->visibility_filter + ); } } diff --git a/src/Psalm/Type/Atomic/TTemplateValueOf.php b/src/Psalm/Type/Atomic/TTemplateValueOf.php index 23fe4d0711d..adcb287d559 100644 --- a/src/Psalm/Type/Atomic/TTemplateValueOf.php +++ b/src/Psalm/Type/Atomic/TTemplateValueOf.php @@ -81,14 +81,25 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } + /** + * @return static + */ public function replaceTemplateTypesWithArgTypes( TemplateResult $template_result, ?Codebase $codebase - ): void { - $this->as = TemplateInferredTypeReplacer::replace( + ): self { + $as = TemplateInferredTypeReplacer::replace( $this->as, $template_result, $codebase ); + if ($as === $this->as) { + return $this; + } + return new static( + $this->param_name, + $this->defining_class, + $as + ); } } diff --git a/src/Psalm/Type/Atomic/TTypeAlias.php b/src/Psalm/Type/Atomic/TTypeAlias.php index 8e70ff71951..58853597c99 100644 --- a/src/Psalm/Type/Atomic/TTypeAlias.php +++ b/src/Psalm/Type/Atomic/TTypeAlias.php @@ -7,6 +7,8 @@ use function array_map; use function implode; +/** + */ final class TTypeAlias extends Atomic { /** diff --git a/src/Psalm/Type/MutableUnion.php b/src/Psalm/Type/MutableUnion.php index 9703ea3b100..85df70b29da 100644 --- a/src/Psalm/Type/MutableUnion.php +++ b/src/Psalm/Type/MutableUnion.php @@ -212,11 +212,41 @@ final class MutableUnion implements TypeNode, Stringable public $different = false; /** - * @param non-empty-array $types + * @param non-empty-array $types */ - public function replaceTypes(array $types): self + public function setTypes(array $types): self { - $this->types = $types; + $this->literal_float_types = []; + $this->literal_int_types = []; + $this->literal_string_types = []; + $this->typed_class_strings = []; + + $from_docblock = false; + + $keyed_types = []; + + foreach ($types as $type) { + $key = $type->getKey(); + $keyed_types[$key] = $type; + + if ($type instanceof TLiteralInt) { + $this->literal_int_types[$key] = $type; + } elseif ($type instanceof TLiteralString) { + $this->literal_string_types[$key] = $type; + } elseif ($type instanceof TLiteralFloat) { + $this->literal_float_types[$key] = $type; + } elseif ($type instanceof TClassString + && ($type->as_type || $type instanceof TTemplateParamClass) + ) { + $this->typed_class_strings[$key] = $type; + } + + $from_docblock = $from_docblock || $type->from_docblock; + } + + $this->types = $keyed_types; + $this->from_docblock = $from_docblock; + return $this; } @@ -410,7 +440,7 @@ public function substitute($old_type, $new_type = null): self public function replaceClassLike(string $old, string $new): self { foreach ($this->types as $key => $atomic_type) { - $atomic_type->replaceClassLike($old, $new); + $atomic_type = $atomic_type->replaceClassLike($old, $new); $this->removeType($key); $this->addType($atomic_type); diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 146ce582a20..61efdd1cc9e 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -229,4 +229,13 @@ public function getBuilder(): MutableUnion } return $union; } + + public function replaceClassLike(string $old, string $new): self + { + $types = $this->types; + foreach ($types as &$atomic_type) { + $atomic_type = $atomic_type->replaceClassLike($old, $new); + } + return $types === $this->types ? $this : $this->getBuilder()->setTypes($types)->freeze(); + } } diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index e69385648bc..b7f0d053399 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -754,10 +754,7 @@ private function assertParameter(array $normalizedEntry, ReflectionParameter $pa } } - /** - * - * @psalm-suppress UndefinedMethod - */ + /** @psalm-suppress UndefinedMethod */ public function assertEntryReturnType(ReflectionFunction $function, string $entryReturnType): void { if (version_compare(PHP_VERSION, '8.1.0', '>=')) { diff --git a/tests/PropertiesOfTest.php b/tests/PropertiesOfTest.php index 9f5d3668552..77d6c248d59 100644 --- a/tests/PropertiesOfTest.php +++ b/tests/PropertiesOfTest.php @@ -16,6 +16,44 @@ class PropertiesOfTest extends TestCase public function providerValidCodeParse(): iterable { return [ + 'propertiesOfIntersection' => [ + 'code' => ' + */ + function test1($a) {} + /** + * @psalm-suppress InvalidReturnType + * @return properties-of + */ + function test2() {} + /** + * @psalm-suppress InvalidReturnType + * @return properties-of + */ + function test3() {} + + /** @var i $i */ + assert($i instanceof b); + $result1 = test1($i); + $result2 = test2(); + $result3 = test3(); + ', + 'assertions' => [ + '$result1===' => 'array{a: int}', + '$result2===' => 'array{a: int}', + '$result3===' => 'array{a: int}', + ] + ], 'publicPropertiesOf' => [ 'code' => ' [ + 'code' => 'test($container); + + if ($container->expr) { + if (random_int(0, 1)) { + self::test( + $container, + ); + } + return $container->expr; + } + return 0; + } + + private static function test( + a $_, + ): void { + } + }' + ], 'noCrashTemplateInsideGenerator' => [ 'code' => ' [ + 'code' => ' + */ + function asArray($obj) { + /** @var properties-of */ + $properties = []; + return $properties; + } + + /** @template T */ + class A { + /** @var bool */ + private $b = true; + /** @var string */ + protected $c = "c"; + + /** @param T $a */ + public function __construct(public $a) {} + } + + $obj = new A(42); + $objAsArray = asArray($obj); + ', + 'assertions' => [ + '$objAsArray===' => 'array{a: 42, b: bool, c: string}' + ] + ], 'privatePropertiesPicksPrivate' => [ 'code' => ' Date: Mon, 3 Oct 2022 11:32:15 +0200 Subject: [PATCH 138/194] Immutable assertions --- .../Statements/Expression/AssertionFinder.php | 57 ++++++++++--------- .../Statements/Expression/CallAnalyzer.php | 3 +- src/Psalm/Storage/Assertion.php | 15 +++-- src/Psalm/Storage/Assertion/Any.php | 5 +- .../Assertion/ArrayKeyDoesNotExist.php | 5 +- .../Storage/Assertion/ArrayKeyExists.php | 5 +- .../Assertion/DoesNotHaveAtLeastCount.php | 6 +- .../Assertion/DoesNotHaveExactCount.php | 5 +- .../Storage/Assertion/DoesNotHaveMethod.php | 5 +- src/Psalm/Storage/Assertion/Empty_.php | 5 +- src/Psalm/Storage/Assertion/Falsy.php | 5 +- src/Psalm/Storage/Assertion/HasArrayKey.php | 5 +- .../Storage/Assertion/HasAtLeastCount.php | 5 +- src/Psalm/Storage/Assertion/HasExactCount.php | 6 +- .../Assertion/HasIntOrStringArrayAccess.php | 5 +- src/Psalm/Storage/Assertion/HasMethod.php | 5 +- .../Assertion/HasStringArrayAccess.php | 5 +- src/Psalm/Storage/Assertion/InArray.php | 5 +- src/Psalm/Storage/Assertion/IsAClass.php | 6 +- src/Psalm/Storage/Assertion/IsClassEqual.php | 6 +- .../Storage/Assertion/IsClassNotEqual.php | 5 +- src/Psalm/Storage/Assertion/IsCountable.php | 5 +- src/Psalm/Storage/Assertion/IsEqualIsset.php | 6 +- src/Psalm/Storage/Assertion/IsGreaterThan.php | 5 +- .../Assertion/IsGreaterThanOrEqualTo.php | 5 +- src/Psalm/Storage/Assertion/IsIdentical.php | 14 +++-- src/Psalm/Storage/Assertion/IsIsset.php | 5 +- src/Psalm/Storage/Assertion/IsLessThan.php | 5 +- .../Storage/Assertion/IsLessThanOrEqualTo.php | 5 +- .../Storage/Assertion/IsLooselyEqual.php | 14 +++-- src/Psalm/Storage/Assertion/IsNotAClass.php | 5 +- .../Storage/Assertion/IsNotCountable.php | 5 +- .../Storage/Assertion/IsNotIdentical.php | 14 +++-- src/Psalm/Storage/Assertion/IsNotIsset.php | 5 +- .../Storage/Assertion/IsNotLooselyEqual.php | 14 +++-- src/Psalm/Storage/Assertion/IsNotType.php | 13 +++-- src/Psalm/Storage/Assertion/IsType.php | 13 +++-- .../Storage/Assertion/NestedAssertions.php | 5 +- src/Psalm/Storage/Assertion/NonEmpty.php | 5 +- .../Storage/Assertion/NonEmptyCountable.php | 6 +- src/Psalm/Storage/Assertion/NotInArray.php | 8 ++- .../Storage/Assertion/NotNestedAssertions.php | 5 +- .../Assertion/NotNonEmptyCountable.php | 5 +- src/Psalm/Storage/Assertion/Truthy.php | 5 +- .../Storage/ImmutableNonCloneableTrait.php | 13 +++++ src/Psalm/Storage/Possibilities.php | 3 +- tests/AlgebraTest.php | 2 +- 47 files changed, 212 insertions(+), 147 deletions(-) create mode 100644 src/Psalm/Storage/ImmutableNonCloneableTrait.php diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index d49901cc710..c1ba0f7539b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -66,6 +66,7 @@ use Psalm\Storage\Assertion\NonEmptyCountable; use Psalm\Storage\Assertion\NotNonEmptyCountable; use Psalm\Storage\Assertion\Truthy; +use Psalm\Storage\Possibilities; use Psalm\Storage\PropertyStorage; use Psalm\Type; use Psalm\Type\Atomic; @@ -803,10 +804,7 @@ public static function processFunctionCall( } } elseif ($class_exists_check_type = self::hasClassExistsCheck($expr)) { if ($first_var_name) { - $class_string_type = new TClassString(); - if ($class_exists_check_type === 1) { - $class_string_type->is_loaded = true; - } + $class_string_type = new TClassString('object', null, $class_exists_check_type === 1); $if_types[$first_var_name] = [[new IsType($class_string_type)]]; } } elseif ($class_exists_check_type = self::hasTraitExistsCheck($expr)) { @@ -819,14 +817,12 @@ public static function processFunctionCall( } } elseif (self::hasEnumExistsCheck($expr)) { if ($first_var_name) { - $class_string = new TClassString(); - $class_string->is_enum = true; + $class_string = new TClassString('object', null, false, false, true); $if_types[$first_var_name] = [[new IsType($class_string)]]; } } elseif (self::hasInterfaceExistsCheck($expr)) { if ($first_var_name) { - $class_string = new TClassString(); - $class_string->is_interface = true; + $class_string = new TClassString('object', null, false, true, false); $if_types[$first_var_name] = [[new IsType($class_string)]]; } } elseif (self::hasFunctionExistsCheck($expr)) { @@ -960,15 +956,15 @@ protected static function processCustomAssertion( foreach ($if_true_assertions as $assertion) { $if_types = []; - $assertion = clone $assertion; + $newRules = []; - foreach ($assertion->rule as $i => $rule) { + foreach ($assertion->rule as $rule) { $rule_type = $rule->getAtomicType(); if ($rule_type instanceof TClassConstant) { $codebase = $source->getCodebase(); - $assertion->rule[$i]->setAtomicType( + $newRules[] = $rule->setAtomicType( TypeExpander::expandAtomic( $codebase, $rule_type, @@ -977,9 +973,13 @@ protected static function processCustomAssertion( null )[0] ); + } else { + $newRules []= $rule; } } + $assertion = new Possibilities($assertion->var_id, $newRules); + if (is_int($assertion->var_id) && isset($expr->getArgs()[$assertion->var_id])) { if ($assertion->var_id === 0) { $var_name = $first_var_name; @@ -992,7 +992,7 @@ protected static function processCustomAssertion( } if ($var_name) { - $if_types[$var_name] = [[clone $assertion->rule[0]]]; + $if_types[$var_name] = [[$assertion->rule[0]]]; } } elseif ($assertion->var_id === '$this') { if (!$expr instanceof PhpParser\Node\Expr\MethodCall) { @@ -1012,7 +1012,7 @@ protected static function processCustomAssertion( ); if ($var_id) { - $if_types[$var_id] = [[clone $assertion->rule[0]]]; + $if_types[$var_id] = [[$assertion->rule[0]]]; } } elseif (is_string($assertion->var_id)) { $is_function = substr($assertion->var_id, -2) === '()'; @@ -1081,7 +1081,7 @@ protected static function processCustomAssertion( ); continue; } - $if_types[$assertion_var_id] = [[clone $assertion->rule[0]]]; + $if_types[$assertion_var_id] = [[$assertion->rule[0]]]; } if ($if_types) { @@ -1094,15 +1094,15 @@ protected static function processCustomAssertion( foreach ($if_false_assertions as $assertion) { $if_types = []; - $assertion = clone $assertion; + $newRules = []; - foreach ($assertion->rule as $i => $rule) { + foreach ($assertion->rule as $rule) { $rule_type = $rule->getAtomicType(); if ($rule_type instanceof TClassConstant) { $codebase = $source->getCodebase(); - $assertion->rule[$i]->setAtomicType( + $newRules []= $rule->setAtomicType( TypeExpander::expandAtomic( $codebase, $rule_type, @@ -1111,9 +1111,13 @@ protected static function processCustomAssertion( null )[0] ); + } else { + $newRules []= $rule; } } + $assertion = new Possibilities($assertion->var_id, $newRules); + if (is_int($assertion->var_id) && isset($expr->getArgs()[$assertion->var_id])) { if ($assertion->var_id === 0) { $var_name = $first_var_name; @@ -1126,7 +1130,7 @@ protected static function processCustomAssertion( } if ($var_name) { - $if_types[$var_name] = [[clone $assertion->rule[0]->getNegation()]]; + $if_types[$var_name] = [[$assertion->rule[0]->getNegation()]]; } } elseif ($assertion->var_id === '$this' && $expr instanceof PhpParser\Node\Expr\MethodCall) { $var_id = ExpressionIdentifier::getExtendedVarId( @@ -1136,7 +1140,7 @@ protected static function processCustomAssertion( ); if ($var_id) { - $if_types[$var_id] = [[clone $assertion->rule[0]->getNegation()]]; + $if_types[$var_id] = [[$assertion->rule[0]->getNegation()]]; } } elseif (is_string($assertion->var_id)) { $is_function = substr($assertion->var_id, -2) === '()'; @@ -1188,7 +1192,7 @@ protected static function processCustomAssertion( } } - $rule = clone $assertion->rule[0]->getNegation(); + $rule = $assertion->rule[0]->getNegation(); $assertion_var_id = str_replace($var_id, $arg_var_id, $assertion->var_id); @@ -1198,7 +1202,7 @@ protected static function processCustomAssertion( if (strpos($var_id, 'self::') === 0) { $var_id = $this_class_name.'::'.substr($var_id, 6); } - $if_types[$var_id] = [[clone $assertion->rule[0]->getNegation()]]; + $if_types[$var_id] = [[$assertion->rule[0]->getNegation()]]; } else { IssueBuffer::maybeAdd( new InvalidDocblock( @@ -1243,10 +1247,10 @@ protected static function getInstanceOfAssertions( if ($this_class_name && (in_array(strtolower($stmt->class->parts[0]), ['self', 'static'], true))) { - $named_object =new TNamedObject($this_class_name); + $is_static = $stmt->class->parts[0] === 'static'; + $named_object = new TNamedObject($this_class_name, $is_static); - if ($stmt->class->parts[0] === 'static') { - $named_object->is_static = true; + if ($is_static) { return [new IsIdentical($named_object)]; } @@ -3337,7 +3341,7 @@ private static function getGetclassEqualityAssertions( new IsIdentical(new TTemplateParam( $type_part->param_name, $type_part->as_type - ? new Union([clone $type_part->as_type]) + ? new Union([$type_part->as_type]) : Type::getObject(), $type_part->defining_class )) @@ -3525,8 +3529,7 @@ private static function getIsaAssertions( if ($class_node->parts === ['static']) { if ($this_class_name) { - $object = new TNamedObject($this_class_name); - $object->is_static = true; + $object = new TNamedObject($this_class_name, true); $if_types[$first_var_name] = [[new IsAClass($object, $third_arg_value === 'true')]]; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index b39e5579ce5..8640158444e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -770,8 +770,7 @@ public static function applyAssertionsToContext( continue; } - $assertion_rule = clone $assertion_rule; - $assertion_rule->setAtomicType($atomic_type); + $assertion_rule = $assertion_rule->setAtomicType($atomic_type); $orred_rules[] = $assertion_rule; } } elseif (isset($context->vars_in_scope[$assertion_var_id])) { diff --git a/src/Psalm/Storage/Assertion.php b/src/Psalm/Storage/Assertion.php index ed7dca2542d..2fcc4324bbc 100644 --- a/src/Psalm/Storage/Assertion.php +++ b/src/Psalm/Storage/Assertion.php @@ -4,12 +4,15 @@ use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ abstract class Assertion { - /** @psalm-mutation-free */ + use ImmutableNonCloneableTrait; + abstract public function getNegation(): Assertion; - /** @psalm-mutation-free */ abstract public function isNegationOf(self $assertion): bool; abstract public function __toString(): string; @@ -19,19 +22,21 @@ public function isNegation(): bool return false; } - /** @psalm-mutation-free */ public function hasEquality(): bool { return false; } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return null; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { + return $this; } } diff --git a/src/Psalm/Storage/Assertion/Any.php b/src/Psalm/Storage/Assertion/Any.php index 4aab3fda16a..c5d61036253 100644 --- a/src/Psalm/Storage/Assertion/Any.php +++ b/src/Psalm/Storage/Assertion/Any.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class Any extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return $this; @@ -17,7 +19,6 @@ public function __toString(): string return 'mixed'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php b/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php index 43a6f0bd557..e8fc33cb2ed 100644 --- a/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php +++ b/src/Psalm/Storage/Assertion/ArrayKeyDoesNotExist.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class ArrayKeyDoesNotExist extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new ArrayKeyExists(); @@ -22,7 +24,6 @@ public function __toString(): string return '!array-key-exists'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof ArrayKeyExists; diff --git a/src/Psalm/Storage/Assertion/ArrayKeyExists.php b/src/Psalm/Storage/Assertion/ArrayKeyExists.php index 0b803cd4dd9..6ef5e84e244 100644 --- a/src/Psalm/Storage/Assertion/ArrayKeyExists.php +++ b/src/Psalm/Storage/Assertion/ArrayKeyExists.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class ArrayKeyExists extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new ArrayKeyDoesNotExist(); @@ -17,7 +19,6 @@ public function __toString(): string return 'array-key-exists'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof ArrayKeyDoesNotExist; diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php b/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php index 297e61ddb83..7913cf60164 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveAtLeastCount.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class DoesNotHaveAtLeastCount extends Assertion { /** @var positive-int */ @@ -15,13 +18,11 @@ public function __construct(int $count) $this->count = $count; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new HasAtLeastCount($this->count); } - /** @psalm-mutation-free */ public function isNegation(): bool { return true; @@ -32,7 +33,6 @@ public function __toString(): string return '!has-at-least-' . $this->count; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof HasAtLeastCount && $this->count === $assertion->count; diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php b/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php index 950bf72eba6..349e1533de4 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveExactCount.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class DoesNotHaveExactCount extends Assertion { /** @var positive-int */ @@ -20,7 +23,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new HasExactCount($this->count); @@ -31,7 +33,6 @@ public function __toString(): string return '!has-exact-count-' . $this->count; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof HasExactCount && $assertion->count === $this->count; diff --git a/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php b/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php index eef48187543..64dc6fb4c95 100644 --- a/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php +++ b/src/Psalm/Storage/Assertion/DoesNotHaveMethod.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class DoesNotHaveMethod extends Assertion { public string $method; @@ -18,7 +21,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new HasMethod($this->method); @@ -29,7 +31,6 @@ public function __toString(): string return '!method-exists-' . $this->method; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof HasMethod && $assertion->method === $this->method; diff --git a/src/Psalm/Storage/Assertion/Empty_.php b/src/Psalm/Storage/Assertion/Empty_.php index 91613b0f4a2..f7e14b675b7 100644 --- a/src/Psalm/Storage/Assertion/Empty_.php +++ b/src/Psalm/Storage/Assertion/Empty_.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class Empty_ extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NonEmpty(); @@ -22,7 +24,6 @@ public function __toString(): string return '!non-empty'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof NonEmpty; diff --git a/src/Psalm/Storage/Assertion/Falsy.php b/src/Psalm/Storage/Assertion/Falsy.php index 13c85088eeb..e555c2d16ea 100644 --- a/src/Psalm/Storage/Assertion/Falsy.php +++ b/src/Psalm/Storage/Assertion/Falsy.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class Falsy extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new Truthy(); @@ -22,7 +24,6 @@ public function __toString(): string return 'falsy'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof Truthy; diff --git a/src/Psalm/Storage/Assertion/HasArrayKey.php b/src/Psalm/Storage/Assertion/HasArrayKey.php index 7442c4a39a7..713eda3fe1e 100644 --- a/src/Psalm/Storage/Assertion/HasArrayKey.php +++ b/src/Psalm/Storage/Assertion/HasArrayKey.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use UnexpectedValueException; +/** + * @psalm-immutable + */ final class HasArrayKey extends Assertion { public $key; @@ -14,7 +17,6 @@ public function __construct(string $key) $this->key = $key; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { throw new UnexpectedValueException('This should never be called'); @@ -25,7 +27,6 @@ public function __toString(): string return 'has-array-key-' . $this->key; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/HasAtLeastCount.php b/src/Psalm/Storage/Assertion/HasAtLeastCount.php index 46ec83abfeb..3a30e1d5666 100644 --- a/src/Psalm/Storage/Assertion/HasAtLeastCount.php +++ b/src/Psalm/Storage/Assertion/HasAtLeastCount.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class HasAtLeastCount extends Assertion { /** @var positive-int */ @@ -15,7 +18,6 @@ public function __construct(int $count) $this->count = $count; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new DoesNotHaveAtLeastCount($this->count); @@ -26,7 +28,6 @@ public function __toString(): string return 'has-at-least-' . $this->count; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof DoesNotHaveAtLeastCount && $this->count === $assertion->count; diff --git a/src/Psalm/Storage/Assertion/HasExactCount.php b/src/Psalm/Storage/Assertion/HasExactCount.php index 36d0400c29c..b76cfc6144e 100644 --- a/src/Psalm/Storage/Assertion/HasExactCount.php +++ b/src/Psalm/Storage/Assertion/HasExactCount.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class HasExactCount extends Assertion { /** @var positive-int */ @@ -15,13 +18,11 @@ public function __construct(int $count) $this->count = $count; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new DoesNotHaveExactCount($this->count); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; @@ -32,7 +33,6 @@ public function __toString(): string return '=has-exact-count-' . $this->count; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof DoesNotHaveExactCount && $this->count === $assertion->count; diff --git a/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php b/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php index 88620c8b0f8..4bcafad2312 100644 --- a/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php +++ b/src/Psalm/Storage/Assertion/HasIntOrStringArrayAccess.php @@ -5,9 +5,11 @@ use Psalm\Storage\Assertion; use UnexpectedValueException; +/** + * @psalm-immutable + */ final class HasIntOrStringArrayAccess extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { throw new UnexpectedValueException('This should never be called'); @@ -18,7 +20,6 @@ public function __toString(): string return 'has-string-or-int-array-access'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/HasMethod.php b/src/Psalm/Storage/Assertion/HasMethod.php index 94508472404..2aef8f39814 100644 --- a/src/Psalm/Storage/Assertion/HasMethod.php +++ b/src/Psalm/Storage/Assertion/HasMethod.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class HasMethod extends Assertion { public string $method; @@ -13,7 +16,6 @@ public function __construct(string $method) $this->method = $method; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new DoesNotHaveMethod($this->method); @@ -24,7 +26,6 @@ public function __toString(): string return 'method-exists-' . $this->method; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof DoesNotHaveMethod && $this->method === $assertion->method; diff --git a/src/Psalm/Storage/Assertion/HasStringArrayAccess.php b/src/Psalm/Storage/Assertion/HasStringArrayAccess.php index d7595287857..d4aeb63c6e8 100644 --- a/src/Psalm/Storage/Assertion/HasStringArrayAccess.php +++ b/src/Psalm/Storage/Assertion/HasStringArrayAccess.php @@ -5,9 +5,11 @@ use Psalm\Storage\Assertion; use UnexpectedValueException; +/** + * @psalm-immutable + */ final class HasStringArrayAccess extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { throw new UnexpectedValueException('This should never be called'); @@ -18,7 +20,6 @@ public function __toString(): string return 'has-string-array-access'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/InArray.php b/src/Psalm/Storage/Assertion/InArray.php index b5e1abc2ea7..ac7220da007 100644 --- a/src/Psalm/Storage/Assertion/InArray.php +++ b/src/Psalm/Storage/Assertion/InArray.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Union; +/** + * @psalm-immutable + */ final class InArray extends Assertion { public Union $type; @@ -14,7 +17,6 @@ public function __construct(Union $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NotInArray($this->type); @@ -25,7 +27,6 @@ public function __toString(): string return 'in-array-' . $this->type->getId(); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof NotInArray && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsAClass.php b/src/Psalm/Storage/Assertion/IsAClass.php index 3b6c5473952..ca5409e9268 100644 --- a/src/Psalm/Storage/Assertion/IsAClass.php +++ b/src/Psalm/Storage/Assertion/IsAClass.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsAClass extends Assertion { /** @var Atomic\TTemplateParamClass|Atomic\TNamedObject */ @@ -18,13 +21,11 @@ public function __construct(Atomic $type, bool $allow_string) $this->allow_string = $allow_string; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotAClass($this->type, $this->allow_string); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; @@ -35,7 +36,6 @@ public function __toString(): string return 'isa-' . ($this->allow_string ? 'string-' : '') . $this->type; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotAClass diff --git a/src/Psalm/Storage/Assertion/IsClassEqual.php b/src/Psalm/Storage/Assertion/IsClassEqual.php index da5307aa7f5..a7c92f2aab2 100644 --- a/src/Psalm/Storage/Assertion/IsClassEqual.php +++ b/src/Psalm/Storage/Assertion/IsClassEqual.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsClassEqual extends Assertion { public string $type; @@ -13,13 +16,11 @@ public function __construct(string $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsClassNotEqual($this->type); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; @@ -30,7 +31,6 @@ public function __toString(): string return '=get-class-' . $this->type; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsClassNotEqual && $this->type === $assertion->type; diff --git a/src/Psalm/Storage/Assertion/IsClassNotEqual.php b/src/Psalm/Storage/Assertion/IsClassNotEqual.php index c8d05dc8b77..ae4ed1329c7 100644 --- a/src/Psalm/Storage/Assertion/IsClassNotEqual.php +++ b/src/Psalm/Storage/Assertion/IsClassNotEqual.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsClassNotEqual extends Assertion { public string $type; @@ -18,7 +21,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsClassEqual($this->type); @@ -29,7 +31,6 @@ public function __toString(): string return '!=get-class-' . $this->type; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsClassEqual && $this->type === $assertion->type; diff --git a/src/Psalm/Storage/Assertion/IsCountable.php b/src/Psalm/Storage/Assertion/IsCountable.php index dea9295b28c..3933c4a13dd 100644 --- a/src/Psalm/Storage/Assertion/IsCountable.php +++ b/src/Psalm/Storage/Assertion/IsCountable.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsCountable extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotCountable(true); @@ -17,7 +19,6 @@ public function __toString(): string return 'countable'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotCountable && $assertion->is_negatable; diff --git a/src/Psalm/Storage/Assertion/IsEqualIsset.php b/src/Psalm/Storage/Assertion/IsEqualIsset.php index bded7c5ce06..8f754b375cf 100644 --- a/src/Psalm/Storage/Assertion/IsEqualIsset.php +++ b/src/Psalm/Storage/Assertion/IsEqualIsset.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsEqualIsset extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new Any(); @@ -17,13 +19,11 @@ public function __toString(): string return '=isset'; } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/IsGreaterThan.php b/src/Psalm/Storage/Assertion/IsGreaterThan.php index 131c3fc94a9..ed58ecabc99 100644 --- a/src/Psalm/Storage/Assertion/IsGreaterThan.php +++ b/src/Psalm/Storage/Assertion/IsGreaterThan.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsGreaterThan extends Assertion { public int $value; @@ -13,7 +16,6 @@ public function __construct(int $value) $this->value = $value; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsLessThanOrEqualTo($this->value); @@ -24,7 +26,6 @@ public function __toString(): string return '>' . $this->value; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsLessThanOrEqualTo && $this->value === $assertion->value; diff --git a/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php b/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php index ce8590eafc8..20a3c05d189 100644 --- a/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php +++ b/src/Psalm/Storage/Assertion/IsGreaterThanOrEqualTo.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsGreaterThanOrEqualTo extends Assertion { public int $value; @@ -18,7 +21,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsLessThan($this->value); @@ -29,7 +31,6 @@ public function __toString(): string return '!<' . $this->value; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsLessThan && $this->value === $assertion->value; diff --git a/src/Psalm/Storage/Assertion/IsIdentical.php b/src/Psalm/Storage/Assertion/IsIdentical.php index 9f6dccfae10..8730bda852d 100644 --- a/src/Psalm/Storage/Assertion/IsIdentical.php +++ b/src/Psalm/Storage/Assertion/IsIdentical.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsIdentical extends Assertion { public Atomic $type; @@ -14,7 +17,6 @@ public function __construct(Atomic $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotIdentical($this->type); @@ -25,24 +27,24 @@ public function __toString(): string return '=' . $this->type->getAssertionString(); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotIdentical && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsIsset.php b/src/Psalm/Storage/Assertion/IsIsset.php index 7b5fd06423b..219649181a1 100644 --- a/src/Psalm/Storage/Assertion/IsIsset.php +++ b/src/Psalm/Storage/Assertion/IsIsset.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsIsset extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotIsset(); @@ -17,7 +19,6 @@ public function __toString(): string return 'isset'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotIsset; diff --git a/src/Psalm/Storage/Assertion/IsLessThan.php b/src/Psalm/Storage/Assertion/IsLessThan.php index 0f26f5c53e1..8d78d4d4669 100644 --- a/src/Psalm/Storage/Assertion/IsLessThan.php +++ b/src/Psalm/Storage/Assertion/IsLessThan.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsLessThan extends Assertion { public int $value; @@ -13,7 +16,6 @@ public function __construct(int $value) $this->value = $value; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsGreaterThanOrEqualTo($this->value); @@ -24,7 +26,6 @@ public function __toString(): string return '<' . $this->value; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsGreaterThanOrEqualTo && $this->value === $assertion->value; diff --git a/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php b/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php index 36afbe8e282..2dd565915a1 100644 --- a/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php +++ b/src/Psalm/Storage/Assertion/IsLessThanOrEqualTo.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsLessThanOrEqualTo extends Assertion { public int $value; @@ -13,7 +16,6 @@ public function __construct(int $value) $this->value = $value; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsGreaterThan($this->value); @@ -29,7 +31,6 @@ public function __toString(): string return '!>' . $this->value; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsGreaterThan && $this->value === $assertion->value; diff --git a/src/Psalm/Storage/Assertion/IsLooselyEqual.php b/src/Psalm/Storage/Assertion/IsLooselyEqual.php index 35b1e6d6259..c0b23dbde7c 100644 --- a/src/Psalm/Storage/Assertion/IsLooselyEqual.php +++ b/src/Psalm/Storage/Assertion/IsLooselyEqual.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsLooselyEqual extends Assertion { public Atomic $type; @@ -14,7 +17,6 @@ public function __construct(Atomic $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotLooselyEqual($this->type); @@ -25,24 +27,24 @@ public function __toString(): string return '~' . $this->type->getAssertionString(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotLooselyEqual && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsNotAClass.php b/src/Psalm/Storage/Assertion/IsNotAClass.php index 45cbd3442ef..7c542e1684c 100644 --- a/src/Psalm/Storage/Assertion/IsNotAClass.php +++ b/src/Psalm/Storage/Assertion/IsNotAClass.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsNotAClass extends Assertion { /** @var Atomic\TTemplateParamClass|Atomic\TNamedObject */ @@ -23,7 +26,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsAClass($this->type, $this->allow_string); @@ -34,7 +36,6 @@ public function __toString(): string return 'isa-' . ($this->allow_string ? 'string-' : '') . $this->type; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsAClass diff --git a/src/Psalm/Storage/Assertion/IsNotCountable.php b/src/Psalm/Storage/Assertion/IsNotCountable.php index b09a7a7dcdc..c76fe24e26e 100644 --- a/src/Psalm/Storage/Assertion/IsNotCountable.php +++ b/src/Psalm/Storage/Assertion/IsNotCountable.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsNotCountable extends Assertion { public $is_negatable; @@ -18,7 +21,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsCountable(); @@ -29,7 +31,6 @@ public function __toString(): string return '!countable'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsCountable; diff --git a/src/Psalm/Storage/Assertion/IsNotIdentical.php b/src/Psalm/Storage/Assertion/IsNotIdentical.php index 2c023300a02..978ca956df6 100644 --- a/src/Psalm/Storage/Assertion/IsNotIdentical.php +++ b/src/Psalm/Storage/Assertion/IsNotIdentical.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsNotIdentical extends Assertion { public Atomic $type; @@ -19,13 +22,11 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsIdentical($this->type); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; @@ -36,18 +37,19 @@ public function __toString(): string return '!=' . $this->type->getAssertionString(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsIdentical && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsNotIsset.php b/src/Psalm/Storage/Assertion/IsNotIsset.php index defa8479a32..d42486326e6 100644 --- a/src/Psalm/Storage/Assertion/IsNotIsset.php +++ b/src/Psalm/Storage/Assertion/IsNotIsset.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class IsNotIsset extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsIsset(); @@ -22,7 +24,6 @@ public function __toString(): string return '!isset'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotIsset; diff --git a/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php b/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php index bcf77c78514..846afb19075 100644 --- a/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php +++ b/src/Psalm/Storage/Assertion/IsNotLooselyEqual.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsNotLooselyEqual extends Assertion { public Atomic $type; @@ -19,13 +22,11 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsLooselyEqual($this->type); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return true; @@ -36,18 +37,19 @@ public function __toString(): string return '!~' . $this->type->getAssertionString(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsLooselyEqual && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsNotType.php b/src/Psalm/Storage/Assertion/IsNotType.php index 321dd744e0c..24d1ee9c380 100644 --- a/src/Psalm/Storage/Assertion/IsNotType.php +++ b/src/Psalm/Storage/Assertion/IsNotType.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsNotType extends Assertion { public Atomic $type; @@ -19,7 +22,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsType($this->type); @@ -30,18 +32,19 @@ public function __toString(): string return '!' . $this->type->getId(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsType && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/IsType.php b/src/Psalm/Storage/Assertion/IsType.php index 2c822a79d40..501a5e06cca 100644 --- a/src/Psalm/Storage/Assertion/IsType.php +++ b/src/Psalm/Storage/Assertion/IsType.php @@ -5,6 +5,9 @@ use Psalm\Storage\Assertion; use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ final class IsType extends Assertion { public Atomic $type; @@ -14,7 +17,6 @@ public function __construct(Atomic $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new IsNotType($this->type); @@ -25,18 +27,19 @@ public function __toString(): string return $this->type->getId(); } - /** @psalm-mutation-free */ public function getAtomicType(): ?Atomic { return $this->type; } - public function setAtomicType(Atomic $type): void + /** + * @return static + */ + public function setAtomicType(Atomic $type): self { - $this->type = $type; + return new static($type); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof IsNotType && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/NestedAssertions.php b/src/Psalm/Storage/Assertion/NestedAssertions.php index c7fa0a48e62..20f56ba585f 100644 --- a/src/Psalm/Storage/Assertion/NestedAssertions.php +++ b/src/Psalm/Storage/Assertion/NestedAssertions.php @@ -8,6 +8,9 @@ use const JSON_THROW_ON_ERROR; +/** + * @psalm-immutable + */ final class NestedAssertions extends Assertion { /** @var array>> */ @@ -19,7 +22,6 @@ public function __construct(array $assertions) $this->assertions = $assertions; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NotNestedAssertions($this->assertions); @@ -30,7 +32,6 @@ public function __toString(): string return '@' . json_encode($this->assertions, JSON_THROW_ON_ERROR); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/NonEmpty.php b/src/Psalm/Storage/Assertion/NonEmpty.php index bb662d9591f..b45076d90c6 100644 --- a/src/Psalm/Storage/Assertion/NonEmpty.php +++ b/src/Psalm/Storage/Assertion/NonEmpty.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class NonEmpty extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new Empty_(); @@ -17,7 +19,6 @@ public function __toString(): string return 'non-empty'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof Empty_; diff --git a/src/Psalm/Storage/Assertion/NonEmptyCountable.php b/src/Psalm/Storage/Assertion/NonEmptyCountable.php index c42eca24d57..8c191d22c0a 100644 --- a/src/Psalm/Storage/Assertion/NonEmptyCountable.php +++ b/src/Psalm/Storage/Assertion/NonEmptyCountable.php @@ -4,6 +4,9 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class NonEmptyCountable extends Assertion { public $is_negatable; @@ -13,13 +16,11 @@ public function __construct(bool $is_negatable) $this->is_negatable = $is_negatable; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return $this->is_negatable ? new NotNonEmptyCountable() : new Any(); } - /** @psalm-mutation-free */ public function hasEquality(): bool { return !$this->is_negatable; @@ -30,7 +31,6 @@ public function __toString(): string return ($this->is_negatable ? '' : '=') . 'non-empty-countable'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $this->is_negatable && $assertion instanceof NotNonEmptyCountable; diff --git a/src/Psalm/Storage/Assertion/NotInArray.php b/src/Psalm/Storage/Assertion/NotInArray.php index 2dbbb71161b..73c352d47dd 100644 --- a/src/Psalm/Storage/Assertion/NotInArray.php +++ b/src/Psalm/Storage/Assertion/NotInArray.php @@ -5,8 +5,14 @@ use Psalm\Storage\Assertion; use Psalm\Type\Union; +/** + * @psalm-immutable + */ final class NotInArray extends Assertion { + /** + * @readonly + */ public Union $type; public function __construct(Union $type) @@ -14,7 +20,6 @@ public function __construct(Union $type) $this->type = $type; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new InArray($this->type); @@ -30,7 +35,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof InArray && $this->type->getId() === $assertion->type->getId(); diff --git a/src/Psalm/Storage/Assertion/NotNestedAssertions.php b/src/Psalm/Storage/Assertion/NotNestedAssertions.php index ad6f46f9ef6..7f2d33564c3 100644 --- a/src/Psalm/Storage/Assertion/NotNestedAssertions.php +++ b/src/Psalm/Storage/Assertion/NotNestedAssertions.php @@ -8,6 +8,9 @@ use const JSON_THROW_ON_ERROR; +/** + * @psalm-immutable + */ final class NotNestedAssertions extends Assertion { /** @var array>> */ @@ -24,7 +27,6 @@ public function isNegation(): bool return true; } - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NestedAssertions($this->assertions); @@ -35,7 +37,6 @@ public function __toString(): string return '!@' . json_encode($this->assertions, JSON_THROW_ON_ERROR); } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return false; diff --git a/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php b/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php index d4a59fbd1a6..48fc743c373 100644 --- a/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php +++ b/src/Psalm/Storage/Assertion/NotNonEmptyCountable.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class NotNonEmptyCountable extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new NonEmptyCountable(true); @@ -22,7 +24,6 @@ public function __toString(): string return '!non-empty-countable'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof NonEmptyCountable && $assertion->is_negatable; diff --git a/src/Psalm/Storage/Assertion/Truthy.php b/src/Psalm/Storage/Assertion/Truthy.php index 1d92be6e158..ef853b0b3b3 100644 --- a/src/Psalm/Storage/Assertion/Truthy.php +++ b/src/Psalm/Storage/Assertion/Truthy.php @@ -4,9 +4,11 @@ use Psalm\Storage\Assertion; +/** + * @psalm-immutable + */ final class Truthy extends Assertion { - /** @psalm-mutation-free */ public function getNegation(): Assertion { return new Falsy(); @@ -17,7 +19,6 @@ public function __toString(): string return '!falsy'; } - /** @psalm-mutation-free */ public function isNegationOf(Assertion $assertion): bool { return $assertion instanceof Falsy; diff --git a/src/Psalm/Storage/ImmutableNonCloneableTrait.php b/src/Psalm/Storage/ImmutableNonCloneableTrait.php new file mode 100644 index 00000000000..7609a73a8a4 --- /dev/null +++ b/src/Psalm/Storage/ImmutableNonCloneableTrait.php @@ -0,0 +1,13 @@ +getAtomicTypes() as $atomic_type) { - $assertion = clone $assertion; - $assertion->setAtomicType($atomic_type); + $assertion = $assertion->setAtomicType($atomic_type); $assertion_rules[] = $assertion; } } else { diff --git a/tests/AlgebraTest.php b/tests/AlgebraTest.php index 6571701c436..a4b7eb8085e 100644 --- a/tests/AlgebraTest.php +++ b/tests/AlgebraTest.php @@ -83,7 +83,7 @@ public function testNegateFormulaWithUnreconcilableTerm(): void $a1 = new IsType(new TInt()); $formula = [ new Clause(['$a' => [(string)$a1 => $a1]], 1, 1), - new Clause(['$b' => [(string)$a1 => clone $a1]], 1, 2, false, false), + new Clause(['$b' => [(string)$a1 => $a1]], 1, 2, false, false), ]; $negated_formula = Algebra::negateFormula($formula); From c2ce7e63daa97a1f31a6f1baa08204cfa2ce0d89 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 3 Oct 2022 11:53:05 +0200 Subject: [PATCH 139/194] Immutable readonly 1 --- .../Expression/Call/ArgumentsAnalyzer.php | 18 +-- .../Expression/Call/FunctionCallAnalyzer.php | 2 +- .../Codebase/InternalCallMapHandler.php | 1 + src/Psalm/Internal/Codebase/Reflection.php | 1 + .../LanguageServer/Client/TextDocument.php | 2 - .../Reflector/FunctionLikeDocblockScanner.php | 1 + .../Reflector/FunctionLikeNodeScanner.php | 1 + .../PdoStatementSetFetchMode.php | 5 + .../Internal/Type/AssertionReconciler.php | 99 +++++++-------- .../Comparator/CallableTypeComparator.php | 26 ++-- .../Type/Comparator/GenericTypeComparator.php | 8 +- .../Type/Comparator/ObjectComparator.php | 15 ++- .../Type/SimpleAssertionReconciler.php | 46 +++---- .../Type/SimpleNegatedAssertionReconciler.php | 8 +- .../Type/TemplateInferredTypeReplacer.php | 2 +- .../Type/TemplateStandinTypeReplacer.php | 8 +- src/Psalm/Internal/Type/TypeCombiner.php | 20 ++- src/Psalm/Internal/Type/TypeExpander.php | 120 +++++++++--------- src/Psalm/Internal/Type/TypeParser.php | 70 +++++----- src/Psalm/Storage/FunctionLikeParameter.php | 7 +- src/Psalm/Type/Atomic/TConditional.php | 26 ++++ src/Psalm/Type/Atomic/TTemplateParam.php | 28 ++-- src/Psalm/Type/Atomic/TTypeAlias.php | 20 ++- src/Psalm/Type/Reconciler.php | 65 +++++----- .../CustomArrayMapFunctionStorageProvider.php | 18 ++- .../Config/Plugin/Hook/FooMethodProvider.php | 2 +- .../Plugin/Hook/MagicFunctionProvider.php | 2 +- tests/IfThisIsTest.php | 6 +- tests/Template/ClassTemplateExtendsTest.php | 5 +- 29 files changed, 341 insertions(+), 291 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index 533cda89821..b04175c0fe0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -551,16 +551,18 @@ private static function handleClosureArg( $function_like_params = []; foreach ($template_result->lower_bounds as $template_name => $_) { + $t = new Union([ + new TTemplateParam( + $template_name, + Type::getMixed(), + $method_id + ) + ]); $function_like_params[] = new FunctionLikeParameter( 'function', false, - new Union([ - new TTemplateParam( - $template_name, - Type::getMixed(), - $method_id - ) - ]) + $t, + $t ); } @@ -724,7 +726,7 @@ public static function checkArgumentsMatch( $codebase = $statements_analyzer->getCodebase(); if ($method_id) { - if (!$in_call_map && $method_id instanceof MethodIdentifier) { + if ($method_id instanceof MethodIdentifier) { $fq_class_name = $method_id->fq_class_name; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php index 96af6145b5d..e4cde739d8c 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallAnalyzer.php @@ -564,7 +564,7 @@ private static function handleNamedFunction( } } catch (UnexpectedValueException $e) { $function_call_info->function_params = [ - new FunctionLikeParameter('args', false, null, null, null, false, false, true) + new FunctionLikeParameter('args', false, null, null, null, null, false, false, true) ]; } } else { diff --git a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php index a41fa171f0c..5cd02209614 100644 --- a/src/Psalm/Internal/Codebase/InternalCallMapHandler.php +++ b/src/Psalm/Internal/Codebase/InternalCallMapHandler.php @@ -305,6 +305,7 @@ public static function getCallablesFromCallMap(string $function_id): ?array $arg_name, $by_reference, $param_type, + $param_type, null, null, $optional, diff --git a/src/Psalm/Internal/Codebase/Reflection.php b/src/Psalm/Internal/Codebase/Reflection.php index 893a02b7ec4..7bd20893404 100644 --- a/src/Psalm/Internal/Codebase/Reflection.php +++ b/src/Psalm/Internal/Codebase/Reflection.php @@ -343,6 +343,7 @@ private function getReflectionParamData(ReflectionParameter $param): FunctionLik $param_name, $param->isPassedByReference(), $param_type, + $param_type, null, null, $is_optional, diff --git a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php index c2b7f8cc704..380b5e0d18e 100644 --- a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php @@ -57,8 +57,6 @@ public function publishDiagnostics(string $uri, array $diagnostics): void * @param TextDocumentIdentifier $textDocument The document to get the content for * * @return Promise The document's current content - * - * @psalm-suppress MixedReturnTypeCoercion due to Psalm bug */ public function xcontent(TextDocumentIdentifier $textDocument): Promise { diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index d79bac47b01..81de78b3d9b 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -765,6 +765,7 @@ private static function improveParamsFromDocblock( null, null, null, + null, false, false, true, diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php index 9f7c130bda5..573cd1d8031 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeNodeScanner.php @@ -877,6 +877,7 @@ private function getTranslatedFunctionParam( $param->var->name, $param->byRef, $param_type, + $param_type, new CodeLocation( $this->file_scanner, $fake_method ? $stmt : $param->var, diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php index c8eba436197..75c6d475910 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/PdoStatementSetFetchMode.php @@ -52,6 +52,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'mode', false, Type::getInt(), + Type::getInt(), null, null, false @@ -66,6 +67,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'colno', false, Type::getInt(), + Type::getInt(), null, null, false @@ -77,6 +79,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'classname', false, Type::getClassString(), + Type::getClassString(), null, null, false @@ -86,6 +89,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'ctorargs', false, Type::getArray(), + Type::getArray(), null, null, true @@ -97,6 +101,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array 'object', false, Type::getObject(), + Type::getObject(), null, null, false diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index e62b04d849f..cf0426a6a3e 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -293,10 +293,6 @@ private static function refine( $old_var_type_string = $existing_var_type->getId(); - if ($new_type_part instanceof TMixed) { - return $existing_var_type; - } - $new_type_has_interface = false; if ($new_type_part->isObjectType()) { @@ -344,9 +340,10 @@ private static function refine( } if ($acceptable_atomic_types) { - $new_type_part->as = new Union($acceptable_atomic_types); - - return new Union([$new_type_part]); + $acceptable_atomic_types = count($acceptable_atomic_types) === count($existing_var_type->getAtomicTypes()) + ? $existing_var_type + : new Union($acceptable_atomic_types); + return new Union([$new_type_part->replaceAs($acceptable_atomic_types)]); } } @@ -623,10 +620,7 @@ private static function filterAtomicWithAnother( && ($codebase->interfaceExists($type_1_atomic->value) || $codebase->interfaceExists($type_2_atomic->value)) ) { - $matching_atomic_type = clone $type_2_atomic; - $matching_atomic_type->extra_types[$type_1_atomic->getKey()] = $type_1_atomic; - - return $matching_atomic_type; + return $type_2_atomic->addIntersectionType($type_1_atomic); } if ($type_2_atomic instanceof TKeyedArray @@ -687,11 +681,7 @@ private static function filterAtomicWithAnother( && $type_2_atomic->as->hasObject() && $type_1_atomic->as->hasObject() ) { - $matching_atomic_type = clone $type_2_atomic; - - $matching_atomic_type->extra_types[$type_1_atomic->getKey()] = $type_1_atomic; - - return $matching_atomic_type; + return $type_2_atomic->addIntersectionType($type_1_atomic); } //we filter both types of standard iterables @@ -831,7 +821,6 @@ private static function refineContainedAtomicWithAnother( && $type_1_atomic instanceof TTemplateParam && $type_1_atomic->as->hasObjectType() ) { - $type_1_atomic = clone $type_1_atomic; $type_1_as = self::filterTypeWithAnother( $codebase, $type_1_atomic->as, @@ -842,9 +831,7 @@ private static function refineContainedAtomicWithAnother( return null; } - $type_1_atomic->as = $type_1_as; - - return $type_1_atomic; + return $type_1_atomic->replaceAs($type_1_as); } else { return clone $type_2_atomic; } @@ -1053,18 +1040,18 @@ private static function handleLiteralEqualityWithInt( return $compatible_int_type; } - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::handleLiteralEquality( - $statements_analyzer, - $assertion, - $assertion_type, - $existing_var_atomic_type->as, - $old_var_type_string, - $var_id, - $negated, - $code_location, - $suppressed_issues + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs( + self::handleLiteralEquality( + $statements_analyzer, + $assertion, + $assertion_type, + $existing_var_atomic_type->as, + $old_var_type_string, + $var_id, + $negated, + $code_location, + $suppressed_issues + ) ); return new Union([$existing_var_atomic_type]); @@ -1195,18 +1182,18 @@ private static function handleLiteralEqualityWithString( return $literal_asserted_type_string; } - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::handleLiteralEquality( - $statements_analyzer, - $assertion, - $assertion_type, - $existing_var_atomic_type->as, - $old_var_type_string, - $var_id, - $negated, - $code_location, - $suppressed_issues + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs( + self::handleLiteralEquality( + $statements_analyzer, + $assertion, + $assertion_type, + $existing_var_atomic_type->as, + $old_var_type_string, + $var_id, + $negated, + $code_location, + $suppressed_issues + ) ); return new Union([$existing_var_atomic_type]); @@ -1337,18 +1324,18 @@ private static function handleLiteralEqualityWithFloat( return $literal_asserted_type; } - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::handleLiteralEquality( - $statements_analyzer, - $assertion, - $assertion_type, - $existing_var_atomic_type->as, - $old_var_type_string, - $var_id, - $negated, - $code_location, - $suppressed_issues + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs( + self::handleLiteralEquality( + $statements_analyzer, + $assertion, + $assertion_type, + $existing_var_atomic_type->as, + $old_var_type_string, + $var_id, + $negated, + $code_location, + $suppressed_issues + ) ); return new Union([$existing_var_atomic_type]); diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index d3a93bd60eb..14967ab6290 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -259,20 +259,20 @@ public static function getCallableFromAtomic( $params = []; foreach ($function_storage->params as $param) { - $param = clone $param; - if ($param->type) { - $param->type = TypeExpander::expandUnion( - $codebase, - $param->type, - null, - null, - null, - true, - true, - false, - false, - true + $param = $param->replaceType( + TypeExpander::expandUnion( + $codebase, + $param->type, + null, + null, + null, + true, + true, + false, + false, + true + ) ); } diff --git a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php index 6d40f771857..e99d00433a4 100644 --- a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php @@ -167,7 +167,13 @@ public static function isContainedBy( ) { // do nothing } else { - $all_types_contain = false; + if ($container_param->hasMixed() || $container_param->isArrayKey()) { + if ($atomic_comparison_result) { + $atomic_comparison_result->type_coerced_from_mixed = true; + } + } else { + $all_types_contain = false; + } if ($atomic_comparison_result) { $atomic_comparison_result->type_coerced = false; diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index a2423abf3b3..5de00062194 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -112,10 +112,8 @@ private static function getIntersectionTypes(Atomic $type_part): array return [$type_part->getKey() => $type_part]; } - $type_part = clone $type_part; - $extra_types = $type_part->extra_types; - $type_part->extra_types = []; + $type_part = $type_part->setIntersectionTypes([]); $extra_types[$type_part->getKey()] = $type_part; @@ -199,11 +197,14 @@ private static function isIntersectionShallowlyContainedBy( } if ($intersection_input_type instanceof TTemplateParam) { - $intersection_container_type = clone $intersection_container_type; - - if ($intersection_container_type instanceof TNamedObject) { + if ($intersection_container_type instanceof TNamedObject && $intersection_container_type->is_static) { // this is extra check is redundant since we're comparing to a template as type - $intersection_container_type->is_static = false; + $intersection_container_type = new TNamedObject( + $intersection_container_type->value, + false, + $intersection_container_type->definite_class, + $intersection_container_type->extra_types, + ); } return UnionTypeComparator::isContainedBy( diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 0ca73f5c665..5def9f98d27 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -105,7 +105,7 @@ public static function reconcile( int &$failed_reconciliation = Reconciler::RECONCILIATION_OK, bool $inside_loop = false ): ?Union { - if ($assertion instanceof Any) { + if ($assertion instanceof Any && $existing_var_type->hasMixed()) { return $existing_var_type; } @@ -693,11 +693,10 @@ private static function reconcileExactlyCountable( ); } elseif ($array_atomic_type instanceof TList) { $non_empty_list = new TNonEmptyList( - $array_atomic_type->type_param + $array_atomic_type->type_param, + $count ); - $non_empty_list->count = $count; - $existing_var_type->addType( $non_empty_list ); @@ -732,8 +731,6 @@ private static function reconcileHasMethod( if ($type instanceof TNamedObject && $codebase->classOrInterfaceExists($type->value) ) { - $object_types[] = $type; - if (!$codebase->methodExists($type->value . '::' . $method_name)) { $match_found = false; @@ -756,21 +753,20 @@ private static function reconcileHasMethod( } if (!$match_found) { - $obj = new TObjectWithProperties( + $type = $type->addIntersectionType(new TObjectWithProperties( [], [$method_name => $type->value . '::' . $method_name] - ); - $type->extra_types[$obj->getKey()] = $obj; + )); $did_remove_type = true; } } - } elseif ($type instanceof TObjectWithProperties) { $object_types[] = $type; - + } elseif ($type instanceof TObjectWithProperties) { if (!isset($type->methods[$method_name])) { $type->methods[$method_name] = 'object::' . $method_name; $did_remove_type = true; } + $object_types[] = $type; } elseif ($type instanceof TObject || $type instanceof TMixed) { $object_types[] = new TObjectWithProperties( [], @@ -861,9 +857,7 @@ private static function reconcileString( $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasString() || $type->as->hasMixed() || $type->as->hasScalar()) { - $type = clone $type; - - $type->as = self::reconcileString( + $type = $type->replaceAs(self::reconcileString( $assertion, $type->as, null, @@ -872,7 +866,7 @@ private static function reconcileString( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $string_types[] = $type; } @@ -1315,11 +1309,13 @@ private static function reconcileObject( $did_remove_type = true; } elseif ($type instanceof TIterable) { - $clone_type = clone $type; - - self::refineArrayKey($clone_type->type_params[0]); + $params = $type->type_params; + $params[0] = self::refineArrayKey($params[0]); - $object_types[] = new TGenericObject('Traversable', $clone_type->type_params); + $object_types[] = new TGenericObject( + 'Traversable', + $params + ); $did_remove_type = true; } else { @@ -1449,7 +1445,7 @@ private static function reconcileCountable( $did_remove_type = true; } elseif ($type instanceof TNamedObject || $type instanceof TIterable) { $countable = new TNamedObject('Countable'); - $type->extra_types[$countable->getKey()] = $countable; + $type = $type->addIntersectionType($countable); $iterable_types[] = $type; $did_remove_type = true; } else { @@ -1869,7 +1865,7 @@ private static function reconcileTraversable( $did_remove_type = true; } elseif ($type instanceof TNamedObject) { $traversable = new TNamedObject('Traversable'); - $type->extra_types[$traversable->getKey()] = $traversable; + $type = $type->addIntersectionType($traversable); $traversable_types[] = $type; $did_remove_type = true; } else { @@ -1939,11 +1935,9 @@ private static function reconcileArray( $did_remove_type = true; } elseif ($type instanceof TIterable) { - $clone_type = clone $type; - - self::refineArrayKey($clone_type->type_params[0]); - - $array_types[] = new TArray($clone_type->type_params); + $params = $type->type_params; + $params[0] = self::refineArrayKey($params[0]); + $array_types[] = new TArray($params); $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index e08262cea9b..357dc0888e9 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -1045,11 +1045,9 @@ private static function reconcileObject( $non_object_types[] = new TCallableString(); $did_remove_type = true; } elseif ($type instanceof TIterable) { - $clone_type = clone $type; - - self::refineArrayKey($clone_type->type_params[0]); - - $non_object_types[] = new TArray($clone_type->type_params); + $params = $type->type_params; + $params[0] = self::refineArrayKey($params[0]); + $non_object_types[] = new TArray($params); $did_remove_type = true; } elseif (!$type->isObjectType()) { diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index d293baecdd6..5ccd43ec185 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -383,7 +383,7 @@ private static function replaceTemplatePropertiesOf( } return new TPropertiesOf( - clone $classlike_type, + $classlike_type, $atomic_type->visibility_filter ); } diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 8ed8450b820..69292371375 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -94,6 +94,7 @@ public static function fillTemplateResult( * * This method fills in the values in $template_result based on how the various atomic types * of $union_type match up to the types inside $input_type + * */ public static function replace( Union $union_type, @@ -374,7 +375,7 @@ private static function handleAtomicStandin( } $atomic_type = new TPropertiesOf( - clone $classlike_type, + $classlike_type, $atomic_type->visibility_filter ); return [$atomic_type]; @@ -869,10 +870,9 @@ private static function handleTemplateParamStandin( || $atomic_type instanceof TIterable || $atomic_type instanceof TObjectWithProperties ) { - $atomic_type->extra_types = $extra_types; + $atomic_type = $atomic_type->setIntersectionTypes($extra_types); } elseif ($atomic_type instanceof TObject && $extra_types) { - $atomic_type = reset($extra_types); - $atomic_type->extra_types = array_slice($extra_types, 1); + $atomic_type = reset($extra_types)->setIntersectionTypes(array_slice($extra_types, 1)); } } diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index b8e3f18ca0f..9a537fac4e3 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -88,7 +88,6 @@ class TypeCombiner * @param non-empty-list $types * @param int $literal_limit any greater number of literal types than this * will be merged to a scalar - * */ public static function combine( array $types, @@ -253,10 +252,7 @@ public static function combine( if ($generic_type === 'iterable') { $new_types[] = new TIterable($generic_type_params); } else { - $generic_object = new TGenericObject($generic_type, $generic_type_params); - - /** @psalm-suppress PropertyTypeCoercion */ - $generic_object->extra_types = $combination->extra_types; + $generic_object = new TGenericObject($generic_type, $generic_type_params, false, false, $combination->extra_types); $new_types[] = $generic_object; if ($combination->named_object_types) { @@ -268,14 +264,14 @@ public static function combine( foreach ($combination->object_type_params as $generic_type => $generic_type_params) { $generic_type = substr($generic_type, 0, (int) strpos($generic_type, '<')); - $generic_object = new TGenericObject($generic_type, $generic_type_params); - - if ($combination->object_static[$generic_type] ?? false) { - $generic_object->is_static = true; - } + $generic_object = new TGenericObject( + $generic_type, + $generic_type_params, + false, + $combination->object_static[$generic_type] ?? false, + $combination->extra_types + ); - /** @psalm-suppress PropertyTypeCoercion */ - $generic_object->extra_types = $combination->extra_types; $new_types[] = $generic_object; } diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 5d831be2cef..dea98798245 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -152,7 +152,8 @@ public static function expandAtomic( if ($return_type->extra_types) { $new_intersection_types = []; - foreach ($return_type->extra_types as &$extra_type) { + $extra_types = []; + foreach ($return_type->extra_types as $extra_type) { self::expandAtomic( $codebase, $extra_type, @@ -171,13 +172,12 @@ public static function expandAtomic( $new_intersection_types, $extra_type->extra_types ); - $extra_type->extra_types = []; + $extra_type = $extra_type->setIntersectionTypes([]); } + $extra_types[$extra_type->getKey()] = $extra_type; } - if ($new_intersection_types) { - $return_type->extra_types = array_merge($return_type->extra_types, $new_intersection_types); - } + $return_type = $return_type->setIntersectionTypes(array_merge($extra_types, $new_intersection_types)); } if ($return_type instanceof TNamedObject) { @@ -239,12 +239,11 @@ public static function expandAtomic( } if ($return_type instanceof TClassConstant) { - if ($return_type->fq_classlike_name === 'self' && $self_class) { - $return_type->fq_classlike_name = $self_class; + if ($self_class) { + $return_type = $return_type->replaceClassLike('self', $self_class); } - - if ($return_type->fq_classlike_name === 'static' && $self_class) { - $return_type->fq_classlike_name = is_string($static_class_type) ? $static_class_type : $self_class; + if (is_string($static_class_type) || $self_class) { + $return_type = $return_type->replaceClassLike('static', is_string($static_class_type) ? $static_class_type : $self_class); } if ($evaluate_class_constants && $codebase->classOrInterfaceOrEnumExists($return_type->fq_classlike_name)) { @@ -644,19 +643,20 @@ private static function expandNamedObject( && ($static_class_type instanceof TNamedObject || $static_class_type instanceof TTemplateParam) ) { - $return_type = clone $return_type; - $extra_static = $static_class_type->extra_types; + $return_type_types = $return_type->getIntersectionTypes(); $cloned_static = $static_class_type->setIntersectionTypes([]); + $extra_static = $static_class_type->extra_types; if ($cloned_static->getKey(false) !== $return_type->getKey(false)) { - $return_type->extra_types[$static_class_type->getKey()] = clone $cloned_static; + $return_type_types[$cloned_static->getKey()] = $cloned_static; } foreach ($extra_static as $extra_static_type) { if ($extra_static_type->getKey(false) !== $return_type->getKey(false)) { - $return_type->extra_types[$extra_static_type->getKey()] = clone $extra_static_type; + $return_type_types[$extra_static_type->getKey()] = clone $extra_static_type; } } + $return_type = $return_type->setIntersectionTypes($return_type_types); } elseif ($return_type->is_static && is_string($static_class_type) && $final) { $return_type->value = $static_class_type; $return_type->is_static = false; @@ -678,7 +678,7 @@ private static function expandNamedObject( */ private static function expandConditional( Codebase $codebase, - TConditional $return_type, + TConditional &$return_type, ?string $self_class, $static_class_type, ?string $parent_class, @@ -703,8 +703,6 @@ private static function expandConditional( $throw_on_unresolvable_constant, ); - $return_type->as_type = $new_as_type; - if ($evaluate_conditional_types) { $assertion = null; @@ -837,52 +835,54 @@ private static function expandConditional( $codebase ); + $return_type = $return_type->replaceTypes($new_as_type); + return array_values($combined->getAtomicTypes()); } } - $return_type->conditional_type = self::expandUnion( - $codebase, - $return_type->conditional_type, - $self_class, - $static_class_type, - $parent_class, - $evaluate_class_constants, - $evaluate_conditional_types, - $final, - $expand_generic, - $expand_templates, - $throw_on_unresolvable_constant, - ); - - $return_type->if_type = self::expandUnion( - $codebase, - $return_type->if_type, - $self_class, - $static_class_type, - $parent_class, - $evaluate_class_constants, - $evaluate_conditional_types, - $final, - $expand_generic, - $expand_templates, - $throw_on_unresolvable_constant, - ); - - $return_type->else_type = self::expandUnion( - $codebase, - $return_type->else_type, - $self_class, - $static_class_type, - $parent_class, - $evaluate_class_constants, - $evaluate_conditional_types, - $final, - $expand_generic, - $expand_templates, - $throw_on_unresolvable_constant, + $return_type = $return_type->replaceTypes( + $new_as_type, + self::expandUnion( + $codebase, + $return_type->conditional_type, + $self_class, + $static_class_type, + $parent_class, + $evaluate_class_constants, + $evaluate_conditional_types, + $final, + $expand_generic, + $expand_templates, + $throw_on_unresolvable_constant, + ), + self::expandUnion( + $codebase, + $return_type->if_type, + $self_class, + $static_class_type, + $parent_class, + $evaluate_class_constants, + $evaluate_conditional_types, + $final, + $expand_generic, + $expand_templates, + $throw_on_unresolvable_constant, + ), + self::expandUnion( + $codebase, + $return_type->else_type, + $self_class, + $static_class_type, + $parent_class, + $evaluate_class_constants, + $evaluate_conditional_types, + $final, + $expand_generic, + $expand_templates, + $throw_on_unresolvable_constant, + ) ); - return [$return_type]; } @@ -997,8 +997,8 @@ private static function expandKeyOfValueOf( continue; } - if ($type_param->fq_classlike_name === 'self' && $self_class) { - $type_param->fq_classlike_name = $self_class; + if ($self_class) { + $type_param = $type_param->replaceClassLike('self', $self_class); } if ($throw_on_unresolvable_constant diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index 711375ba150..d658852d891 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -96,6 +96,7 @@ use function substr; /** + * @psalm-suppress InaccessibleProperty Allowed during construction * @internal */ class TypeParser @@ -129,10 +130,15 @@ public static function parseTokens( } else { $only_token[0] = TypeTokenizer::fixScalarTerms($only_token[0], $analysis_php_version_id); - $atomic = Atomic::create($only_token[0], $analysis_php_version_id, $template_type_map, $type_aliases); - $atomic->offset_start = 0; - $atomic->offset_end = strlen($only_token[0]); - $atomic->text = isset($only_token[2]) && $only_token[2] !== $only_token[0] ? $only_token[2] : null; + $atomic = Atomic::create( + $only_token[0], + $analysis_php_version_id, + $template_type_map, + $type_aliases, + 0, + strlen($only_token[0]), + isset($only_token[2]) && $only_token[2] !== $only_token[0] ? $only_token[2] : null + ); return new Union([$atomic]); } @@ -385,13 +391,15 @@ public static function getTypeFromTree( $atomic_type_string = TypeTokenizer::fixScalarTerms($parse_tree->value, $analysis_php_version_id); - $atomic_type = Atomic::create($atomic_type_string, $analysis_php_version_id, $template_type_map, $type_aliases); - - $atomic_type->offset_start = $parse_tree->offset_start; - $atomic_type->offset_end = $parse_tree->offset_end; - $atomic_type->text = $parse_tree->text; - - return $atomic_type; + return Atomic::create( + $atomic_type_string, + $analysis_php_version_id, + $template_type_map, + $type_aliases, + $parse_tree->offset_start, + $parse_tree->offset_end, + $parse_tree->text + ); } private static function getGenericParamClass( @@ -586,14 +594,18 @@ private static function getTypeFromGenericTree( } if ($generic_type_value === 'arraylike-object') { - $traversable = new TGenericObject('Traversable', $generic_params); $array_acccess = new TGenericObject('ArrayAccess', $generic_params); $countable = new TNamedObject('Countable'); - - $traversable->extra_types[$array_acccess->getKey()] = $array_acccess; - $traversable->extra_types[$countable->getKey()] = $countable; - - return $traversable; + return new TGenericObject( + 'Traversable', + $generic_params, + false, + false, + [ + $array_acccess->getKey() => $array_acccess, + $countable->getKey() => $countable + ] + ); } if ($generic_type_value === 'non-empty-array') { @@ -1094,7 +1106,7 @@ private static function getTypeFromIntersectionTree( $first_type = array_shift($keyed_intersection_types); if ($keyed_intersection_types) { - $first_type->extra_types = $keyed_intersection_types; + return $first_type->setIntersectionTypes($keyed_intersection_types); } } else { foreach ($intersection_types as $intersection_type) { @@ -1134,7 +1146,7 @@ private static function getTypeFromIntersectionTree( } if ($keyed_intersection_types) { - $first_type->extra_types = $keyed_intersection_types; + return $first_type->setIntersectionTypes($keyed_intersection_types); } } @@ -1194,14 +1206,12 @@ private static function getTypeFromCallableTree( $tree_type instanceof Union ? $tree_type : new Union([$tree_type]), null, null, + null, $is_optional, false, $is_variadic ); - // type is not authoritative - $param->signature_type = null; - $params[] = $param; } @@ -1360,13 +1370,13 @@ private static function getTypeFromKeyedArrayTree( return new TCallableKeyedArray($properties); } - $object_like = new TKeyedArray($properties, $class_strings); - - if ($is_tuple) { - $object_like->sealed = true; - $object_like->is_list = true; - } - - return $object_like; + return new TKeyedArray( + $properties, + $class_strings, + $is_tuple, + null, + null, + $is_tuple + ); } } diff --git a/src/Psalm/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php index f333dbeba93..d854c16f2f3 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -117,17 +117,19 @@ public function __construct( string $name, bool $by_ref, ?Union $type = null, + ?Union $signature_type = null, ?CodeLocation $location = null, ?CodeLocation $type_location = null, bool $is_optional = true, bool $is_nullable = false, bool $is_variadic = false, - $default_type = null + $default_type = null, + ?Union $out_type = null ) { $this->name = $name; $this->by_ref = $by_ref; $this->type = $type; - $this->signature_type = $type; + $this->signature_type = $signature_type; $this->is_optional = $is_optional; $this->is_nullable = $is_nullable; $this->is_variadic = $is_variadic; @@ -135,6 +137,7 @@ public function __construct( $this->type_location = $type_location; $this->signature_type_location = $type_location; $this->default_type = $default_type; + $this->out_type = $out_type; } public function getId(): string diff --git a/src/Psalm/Type/Atomic/TConditional.php b/src/Psalm/Type/Atomic/TConditional.php index e9caef03ddc..b24192b33ee 100644 --- a/src/Psalm/Type/Atomic/TConditional.php +++ b/src/Psalm/Type/Atomic/TConditional.php @@ -59,6 +59,32 @@ public function __construct( $this->else_type = $else_type; } + public function replaceTypes( + ?Union $as_type, + ?Union $conditional_type = null, + ?Union $if_type = null, + ?Union $else_type = null + ): self { + $as_type ??= $this->as_type; + $conditional_type ??= $this->conditional_type; + $if_type ??= $this->if_type; + $else_type ??= $this->else_type; + + if ($as_type === $this->as_type + && $conditional_type === $this->conditional_type + && $if_type === $this->if_type + && $else_type === $this->else_type + ) { + return $this; + } + $cloned = clone $this; + $cloned->as_type = $as_type; + $cloned->conditional_type = $conditional_type; + $cloned->if_type = $if_type; + $cloned->else_type = $else_type; + return $cloned; + } + public function __clone() { $this->conditional_type = clone $this->conditional_type; diff --git a/src/Psalm/Type/Atomic/TTemplateParam.php b/src/Psalm/Type/Atomic/TTemplateParam.php index eca94f16c16..af4e033f525 100644 --- a/src/Psalm/Type/Atomic/TTemplateParam.php +++ b/src/Psalm/Type/Atomic/TTemplateParam.php @@ -53,12 +53,9 @@ public function replaceAs(Union $as): self if ($as === $this->as) { return $this; } - return new static( - $this->param_name, - $as, - $this->defining_class, - $this->extra_types - ); + $cloned = clone $this; + $cloned->as = $as; + return $cloned; } public function getKey(bool $include_extra = true): string @@ -155,12 +152,10 @@ public function replaceClassLike(string $old, string $new): self if (!$intersection && $replaced === $this->as) { return $this; } - return new static( - $this->param_name, - $replaced, - $this->defining_class, - $intersection ?? $this->extra_types - ); + $cloned = clone $this; + $cloned->as = $replaced; + $cloned->extra_types = $intersection ?? $this->extra_types; + return $cloned; } /** @@ -174,11 +169,8 @@ public function replaceTemplateTypesWithArgTypes( if (!$intersection) { return $this; } - return new static( - $this->param_name, - $this->as, - $this->defining_class, - $intersection - ); + $cloned = clone $this; + $cloned->extra_types = $intersection; + return $cloned; } } diff --git a/src/Psalm/Type/Atomic/TTypeAlias.php b/src/Psalm/Type/Atomic/TTypeAlias.php index 58853597c99..a9da4aa8834 100644 --- a/src/Psalm/Type/Atomic/TTypeAlias.php +++ b/src/Psalm/Type/Atomic/TTypeAlias.php @@ -22,10 +22,28 @@ final class TTypeAlias extends Atomic /** @var string */ public $alias_name; - public function __construct(string $declaring_fq_classlike_name, string $alias_name) + /** + * @param array|null $extra_types + */ + public function __construct(string $declaring_fq_classlike_name, string $alias_name, ?array $extra_types = null) { $this->declaring_fq_classlike_name = $declaring_fq_classlike_name; $this->alias_name = $alias_name; + $this->extra_types = $extra_types; + } + /** + * @param array|null $extra_types + */ + public function setIntersectionTypes(?array $extra_types): self + { + if ($extra_types === $this->extra_types) { + return $this; + } + return new self( + $this->declaring_fq_classlike_name, + $this->alias_name, + $extra_types + ); } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Reconciler.php b/src/Psalm/Type/Reconciler.php index 7fd5fb055d2..c1585ac75ca 100644 --- a/src/Psalm/Type/Reconciler.php +++ b/src/Psalm/Type/Reconciler.php @@ -50,7 +50,6 @@ use Psalm\Type\Atomic\TNever; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TObject; -use Psalm\Type\Atomic\TScalar; use Psalm\Type\Atomic\TString; use Psalm\Type\Atomic\TTemplateParam; use ReflectionProperty; @@ -1137,13 +1136,11 @@ private static function adjustTKeyedArrayType( [ $array_key_offset => clone $result_type, ], - null + null, + false, + $previous_key_type->isNever() ? null : $previous_key_type, + $previous_value_type ); - - if (!$previous_key_type->isNever()) { - $base_atomic_type->previous_key_type = $previous_key_type; - } - $base_atomic_type->previous_value_type = $previous_value_type; } elseif ($base_atomic_type instanceof TList) { $previous_key_type = Type::getInt(); $previous_value_type = clone $base_atomic_type->type_param; @@ -1152,18 +1149,18 @@ private static function adjustTKeyedArrayType( [ $array_key_offset => clone $result_type, ], - null + null, + false, + $previous_key_type, + $previous_value_type, + true ); - - $base_atomic_type->is_list = true; - - $base_atomic_type->previous_key_type = $previous_key_type; - $base_atomic_type->previous_value_type = $previous_value_type; } elseif ($base_atomic_type instanceof TClassStringMap) { // do nothing } else { - $base_atomic_type = clone $base_atomic_type; - $base_atomic_type->properties[$array_key_offset] = clone $result_type; + $properties = $base_atomic_type->properties; + $properties[$array_key_offset] = clone $result_type; + $base_atomic_type = $base_atomic_type->setProperties($properties); } $new_base_type = $new_base_type->getBuilder()->addType($base_atomic_type)->freeze(); @@ -1186,26 +1183,34 @@ private static function adjustTKeyedArrayType( } } - protected static function refineArrayKey(Union &$key_type): void + protected static function refineArrayKey(Union $key_type): Union { - $key_type = $key_type->getBuilder(); - foreach ($key_type->getAtomicTypes() as $key => $cat) { + return self::refineArrayKeyInner($key_type) ?? $key_type; + } + private static function refineArrayKeyInner(Union $key_type): ?Union + { + $refined = false; + $types = []; + foreach ($key_type->getAtomicTypes() as $cat) { if ($cat instanceof TTemplateParam) { - self::refineArrayKey($cat->as); - $key_type->bustCache(); - } elseif ($cat instanceof TScalar || $cat instanceof TMixed) { - $key_type->removeType($key); - $key_type->addType(new TArrayKey()); - } elseif (!$cat instanceof TString && !$cat instanceof TInt) { - $key_type->removeType($key); - $key_type->addType(new TArrayKey()); + $as = self::refineArrayKeyInner($cat->as); + if ($as) { + $refined = true; + $types []= $cat->replaceAs($as); + } else { + $types []= $cat; + } + } elseif ($cat instanceof TArrayKey || $cat instanceof TString || $cat instanceof TInt) { + $types []= $cat; + } else { + $refined = true; + $types []= new TArrayKey; } } - if ($key_type->isUnionEmpty()) { - // this should ideally prompt some sort of error - $key_type->addType(new TArrayKey()); + if ($refined) { + return $key_type->getBuilder()->setTypes($types)->freeze(); } - $key_type = $key_type->freeze(); + return null; } } diff --git a/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php index d3b938f8833..22085fce489 100644 --- a/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php +++ b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php @@ -55,7 +55,8 @@ public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $e $custom_array_map_storage->params = [ ...array_map( function (TCallable $expected, int $offset) { - $param = new FunctionLikeParameter('fn' . $offset, false, new Union([$expected])); + $t = new Union([$expected]); + $param = new FunctionLikeParameter('fn' . $offset, false, $t, $t); $param->is_optional = false; return $param; @@ -71,10 +72,15 @@ function (TCallable $expected, int $offset) { private static function createLastArrayMapParam(Union $input_array_type): FunctionLikeParameter { - $last_array_map_param = new FunctionLikeParameter('input', false, $input_array_type); - $last_array_map_param->is_optional = false; - - return $last_array_map_param; + return new FunctionLikeParameter( + 'input', + false, + $input_array_type, + $input_array_type, + null, + null, + false + ); } /** @@ -107,7 +113,7 @@ private static function createExpectedCallable( int $return_template_offset = 0 ): TCallable { $expected_callable = new TCallable('callable'); - $expected_callable->params = [new FunctionLikeParameter('a', false, $input_type)]; + $expected_callable->params = [new FunctionLikeParameter('a', false, $input_type, $input_type)]; $expected_callable->return_type = new Union([ $template_provider->createTemplate('T' . $return_template_offset) ]); diff --git a/tests/Config/Plugin/Hook/FooMethodProvider.php b/tests/Config/Plugin/Hook/FooMethodProvider.php index 222a51ed537..b5d150e0326 100644 --- a/tests/Config/Plugin/Hook/FooMethodProvider.php +++ b/tests/Config/Plugin/Hook/FooMethodProvider.php @@ -43,7 +43,7 @@ public static function getMethodParams(MethodParamsProviderEvent $event): ?array { $method_name_lowercase = $event->getMethodNameLowercase(); if ($method_name_lowercase === 'magicmethod' || $method_name_lowercase === 'magicmethod2') { - return [new FunctionLikeParameter('first', false, Type::getString())]; + return [new FunctionLikeParameter('first', false, Type::getString(), Type::getString())]; } return null; diff --git a/tests/Config/Plugin/Hook/MagicFunctionProvider.php b/tests/Config/Plugin/Hook/MagicFunctionProvider.php index b9ec9ae1d6c..9ffa3706ccf 100644 --- a/tests/Config/Plugin/Hook/MagicFunctionProvider.php +++ b/tests/Config/Plugin/Hook/MagicFunctionProvider.php @@ -36,7 +36,7 @@ public static function doesFunctionExist(FunctionExistenceProviderEvent $event): */ public static function getFunctionParams(FunctionParamsProviderEvent $event): ?array { - return [new FunctionLikeParameter('first', false, Type::getString())]; + return [new FunctionLikeParameter('first', false, Type::getString(), Type::getString())]; } public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union diff --git a/tests/IfThisIsTest.php b/tests/IfThisIsTest.php index 248fe495583..38e2821f7ee 100644 --- a/tests/IfThisIsTest.php +++ b/tests/IfThisIsTest.php @@ -219,7 +219,7 @@ public function compact(): ArrayList 'ifThisIsResolveTemplateParams' => [ 'code' => ' */ + /** @var SplObjectStorage<\stdClass, string> */ $storage = new SplObjectStorage(); new SomeService($storage); $c = new \stdClass(); $storage[$c] = "hello"; - /** @psalm-suppress MixedAssignment */ $b = $storage->offsetGet($c);', 'assertions' => [ - '$b' => 'mixed', + '$b' => 'string', ], ], 'extendsArrayIterator' => [ From 79b13fab043f4354826f25dae1784f32df6b435d Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 3 Oct 2022 12:09:42 +0200 Subject: [PATCH 140/194] Cleanup --- src/Psalm/Internal/Type/AssertionReconciler.php | 8 +++++--- src/Psalm/Internal/Type/TypeCombiner.php | 8 +++++++- src/Psalm/Internal/Type/TypeExpander.php | 5 ++++- 3 files changed, 16 insertions(+), 5 deletions(-) diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index cf0426a6a3e..bb7e71ffaf2 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -340,9 +340,11 @@ private static function refine( } if ($acceptable_atomic_types) { - $acceptable_atomic_types = count($acceptable_atomic_types) === count($existing_var_type->getAtomicTypes()) - ? $existing_var_type - : new Union($acceptable_atomic_types); + $acceptable_atomic_types = + count($acceptable_atomic_types) === count($existing_var_type->getAtomicTypes()) + ? $existing_var_type + : new Union($acceptable_atomic_types) + ; return new Union([$new_type_part->replaceAs($acceptable_atomic_types)]); } } diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 9a537fac4e3..c139314cf19 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -252,7 +252,13 @@ public static function combine( if ($generic_type === 'iterable') { $new_types[] = new TIterable($generic_type_params); } else { - $generic_object = new TGenericObject($generic_type, $generic_type_params, false, false, $combination->extra_types); + $generic_object = new TGenericObject( + $generic_type, + $generic_type_params, + false, + false, + $combination->extra_types + ); $new_types[] = $generic_object; if ($combination->named_object_types) { diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index dea98798245..655bfa74562 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -243,7 +243,10 @@ public static function expandAtomic( $return_type = $return_type->replaceClassLike('self', $self_class); } if (is_string($static_class_type) || $self_class) { - $return_type = $return_type->replaceClassLike('static', is_string($static_class_type) ? $static_class_type : $self_class); + $return_type = $return_type->replaceClassLike( + 'static', + is_string($static_class_type) ? $static_class_type : $self_class + ); } if ($evaluate_class_constants && $codebase->classOrInterfaceOrEnumExists($return_type->fq_classlike_name)) { From 5984ccd908b10afb4f85e5b6ff68b78e34ec4513 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 3 Oct 2022 12:53:44 +0200 Subject: [PATCH 141/194] Fix --- src/Psalm/Internal/Type/TypeCombiner.php | 2 ++ src/Psalm/Internal/Type/TypeExpander.php | 1 + 2 files changed, 3 insertions(+) diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index c139314cf19..843673f894d 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -252,6 +252,7 @@ public static function combine( if ($generic_type === 'iterable') { $new_types[] = new TIterable($generic_type_params); } else { + /** @psalm-suppress ArgumentTypeCoercion Caused by the above assignment */ $generic_object = new TGenericObject( $generic_type, $generic_type_params, @@ -270,6 +271,7 @@ public static function combine( foreach ($combination->object_type_params as $generic_type => $generic_type_params) { $generic_type = substr($generic_type, 0, (int) strpos($generic_type, '<')); + /** @psalm-suppress ArgumentTypeCoercion */ $generic_object = new TGenericObject( $generic_type, $generic_type_params, diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 655bfa74562..ec40872af57 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -177,6 +177,7 @@ public static function expandAtomic( $extra_types[$extra_type->getKey()] = $extra_type; } + /** @psalm-suppress ArgumentTypeCoercion */ $return_type = $return_type->setIntersectionTypes(array_merge($extra_types, $new_intersection_types)); } From 45452c7125c17226580ad12be70984d2015e9dc4 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 3 Oct 2022 13:58:01 +0200 Subject: [PATCH 142/194] Immutable readonly 2 --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 6 +- .../Internal/Analyzer/ClosureAnalyzer.php | 3 +- .../Analyzer/FunctionLikeAnalyzer.php | 20 +- .../Statements/Block/ForeachAnalyzer.php | 13 +- .../Statements/Expression/ArrayAnalyzer.php | 16 +- .../Assignment/ArrayAssignmentAnalyzer.php | 5 + .../BinaryOp/ArithmeticOpAnalyzer.php | 7 +- .../Expression/BitwiseNotAnalyzer.php | 4 +- .../Expression/Call/ArgumentAnalyzer.php | 36 +++- .../Call/ArrayFunctionArgumentsAnalyzer.php | 9 +- .../Call/FunctionCallReturnTypeFetcher.php | 8 +- .../Call/Method/AtomicMethodCallAnalyzer.php | 4 +- .../Expression/Call/NewAnalyzer.php | 12 +- .../Statements/Expression/CastAnalyzer.php | 4 +- .../Expression/ClassConstAnalyzer.php | 3 +- .../Expression/Fetch/ArrayFetchAnalyzer.php | 190 ++++++++++-------- .../Expression/SimpleTypeInferer.php | 18 +- .../Expression/UnaryPlusMinusAnalyzer.php | 39 ++-- .../Codebase/ConstantTypeResolver.php | 16 +- src/Psalm/Internal/Codebase/Methods.php | 4 +- .../ArrayFilterReturnTypeProvider.php | 9 +- .../ArrayMapReturnTypeProvider.php | 11 +- .../ArrayMergeReturnTypeProvider.php | 22 +- .../ArrayUniqueReturnTypeProvider.php | 4 +- src/Psalm/Type.php | 16 +- src/Psalm/Type/Atomic/TNamedObject.php | 14 +- src/Psalm/Type/Atomic/TNonEmptyArray.php | 15 ++ src/Psalm/Type/Atomic/TNonEmptyList.php | 15 ++ .../CustomArrayMapFunctionStorageProvider.php | 14 +- tests/UnusedVariableTest.php | 10 +- 30 files changed, 294 insertions(+), 253 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index f8148f1c183..09113c949ad 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -295,8 +295,7 @@ public function analyze( $mixins = array_merge($storage->templatedMixins, $storage->namedMixins); $union = new Union($mixins); - $static_self = new TNamedObject($storage->name); - $static_self->is_static = true; + $static_self = new TNamedObject($storage->name, true); $union = TypeExpander::expandUnion( $codebase, @@ -1223,8 +1222,7 @@ static function (FunctionLikeParameter $param): PhpParser\Node\Arg { $method_context->collect_nonprivate_initializations = !$uninitialized_private_properties; $method_context->self = $fq_class_name; - $this_atomic_object_type = new TNamedObject($fq_class_name); - $this_atomic_object_type->is_static = !$storage->final; + $this_atomic_object_type = new TNamedObject($fq_class_name, !$storage->final); $method_context->vars_in_scope['$this'] = new Union([$this_atomic_object_type]); $method_context->vars_possibly_in_scope['$this'] = true; diff --git a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php index 4a3ce8a2fc3..59d1165c014 100644 --- a/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClosureAnalyzer.php @@ -92,8 +92,7 @@ public static function analyzeExpression( /** @psalm-suppress PossiblyUndefinedStringArrayOffset */ $use_context->vars_in_scope['$this'] = clone $context->vars_in_scope['$this']; } elseif ($context->self) { - $this_atomic = new TNamedObject($context->self); - $this_atomic->is_static = true; + $this_atomic = new TNamedObject($context->self, true); $use_context->vars_in_scope['$this'] = new Union([$this_atomic]); } diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index b45ef64e430..2cbe91e9228 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -1826,14 +1826,17 @@ private function getFunctionInformation( $this_object_type = new TGenericObject( $context->self, - $template_params + $template_params, + false, + !$storage->final ); } else { - $this_object_type = new TNamedObject($context->self); + $this_object_type = new TNamedObject( + $context->self, + !$storage->final + ); } - $this_object_type->is_static = !$storage->final; - if ($this->storage instanceof MethodStorage && $this->storage->if_this_is_type) { $template_result = new TemplateResult($this->getTemplateTypeMap() ?? [], []); @@ -1984,14 +1987,11 @@ private function getFunctionInformation( $closure_type = new TClosure( 'Closure', $storage->params, - $closure_return_type + $closure_return_type, + $storage instanceof FunctionStorage ? $storage->pure : null, + $storage instanceof FunctionStorage ? $storage->byref_uses : [], ); - if ($storage instanceof FunctionStorage) { - $closure_type->byref_uses = $storage->byref_uses; - $closure_type->is_pure = $storage->pure; - } - $type_provider->setType( $this->function, new Union([ diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php index 209080eefef..6939ec53888 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php @@ -550,11 +550,8 @@ public static function checkIteratorType( } } elseif ($iterator_atomic_type instanceof TIterable) { if ($iterator_atomic_type->extra_types) { - $iterator_atomic_type_copy = clone $iterator_atomic_type; - $iterator_atomic_type_copy->extra_types = []; - $iterator_atomic_types = [$iterator_atomic_type_copy]; $iterator_atomic_types = array_merge( - $iterator_atomic_types, + [$iterator_atomic_type->setIntersectionTypes([])], $iterator_atomic_type->extra_types ); } else { @@ -736,10 +733,10 @@ public static function handleIterable( bool &$has_valid_iterator ): void { if ($iterator_atomic_type->extra_types) { - $iterator_atomic_type_copy = clone $iterator_atomic_type; - $iterator_atomic_type_copy->extra_types = []; - $iterator_atomic_types = [$iterator_atomic_type_copy]; - $iterator_atomic_types = array_merge($iterator_atomic_types, $iterator_atomic_type->extra_types); + $iterator_atomic_types = array_merge( + [$iterator_atomic_type->setIntersectionTypes([])], + $iterator_atomic_type->extra_types + ); } else { $iterator_atomic_types = [$iterator_atomic_type]; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php index a3c55b1b1c0..52eb2c41d08 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ArrayAnalyzer.php @@ -120,14 +120,14 @@ public static function analyze( // if this array looks like an object-like array, let's return that instead if (count($array_creation_info->property_types) !== 0) { - $atomic_type = new TKeyedArray($array_creation_info->property_types, $array_creation_info->class_strings); - if ($array_creation_info->can_create_objectlike) { - $atomic_type->sealed = true; - } else { - $atomic_type->previous_key_type = $item_key_type ?? Type::getArrayKey(); - $atomic_type->previous_value_type = $item_value_type ?? Type::getMixed(); - } - $atomic_type->is_list = $array_creation_info->all_list; + $atomic_type = new TKeyedArray( + $array_creation_info->property_types, + $array_creation_info->class_strings, + $array_creation_info->can_create_objectlike, + $array_creation_info->can_create_objectlike ? null : ($item_key_type ?? Type::getArrayKey()), + $array_creation_info->can_create_objectlike ? null : ($item_value_type ?? Type::getMixed()), + $array_creation_info->all_list + ); $stmt_type = new Union([$atomic_type]); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index e0bd6b08401..8a3657acd25 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -340,6 +340,11 @@ private static function updateTypeWithKeyValues( && $key_value instanceof TLiteralInt && count($key_values) === 1 ) { + $count = ($type->count ?? $type->min_count) ?? 1; + if ($key_value->value >= $count) { + continue; + } + $has_matching_objectlike_property = true; $type->type_param = Type::combineUnionTypes( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index 7a85011e61d..0ce8d3c2d48 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -576,8 +576,11 @@ private static function analyzeOperands( } } - $new_keyed_array = new TKeyedArray($properties); - $new_keyed_array->sealed = $left_type_part->sealed && $right_type_part->sealed; + $new_keyed_array = new TKeyedArray( + $properties, + null, + $left_type_part->sealed && $right_type_part->sealed + ); $result_type_member = new Union([$new_keyed_array]); } else { $result_type_member = TypeCombiner::combine( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php index 8774fbf5227..0aae409e996 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BitwiseNotAnalyzer.php @@ -48,9 +48,9 @@ public static function analyze( foreach ($stmt_expr_type->getAtomicTypes() as $type_string => $type_part) { if ($type_part instanceof TInt || $type_part instanceof TString) { if ($type_part instanceof TLiteralInt) { - $type_part->value = ~$type_part->value; + $type_part = new TLiteralInt(~$type_part->value); } elseif ($type_part instanceof TLiteralString) { - $type_part->value = ~$type_part->value; + $type_part = new TLiteralString(~$type_part->value); } $acceptable_types[] = $type_part; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 34cf5152e7e..b5ac0a0b7b2 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -58,6 +58,7 @@ use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Union; use function count; @@ -1351,21 +1352,30 @@ private static function coerceValueAfterGatekeeperArgument( } if (!$input_type_changed && $param_type->from_docblock && !$input_type->hasMixed()) { - $input_type = clone $input_type; - + $types = $input_type->getAtomicTypes(); foreach ($param_type->getAtomicTypes() as $param_atomic_type) { if ($param_atomic_type instanceof TGenericObject) { - foreach ($input_type->getAtomicTypes() as $input_atomic_type) { + foreach ($types as &$input_atomic_type) { if ($input_atomic_type instanceof TGenericObject && $input_atomic_type->value === $param_atomic_type->value ) { + $new_type_params = []; foreach ($input_atomic_type->type_params as $i => $type_param) { if ($type_param->isNever() && isset($param_atomic_type->type_params[$i])) { $input_type_changed = true; - $input_atomic_type->type_params[$i] = clone $param_atomic_type->type_params[$i]; + $new_type_params[$i] = $param_atomic_type->type_params[$i]; } } + if ($new_type_params) { + $input_atomic_type = new TGenericObject( + $input_atomic_type->value, + [...$input_atomic_type->type_params, ...$new_type_params], + $input_atomic_type->remapped_params, + false, + $input_atomic_type->extra_types + ); + } } } } @@ -1374,6 +1384,8 @@ private static function coerceValueAfterGatekeeperArgument( if (!$input_type_changed) { return; } + + $input_type = new Union($types); } $var_id = ExpressionIdentifier::getVarId( @@ -1429,20 +1441,24 @@ private static function coerceValueAfterGatekeeperArgument( if ($unpack) { if ($unpacked_atomic_array instanceof TList) { - $unpacked_atomic_array = clone $unpacked_atomic_array; - $unpacked_atomic_array->type_param = $input_type; + $unpacked_atomic_array = $unpacked_atomic_array->replaceTypeParam($input_type); $context->vars_in_scope[$var_id] = new Union([$unpacked_atomic_array]); } elseif ($unpacked_atomic_array instanceof TArray) { - $unpacked_atomic_array = clone $unpacked_atomic_array; - $unpacked_atomic_array->type_params[1] = $input_type; + $unpacked_atomic_array = $unpacked_atomic_array->replaceTypeParams([ + $unpacked_atomic_array->type_params[0], + $input_type + ]); $context->vars_in_scope[$var_id] = new Union([$unpacked_atomic_array]); } elseif ($unpacked_atomic_array instanceof TKeyedArray && $unpacked_atomic_array->is_list ) { - $unpacked_atomic_array = $unpacked_atomic_array->getList(); - $unpacked_atomic_array->type_param = $input_type; + if ($unpacked_atomic_array->isNonEmpty()) { + $unpacked_atomic_array = new TNonEmptyList($input_type); + } else { + $unpacked_atomic_array = new TList($input_type); + } $context->vars_in_scope[$var_id] = new Union([$unpacked_atomic_array]); } else { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index 88b6df7a3a6..29d4234fefe 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -43,7 +43,6 @@ use function array_filter; use function array_shift; -use function array_unshift; use function assert; use function count; use function explode; @@ -300,9 +299,10 @@ public static function handleAddition( ); } else { if ($objectlike_list) { - array_unshift($objectlike_list->properties, $arg_value_type); + $properties = $objectlike_list->properties; + array_unshift($properties, $arg_value_type); - $by_ref_type = new Union([$objectlike_list]); + $by_ref_type = new Union([$objectlike_list->setProperties($properties)]); } elseif ($array_type instanceof TList) { $by_ref_type = Type::combineUnionTypes( $by_ref_type, @@ -844,9 +844,6 @@ private static function checkClosureTypeArgs( && $closure_type->return_type && $closure_param_type->hasTemplate() ) { - $closure_param_type = clone $closure_param_type; - $closure_type->return_type = clone $closure_type->return_type; - $template_result = new TemplateResult( [], [] diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php index e5be6f5244e..a084fb7380a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/FunctionCallReturnTypeFetcher.php @@ -328,9 +328,7 @@ private static function getReturnTypeFromCallMapWithArgs( $keyed_array = new TKeyedArray([ Type::getInt(), Type::getInt() - ]); - $keyed_array->sealed = true; - $keyed_array->is_list = true; + ], null, true, null, null, true); return new Union([$keyed_array]); case 'get_called_class': @@ -441,9 +439,7 @@ private static function getReturnTypeFromCallMapWithArgs( $keyed_array = new TKeyedArray([ Type::getInt(), Type::getInt() - ]); - $keyed_array->sealed = true; - $keyed_array->is_list = true; + ], null, true, null, null, true); if ((string) $first_arg_type === 'false') { return new Union([$keyed_array]); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php index 9d6401c66c0..24215578320 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -92,11 +92,11 @@ public static function analyze( $lhs_type_part->from_docblock = true; if ($lhs_type_part instanceof TNamedObject) { - $lhs_type_part->extra_types = $extra_types; + $lhs_type_part = $lhs_type_part->setIntersectionTypes($extra_types); } elseif ($lhs_type_part instanceof TObject && $extra_types) { $lhs_type_part = array_shift($extra_types); if ($extra_types) { - $lhs_type_part->extra_types = $extra_types; + $lhs_type_part = $lhs_type_part->setIntersectionTypes($extra_types); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index 324861eada9..d15daac482f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -525,11 +525,11 @@ private static function analyzeNamedConstructor( if ($generic_param_types) { $result_atomic_type = new TGenericObject( $fq_class_name, - $generic_param_types + $generic_param_types, + false, + $from_static ); - $result_atomic_type->is_static = $from_static; - $statements_analyzer->node_data->setType( $stmt, new Union([$result_atomic_type]) @@ -552,11 +552,11 @@ private static function analyzeNamedConstructor( static fn($map) => clone reset($map), $storage->template_types ) - ) + ), + false, + $from_static ); - $result_atomic_type->is_static = $from_static; - $statements_analyzer->node_data->setType( $stmt, new Union([$result_atomic_type]) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 1e7624f9feb..a922f267c65 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -253,9 +253,7 @@ public static function analyze( foreach ($stmt_expr_type->getAtomicTypes() as $type) { if ($type instanceof Scalar) { - $keyed_array = new TKeyedArray([new Union([$type])]); - $keyed_array->is_list = true; - $keyed_array->sealed = true; + $keyed_array = new TKeyedArray([new Union([$type])], null, true, null, null, true); $permissible_atomic_types[] = $keyed_array; } elseif ($type instanceof TNull) { $permissible_atomic_types[] = new TArray([Type::getNever(), Type::getNever()]); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php index 923829c562c..8360fb5ead0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php @@ -169,8 +169,7 @@ public static function analyzeFetch( } if ($first_part_lc === 'static') { - $static_named_object = new TNamedObject($fq_class_name); - $static_named_object->is_static = true; + $static_named_object = new TNamedObject($fq_class_name, true); $statements_analyzer->node_data->setType( $stmt, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index 27e89050bc4..b7ab06bd180 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -459,7 +459,7 @@ public static function taintArrayFetch( public static function getArrayAccessTypeGivenOffset( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\ArrayDimFetch $stmt, - Union &$array_type_original, + Union &$array_type, Union &$offset_type_original, bool $in_assignment, ?string $extended_var_id, @@ -467,7 +467,6 @@ public static function getArrayAccessTypeGivenOffset( PhpParser\Node\Expr $assign_value = null, Union $replacement_type = null ): Union { - $array_type = $array_type_original->getBuilder(); $offset_type = $offset_type_original->getBuilder(); $codebase = $statements_analyzer->getCodebase(); @@ -555,7 +554,10 @@ public static function getArrayAccessTypeGivenOffset( $has_valid_absolute_offset = true; } - foreach ($array_type->getAtomicTypes() as $type_string => $type) { + $types = $array_type->getAtomicTypes(); + $changed = false; + foreach ($types as $type_string => $type) { + $original_type_real = $type; $original_type = $type; if ($type instanceof TMixed @@ -580,6 +582,7 @@ public static function getArrayAccessTypeGivenOffset( } $type = clone $type->as->getSingleAtomic(); + $original_type = $type; } if ($type instanceof TNull) { @@ -629,12 +632,11 @@ public static function getArrayAccessTypeGivenOffset( $in_assignment, $type, $key_values, - $array_type, - $type_string, + $array_type->hasMixed(), $stmt, $replacement_type, $offset_type, - $original_type, + $original_type_real, $codebase, $extended_var_id, $context, @@ -645,6 +647,12 @@ public static function getArrayAccessTypeGivenOffset( $has_valid_expected_offset ); + if ($type !== $original_type) { + $changed = true; + unset($types[$type_string]); + $types[$type->getKey()] = $type; + } + continue; } @@ -695,6 +703,9 @@ public static function getArrayAccessTypeGivenOffset( $non_array_types[] = (string)$type; } } + if ($changed) { + $array_type = $array_type->getBuilder()->setTypes($types)->freeze(); + } if ($non_array_types) { if ($has_array_access) { @@ -853,7 +864,6 @@ public static function getArrayAccessTypeGivenOffset( } } - $array_type_original = $array_type->freeze(); $offset_type_original = $offset_type->freeze(); if ($array_access_type === null) { @@ -865,10 +875,6 @@ public static function getArrayAccessTypeGivenOffset( $array_access_type->by_ref = true; } - if ($in_assignment) { - $array_type->bustCache(); - } - return $array_access_type; } @@ -1078,14 +1084,14 @@ public static function handleMixedArrayAccess( /** * @param list $expected_offset_types * @param TArray|TKeyedArray|TList|TClassStringMap $type + * @param-out TArray|TKeyedArray|TList|TClassStringMap $type * @param list $key_values */ private static function handleArrayAccessOnArray( bool $in_assignment, Atomic &$type, array &$key_values, - MutableUnion $array_type, - string $type_string, + bool $hasMixed, PhpParser\Node\Expr\ArrayDimFetch $stmt, ?Union $replacement_type, MutableUnion $offset_type, @@ -1112,25 +1118,19 @@ private static function handleArrayAccessOnArray( [$previous_key_type, $previous_value_type] = $type->type_params; // ok, type becomes an TKeyedArray - $array_type->removeType($type_string); - $type = new TKeyedArray([ - $single_atomic->value => $from_mixed_array ? Type::getMixed() : Type::getNever() - ]); - if ($single_atomic instanceof TLiteralClassString) { - $type->class_strings[$single_atomic->value] = true; - } - - $type->sealed = $from_empty_array; - - if (!$from_empty_array) { - $type->previous_value_type = clone $previous_value_type; - $type->previous_key_type = clone $previous_key_type; - } - - $array_type->addType($type); + $type = new TKeyedArray( + [ + $single_atomic->value => $from_mixed_array ? Type::getMixed() : Type::getNever(), + ], + $single_atomic instanceof TLiteralClassString ? [ + $single_atomic->value => true + ] : null, + $from_empty_array, + $from_empty_array ? null : $previous_key_type, + $from_empty_array ? null : $previous_value_type, + ); } elseif (!$stmt->dim && $from_empty_array && $replacement_type) { - $array_type->removeType($type_string); - $array_type->addType(new TNonEmptyList($replacement_type)); + $type = new TNonEmptyList($replacement_type); return; } } elseif ($type instanceof TKeyedArray @@ -1138,7 +1138,9 @@ private static function handleArrayAccessOnArray( && $type->previous_value_type->isMixed() && count($key_values) === 1 ) { - $type->properties[$key_values[0]->value] = Type::getMixed(); + $properties = $type->properties; + $properties[$key_values[0]->value] = Type::getMixed(); + $type = $type->setProperties($properties); } } @@ -1149,16 +1151,29 @@ private static function handleArrayAccessOnArray( || $original_type instanceof TTemplateParam || !$offset_type->isInt()) ) { - $type = new TArray([Type::getInt(), $type->type_param]); - } - - if ($type instanceof TArray) { + $temp = new TArray([Type::getInt(), $type->type_param]); self::handleArrayAccessOnTArray( $statements_analyzer, $codebase, $context, $stmt, - $array_type, + $hasMixed, + $extended_var_id, + $temp, + $offset_type, + $in_assignment, + $expected_offset_types, + $array_access_type, + $original_type, + $has_valid_offset + ); + } elseif ($type instanceof TArray) { + self::handleArrayAccessOnTArray( + $statements_analyzer, + $codebase, + $context, + $stmt, + $hasMixed, $extended_var_id, $type, $offset_type, @@ -1205,9 +1220,8 @@ private static function handleArrayAccessOnArray( $extended_var_id, $context, $type, - $array_type, + $hasMixed, $expected_offset_types, - $type_string, $has_valid_offset ); } @@ -1219,15 +1233,16 @@ private static function handleArrayAccessOnArray( /** * @param list $expected_offset_types + * @param-out TArray $type */ private static function handleArrayAccessOnTArray( StatementsAnalyzer $statements_analyzer, Codebase $codebase, Context $context, PhpParser\Node\Expr\ArrayDimFetch $stmt, - MutableUnion $array_type, + bool $hasMixed, ?string $extended_var_id, - TArray $type, + TArray &$type, MutableUnion $offset_type, bool $in_assignment, array &$expected_offset_types, @@ -1238,9 +1253,12 @@ private static function handleArrayAccessOnTArray( // if we're assigning to an empty array with a key offset, refashion that array if ($in_assignment) { if ($type->isEmptyArray()) { - $type->type_params[0] = $offset_type->isMixed() - ? Type::getArrayKey() - : $offset_type->freeze(); + $type = $type->replaceTypeParams([ + $offset_type->isMixed() + ? Type::getArrayKey() + : $offset_type->freeze(), + $type->type_params[1] + ]); } } elseif (!$type->isEmptyArray()) { $expected_offset_type = $type->type_params[0]->hasMixed() @@ -1263,12 +1281,15 @@ private static function handleArrayAccessOnTArray( && $offset_as->param_name === $original_type->param_name && $offset_as->defining_class === $original_type->defining_class ) { - $type->type_params[1] = new Union([ - new TTemplateIndexedAccess( - $offset_as->param_name, - $templated_offset_type->param_name, - $offset_as->defining_class - ) + $type = $type->replaceTypeParams([ + $type->type_params[0], + new Union([ + new TTemplateIndexedAccess( + $offset_as->param_name, + $templated_offset_type->param_name, + $offset_as->defining_class + ) + ]) ]); $has_valid_offset = true; @@ -1347,7 +1368,7 @@ private static function handleArrayAccessOnTArray( } if (!$stmt->dim && $type instanceof TNonEmptyArray && $type->count !== null) { - $type->count++; + $type = $type->setCount($type->count+1); } $array_access_type = Type::combineUnionTypes( @@ -1356,7 +1377,7 @@ private static function handleArrayAccessOnTArray( ); if ($array_access_type->isNever() - && !$array_type->hasMixed() + && !$hasMixed && !$in_assignment && !$context->inside_isset ) { @@ -1376,7 +1397,7 @@ private static function handleArrayAccessOnTArray( private static function handleArrayAccessOnClassStringMap( Codebase $codebase, - TClassStringMap $type, + TClassStringMap &$type, MutableUnion $offset_type, ?Union $replacement_type, ?Union &$array_access_type @@ -1437,27 +1458,27 @@ private static function handleArrayAccessOnClassStringMap( ); } - $expected_value_param_get = clone $type->value_param; - $expected_value_param_get = TemplateInferredTypeReplacer::replace( - $expected_value_param_get, + $type->value_param, $template_result_get, $codebase ); if ($replacement_type) { - $expected_value_param_set = clone $type->value_param; - $replacement_type = TemplateInferredTypeReplacer::replace( $replacement_type, $template_result_set, $codebase ); - $type->value_param = Type::combineUnionTypes( - $replacement_type, - $expected_value_param_set, - $codebase + $type = new TClassStringMap( + $type->param_name, + $type->as_type, + Type::combineUnionTypes( + $replacement_type, + $type->value_param, + $codebase + ) ); } @@ -1473,6 +1494,7 @@ private static function handleArrayAccessOnClassStringMap( /** * @param list $expected_offset_types * @param list $key_values + * @param-out TArray|TKeyedArray|TList $type */ private static function handleArrayAccessOnKeyedArray( StatementsAnalyzer $statements_analyzer, @@ -1485,10 +1507,9 @@ private static function handleArrayAccessOnKeyedArray( MutableUnion $offset_type, ?string $extended_var_id, Context $context, - TKeyedArray $type, - MutableUnion $array_type, + TKeyedArray &$type, + bool $hasMixed, array &$expected_offset_types, - string $type_string, bool &$has_valid_offset ): void { $generic_key_type = $type->getGenericKeyType(); @@ -1498,27 +1519,28 @@ private static function handleArrayAccessOnKeyedArray( } if ($key_values) { + $properties = $type->properties; foreach ($key_values as $key_value) { - if (isset($type->properties[$key_value->value]) || $replacement_type) { + if (isset($properties[$key_value->value]) || $replacement_type) { $has_valid_offset = true; if ($replacement_type) { - $type->properties[$key_value->value] = Type::combineUnionTypes( - $type->properties[$key_value->value] ?? null, + $properties[$key_value->value] = Type::combineUnionTypes( + $properties[$key_value->value] ?? null, $replacement_type ); } $array_access_type = Type::combineUnionTypes( $array_access_type, - clone $type->properties[$key_value->value] + clone $properties[$key_value->value] ); } elseif ($in_assignment) { - $type->properties[$key_value->value] = new Union([new TNever]); + $properties[$key_value->value] = new Union([new TNever]); $array_access_type = Type::combineUnionTypes( $array_access_type, - clone $type->properties[$key_value->value] + clone $properties[$key_value->value] ); } elseif ($type->previous_value_type) { if ($codebase->config->ensure_array_string_offsets_exist) { @@ -1543,16 +1565,16 @@ private static function handleArrayAccessOnKeyedArray( ); } - $type->properties[$key_value->value] = clone $type->previous_value_type; + $properties[$key_value->value] = clone $type->previous_value_type; $array_access_type = clone $type->previous_value_type; - } elseif ($array_type->hasMixed()) { + } elseif ($hasMixed) { $has_valid_offset = true; $array_access_type = Type::getMixed(); } else { if ($type->sealed || !$context->inside_isset) { - $object_like_keys = array_keys($type->properties); + $object_like_keys = array_keys($properties); $last_key = array_pop($object_like_keys); @@ -1579,6 +1601,8 @@ private static function handleArrayAccessOnKeyedArray( $array_access_type = Type::getMixed(); } } + + $type = $type->setProperties($properties); } else { $key_type = $generic_key_type->hasMixed() ? Type::getArrayKey() @@ -1626,16 +1650,11 @@ private static function handleArrayAccessOnKeyedArray( if (!$stmt->dim && $property_count) { ++$property_count; - $array_type->removeType($type_string); $type = new TNonEmptyArray([ $new_key_type, $generic_params, - ]); - $array_type->addType($type); - $type->count = $property_count; + ], $property_count); } else { - $array_type->removeType($type_string); - if (!$stmt->dim && $type->is_list) { $type = new TList($generic_params); } else { @@ -1644,8 +1663,6 @@ private static function handleArrayAccessOnKeyedArray( $generic_params, ]); } - - $array_type->addType($type); } $array_access_type = Type::combineUnionTypes( @@ -1675,12 +1692,13 @@ private static function handleArrayAccessOnKeyedArray( /** * @param list $expected_offset_types * @param list $key_values + * @param-out TList $type */ private static function handleArrayAccessOnList( StatementsAnalyzer $statements_analyzer, Codebase $codebase, PhpParser\Node\Expr\ArrayDimFetch $stmt, - TList $type, + TList &$type, MutableUnion $offset_type, ?string $extended_var_id, array $key_values, @@ -1725,15 +1743,15 @@ private static function handleArrayAccessOnList( } if ($in_assignment && $type instanceof TNonEmptyList && $type->count !== null) { - $type->count++; + $type = $type->setCount($type->count+1); } if ($in_assignment && $replacement_type) { - $type->type_param = Type::combineUnionTypes( + $type = $type->replaceTypeParam(Type::combineUnionTypes( $type->type_param, $replacement_type, $codebase - ); + )); } $array_access_type = Type::combineUnionTypes( diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index 1549caeb5ce..3d94b60c310 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -430,19 +430,21 @@ public static function infer( return null; } + $new_types = []; foreach ($type_to_invert->getAtomicTypes() as $type_part) { if ($type_part instanceof TLiteralInt && $stmt instanceof PhpParser\Node\Expr\UnaryMinus ) { - $type_part->value = -$type_part->value; + $new_types []= new TLiteralInt(-$type_part->value); } elseif ($type_part instanceof TLiteralFloat && $stmt instanceof PhpParser\Node\Expr\UnaryMinus ) { - $type_part->value = -$type_part->value; + $new_types []= new TLiteralFloat(-$type_part->value); + } else { + $new_types []= $type_part; } } - - return $type_to_invert; + return new Union($new_types); } if ($stmt instanceof PhpParser\Node\Expr\ArrayDimFetch) { @@ -574,10 +576,12 @@ private static function inferArrayType( ) { $objectlike = new TKeyedArray( $array_creation_info->property_types, - $array_creation_info->class_strings + $array_creation_info->class_strings, + true, + null, + null, + $array_creation_info->all_list ); - $objectlike->sealed = true; - $objectlike->is_list = $array_creation_info->all_list; return new Union([$objectlike]); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php index b46ac3df06c..60b8e3f6593 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/UnaryPlusMinusAnalyzer.php @@ -18,6 +18,7 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TString; use Psalm\Type\Union; +use RuntimeException; /** * @internal @@ -45,39 +46,37 @@ public static function analyze( foreach ($stmt_expr_type->getAtomicTypes() as $type_part) { if ($type_part instanceof TInt || $type_part instanceof TFloat) { - if ($type_part instanceof TLiteralInt - && $stmt instanceof PhpParser\Node\Expr\UnaryMinus - ) { - $type_part->value = -$type_part->value; - } elseif ($type_part instanceof TLiteralFloat - && $stmt instanceof PhpParser\Node\Expr\UnaryMinus - ) { - $type_part->value = -$type_part->value; + if (!$stmt instanceof PhpParser\Node\Expr\UnaryMinus) { + $acceptable_types []= $type_part; + continue; } - - if ($type_part instanceof TIntRange - && $stmt instanceof PhpParser\Node\Expr\UnaryMinus - ) { + if ($type_part instanceof TLiteralInt) { + $type_part = new TLiteralInt(-$type_part->value); + } elseif ($type_part instanceof TLiteralFloat) { + $type_part = new TLiteralFloat(-$type_part->value); + } elseif ($type_part instanceof TIntRange) { //we'll have to inverse min and max bound and negate any literal $old_min_bound = $type_part->min_bound; $old_max_bound = $type_part->max_bound; if ($old_min_bound === null) { //min bound is null, max bound will be null - $type_part->max_bound = null; + $new_max_bound = null; } elseif ($old_min_bound === 0) { - $type_part->max_bound = 0; + $new_max_bound = 0; } else { - $type_part->max_bound = -$old_min_bound; + $new_max_bound = -$old_min_bound; } if ($old_max_bound === null) { //max bound is null, min bound will be null - $type_part->min_bound = null; + $new_min_bound = null; } elseif ($old_max_bound === 0) { - $type_part->min_bound = 0; + $new_min_bound = 0; } else { - $type_part->min_bound = -$old_max_bound; + $new_min_bound = -$old_max_bound; } + + $type_part = new TIntRange($new_min_bound, $new_max_bound); } $acceptable_types[] = $type_part; @@ -89,6 +88,10 @@ public static function analyze( } } + if (!$acceptable_types) { + throw new RuntimeException("Impossible!"); + } + $statements_analyzer->node_data->setType($stmt, new Union($acceptable_types)); } diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php index 88c9e03b189..e6e0fcae640 100644 --- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php +++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php @@ -139,8 +139,11 @@ public static function resolve( } if ($left instanceof TKeyedArray && $right instanceof TKeyedArray) { - $type = new TKeyedArray($left->properties + $right->properties); - $type->sealed = true; + $type = new TKeyedArray( + $left->properties + $right->properties, + null, + true + ); return $type; } @@ -263,10 +266,7 @@ public static function resolve( new Union([new TNever()]), ]); } else { - $resolved_type = new TKeyedArray($properties); - - $resolved_type->is_list = $is_list; - $resolved_type->sealed = true; + $resolved_type = new TKeyedArray($properties, null, true, null, null, $is_list); } return $resolved_type; @@ -352,9 +352,7 @@ public static function getLiteralTypeFromScalarValue($value, bool $sealed_array foreach ($value as $key => $val) { $types[$key] = new Union([self::getLiteralTypeFromScalarValue($val, $sealed_array)]); } - $type = new TKeyedArray($types); - $type->sealed = $sealed_array; - return $type; + return new TKeyedArray($types, null, $sealed_array); } if (is_string($value)) { diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index 262101a74f1..30fd46fe044 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -723,9 +723,7 @@ public function getMethodReturnType( $types[] = new Union([new TEnumCase($original_fq_class_name, $case_name)]); } - $list = new TKeyedArray($types); - $list->is_list = true; - $list->sealed = true; + $list = new TKeyedArray($types, null, true, null, null, true); return new Union([$list]); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php index 5cc17218c32..c287dbd9b3e 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php @@ -88,8 +88,6 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev if (!isset($call_args[1]) && !$first_arg_array->previous_value_type) { $had_one = count($first_arg_array->properties) === 1; - $first_arg_array = clone $first_arg_array; - $new_properties = array_filter( array_map( static function ($keyed_type) use ($statements_source, $context) { @@ -119,12 +117,7 @@ static function ($keyed_type) use ($statements_source, $context) { return Type::getEmptyArray(); } - $first_arg_array->properties = $new_properties; - - $first_arg_array->is_list = $first_arg_array->is_list && $had_one; - $first_arg_array->sealed = false; - - return new Union([$first_arg_array]); + return new Union([new TKeyedArray($new_properties, null, false, null, null, $first_arg_array->is_list && $had_one)]); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php index 5c55b900ce6..46a6bf10bd1 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php @@ -197,12 +197,13 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev array_map( static fn(Union $_): Union => clone $mapping_return_type, $array_arg_atomic_type->properties - ) + ), + null, + $array_arg_atomic_type->sealed, + $array_arg_atomic_type->previous_key_type, + $mapping_return_type, + $array_arg_atomic_type->is_list ); - $atomic_type->is_list = $array_arg_atomic_type->is_list; - $atomic_type->sealed = $array_arg_atomic_type->sealed; - $atomic_type->previous_key_type = $array_arg_atomic_type->previous_key_type; - $atomic_type->previous_value_type = $mapping_return_type; return new Union([$atomic_type]); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php index 90257156ecd..36970468665 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMergeReturnTypeProvider.php @@ -235,20 +235,14 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev && ($generic_property_count < $max_keyed_array_size * 2 || $generic_property_count < 16) ) { - $objectlike = new TKeyedArray($generic_properties); - - if ($class_strings !== []) { - $objectlike->class_strings = $class_strings; - } - - if ($all_nonempty_lists || $all_int_offsets) { - $objectlike->is_list = true; - } - - if (!$all_keyed_arrays) { - $objectlike->previous_key_type = $inner_key_type; - $objectlike->previous_value_type = $inner_value_type; - } + $objectlike = new TKeyedArray( + $generic_properties, + $class_strings ?: null, + false, + $all_keyed_arrays ? null : $inner_key_type, + $all_keyed_arrays ? null : $inner_value_type, + $all_nonempty_lists || $all_int_offsets + ); return new Union([$objectlike]); } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php index eb6ef09c806..766895b9c4e 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayUniqueReturnTypeProvider.php @@ -51,10 +51,8 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } if ($first_arg_array instanceof TArray) { - $first_arg_array = clone $first_arg_array; - if ($first_arg_array instanceof TNonEmptyArray) { - $first_arg_array->count = null; + $first_arg_array = $first_arg_array->setCount(null); } return new Union([$first_arg_array]); diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 2e9670fd6af..28e7f75ada8 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -773,24 +773,24 @@ private static function intersectAtomicTypes( .' Check the preceding code for errors.' ); } - if (!$intersection_atomic->extra_types) { - $intersection_atomic->extra_types = []; - } $intersection_performed = true; - $wider_type_clone = clone $wider_type; - - $wider_type_clone->extra_types = []; + $wider_type_clone = $wider_type->setIntersectionTypes([]); - $intersection_atomic->extra_types[$wider_type_clone->getKey()] = $wider_type_clone; + $final_intersection = array_merge( + [$wider_type_clone->getKey() => $wider_type_clone], + $intersection_atomic->getIntersectionTypes() + ); $wider_type_intersection_types = $wider_type->getIntersectionTypes(); foreach ($wider_type_intersection_types as $wider_type_intersection_type) { - $intersection_atomic->extra_types[$wider_type_intersection_type->getKey()] + $final_intersection[$wider_type_intersection_type->getKey()] = clone $wider_type_intersection_type; } + + return $intersection_atomic->setIntersectionTypes($final_intersection); } return $intersection_atomic; diff --git a/src/Psalm/Type/Atomic/TNamedObject.php b/src/Psalm/Type/Atomic/TNamedObject.php index 77707a699ab..15c64b9d7d0 100644 --- a/src/Psalm/Type/Atomic/TNamedObject.php +++ b/src/Psalm/Type/Atomic/TNamedObject.php @@ -179,18 +179,8 @@ public function replaceTemplateTypesWithArgTypes( /** * @return static */ - public function replaceTemplateTypesWithStandins( - TemplateResult $template_result, - Codebase $codebase, - ?StatementsAnalyzer $statements_analyzer = null, - ?Atomic $input_type = null, - ?int $input_arg_offset = null, - ?string $calling_class = null, - ?string $calling_function = null, - bool $replace = true, - bool $add_lower_bound = false, - int $depth = 0 - ): self { + public function replaceTemplateTypesWithStandins(TemplateResult $template_result, Codebase $codebase, ?StatementsAnalyzer $statements_analyzer = null, ?Atomic $input_type = null, ?int $input_arg_offset = null, ?string $calling_class = null, ?string $calling_function = null, bool $replace = true, bool $add_lower_bound = false, int $depth = 0): self + { $intersection = $this->replaceIntersectionTemplateTypesWithStandins( $template_result, $codebase, diff --git a/src/Psalm/Type/Atomic/TNonEmptyArray.php b/src/Psalm/Type/Atomic/TNonEmptyArray.php index ed24494a74d..061e180c87c 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyArray.php +++ b/src/Psalm/Type/Atomic/TNonEmptyArray.php @@ -41,4 +41,19 @@ public function __construct( $this->min_count = $min_count; $this->value = $value; } + + /** + * @param positive-int|null $count + * + * @return static + */ + public function setCount(?int $count): self + { + if ($count === $this->count) { + return $this; + } + $cloned = clone $this; + $cloned->count = $count; + return $cloned; + } } diff --git a/src/Psalm/Type/Atomic/TNonEmptyList.php b/src/Psalm/Type/Atomic/TNonEmptyList.php index 3a73f9790f2..ef9dc0c05e2 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyList.php +++ b/src/Psalm/Type/Atomic/TNonEmptyList.php @@ -35,6 +35,21 @@ public function __construct(Union $type_param, ?int $count = null, ?int $min_cou $this->min_count = $min_count; } + /** + * @param positive-int|null $count + * + * @return static + */ + public function setCount(?int $count): self + { + if ($count === $this->count) { + return $this; + } + $cloned = clone $this; + $cloned->count = $count; + return $cloned; + } + public function getAssertionString(): string { return 'non-empty-list'; diff --git a/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php index 22085fce489..49ab9eeb1e8 100644 --- a/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php +++ b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php @@ -112,13 +112,13 @@ private static function createExpectedCallable( DynamicTemplateProvider $template_provider, int $return_template_offset = 0 ): TCallable { - $expected_callable = new TCallable('callable'); - $expected_callable->params = [new FunctionLikeParameter('a', false, $input_type, $input_type)]; - $expected_callable->return_type = new Union([ - $template_provider->createTemplate('T' . $return_template_offset) - ]); - - return $expected_callable; + return new TCallable( + 'callable', + [new FunctionLikeParameter('a', false, $input_type, $input_type)], + new Union([ + $template_provider->createTemplate('T' . $return_template_offset) + ]) + ); } /** diff --git a/tests/UnusedVariableTest.php b/tests/UnusedVariableTest.php index a6767d8e200..230a8a481ba 100644 --- a/tests/UnusedVariableTest.php +++ b/tests/UnusedVariableTest.php @@ -1960,7 +1960,10 @@ function foo(array &$arr): void { ], 'byRefDeeplyNestedArrayParam' => [ 'code' => '> $arr */ + /** + * @param non-empty-list> $arr + * @param-out non-empty-list> $arr + */ function foo(array &$arr): void { $b = 5; $arr[0][0] = $b; @@ -1968,7 +1971,10 @@ function foo(array &$arr): void { ], 'nestedReferencesToByRefParam' => [ 'code' => '> $arr */ + /** + * @param non-empty-list> $arr + * @param-out non-empty-list> $arr + */ function foo(array &$arr): void { $a = &$arr[0]; $b = &$a[0]; From 0155ad74726c19944ee160f492f06682f9ba0fac Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 3 Oct 2022 15:13:47 +0200 Subject: [PATCH 143/194] Immutable readonly 3 --- src/Psalm/Context.php | 6 +- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 2 +- .../Internal/Analyzer/CommentAnalyzer.php | 7 +- .../FunctionLike/ReturnTypeAnalyzer.php | 19 +- .../Analyzer/FunctionLikeAnalyzer.php | 6 +- .../Assignment/ArrayAssignmentAnalyzer.php | 95 +++--- .../Expression/AssignmentAnalyzer.php | 4 +- .../Expression/BooleanNotAnalyzer.php | 5 +- .../Expression/Call/ArgumentAnalyzer.php | 11 +- .../Expression/Call/ArgumentsAnalyzer.php | 3 +- .../Call/ArrayFunctionArgumentsAnalyzer.php | 50 ++- .../Call/Method/AtomicMethodCallAnalyzer.php | 8 +- .../Expression/Call/MethodCallAnalyzer.php | 20 +- .../Statements/Expression/CallAnalyzer.php | 30 +- .../Statements/Expression/IncludeAnalyzer.php | 5 + .../Analyzer/Statements/StaticAnalyzer.php | 2 +- .../Analyzer/Statements/UnsetAnalyzer.php | 84 +++-- src/Psalm/Internal/Cli/Psalter.php | 5 + src/Psalm/Internal/Cli/Refactor.php | 5 + src/Psalm/Internal/CliUtils.php | 5 + src/Psalm/Internal/Codebase/ClassLikes.php | 14 +- src/Psalm/Internal/Codebase/Methods.php | 110 +------ .../LanguageServer/ProtocolStreamReader.php | 2 +- .../Reflector/ClassLikeDocblockParser.php | 7 + .../Reflector/ClassLikeNodeScanner.php | 39 ++- .../Reflector/FunctionLikeDocblockParser.php | 17 + .../Reflector/FunctionLikeDocblockScanner.php | 16 +- .../PhpVisitor/Reflector/TypeHintResolver.php | 1 + .../ArrayFilterReturnTypeProvider.php | 9 +- .../IteratorToArrayReturnTypeProvider.php | 2 +- .../Stubs/Generator/StubsGenerator.php | 4 - .../Internal/Type/AssertionReconciler.php | 93 +++--- .../Type/Comparator/ArrayTypeComparator.php | 1 + .../Comparator/CallableTypeComparator.php | 3 +- .../Type/Comparator/GenericTypeComparator.php | 51 ++- .../Comparator/IntegerRangeComparator.php | 2 + .../Type/Comparator/ObjectComparator.php | 3 +- .../Type/SimpleAssertionReconciler.php | 308 +++++++++--------- .../Type/SimpleNegatedAssertionReconciler.php | 139 ++++---- .../Type/TemplateInferredTypeReplacer.php | 49 +-- .../Type/TemplateStandinTypeReplacer.php | 48 +-- src/Psalm/Internal/Type/TypeCombiner.php | 160 ++++----- src/Psalm/Internal/Type/TypeExpander.php | 104 ++++-- src/Psalm/Internal/Type/TypeParser.php | 303 +++++++++++------ .../TypeVisitor/ClasslikeReplacer.php | 53 +++ .../TypeVisitor/ContainsClassLikeVisitor.php | 18 +- .../TypeVisitor/ContainsLiteralVisitor.php | 9 +- .../TypeVisitor/FromDocblockSetter.php | 27 +- .../TypeVisitor/TemplateTypeCollector.php | 4 +- .../Internal/TypeVisitor/TypeChecker.php | 16 +- .../Internal/TypeVisitor/TypeLocalizer.php | 98 ++++++ .../Internal/TypeVisitor/TypeScanner.php | 13 +- .../Event/StringInterpreterEvent.php | 2 + src/Psalm/Storage/FunctionLikeParameter.php | 16 +- src/Psalm/Type.php | 121 +++++-- src/Psalm/Type/Atomic.php | 93 ++++-- src/Psalm/Type/Atomic/CallableTrait.php | 76 +---- src/Psalm/Type/Atomic/DependentType.php | 3 + src/Psalm/Type/Atomic/GenericTrait.php | 29 +- .../Type/Atomic/HasIntersectionTrait.php | 19 +- src/Psalm/Type/Atomic/Scalar.php | 3 + .../Type/Atomic/TAnonymousClassInstance.php | 1 + src/Psalm/Type/Atomic/TArray.php | 27 +- src/Psalm/Type/Atomic/TArrayKey.php | 1 + src/Psalm/Type/Atomic/TBool.php | 1 + src/Psalm/Type/Atomic/TCallable.php | 21 +- src/Psalm/Type/Atomic/TCallableArray.php | 1 + src/Psalm/Type/Atomic/TCallableKeyedArray.php | 1 + src/Psalm/Type/Atomic/TCallableList.php | 1 + src/Psalm/Type/Atomic/TCallableObject.php | 1 + src/Psalm/Type/Atomic/TCallableString.php | 2 +- src/Psalm/Type/Atomic/TClassConstant.php | 20 +- src/Psalm/Type/Atomic/TClassString.php | 26 +- src/Psalm/Type/Atomic/TClassStringMap.php | 25 +- src/Psalm/Type/Atomic/TClosedResource.php | 1 + src/Psalm/Type/Atomic/TClosure.php | 31 +- src/Psalm/Type/Atomic/TConditional.php | 17 +- src/Psalm/Type/Atomic/TDependentGetClass.php | 1 + .../Type/Atomic/TDependentGetDebugType.php | 1 + src/Psalm/Type/Atomic/TDependentGetType.php | 1 + src/Psalm/Type/Atomic/TDependentListKey.php | 1 + src/Psalm/Type/Atomic/TEmptyMixed.php | 1 + src/Psalm/Type/Atomic/TEmptyNumeric.php | 1 + src/Psalm/Type/Atomic/TEmptyScalar.php | 1 + src/Psalm/Type/Atomic/TEnumCase.php | 1 + src/Psalm/Type/Atomic/TFalse.php | 1 + src/Psalm/Type/Atomic/TFloat.php | 1 + src/Psalm/Type/Atomic/TGenericObject.php | 34 +- src/Psalm/Type/Atomic/TInt.php | 1 + src/Psalm/Type/Atomic/TIntMask.php | 4 +- src/Psalm/Type/Atomic/TIntMaskOf.php | 9 +- src/Psalm/Type/Atomic/TIntRange.php | 4 +- src/Psalm/Type/Atomic/TIterable.php | 36 +- src/Psalm/Type/Atomic/TKeyOf.php | 4 +- src/Psalm/Type/Atomic/TKeyedArray.php | 39 +-- src/Psalm/Type/Atomic/TList.php | 14 +- src/Psalm/Type/Atomic/TLiteralClassString.php | 16 +- src/Psalm/Type/Atomic/TLiteralFloat.php | 4 +- src/Psalm/Type/Atomic/TLiteralInt.php | 4 +- src/Psalm/Type/Atomic/TLiteralString.php | 4 +- src/Psalm/Type/Atomic/TLowercaseString.php | 1 + src/Psalm/Type/Atomic/TMixed.php | 4 +- src/Psalm/Type/Atomic/TNamedObject.php | 52 +-- src/Psalm/Type/Atomic/TNever.php | 1 + src/Psalm/Type/Atomic/TNonEmptyArray.php | 5 +- src/Psalm/Type/Atomic/TNonEmptyList.php | 10 +- .../Type/Atomic/TNonEmptyLowercaseString.php | 1 + src/Psalm/Type/Atomic/TNonEmptyMixed.php | 1 + .../TNonEmptyNonspecificLiteralString.php | 1 + src/Psalm/Type/Atomic/TNonEmptyScalar.php | 1 + src/Psalm/Type/Atomic/TNonEmptyString.php | 1 + src/Psalm/Type/Atomic/TNonFalsyString.php | 1 + .../Type/Atomic/TNonspecificLiteralInt.php | 1 + .../Type/Atomic/TNonspecificLiteralString.php | 1 + src/Psalm/Type/Atomic/TNull.php | 1 + src/Psalm/Type/Atomic/TNumeric.php | 1 + src/Psalm/Type/Atomic/TNumericString.php | 1 + src/Psalm/Type/Atomic/TObject.php | 1 + .../Type/Atomic/TObjectWithProperties.php | 77 +++-- src/Psalm/Type/Atomic/TPropertiesOf.php | 25 +- src/Psalm/Type/Atomic/TResource.php | 1 + src/Psalm/Type/Atomic/TScalar.php | 1 + src/Psalm/Type/Atomic/TSingleLetter.php | 1 + src/Psalm/Type/Atomic/TString.php | 1 + .../Type/Atomic/TTemplateIndexedAccess.php | 5 +- src/Psalm/Type/Atomic/TTemplateKeyOf.php | 5 +- src/Psalm/Type/Atomic/TTemplateParam.php | 33 +- src/Psalm/Type/Atomic/TTemplateParamClass.php | 5 +- .../Type/Atomic/TTemplatePropertiesOf.php | 5 +- src/Psalm/Type/Atomic/TTemplateValueOf.php | 5 +- src/Psalm/Type/Atomic/TTraitString.php | 1 + src/Psalm/Type/Atomic/TTrue.php | 1 + src/Psalm/Type/Atomic/TTypeAlias.php | 1 + src/Psalm/Type/Atomic/TValueOf.php | 4 +- src/Psalm/Type/Atomic/TVoid.php | 1 + ...deVisitor.php => ImmutableTypeVisitor.php} | 29 +- src/Psalm/Type/MutableUnion.php | 43 ++- src/Psalm/Type/TypeNode.php | 4 +- src/Psalm/Type/TypeVisitor.php | 102 ++++++ src/Psalm/Type/Union.php | 35 +- src/Psalm/Type/UnionTrait.php | 277 +++++++++++++--- tests/ArrayAccessTest.php | 19 +- tests/DocumentationTest.php | 3 + tests/IfThisIsTest.php | 6 +- tests/Template/ClassTemplateExtendsTest.php | 5 +- tests/TypeCombinationTest.php | 1 + 146 files changed, 2115 insertions(+), 1483 deletions(-) create mode 100644 src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php create mode 100644 src/Psalm/Internal/TypeVisitor/TypeLocalizer.php rename src/Psalm/Type/{NodeVisitor.php => ImmutableTypeVisitor.php} (50%) create mode 100644 src/Psalm/Type/TypeVisitor.php diff --git a/src/Psalm/Context.php b/src/Psalm/Context.php index 8b894dad1d8..66d30f53654 100644 --- a/src/Psalm/Context.php +++ b/src/Psalm/Context.php @@ -487,12 +487,12 @@ public function update( ) { $existing_type = $existing_type ->getBuilder() - ->substitute($old_type, $new_type) - ->freeze(); + ->substitute($old_type, $new_type); if ($new_type && $new_type->from_docblock) { - $existing_type->setFromDocblock(); + $existing_type = $existing_type->setFromDocblock(); } + $existing_type = $existing_type->freeze(); $updated_vars[$var_id] = true; } diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 09113c949ad..1fc06f1ef42 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -1296,8 +1296,8 @@ static function (FunctionLikeParameter $param): PhpParser\Node\Arg { $this->inferred_property_types[$property_name] ->getBuilder() ->addType(new TNull()) + ->setFromDocblock(true) ->freeze(); - $this->inferred_property_types[$property_name]->setFromDocblock(); } } } diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index 5af3a43e8a3..925af7f35d6 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -159,7 +159,8 @@ public static function arrayToDocblocks( $var_type_tokens, null, $template_type_map ?: [], - $type_aliases ?: [] + $type_aliases ?: [], + true ); } catch (TypeParseTreeException $e) { throw new DocblockParseException( @@ -173,8 +174,6 @@ public static function arrayToDocblocks( ); } - $defined_type->setFromDocblock(); - $var_comment = new VarDocblockComment(); $var_comment->type = $defined_type; $var_comment->var_id = $var_id; @@ -375,7 +374,7 @@ public static function splitDocLine(string $return_block): array $remaining = trim(preg_replace('@^[ \t]*\* *@m', ' ', substr($return_block, $i + 1))); if ($remaining) { - return array_merge([rtrim($type)], preg_split('/[ \s]+/', $remaining)); + return array_merge([rtrim($type)], preg_split('/[ \s]+/', $remaining) ?: []); } return [$type]; diff --git a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php index 76e23c93eae..7e18ff9a250 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php @@ -168,8 +168,7 @@ public static function verifyReturnType( // only add null if we have a return statement elsewhere and it wasn't void foreach ($inferred_return_type_parts as $inferred_return_type_part) { if (!$inferred_return_type_part->isVoid()) { - $atomic_null = new TNull(); - $atomic_null->from_docblock = true; + $atomic_null = new TNull(true); $inferred_return_type_parts[] = new Union([$atomic_null]); break; } @@ -577,15 +576,13 @@ public static function verifyReturnType( return false; } } - } elseif (!$inferred_return_type->hasMixed() - && !UnionTypeComparator::isContainedBy( - $codebase, - $declared_return_type, - $inferred_return_type, - false, - false - ) - ) { + } elseif (!UnionTypeComparator::isContainedBy( + $codebase, + $declared_return_type, + $inferred_return_type, + false, + false + )) { if ($codebase->alter_code) { if (isset($project_analyzer->getIssuesToFix()['LessSpecificReturnType']) && !in_array('LessSpecificReturnType', $suppressed_issues) diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 2cbe91e9228..ccba489a8d6 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -623,7 +623,7 @@ public function analyze( /** * @var TClosure */ - $closure_atomic = $function_type->getSingleAtomic(); + $closure_atomic = clone $function_type->getSingleAtomic(); if (($storage->return_type === $storage->signature_return_type) && (!$storage->return_type @@ -634,10 +634,14 @@ public function analyze( $storage->return_type )) ) { + /** @psalm-suppress InaccessibleProperty Acting on clone */ $closure_atomic->return_type = $closure_return_type; } + /** @psalm-suppress InaccessibleProperty Acting on clone */ $closure_atomic->is_pure = !$this->inferred_impure; + + $statements_analyzer->node_data->setType($this->function, new Union([$closure_atomic])); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php index 8a3657acd25..fecc1cda373 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/ArrayAssignmentAnalyzer.php @@ -297,68 +297,74 @@ private static function updateTypeWithKeyValues( $has_matching_objectlike_property = false; $has_matching_string = false; - $child_stmt_type = $child_stmt_type->getBuilder(); - + $changed = false; + $types = []; foreach ($child_stmt_type->getAtomicTypes() as $type) { + $old_type = $type; if ($type instanceof TTemplateParam) { - $type->as = self::updateTypeWithKeyValues( + $type = $type->replaceAs(self::updateTypeWithKeyValues( $codebase, $type->as, $current_type, $key_values - ); - + )); $has_matching_objectlike_property = true; - - $child_stmt_type->substitute(new Union([$type]), $type->as); - - continue; - } - - foreach ($key_values as $key_value) { - if ($type instanceof TKeyedArray) { - if (isset($type->properties[$key_value->value])) { + } elseif ($type instanceof TKeyedArray) { + $properties = $type->properties; + foreach ($key_values as $key_value) { + if (isset($properties[$key_value->value])) { $has_matching_objectlike_property = true; - $type->properties[$key_value->value] = clone $current_type; + $properties[$key_value->value] = clone $current_type; } - } elseif ($type instanceof TString - && $key_value instanceof TLiteralInt - ) { - $has_matching_string = true; - - if ($type instanceof TLiteralString - && $current_type->isSingleStringLiteral() - ) { - $new_char = $current_type->getSingleStringLiteral()->value; - - if (strlen($new_char) === 1) { - $type->value[0] = $new_char; + } + $type = $type->setProperties($properties); + } elseif ($type instanceof TString) { + foreach ($key_values as $key_value) { + if ($key_value instanceof TLiteralInt) { + $has_matching_string = true; + + if ($type instanceof TLiteralString + && $current_type->isSingleStringLiteral() + ) { + $new_char = $current_type->getSingleStringLiteral()->value; + + if (strlen($new_char) === 1 && $type->value[0] !== $new_char) { + $v = $type->value; + $v[0] = $new_char; + $changed = true; + $type = new TLiteralString($v); + break; + } } } - } elseif ($type instanceof TNonEmptyList - && $key_value instanceof TLiteralInt - && count($key_values) === 1 - ) { - $count = ($type->count ?? $type->min_count) ?? 1; - if ($key_value->value >= $count) { - continue; - } - + } + } elseif ($type instanceof TNonEmptyList + && count($key_values) === 1 + && $key_values[0] instanceof TLiteralInt + ) { + $key_value = $key_values[0]; + $count = ($type->count ?? $type->min_count) ?? 1; + if ($key_value->value < $count) { $has_matching_objectlike_property = true; - $type->type_param = Type::combineUnionTypes( + $changed = true; + $type = $type->replaceTypeParam(Type::combineUnionTypes( clone $current_type, $type->type_param, $codebase, true, false - ); + )); } } + $types[$type->getKey()] = $type; + $changed = $changed || $old_type !== $type; } - $child_stmt_type = $child_stmt_type->freeze(); + if ($changed) { + $child_stmt_type = $child_stmt_type->getBuilder()->setTypes($types)->freeze(); + } if (!$has_matching_objectlike_property && !$has_matching_string) { if (count($key_values) === 1) { @@ -368,11 +374,10 @@ private static function updateTypeWithKeyValues( [$key_value->value => clone $current_type], $key_value instanceof TLiteralClassString ? [$key_value->value => true] - : null + : null, + true ); - $object_like->sealed = true; - $array_assignment_type = new Union([ $object_like, ]); @@ -605,10 +610,12 @@ private static function updateArrayAssignmentChildType( } elseif ($atomic_root_types['array'] instanceof TNonEmptyArray || $atomic_root_types['array'] instanceof TNonEmptyList ) { + /** @psalm-suppress InaccessibleProperty We just created this object */ $array_atomic_type->count = $atomic_root_types['array']->count; } elseif ($atomic_root_types['array'] instanceof TKeyedArray && $atomic_root_types['array']->sealed ) { + /** @psalm-suppress InaccessibleProperty We just created this object */ $array_atomic_type->count = count($atomic_root_types['array']->properties); $from_countable_object_like = true; @@ -659,7 +666,9 @@ private static function updateArrayAssignmentChildType( || $atomic_root_types['array'] instanceof TNonEmptyList) && $atomic_root_types['array']->count !== null ) { - $atomic_root_types['array']->count++; + $atomic_root_types['array'] = + $atomic_root_types['array']->setCount($atomic_root_types['array']->count+1); + $new_child_type = new Union($atomic_root_types); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index c185eb579d5..da243bb3e9b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -716,7 +716,7 @@ public static function assignTypeFromVarDocblock( $statements_analyzer->getParentFQCLN() ); - $var_comment_type->setFromDocblock(); + $var_comment_type = $var_comment_type->setFromDocblock(); $var_comment_type->check( $statements_analyzer, @@ -1497,7 +1497,7 @@ private static function analyzeDestructuringAssignment( $statements_analyzer->getParentFQCLN() ); - $var_comment_type->setFromDocblock(); + $var_comment_type = $var_comment_type->setFromDocblock(); $new_assign_type = $var_comment_type; break; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php index fc3d6eb73fd..6a2078b02d9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BooleanNotAnalyzer.php @@ -33,12 +33,11 @@ public static function analyze( $stmt_type = Type::getBool(); if ($expr_type) { if ($expr_type->isAlwaysTruthy()) { - $stmt_type = Type::getFalse(); + $stmt_type = Type::getFalse($expr_type->from_docblock); } elseif ($expr_type->isAlwaysFalsy()) { - $stmt_type = Type::getTrue(); + $stmt_type = Type::getTrue($expr_type->from_docblock); } - $stmt_type->from_docblock = $expr_type->from_docblock; $stmt_type->parent_nodes = $expr_type->parent_nodes; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index b5ac0a0b7b2..154c4bf9b6d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -1406,16 +1406,7 @@ private static function coerceValueAfterGatekeeperArgument( if ($input_type->getId() === $param_type->getId()) { if ($input_type->from_docblock) { - if (!$was_cloned) { - $was_cloned = true; - $input_type = clone $input_type; - } - - $input_type->from_docblock = false; - - foreach ($input_type->getAtomicTypes() as $atomic_type) { - $atomic_type->from_docblock = false; - } + $input_type = $input_type->setFromDocblock(false); } } elseif ($input_type->hasMixed() && $signature_param_type) { $was_cloned = true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php index b04175c0fe0..a3d5f7f46ea 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentsAnalyzer.php @@ -897,7 +897,8 @@ public static function checkArgumentsMatch( $array_type = $arg_value_type->getAtomicTypes()['array']; if ($array_type instanceof TKeyedArray) { - $key_types = $array_type->getGenericArrayType()->getChildNodes()[0]->getChildNodes(); + $array_type = $array_type->getGenericArrayType(); + $key_types = $array_type->type_params[0]->getAtomicTypes(); foreach ($key_types as $key_type) { if (!$key_type instanceof TLiteralString diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php index 29d4234fefe..67c366dc3d3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArrayFunctionArgumentsAnalyzer.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression\Call; +use AssertionError; use PhpParser; use Psalm\CodeLocation; use Psalm\Context; @@ -35,7 +36,6 @@ use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TKeyedArray; use Psalm\Type\Atomic\TList; -use Psalm\Type\Atomic\TNever; use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Union; @@ -43,6 +43,7 @@ use function array_filter; use function array_shift; +use function array_unshift; use function assert; use function count; use function explode; @@ -516,33 +517,24 @@ public static function handleByRefArrayAdjustment( $context->removeVarFromConflictingClauses($var_id, null, $statements_analyzer); if (isset($context->vars_in_scope[$var_id])) { - $array_type = $context->vars_in_scope[$var_id]->getBuilder(); + $array_atomic_types = []; - $array_atomic_types = $array_type->getAtomicTypes(); - - foreach ($array_atomic_types as $array_atomic_type) { + foreach ($context->vars_in_scope[$var_id]->getAtomicTypes() as $array_atomic_type) { if ($array_atomic_type instanceof TKeyedArray) { if ($is_array_shift && $array_atomic_type->is_list) { - $array_atomic_type = clone $array_atomic_type; - $array_properties = $array_atomic_type->properties; array_shift($array_properties); if (!$array_properties) { - $array_atomic_type = new TList( - $array_atomic_type->previous_value_type ?: Type::getMixed() - ); - - $array_type->addType($array_atomic_type); + $array_atomic_types []= new TList(Type::getNever()); } else { - $array_atomic_type->properties = $array_properties; + $array_atomic_types []= $array_atomic_type->setProperties($array_properties); } + continue; } - if ($array_atomic_type instanceof TKeyedArray) { - $array_atomic_type = $array_atomic_type->getGenericArrayType(); - } + $array_atomic_type = $array_atomic_type->getGenericArrayType(); } if ($array_atomic_type instanceof TNonEmptyArray) { @@ -550,39 +542,39 @@ public static function handleByRefArrayAdjustment( if ($array_atomic_type->count === 1) { $array_atomic_type = new TArray( [ - new Union([new TNever]), - new Union([new TNever]), + Type::getNever(), + Type::getNever(), ] ); } else { - $array_atomic_type->count--; + $array_atomic_type = $array_atomic_type->setCount($array_atomic_type->count-1); } } else { $array_atomic_type = new TArray($array_atomic_type->type_params); } - $array_type->addType($array_atomic_type); + $array_atomic_types[] = $array_atomic_type; } elseif ($array_atomic_type instanceof TNonEmptyList) { if (!$context->inside_loop && $array_atomic_type->count !== null) { if ($array_atomic_type->count === 1) { - $array_atomic_type = new TArray( - [ - new Union([new TNever]), - new Union([new TNever]), - ] - ); + $array_atomic_type = new TList(Type::getNever()); } else { - $array_atomic_type->count--; + $array_atomic_type = $array_atomic_type->setCount($array_atomic_type->count-1); } } else { $array_atomic_type = new TList($array_atomic_type->type_param); } - $array_type->addType($array_atomic_type); + $array_atomic_types[] = $array_atomic_type; + } else { + $array_atomic_types[] = $array_atomic_type; } } - $array_type = $array_type->freeze(); + if (!$array_atomic_types) { + throw new AssertionError("We must have some types here!"); + } + $array_type = new Union($array_atomic_types); $context->removeDescendents($var_id, $array_type); $context->vars_in_scope[$var_id] = $array_type; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php index 24215578320..70ecf984149 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -89,15 +89,15 @@ public static function analyze( $lhs_type_part->as->getAtomicTypes() )[0]; - $lhs_type_part->from_docblock = true; - if ($lhs_type_part instanceof TNamedObject) { - $lhs_type_part = $lhs_type_part->setIntersectionTypes($extra_types); + $lhs_type_part = $lhs_type_part->setIntersectionTypes($extra_types)->setFromDocblock(true); } elseif ($lhs_type_part instanceof TObject && $extra_types) { - $lhs_type_part = array_shift($extra_types); + $lhs_type_part = array_shift($extra_types)->setFromDocblock(true); if ($extra_types) { $lhs_type_part = $lhs_type_part->setIntersectionTypes($extra_types); } + } else { + $lhs_type_part = $lhs_type_part->setFromDocblock(true); } $result->has_mixed_method_call = true; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index db15ccafa41..864b353b725 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression\Call; +use AssertionError; use PhpParser; use Psalm\CodeLocation; use Psalm\Context; @@ -398,26 +399,23 @@ public static function analyze( && ($class_type->from_docblock || $class_type->isNullable()) && $real_method_call ) { - $keys_to_remove = []; + $types = $class_type->getAtomicTypes(); - $class_type = $class_type->getBuilder(); - - foreach ($class_type->getAtomicTypes() as $key => $type) { + foreach ($types as $key => &$type) { if (!$type instanceof TNamedObject) { - $keys_to_remove[] = $key; + unset($types[$key]); } else { - $type->from_docblock = false; + $type = $type->setFromDocblock(false); } } - - foreach ($keys_to_remove as $key) { - $class_type->removeType($key); + if (!$types) { + throw new AssertionError("We must have some types here!"); } - $class_type->from_docblock = false; - $context->removeVarFromConflictingClauses($lhs_var_id, null, $statements_analyzer); + $class_type = $class_type->getBuilder()->setTypes($types); + $class_type->from_docblock = false; $context->vars_in_scope[$lhs_var_id] = $class_type->freeze(); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php index 8640158444e..523ffff8a22 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CallAnalyzer.php @@ -578,16 +578,14 @@ public static function getFunctionIdsFromCallableArg( if ($type_part instanceof TNamedObject) { $method_id = $type_part->value . '::' . $method_name_arg->value; - if ($type_part->extra_types) { - foreach ($type_part->extra_types as $extra_type) { - if ($extra_type instanceof TTemplateParam - || $extra_type instanceof TObjectWithProperties - ) { - throw new UnexpectedValueException('Shouldn’t get a generic param here'); - } - - $method_id .= '&' . $extra_type->value . '::' . $method_name_arg->value; + foreach ($type_part->extra_types as $extra_type) { + if ($extra_type instanceof TTemplateParam + || $extra_type instanceof TObjectWithProperties + ) { + throw new UnexpectedValueException('Shouldn’t get a generic param here'); } + + $method_id .= '&' . $extra_type->value . '::' . $method_name_arg->value; } $method_ids[] = '$' . $method_id; @@ -938,19 +936,7 @@ public static function applyAssertionsToContext( ); } - $op_vars_in_scope[$var_id]->from_docblock = true; - - foreach ($op_vars_in_scope[$var_id]->getAtomicTypes() as $changed_atomic_type) { - $changed_atomic_type->from_docblock = true; - - if ($changed_atomic_type instanceof TNamedObject - && $changed_atomic_type->extra_types - ) { - foreach ($changed_atomic_type->extra_types as $extra_type) { - $extra_type->from_docblock = true; - } - } - } + $op_vars_in_scope[$var_id] = $op_vars_in_scope[$var_id]->setFromDocblock(true); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php index 12e98ae5d9d..4b6c3dae06f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/IncludeAnalyzer.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Analyzer\Statements\Expression; +use AssertionError; use PhpParser; use Psalm\CodeLocation; use Psalm\Config; @@ -30,6 +31,7 @@ use function implode; use function in_array; use function is_string; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -379,6 +381,9 @@ public static function resolveIncludePath(string $file_name, string $current_dir ? preg_split('#(?getParentFQCLN() ); - $var_comment_type->setFromDocblock(); + $var_comment_type = $var_comment_type->setFromDocblock(); $var_comment_type->check( $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php index 45347e0dc13..f6f5bd73729 100644 --- a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php @@ -57,76 +57,90 @@ public static function analyze( $statements_analyzer ); - if ($root_var_id && isset($context->vars_in_scope[$root_var_id])) { - $root_type = $context->vars_in_scope[$root_var_id]->getBuilder(); + $key_type = $statements_analyzer->node_data->getType($var->dim); + if ($root_var_id && isset($context->vars_in_scope[$root_var_id]) && $key_type) { + $root_types = []; - foreach ($root_type->getAtomicTypes() as $atomic_root_type) { + foreach ($context->vars_in_scope[$root_var_id]->getAtomicTypes() as $atomic_root_type) { if ($atomic_root_type instanceof TKeyedArray) { - if ($var->dim instanceof PhpParser\Node\Scalar\String_ - || $var->dim instanceof PhpParser\Node\Scalar\LNumber - ) { - if (isset($atomic_root_type->properties[$var->dim->value])) { - if ($atomic_root_type->is_list - && $var->dim->value !== count($atomic_root_type->properties)-1 + $key_value = null; + if ($key_type->isSingleIntLiteral()) { + $key_value = $key_type->getSingleIntLiteral()->value; + } elseif ($key_type->isSingleStringLiteral()) { + $key_value = $key_type->getSingleStringLiteral()->value; + } + if ($key_value !== null) { + $properties = $atomic_root_type->properties; + $is_list = $atomic_root_type->is_list; + if (isset($properties[$key_value])) { + if ($is_list + && $key_value !== count($properties)-1 ) { - $atomic_root_type->is_list = false; + $is_list = false; } - unset($atomic_root_type->properties[$var->dim->value]); - $root_type->bustCache(); //remove id cache + unset($properties[$key_value]); } - if (!$atomic_root_type->properties) { + if (!$properties) { if ($atomic_root_type->previous_value_type) { - $root_type->addType( + $root_types [] = new TArray([ $atomic_root_type->previous_key_type ? clone $atomic_root_type->previous_key_type : new Union([new TArrayKey]), clone $atomic_root_type->previous_value_type, ]) - ); + ; } else { - $root_type->addType( + $root_types [] = new TArray([ new Union([new TNever]), new Union([new TNever]), ]) - ); + ; } + } else { + $root_types []= new TKeyedArray( + $properties, + null, + $atomic_root_type->sealed, + $atomic_root_type->previous_key_type, + $atomic_root_type->previous_value_type, + $is_list + ); } } else { + $properties = []; foreach ($atomic_root_type->properties as $key => $type) { - $atomic_root_type->properties[$key] = clone $type; - $atomic_root_type->properties[$key]->possibly_undefined = true; + $properties[$key] = clone $type; + $properties[$key]->possibly_undefined = true; } - - $atomic_root_type->sealed = false; - - $root_type->addType( - $atomic_root_type->getGenericArrayType(false) + $root_types []= new TKeyedArray( + $properties, + null, + false, + $atomic_root_type->previous_key_type, + $atomic_root_type->previous_value_type, + false, ); - - $atomic_root_type->is_list = false; } } elseif ($atomic_root_type instanceof TNonEmptyArray) { - $root_type->addType( - new TArray($atomic_root_type->type_params) - ); + $root_types []= new TArray($atomic_root_type->type_params); } elseif ($atomic_root_type instanceof TNonEmptyMixed) { - $root_type->addType( - new TMixed() - ); + $root_types []= new TMixed(); } elseif ($atomic_root_type instanceof TList) { - $root_type->addType( + $root_types []= new TArray([ Type::getInt(), $atomic_root_type->type_param ]) - ); + ; + } else { + $root_types []= $atomic_root_type; } } - $context->vars_in_scope[$root_var_id] = $root_type->freeze(); + $context->vars_in_scope[$root_var_id] = new Union($root_types); $context->removeVarFromConflictingClauses( $root_var_id, diff --git a/src/Psalm/Internal/Cli/Psalter.php b/src/Psalm/Internal/Cli/Psalter.php index 7da3c7498f7..4bcd3436a7d 100644 --- a/src/Psalm/Internal/Cli/Psalter.php +++ b/src/Psalm/Internal/Cli/Psalter.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Cli; +use AssertionError; use Composer\XdebugHandler\XdebugHandler; use Psalm\Config; use Psalm\Exception\UnsupportedIssueToFixException; @@ -47,6 +48,7 @@ use function is_string; use function microtime; use function pathinfo; +use function preg_last_error_msg; use function preg_replace; use function preg_split; use function realpath; @@ -498,6 +500,9 @@ private static function loadCodeowners(Providers $providers): array $codeowner_lines = array_map( static function (string $line): array { $line_parts = preg_split('/\s+/', $line); + if ($line_parts === false) { + throw new AssertionError("An error occurred: ".preg_last_error_msg()); + } $file_selector = substr(array_shift($line_parts), 1); return [$file_selector, $line_parts]; diff --git a/src/Psalm/Internal/Cli/Refactor.php b/src/Psalm/Internal/Cli/Refactor.php index 37385bed493..5cf84db04eb 100644 --- a/src/Psalm/Internal/Cli/Refactor.php +++ b/src/Psalm/Internal/Cli/Refactor.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Cli; +use AssertionError; use Composer\XdebugHandler\XdebugHandler; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Internal\CliUtils; @@ -38,6 +39,7 @@ use function is_string; use function max; use function microtime; +use function preg_last_error_msg; use function preg_replace; use function preg_split; use function realpath; @@ -240,6 +242,9 @@ static function (string $arg) use ($valid_long_options): void { if ($operation === 'move_into') { $last_arg_parts = preg_split('/, ?/', $last_arg); + if ($last_arg_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } foreach ($last_arg_parts as $last_arg_part) { if (strpos($last_arg_part, '::')) { diff --git a/src/Psalm/Internal/CliUtils.php b/src/Psalm/Internal/CliUtils.php index 12592a39030..05dea134a7a 100644 --- a/src/Psalm/Internal/CliUtils.php +++ b/src/Psalm/Internal/CliUtils.php @@ -12,6 +12,7 @@ use Psalm\Exception\ConfigNotFoundException; use Psalm\Internal\Analyzer\ProjectAnalyzer; use Psalm\Report; +use RuntimeException; use function array_slice; use function assert; @@ -30,6 +31,7 @@ use function is_dir; use function is_string; use function json_decode; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -298,6 +300,9 @@ public static function getPathsToCheck($f_paths): ?array stream_set_blocking(STDIN, false); if ($stdin = fgets(STDIN)) { $filtered_input_paths = preg_split('/\s+/', trim($stdin)); + if ($filtered_input_paths === false) { + throw new RuntimeException('Invalid paths: '.preg_last_error_msg()); + } } $blocked = $meta['blocked']; stream_set_blocking(STDIN, $blocked); diff --git a/src/Psalm/Internal/Codebase/ClassLikes.php b/src/Psalm/Internal/Codebase/ClassLikes.php index d7f760e396b..ffec050456c 100644 --- a/src/Psalm/Internal/Codebase/ClassLikes.php +++ b/src/Psalm/Internal/Codebase/ClassLikes.php @@ -1460,9 +1460,10 @@ public function handleDocblockTypeInMigration( foreach ($codebase->class_transforms as $old_fq_class_name => $new_fq_class_name) { if ($type->containsClassLike($old_fq_class_name)) { - $type = $type->getBuilder(); - - $type = $type->replaceClassLike($old_fq_class_name, $new_fq_class_name)->freeze(); + $type = $type->replaceClassLike( + $old_fq_class_name, + $new_fq_class_name + ); $bounds = $type_location->getSelectionBounds(); @@ -1500,9 +1501,10 @@ public function handleDocblockTypeInMigration( $destination_class = $codebase->classes_to_move[$fq_class_name_lc]; if ($type->containsClassLike($fq_class_name_lc)) { - $type = $type->getBuilder(); - - $type = $type->replaceClassLike($fq_class_name_lc, $destination_class)->freeze(); + $type = $type->replaceClassLike( + $fq_class_name_lc, + $destination_class + ); } $this->airliftClassDefinedDocblockType( diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index 30fd46fe044..e92134bcf82 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -19,29 +19,24 @@ use Psalm\Internal\Provider\MethodVisibilityProvider; use Psalm\Internal\Type\Comparator\UnionTypeComparator; use Psalm\Internal\Type\TypeExpander; +use Psalm\Internal\TypeVisitor\TypeLocalizer; use Psalm\StatementsSource; use Psalm\Storage\ClassLikeStorage; use Psalm\Storage\FunctionLikeParameter; use Psalm\Storage\MethodStorage; use Psalm\Type; use Psalm\Type\Atomic; -use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TEnumCase; -use Psalm\Type\Atomic\TGenericObject; -use Psalm\Type\Atomic\TIterable; use Psalm\Type\Atomic\TKeyedArray; -use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\Atomic\TTemplateParamClass; use Psalm\Type\Union; use UnexpectedValueException; use function array_pop; -use function array_values; use function assert; use function count; use function explode; @@ -520,105 +515,12 @@ public static function localizeType( return $type; } - $type = $type->getBuilder(); + (new TypeLocalizer( + $extends, + $base_fq_class_name + ))->traverse($type); - foreach ($type->getAtomicTypes() as $key => $atomic_type) { - if ($atomic_type instanceof TTemplateParam - && ($atomic_type->defining_class === $base_fq_class_name - || isset($extends[$atomic_type->defining_class])) - ) { - $types_to_add = self::getExtendedTemplatedTypes( - $atomic_type, - $extends - ); - - if ($types_to_add) { - $type->removeType($key); - - foreach ($types_to_add as $extra_added_type) { - $type->addType($extra_added_type); - } - } - } - - if ($atomic_type instanceof TTemplateParamClass) { - if ($atomic_type->defining_class === $base_fq_class_name) { - if (isset($extends[$base_fq_class_name][$atomic_type->param_name])) { - $extended_param = $extends[$base_fq_class_name][$atomic_type->param_name]; - - $types = array_values($extended_param->getAtomicTypes()); - - if (count($types) === 1 && $types[0] instanceof TNamedObject) { - $atomic_type->as_type = $types[0]; - } else { - $atomic_type->as_type = null; - } - } - } - } - - if ($atomic_type instanceof TArray - || $atomic_type instanceof TIterable - || $atomic_type instanceof TGenericObject - ) { - foreach ($atomic_type->type_params as &$type_param) { - $type_param = self::localizeType( - $codebase, - $type_param, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - } - - if ($atomic_type instanceof TList) { - $atomic_type->type_param = self::localizeType( - $codebase, - $atomic_type->type_param, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - - if ($atomic_type instanceof TKeyedArray) { - foreach ($atomic_type->properties as &$property_type) { - $property_type = self::localizeType( - $codebase, - $property_type, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - } - - if ($atomic_type instanceof TCallable - || $atomic_type instanceof TClosure - ) { - if ($atomic_type->params) { - foreach ($atomic_type->params as $param) { - if ($param->type) { - $param->type = self::localizeType( - $codebase, - $param->type, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - } - } - - if ($atomic_type->return_type) { - $atomic_type->return_type = self::localizeType( - $codebase, - $atomic_type->return_type, - $appearing_fq_class_name, - $base_fq_class_name - ); - } - } - } - - return $type->freeze(); + return $type; } /** diff --git a/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php b/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php index e1391b4c6eb..bb1759675b8 100644 --- a/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php +++ b/src/Psalm/Internal/LanguageServer/ProtocolStreamReader.php @@ -119,7 +119,7 @@ private function readMessages(string $buffer): int ++$emitted_messages; $this->emit('message', [$msg]); /** - * @psalm-suppress DocblockTypeContradiction + * @psalm-suppress TypeDoesNotContainType */ if (!$this->is_accepting_new_requests) { // If we fork, don't read any bytes in the input buffer from the worker process. diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php index 770e1298779..a499b4be2f8 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeDocblockParser.php @@ -30,6 +30,7 @@ use function explode; use function implode; use function in_array; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -66,6 +67,9 @@ public static function parse( if (isset($parsed_docblock->combined_tags['template'])) { foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) { $template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); + if ($template_type === false) { + throw new IncorrectDocblockException('Invalid @ŧemplate tag: '.preg_last_error_msg()); + } $template_name = array_shift($template_type); @@ -106,6 +110,9 @@ public static function parse( if (isset($parsed_docblock->combined_tags['template-covariant'])) { foreach ($parsed_docblock->combined_tags['template-covariant'] as $offset => $template_line) { $template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); + if ($template_type === false) { + throw new IncorrectDocblockException('Invalid @template-covariant tag: '.preg_last_error_msg()); + } $template_name = array_shift($template_type); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index 9987fe6d70f..3c51ac6b52b 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -499,9 +499,9 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $yield_type_tokens, null, $storage->template_types ?: [], - $this->type_aliases + $this->type_aliases, + true ); - $yield_type->setFromDocblock(); $yield_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -559,9 +559,9 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $pseudo_property_type_tokens, null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); - $pseudo_property_type->setFromDocblock(); $pseudo_property_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -660,7 +660,8 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool ), null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); $mixin_type->queueClassLikesForScanning( @@ -669,8 +670,6 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool $storage->template_types ?: [] ); - $mixin_type->setFromDocblock(); - if ($mixin_type->isSingle()) { $mixin_type = $mixin_type->getSingleAtomic(); @@ -797,11 +796,10 @@ public function finish(PhpParser\Node\Stmt\ClassLike $node): ClassLikeStorage $type->replacement_tokens, null, [], - $this->type_aliases + $this->type_aliases, + true ); - $union->setFromDocblock(); - $converted_aliases[$key] = new ClassTypeAlias(array_values($union->getAtomicTypes())); } catch (Exception $e) { $classlike_storage->docblock_issues[] = new InvalidDocblock( @@ -924,7 +922,8 @@ private function extendTemplatedType( ), null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); } catch (TypeParseTreeException $e) { $storage->docblock_issues[] = new InvalidDocblock( @@ -942,8 +941,6 @@ private function extendTemplatedType( ); } - $extended_union_type->setFromDocblock(); - $extended_union_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -1008,7 +1005,8 @@ private function implementTemplatedType( ), null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); } catch (TypeParseTreeException $e) { $storage->docblock_issues[] = new InvalidDocblock( @@ -1028,8 +1026,6 @@ private function implementTemplatedType( return; } - $implemented_union_type->setFromDocblock(); - $implemented_union_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -1094,7 +1090,8 @@ private function useTemplatedType( ), null, $this->class_template_types, - $this->type_aliases + $this->type_aliases, + true ); } catch (TypeParseTreeException $e) { $storage->docblock_issues[] = new InvalidDocblock( @@ -1114,8 +1111,6 @@ private function useTemplatedType( return; } - $used_union_type->setFromDocblock(); - $used_union_type->queueClassLikesForScanning( $this->codebase, $this->file_storage, @@ -1543,7 +1538,6 @@ private function visitPropertyDeclaration( if ($doc_var_group_type) { $doc_var_group_type->queueClassLikesForScanning($this->codebase, $this->file_storage); - $doc_var_group_type->setFromDocblock(); } foreach ($stmt->props as $property) { @@ -1616,6 +1610,7 @@ private function visitPropertyDeclaration( foreach ($property_storage->type->getAtomicTypes() as $key => $type) { if (isset($signature_atomic_types[$key])) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $type->from_docblock = false; } else { $all_typehint_types_match = false; @@ -1849,6 +1844,10 @@ private static function getTypeAliasesFromCommentLines( array_shift($var_line_parts); } + if (!isset($var_line_parts[0])) { + continue; + } + if ($var_line_parts[0] === '=') { array_shift($var_line_parts); } diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php index 4de223c0ed7..7e3b7abdf1f 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockParser.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\PhpVisitor\Reflector; +use AssertionError; use PhpParser; use Psalm\CodeLocation; use Psalm\DocComment; @@ -20,6 +21,7 @@ use function explode; use function implode; use function in_array; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -229,6 +231,9 @@ public static function parse( if (isset($parsed_docblock->tags['psalm-taint-sink'])) { foreach ($parsed_docblock->tags['psalm-taint-sink'] as $param) { $param_parts = preg_split('/\s+/', trim($param)); + if ($param_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } if (count($param_parts) >= 2) { $info->taint_sink_params[] = ['name' => $param_parts[1], 'taint' => $param_parts[0]]; @@ -240,6 +245,9 @@ public static function parse( if (isset($parsed_docblock->tags['param-taint'])) { foreach ($parsed_docblock->tags['param-taint'] as $param) { $param_parts = preg_split('/\s+/', trim($param)); + if ($param_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } if (count($param_parts) === 2) { $taint_type = $param_parts[1]; @@ -264,6 +272,9 @@ public static function parse( if (isset($parsed_docblock->tags['psalm-taint-source'])) { foreach ($parsed_docblock->tags['psalm-taint-source'] as $param) { $param_parts = preg_split('/\s+/', trim($param)); + if ($param_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } if ($param_parts[0]) { $info->taint_source_types[] = $param_parts[0]; @@ -273,6 +284,9 @@ public static function parse( // support for MediaWiki taint plugin foreach ($parsed_docblock->tags['return-taint'] as $param) { $param_parts = preg_split('/\s+/', trim($param)); + if ($param_parts === false) { + throw new AssertionError(preg_last_error_msg()); + } if ($param_parts[0]) { if ($param_parts[0] === 'tainted') { @@ -429,6 +443,9 @@ public static function parse( if (isset($parsed_docblock->combined_tags['template'])) { foreach ($parsed_docblock->combined_tags['template'] as $offset => $template_line) { $template_type = preg_split('/[\s]+/', preg_replace('@^[ \t]*\*@m', '', $template_line)); + if ($template_type === false) { + throw new AssertionError(preg_last_error_msg()); + } $template_name = array_shift($template_type); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php index 81de78b3d9b..2b9316afa4c 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/FunctionLikeDocblockScanner.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\PhpVisitor\Reflector; +use AssertionError; use PhpParser; use Psalm\Aliases; use Psalm\CodeLocation; @@ -52,6 +53,7 @@ use function count; use function explode; use function in_array; +use function preg_last_error_msg; use function preg_match; use function preg_replace; use function preg_split; @@ -786,7 +788,8 @@ private static function improveParamsFromDocblock( ), null, $function_template_types + $class_template_types, - $type_aliases + $type_aliases, + true ); } catch (TypeParseTreeException $e) { $storage->docblock_issues[] = new InvalidDocblock( @@ -798,7 +801,6 @@ private static function improveParamsFromDocblock( } $storage_param->has_docblock_type = true; - $new_param_type->setFromDocblock(); $new_param_type->queueClassLikesForScanning( $codebase, @@ -871,6 +873,7 @@ private static function improveParamsFromDocblock( foreach ($new_param_type->getAtomicTypes() as $key => $type) { if (isset($storage_param_atomic_types[$key])) { + /** @psalm-suppress InaccessibleProperty We just created this type */ $type->from_docblock = false; if ($storage_param_atomic_types[$key] instanceof TArray @@ -967,11 +970,10 @@ private static function handleReturn( array_values($fixed_type_tokens), null, $function_template_types + $class_template_types, - $type_aliases + $type_aliases, + true ); - $storage->return_type->setFromDocblock(); - if ($storage instanceof MethodStorage) { $storage->has_docblock_return_type = true; } @@ -982,6 +984,7 @@ private static function handleReturn( foreach ($storage->return_type->getAtomicTypes() as $key => $type) { if (isset($signature_return_atomic_types[$key])) { + /** @psalm-suppress InaccessibleProperty We just created this atomic type */ $type->from_docblock = false; } else { $all_typehint_types_match = false; @@ -1080,6 +1083,9 @@ private static function handleTaintFlow( if ($source_param_string[0] === '(' && substr($source_param_string, -1) === ')') { $source_params = preg_split('/, ?/', substr($source_param_string, 1, -1)); + if ($source_params === false) { + throw new AssertionError(preg_last_error_msg()); + } foreach ($source_params as $source_param) { $source_param = substr($source_param, 1); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php index b359c60ec2e..336d8984cff 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/TypeHintResolver.php @@ -165,6 +165,7 @@ public static function resolve( if ($type_string) { $atomic_type = $type->getSingleAtomic(); + /** @psalm-suppress InaccessibleProperty We just created this type */ $atomic_type->text = $type_string; } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php index c287dbd9b3e..759053d5cd0 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayFilterReturnTypeProvider.php @@ -117,7 +117,14 @@ static function ($keyed_type) use ($statements_source, $context) { return Type::getEmptyArray(); } - return new Union([new TKeyedArray($new_properties, null, false, null, null, $first_arg_array->is_list && $had_one)]); + return new Union([new TKeyedArray( + $new_properties, + null, + false, + null, + null, + $first_arg_array->is_list && $had_one + )]); } } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php index a5176b1a1ef..f3732474502 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/IteratorToArrayReturnTypeProvider.php @@ -109,7 +109,7 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $template_types = $key_type->getTemplateTypes(); $template_type = array_shift($template_types); if ($template_type->as->hasMixed()) { - $template_type->as = Type::getArrayKey(); + $template_type = $template_type->replaceAs(Type::getArrayKey()); $key_type = new Union([$template_type]); } } diff --git a/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php b/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php index e65c343324a..df5c8d40e7e 100644 --- a/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php +++ b/src/Psalm/Internal/Stubs/Generator/StubsGenerator.php @@ -177,10 +177,6 @@ public static function getAll( continue; } - if ($type->isMixed()) { - continue; - } - $name_parts = explode('\\', $fq_name); $constant_name = array_pop($name_parts); diff --git a/src/Psalm/Internal/Type/AssertionReconciler.php b/src/Psalm/Internal/Type/AssertionReconciler.php index bb7e71ffaf2..ab4bca19c46 100644 --- a/src/Psalm/Internal/Type/AssertionReconciler.php +++ b/src/Psalm/Internal/Type/AssertionReconciler.php @@ -282,7 +282,7 @@ private static function refine( StatementsAnalyzer $statements_analyzer, Assertion $assertion, Atomic $new_type_part, - Union $existing_var_type, + Union &$existing_var_type, ?string $key, bool $negated, ?CodeLocation $code_location, @@ -293,6 +293,10 @@ private static function refine( $old_var_type_string = $existing_var_type->getId(); + if ($new_type_part instanceof TMixed) { + return $existing_var_type; + } + $new_type_has_interface = false; if ($new_type_part->isObjectType()) { @@ -343,8 +347,7 @@ private static function refine( $acceptable_atomic_types = count($acceptable_atomic_types) === count($existing_var_type->getAtomicTypes()) ? $existing_var_type - : new Union($acceptable_atomic_types) - ; + : new Union($acceptable_atomic_types); return new Union([$new_type_part->replaceAs($acceptable_atomic_types)]); } } @@ -358,13 +361,10 @@ private static function refine( $existing_var_type_part->properties, $new_type_part->properties )) { - $existing_var_type_part = clone $existing_var_type_part; - $existing_var_type_part->properties = array_merge( + $acceptable_atomic_types[] = $existing_var_type_part->setProperties(array_merge( $existing_var_type_part->properties, $new_type_part->properties - ); - - $acceptable_atomic_types[] = $existing_var_type_part; + )); } } } @@ -527,7 +527,7 @@ private static function refine( */ private static function filterTypeWithAnother( Codebase $codebase, - Union $existing_type, + Union &$existing_type, Union $new_type, bool &$any_scalar_type_match_found = false ): ?Union { @@ -535,8 +535,9 @@ private static function filterTypeWithAnother( $new_type = clone $new_type; + $existing_types = $existing_type->getAtomicTypes(); foreach ($new_type->getAtomicTypes() as $new_type_part) { - foreach ($existing_type->getAtomicTypes() as $existing_type_part) { + foreach ($existing_types as &$existing_type_part) { $matching_atomic_type = self::filterAtomicWithAnother( $existing_type_part, $new_type_part, @@ -549,9 +550,9 @@ private static function filterTypeWithAnother( } } } + $existing_type = $existing_type->setTypes($existing_types); if ($matching_atomic_types) { - $existing_type->bustCache(); return new Union($matching_atomic_types); } @@ -559,7 +560,7 @@ private static function filterTypeWithAnother( } private static function filterAtomicWithAnother( - Atomic $type_1_atomic, + Atomic &$type_1_atomic, Atomic $type_2_atomic, Codebase $codebase, bool &$any_scalar_type_match_found @@ -572,7 +573,7 @@ private static function filterAtomicWithAnother( } if ($type_1_atomic instanceof TNamedObject) { - $type_1_atomic->is_static = false; + $type_1_atomic = $type_1_atomic->setIsStatic(false); } $atomic_comparison_results = new TypeComparisonResult(); @@ -632,23 +633,27 @@ private static function filterAtomicWithAnother( $type_2_value = $type_2_atomic->getGenericValueType(); if (!$type_2_key->hasString()) { + $type_1_type_param = $type_1_atomic->type_param; $type_2_value = self::filterTypeWithAnother( $codebase, - $type_1_atomic->type_param, + $type_1_type_param, $type_2_value, $any_scalar_type_match_found ); + $type_1_atomic = $type_1_atomic->replaceTypeParam($type_1_type_param); if ($type_2_value === null) { return null; } - $hybrid_type_part = new TKeyedArray($type_2_atomic->properties); - $hybrid_type_part->previous_key_type = Type::getInt(); - $hybrid_type_part->previous_value_type = $type_2_value; - $hybrid_type_part->is_list = true; - - return $hybrid_type_part; + return new TKeyedArray( + $type_2_atomic->properties, + null, + false, + Type::getInt(), + $type_2_value, + true + ); } } elseif ($type_1_atomic instanceof TKeyedArray && $type_2_atomic instanceof TList @@ -657,9 +662,10 @@ private static function filterAtomicWithAnother( $type_1_value = $type_1_atomic->getGenericValueType(); if (!$type_1_key->hasString()) { + $type_2_type_param = $type_2_atomic->type_param; $type_1_value = self::filterTypeWithAnother( $codebase, - $type_2_atomic->type_param, + $type_2_type_param, $type_1_value, $any_scalar_type_match_found ); @@ -668,12 +674,14 @@ private static function filterAtomicWithAnother( return null; } - $hybrid_type_part = new TKeyedArray($type_1_atomic->properties); - $hybrid_type_part->previous_key_type = Type::getInt(); - $hybrid_type_part->previous_value_type = $type_1_value; - $hybrid_type_part->is_list = true; - - return $hybrid_type_part; + return new TKeyedArray( + $type_1_atomic->properties, + null, + false, + Type::getInt(), + $type_1_value, + true + ); } } @@ -695,8 +703,9 @@ private static function filterAtomicWithAnother( || $type_1_atomic instanceof TIterable) && count($type_2_atomic->type_params) === count($type_1_atomic->type_params) ) { + $type_1_params = $type_1_atomic->type_params; foreach ($type_2_atomic->type_params as $i => $type_2_param) { - $type_1_param = $type_1_atomic->type_params[$i]; + $type_1_param = $type_1_params[$i]; $type_2_param_id = $type_2_param->getId(); @@ -711,12 +720,16 @@ private static function filterAtomicWithAnother( return null; } - if ($type_1_atomic->type_params[$i]->getId() !== $type_2_param_id) { - /** @psalm-suppress PropertyTypeCoercion */ - $type_1_atomic->type_params[$i] = $type_2_param; + if ($type_1_params[$i]->getId() !== $type_2_param_id) { + $type_1_params[$i] = $type_2_param; } } + /** @psalm-suppress ArgumentTypeCoercion */ + $type_1_atomic = $type_1_atomic->replaceTypeParams( + $type_1_params + ); + $matching_atomic_type = $type_1_atomic; $atomic_comparison_results->type_coerced = true; } @@ -740,8 +753,10 @@ private static function filterAtomicWithAnother( return null; } - if ($type_1_atomic->type_param->getId() !== $type_2_param->getId()) { - $type_1_atomic->type_param = $type_2_param; + if ($type_1_param->getId() !== $type_2_param->getId()) { + $type_1_atomic = $type_1_atomic->replaceTypeParam($type_2_param); + } elseif ($type_1_param !== $type_1_atomic->type_param) { + $type_1_atomic = $type_1_atomic->replaceTypeParam($type_1_param); } $matching_atomic_type = $type_1_atomic; @@ -754,7 +769,8 @@ private static function filterAtomicWithAnother( && $type_1_atomic instanceof TKeyedArray ) { $type_2_param = $type_2_atomic->type_params[1]; - foreach ($type_1_atomic->properties as $property_key => $type_1_param) { + $type_1_properties = $type_1_atomic->properties; + foreach ($type_1_properties as &$type_1_param) { $type_2_param = self::filterTypeWithAnother( $codebase, $type_1_param, @@ -766,12 +782,12 @@ private static function filterAtomicWithAnother( return null; } - if ($type_1_atomic->properties[$property_key]->getId() !== $type_2_param->getId()) { - $type_1_atomic->properties[$property_key] = $type_2_param; + if ($type_1_param->getId() !== $type_2_param->getId()) { + $type_1_param = $type_2_param; } } - $matching_atomic_type = $type_1_atomic; + $matching_atomic_type = $type_1_atomic->setProperties($type_1_properties); $atomic_comparison_results->type_coerced = true; } @@ -823,9 +839,10 @@ private static function refineContainedAtomicWithAnother( && $type_1_atomic instanceof TTemplateParam && $type_1_atomic->as->hasObjectType() ) { + $type_1_as_init = $type_1_atomic->as; $type_1_as = self::filterTypeWithAnother( $codebase, - $type_1_atomic->as, + $type_1_as_init, new Union([$type_2_atomic]) ); diff --git a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php index b267ffca9e4..5948512b93b 100644 --- a/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ArrayTypeComparator.php @@ -111,6 +111,7 @@ public static function isContainedBy( && $input_type_part instanceof TNonEmptyArray && $input_type_part->type_params[0]->isSingleIntLiteral() && $input_type_part->type_params[0]->getSingleIntLiteral()->value === 0 + && isset($input_type_part->type_params[1]) ) { //this is a special case where the only offset value of an non empty array is 0, so it's a non empty list return UnionTypeComparator::isContainedBy( diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 14967ab6290..643c11d0e75 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -326,7 +326,7 @@ public static function getCallableFromAtomic( } } - $matching_callable = InternalCallMapHandler::getCallableFromCallMapById( + $matching_callable = clone InternalCallMapHandler::getCallableFromCallMapById( $codebase, $input_type_part->value, $args, @@ -335,6 +335,7 @@ public static function getCallableFromAtomic( $must_use = false; + /** @psalm-suppress InaccessibleProperty We just cloned this object */ $matching_callable->is_pure = $codebase->functions->isCallMapFunctionPure( $codebase, $statements_analyzer->node_data ?? null, diff --git a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php index e99d00433a4..4c563f7c0a9 100644 --- a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php @@ -57,6 +57,16 @@ public static function isContainedBy( $container_type_params_covariant ); + $atomic_comparison_result_type_params = null; + if ($atomic_comparison_result) { + if (!$atomic_comparison_result->replacement_atomic_type) { + $atomic_comparison_result->replacement_atomic_type = clone $input_type_part; + } + + if ($atomic_comparison_result->replacement_atomic_type instanceof TGenericObject) { + $atomic_comparison_result_type_params = $atomic_comparison_result->replacement_atomic_type->type_params; + } + } foreach ($input_type_params as $i => $input_param) { if (!isset($container_type_part->type_params[$i])) { break; @@ -65,16 +75,8 @@ public static function isContainedBy( $container_param = $container_type_part->type_params[$i]; if ($input_param->isNever()) { - if ($atomic_comparison_result) { - if (!$atomic_comparison_result->replacement_atomic_type) { - $atomic_comparison_result->replacement_atomic_type = clone $input_type_part; - } - - if ($atomic_comparison_result->replacement_atomic_type instanceof TGenericObject) { - /** @psalm-suppress PropertyTypeCoercion */ - $atomic_comparison_result->replacement_atomic_type->type_params[$i] - = clone $container_param; - } + if ($atomic_comparison_result_type_params !== null) { + $atomic_comparison_result_type_params[$i] = clone $container_param; } continue; @@ -136,16 +138,8 @@ public static function isContainedBy( && !$input_param->hasTemplate() ) { if ($input_param->containsAnyLiteral()) { - if ($atomic_comparison_result) { - if (!$atomic_comparison_result->replacement_atomic_type) { - $atomic_comparison_result->replacement_atomic_type = clone $input_type_part; - } - - if ($atomic_comparison_result->replacement_atomic_type instanceof TGenericObject) { - /** @psalm-suppress PropertyTypeCoercion */ - $atomic_comparison_result->replacement_atomic_type->type_params[$i] - = clone $container_param; - } + if ($atomic_comparison_result_type_params !== null) { + $atomic_comparison_result_type_params[$i] = clone $container_param; } } else { if (!($container_type_params_covariant[$i] ?? false) @@ -167,13 +161,7 @@ public static function isContainedBy( ) { // do nothing } else { - if ($container_param->hasMixed() || $container_param->isArrayKey()) { - if ($atomic_comparison_result) { - $atomic_comparison_result->type_coerced_from_mixed = true; - } - } else { - $all_types_contain = false; - } + $all_types_contain = false; if ($atomic_comparison_result) { $atomic_comparison_result->type_coerced = false; @@ -185,6 +173,15 @@ public static function isContainedBy( } } + if ($atomic_comparison_result + && $atomic_comparison_result->replacement_atomic_type instanceof TGenericObject + && $atomic_comparison_result_type_params + ) { + $atomic_comparison_result->replacement_atomic_type = + $atomic_comparison_result->replacement_atomic_type + ->replaceTypeParams($atomic_comparison_result_type_params); + } + if ($all_types_contain) { if ($atomic_comparison_result) { $atomic_comparison_result->to_string_cast = false; diff --git a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php index 6b98ebb6c4c..96ed14f299c 100644 --- a/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/IntegerRangeComparator.php @@ -84,6 +84,8 @@ public static function isContainedByUnion( * This method receives an array of atomics from the container and a range. * The goal is to use values in atomics in order to reduce the range. * Once the range is empty, it means that every value in range was covered by some atomics combination + * + * @psalm-suppress InaccessibleProperty $reduced_range was already cloned * @param array $container_atomic_types */ private static function reduceRangeIncrementally(array &$container_atomic_types, TIntRange $reduced_range): ?bool diff --git a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php index 5de00062194..a0a15900294 100644 --- a/src/Psalm/Internal/Type/Comparator/ObjectComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ObjectComparator.php @@ -100,8 +100,7 @@ private static function getIntersectionTypes(Atomic $type_part): array // T1 as T2 as object becomes (T1 as object) & (T2 as object) if ($as_atomic_type instanceof TTemplateParam) { $intersection_types += self::getIntersectionTypes($as_atomic_type); - $type_part = clone $type_part; - $type_part->as = $as_atomic_type->as; + $type_part = $type_part->replaceAs($as_atomic_type->as); $intersection_types[$type_part->getKey()] = $type_part; return $intersection_types; diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 5def9f98d27..95a2bc30c04 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -2,6 +2,7 @@ namespace Psalm\Internal\Type; +use AssertionError; use Psalm\CodeLocation; use Psalm\Codebase; use Psalm\Internal\Codebase\ClassConstantByWildcardResolver; @@ -74,7 +75,7 @@ use Psalm\Type\Reconciler; use Psalm\Type\Union; -use function assert; +use function array_merge; use function count; use function explode; use function get_class; @@ -105,7 +106,7 @@ public static function reconcile( int &$failed_reconciliation = Reconciler::RECONCILIATION_OK, bool $inside_loop = false ): ?Union { - if ($assertion instanceof Any && $existing_var_type->hasMixed()) { + if ($assertion instanceof Any) { return $existing_var_type; } @@ -473,14 +474,16 @@ public static function reconcile( if ($existing_var_type->isSingle() && $existing_var_type->hasTemplate() ) { - foreach ($existing_var_type->getAtomicTypes() as $atomic_type) { + $types = $existing_var_type->getAtomicTypes(); + foreach ($types as $k => $atomic_type) { if ($atomic_type instanceof TTemplateParam && $assertion_type) { if ($atomic_type->as->hasMixed() || $atomic_type->as->hasObject() ) { - $atomic_type->as = new Union([clone $assertion_type]); - - return $existing_var_type; + unset($types[$k]); + $atomic_type = $atomic_type->replaceAs(new Union([clone $assertion_type])); + $types[$atomic_type->getKey()] = $atomic_type; + return new Union($types); } } } @@ -586,13 +589,11 @@ private static function reconcileNonEmptyCountable( $existing_var_type->removeType('array'); } else { $non_empty_array = new TNonEmptyArray( - $array_atomic_type->type_params + $array_atomic_type->type_params, + null, + $assertion instanceof HasAtLeastCount ? $assertion->count : null ); - if ($assertion instanceof HasAtLeastCount) { - $non_empty_array->min_count = $assertion->count; - } - $existing_var_type->addType($non_empty_array); } @@ -604,13 +605,11 @@ private static function reconcileNonEmptyCountable( && $array_atomic_type->count < $assertion->count) ) { $non_empty_list = new TNonEmptyList( - $array_atomic_type->type_param + $array_atomic_type->type_param, + null, + $assertion instanceof HasAtLeastCount ? $assertion->count : null ); - if ($assertion instanceof HasAtLeastCount) { - $non_empty_list->min_count = $assertion->count; - } - $did_remove_type = true; $existing_var_type->addType($non_empty_list); } @@ -635,10 +634,14 @@ private static function reconcileNonEmptyCountable( // this means a redundant condition } else { $did_remove_type = true; + $properties = $array_atomic_type->properties; for ($i = $prop_count; $i < $assertion->count; $i++) { - $array_atomic_type->properties[$i] + $properties[$i] = clone ($array_atomic_type->previous_value_type ?: Type::getMixed()); } + $array_atomic_type = $array_atomic_type->setProperties($properties); + $existing_var_type->removeType('array'); + $existing_var_type->addType($array_atomic_type); } } else { $did_remove_type = true; @@ -683,11 +686,10 @@ private static function reconcileExactlyCountable( if ($array_atomic_type instanceof TArray) { $non_empty_array = new TNonEmptyArray( - $array_atomic_type->type_params + $array_atomic_type->type_params, + $count ); - $non_empty_array->count = $count; - $existing_var_type->addType( $non_empty_array ); @@ -734,36 +736,44 @@ private static function reconcileHasMethod( if (!$codebase->methodExists($type->value . '::' . $method_name)) { $match_found = false; - if ($type->extra_types) { - foreach ($type->extra_types as $extra_type) { - if ($extra_type instanceof TNamedObject - && $codebase->classOrInterfaceExists($extra_type->value) - && $codebase->methodExists($extra_type->value . '::' . $method_name) - ) { - $match_found = true; - } elseif ($extra_type instanceof TObjectWithProperties) { - $match_found = true; - - if (!isset($extra_type->methods[$method_name])) { - $extra_type->methods[$method_name] = 'object::' . $method_name; - $did_remove_type = true; - } + $extra_types = $type->extra_types; + foreach ($type->extra_types as $k => $extra_type) { + if ($extra_type instanceof TNamedObject + && $codebase->classOrInterfaceExists($extra_type->value) + && $codebase->methodExists($extra_type->value . '::' . $method_name) + ) { + $match_found = true; + } elseif ($extra_type instanceof TObjectWithProperties) { + $match_found = true; + + if (!isset($extra_type->methods[$method_name])) { + unset($extra_types[$k]); + $extra_type = $extra_type->setMethods(array_merge($extra_type->methods, [ + $method_name => 'object::' . $method_name + ])); + $extra_types[$extra_type->getKey()] = $extra_type; + $did_remove_type = true; } } } if (!$match_found) { - $type = $type->addIntersectionType(new TObjectWithProperties( + $extra_type = new TObjectWithProperties( [], [$method_name => $type->value . '::' . $method_name] - )); + ); + $extra_types[$extra_type->getKey()] = $extra_type; $did_remove_type = true; } + + $type = $type->setIntersectionTypes($extra_types); } $object_types[] = $type; } elseif ($type instanceof TObjectWithProperties) { if (!isset($type->methods[$method_name])) { - $type->methods[$method_name] = 'object::' . $method_name; + $type = $type->setMethods(array_merge($type->methods, [ + $method_name => 'object::' . $method_name + ])); $did_remove_type = true; } $object_types[] = $type; @@ -841,11 +851,10 @@ private static function reconcileString( foreach ($existing_var_atomic_types as $type) { if ($type instanceof TString) { - $string_types[] = $type; - if (get_class($type) === TString::class) { - $type->from_docblock = false; + $type = $type->setFromDocblock(false); } + $string_types[] = $type; } elseif ($type instanceof TCallable) { $string_types[] = new TCallableString; $did_remove_type = true; @@ -932,12 +941,12 @@ private static function reconcileInt( foreach ($existing_var_atomic_types as $type) { if ($type instanceof TInt) { - $int_types[] = $type; - if (get_class($type) === TInt::class) { - $type->from_docblock = false; + $type = $type->setFromDocblock(false); } + $int_types[] = $type; + if ($existing_var_type->from_calculation) { $did_remove_type = true; } @@ -949,9 +958,7 @@ private static function reconcileInt( $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasInt() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileInt( + $type = $type->replaceAs(self::reconcileInt( $assertion, $type->as, null, @@ -959,7 +966,7 @@ private static function reconcileInt( null, $suppressed_issues, $failed_reconciliation - ); + )); $int_types[] = $type; } @@ -1025,16 +1032,14 @@ private static function reconcileBool( foreach ($existing_var_atomic_types as $type) { if ($type instanceof TBool) { + $type = $type->setFromDocblock(false); $bool_types[] = $type; - $type->from_docblock = false; } elseif ($type instanceof TScalar) { $bool_types[] = new TBool; $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasBool() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileBool( + $type = $type->replaceAs(self::reconcileBool( $assertion, $type->as, null, @@ -1043,7 +1048,7 @@ private static function reconcileBool( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $bool_types[] = $type; } @@ -1109,9 +1114,7 @@ private static function reconcileScalar( $scalar_types[] = $type; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasScalar() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileScalar( + $type = $type->replaceAs(self::reconcileScalar( $assertion, $type->as, null, @@ -1120,7 +1123,7 @@ private static function reconcileScalar( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $scalar_types[] = $type; } @@ -1204,9 +1207,7 @@ private static function reconcileNumeric( $numeric_types[] = new TNumericString(); } elseif ($type instanceof TTemplateParam) { if ($type->as->hasNumeric() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileNumeric( + $type = $type->replaceAs(self::reconcileNumeric( $assertion, $type->as, null, @@ -1215,7 +1216,7 @@ private static function reconcileNumeric( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $numeric_types[] = $type; } @@ -1285,15 +1286,12 @@ private static function reconcileObject( } elseif ($type instanceof TTemplateParam && $type->as->isMixed() ) { - $type = clone $type; - $type->as = Type::getObject(); + $type = $type->replaceAs(Type::getObject()); $object_types[] = $type; $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasObject() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileObject( + $type = $type->replaceAs(self::reconcileObject( $assertion, $type->as, null, @@ -1302,7 +1300,7 @@ private static function reconcileObject( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $object_types[] = $type; } @@ -1594,7 +1592,8 @@ private static function reconcileHasArrayKey( HasArrayKey $assertion ): Union { $assertion = $assertion->key; - foreach ($existing_var_type->getAtomicTypes() as $atomic_type) { + $types = $existing_var_type->getAtomicTypes(); + foreach ($types as &$atomic_type) { if ($atomic_type instanceof TKeyedArray) { $is_class_string = false; @@ -1606,16 +1605,24 @@ private static function reconcileHasArrayKey( if (isset($atomic_type->properties[$assertion])) { $atomic_type->properties[$assertion]->possibly_undefined = false; } else { - $atomic_type->properties[$assertion] = Type::getMixed(); - - if ($is_class_string) { - $atomic_type->class_strings[$assertion] = true; - } + $atomic_type = new TKeyedArray( + array_merge( + $atomic_type->properties, + [$assertion => Type::getMixed()] + ), + $is_class_string ? array_merge( + $atomic_type->class_strings ?? [], + [$assertion => true] + ) : $atomic_type->class_strings, + $atomic_type->sealed, + $atomic_type->previous_key_type, + $atomic_type->previous_value_type, + $atomic_type->is_list + ); } } } - - return $existing_var_type; + return $existing_var_type->setTypes($types); } /** @@ -1652,15 +1659,19 @@ private static function reconcileIsGreaterThan( // if the range contains the assertion, the range must be adapted $did_remove_type = true; $existing_var_type->removeType($atomic_type->getKey()); - if ($atomic_type->min_bound === null) { - $atomic_type->min_bound = $assertion_value; + $min_bound = $atomic_type->min_bound; + if ($min_bound === null) { + $min_bound = $assertion_value; } else { - $atomic_type->min_bound = TIntRange::getNewHighestBound( + $min_bound = TIntRange::getNewHighestBound( $assertion_value, - $atomic_type->min_bound + $min_bound ); } - $existing_var_type->addType($atomic_type); + $existing_var_type->addType(new TIntRange( + $min_bound, + $atomic_type->max_bound + )); } elseif ($atomic_type->isLesserThan($assertion_value)) { // if the range is lesser than the assertion, the type must be removed $did_remove_type = true; @@ -1758,12 +1769,16 @@ private static function reconcileIsLessThan( // if the range contains the assertion, the range must be adapted $did_remove_type = true; $existing_var_type->removeType($atomic_type->getKey()); - if ($atomic_type->max_bound === null) { - $atomic_type->max_bound = $assertion_value; + $max_bound = $atomic_type->max_bound; + if ($max_bound === null) { + $max_bound = $assertion_value; } else { - $atomic_type->max_bound = min($atomic_type->max_bound, $assertion_value); + $max_bound = min($max_bound, $assertion_value); } - $existing_var_type->addType($atomic_type); + $existing_var_type->addType(new TIntRange( + $atomic_type->min_bound, + $max_bound + )); } elseif ($atomic_type->isLesserThan($assertion_value)) { // if the range is lesser than the assertion, the check is redundant } elseif ($atomic_type->isGreaterThan($assertion_value)) { @@ -1942,9 +1957,7 @@ private static function reconcileArray( $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasArray() || $type->as->hasIterable() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileArray( + $type = $type->replaceAs(self::reconcileArray( $assertion, $type->as, null, @@ -1953,7 +1966,7 @@ private static function reconcileArray( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); $array_types[] = $type; } @@ -2290,9 +2303,7 @@ private static function reconcileCallable( $did_remove_type = true; } elseif ($type instanceof TTemplateParam) { if ($type->as->hasCallableType() || $type->as->hasMixed()) { - $type = clone $type; - - $type->as = self::reconcileCallable( + $type = $type->replaceAs(self::reconcileCallable( $assertion, $codebase, $type->as, @@ -2302,7 +2313,7 @@ private static function reconcileCallable( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); } $did_remove_type = true; @@ -2354,7 +2365,7 @@ private static function reconcileTruthyOrNonEmpty( int &$failed_reconciliation, bool $recursive_check ): Union { - $existing_var_type = $existing_var_type->getBuilder(); + $types = $existing_var_type->getAtomicTypes(); $old_var_type_string = $existing_var_type->getId(); //empty is used a lot to check for array offset existence, so we have to silent errors a lot @@ -2363,12 +2374,12 @@ private static function reconcileTruthyOrNonEmpty( $did_remove_type = $existing_var_type->possibly_undefined || $existing_var_type->possibly_undefined_from_try; - foreach ($existing_var_type->getAtomicTypes() as $existing_var_type_key => $existing_var_type_part) { + foreach ($types as $existing_var_type_key => $existing_var_type_part) { //if any atomic in the union is either always falsy, we remove it. If not always truthy, we mark the check //as not redundant. if ($existing_var_type_part->isFalsy()) { $did_remove_type = true; - $existing_var_type->removeType($existing_var_type_key); + unset($types[$existing_var_type_key]); } elseif ($existing_var_type->possibly_undefined || $existing_var_type->possibly_undefined_from_try || !$existing_var_type_part->isTruthy() @@ -2377,7 +2388,7 @@ private static function reconcileTruthyOrNonEmpty( } } - if ($did_remove_type && $existing_var_type->isUnionEmpty()) { + if ($did_remove_type && !$types) { //every type was removed, this is an impossible assertion if ($code_location && $key && !$is_empty_assertion && !$recursive_check) { self::triggerIssueForImpossible( @@ -2413,74 +2424,66 @@ private static function reconcileTruthyOrNonEmpty( $failed_reconciliation = 1; - return $existing_var_type->freeze(); + if (!$types) { + throw new AssertionError("We must have some types here!"); + } + return $existing_var_type->setTypes($types); } - $existing_var_type->possibly_undefined = false; - $existing_var_type->possibly_undefined_from_try = false; - - if ($existing_var_type->hasType('bool')) { - $existing_var_type->removeType('bool'); - $existing_var_type->addType(new TTrue()); + if (isset($types['bool'])) { + unset($types['bool']); + $types []= new TTrue; } - if ($existing_var_type->hasArray()) { - $array_atomic_type = $existing_var_type->getAtomicTypes()['array']; + if (isset($types['array'])) { + $array_atomic_type = $types['array']; if ($array_atomic_type instanceof TArray && !$array_atomic_type instanceof TNonEmptyArray ) { - $existing_var_type->removeType('array'); - $existing_var_type->addType( - new TNonEmptyArray( - $array_atomic_type->type_params - ) - ); + unset($types['array']); + $types [] = new TNonEmptyArray($array_atomic_type->type_params); } elseif ($array_atomic_type instanceof TList && !$array_atomic_type instanceof TNonEmptyList ) { - $existing_var_type->removeType('array'); - $existing_var_type->addType( - new TNonEmptyList( - $array_atomic_type->type_param - ) - ); + unset($types['array']); + $types [] = new TNonEmptyList($array_atomic_type->type_param); } } - if ($existing_var_type->hasMixed()) { - $mixed_atomic_type = $existing_var_type->getAtomicTypes()['mixed']; + if (isset($types['mixed'])) { + $mixed_atomic_type = $types['mixed']; if (get_class($mixed_atomic_type) === TMixed::class) { - $existing_var_type->removeType('mixed'); - $existing_var_type->addType(new TNonEmptyMixed()); + unset($types['mixed']); + $types []= new TNonEmptyMixed(); } } - if ($existing_var_type->hasScalar()) { - $scalar_atomic_type = $existing_var_type->getAtomicTypes()['scalar']; + if (isset($types['scalar'])) { + $scalar_atomic_type = $types['scalar']; if (get_class($scalar_atomic_type) === TScalar::class) { - $existing_var_type->removeType('scalar'); - $existing_var_type->addType(new TNonEmptyScalar()); + unset($types['scalar']); + $types []= new TNonEmptyScalar(); } } - if ($existing_var_type->hasType('string')) { - $string_atomic_type = $existing_var_type->getAtomicTypes()['string']; + if (isset($types['string'])) { + $string_atomic_type = $types['string']; if (get_class($string_atomic_type) === TString::class) { - $existing_var_type->removeType('string'); - $existing_var_type->addType(new TNonFalsyString()); + unset($types['string']); + $types []= new TNonFalsyString(); } elseif (get_class($string_atomic_type) === TLowercaseString::class) { - $existing_var_type->removeType('string'); - $existing_var_type->addType(new TNonEmptyLowercaseString()); + unset($types['string']); + $types []= new TNonEmptyLowercaseString(); } elseif (get_class($string_atomic_type) === TNonspecificLiteralString::class) { - $existing_var_type->removeType('string'); - $existing_var_type->addType(new TNonEmptyNonspecificLiteralString()); + unset($types['string']); + $types []= new TNonEmptyNonspecificLiteralString(); } elseif (get_class($string_atomic_type) === TNonEmptyString::class) { - $existing_var_type->removeType('string'); - $existing_var_type->addType(new TNonFalsyString()); + unset($types['string']); + $types []= new TNonFalsyString(); } } @@ -2490,30 +2493,24 @@ private static function reconcileTruthyOrNonEmpty( if ($existing_range_types) { foreach ($existing_range_types as $int_key => $literal_type) { if ($literal_type->contains(0)) { - $existing_var_type->removeType($int_key); + unset($types[$int_key]); if ($literal_type->min_bound === null || $literal_type->min_bound <= -1) { - $existing_var_type->addType(new TIntRange($literal_type->min_bound, -1)); + $types []= new TIntRange($literal_type->min_bound, -1); } if ($literal_type->max_bound === null || $literal_type->max_bound >= 1) { - $existing_var_type->addType(new TIntRange(1, $literal_type->max_bound)); + $types []= new TIntRange(1, $literal_type->max_bound); } } } } - - if ($existing_var_type->isSingle()) { - return $existing_var_type->freeze(); - } } - foreach ($existing_var_type->getAtomicTypes() as $type_key => $existing_var_atomic_type) { + foreach ($types as $type_key => $existing_var_atomic_type) { if ($existing_var_atomic_type instanceof TTemplateParam) { if (!$existing_var_atomic_type->as->isMixed()) { $template_did_fail = 0; - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::reconcileTruthyOrNonEmpty( + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs(self::reconcileTruthyOrNonEmpty( $assertion, $existing_var_atomic_type->as, $key, @@ -2522,18 +2519,29 @@ private static function reconcileTruthyOrNonEmpty( $suppressed_issues, $template_did_fail, true - ); + )); if (!$template_did_fail) { - $existing_var_type->removeType($type_key); - $existing_var_type->addType($existing_var_atomic_type); + unset($types[$type_key]); + $types []= $existing_var_atomic_type; } } } } - assert(!$existing_var_type->isUnionEmpty()); - return $existing_var_type->freeze(); + if (!$types) { + throw new AssertionError("We must have some types here!"); + } + $new = $existing_var_type->setTypes($types); + if ($new === $existing_var_type && ($new->possibly_undefined || $new->possibly_undefined_from_try)) { + $new = clone $existing_var_type; + $new->possibly_undefined = false; + $new->possibly_undefined_from_try = false; + } else { + $new->possibly_undefined = false; + $new->possibly_undefined_from_try = false; + } + return $new; } /** diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index 357dc0888e9..439d887907c 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -589,18 +589,18 @@ private static function reconcileNull( int &$failed_reconciliation, bool $is_equality ): Union { - $existing_var_type = $existing_var_type->getBuilder(); + $types = $existing_var_type->getAtomicTypes(); $old_var_type_string = $existing_var_type->getId(); $did_remove_type = false; - if ($existing_var_type->hasType('null')) { + if (isset($types['null'])) { $did_remove_type = true; - $existing_var_type->removeType('null'); + unset($types['null']); } - foreach ($existing_var_type->getAtomicTypes() as $type) { + foreach ($types as &$type) { if ($type instanceof TTemplateParam) { - $type->as = self::reconcileNull( + $new = $type->replaceAs(self::reconcileNull( $assertion, $type->as, null, @@ -609,14 +609,17 @@ private static function reconcileNull( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); + // $did_remove_type = $did_remove_type || $new !== $type; + // TODO: This is technically wrong, but for some reason we get a + // duplicated assertion here when using template types. $did_remove_type = true; - $existing_var_type->bustCache(); + $type = $new; } } - if (!$did_remove_type || $existing_var_type->isUnionEmpty()) { + if (!$did_remove_type || !$types) { if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, @@ -635,8 +638,8 @@ private static function reconcileNull( } } - if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type->freeze(); + if ($types) { + return $existing_var_type->setTypes($types); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -660,18 +663,18 @@ private static function reconcileFalse( int &$failed_reconciliation, bool $is_equality ): Union { + $types = $existing_var_type->getAtomicTypes(); $old_var_type_string = $existing_var_type->getId(); - $did_remove_type = $existing_var_type->hasScalar(); + $did_remove_type = false; - $existing_var_type = $existing_var_type->getBuilder(); - if ($existing_var_type->hasType('false')) { + if (isset($types['false'])) { $did_remove_type = true; - $existing_var_type->removeType('false'); + unset($types['false']); } - foreach ($existing_var_type->getAtomicTypes() as $type) { + foreach ($types as &$type) { if ($type instanceof TTemplateParam) { - $type->as = self::reconcileFalse( + $new = $type->replaceAs(self::reconcileFalse( $assertion, $type->as, null, @@ -680,14 +683,14 @@ private static function reconcileFalse( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); - $did_remove_type = true; - $existing_var_type->bustCache(); + $did_remove_type = $did_remove_type || $new !== $type; + $type = $new; } } - if (!$did_remove_type || $existing_var_type->isUnionEmpty()) { + if (!$did_remove_type || !$types) { if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, @@ -706,8 +709,8 @@ private static function reconcileFalse( } } - if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type->freeze(); + if ($types) { + return $existing_var_type->setTypes($types); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -877,9 +880,7 @@ private static function reconcileFalsyOrEmpty( if (!$existing_var_atomic_type->as->isMixed()) { $template_did_fail = 0; - $existing_var_atomic_type = clone $existing_var_atomic_type; - - $existing_var_atomic_type->as = self::reconcileFalsyOrEmpty( + $existing_var_atomic_type = $existing_var_atomic_type->replaceAs(self::reconcileFalsyOrEmpty( $assertion, $existing_var_atomic_type->as, $key, @@ -888,7 +889,7 @@ private static function reconcileFalsyOrEmpty( $suppressed_issues, $template_did_fail, $recursive_check - ); + )); if (!$template_did_fail) { $existing_var_type->removeType($type_key); @@ -925,9 +926,7 @@ private static function reconcileScalar( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileScalar( + $type = $type->replaceAs(self::reconcileScalar( $assertion, $type->as, null, @@ -936,7 +935,7 @@ private static function reconcileScalar( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1015,9 +1014,7 @@ private static function reconcileObject( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileObject( + $type = $type->replaceAs(self::reconcileObject( $assertion, $type->as, null, @@ -1026,7 +1023,7 @@ private static function reconcileObject( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1119,9 +1116,7 @@ private static function reconcileNumeric( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileNumeric( + $type = $type->replaceAs(self::reconcileNumeric( $assertion, $type->as, null, @@ -1130,7 +1125,7 @@ private static function reconcileNumeric( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1212,9 +1207,7 @@ private static function reconcileInt( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileInt( + $type = $type->replaceAs(self::reconcileInt( $assertion, $type->as, null, @@ -1223,7 +1216,7 @@ private static function reconcileInt( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1316,9 +1309,7 @@ private static function reconcileFloat( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileFloat( + $type = $type->replaceAs(self::reconcileFloat( $assertion, $type->as, null, @@ -1327,7 +1318,7 @@ private static function reconcileFloat( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1415,9 +1406,7 @@ private static function reconcileString( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileString( + $type = $type->replaceAs(self::reconcileString( $assertion, $type->as, null, @@ -1426,7 +1415,7 @@ private static function reconcileString( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1523,9 +1512,7 @@ private static function reconcileArray( if (!$is_equality && !$type->as->isMixed()) { $template_did_fail = 0; - $type = clone $type; - - $type->as = self::reconcileArray( + $type = $type->replaceAs(self::reconcileArray( $assertion, $type->as, null, @@ -1534,7 +1521,7 @@ private static function reconcileArray( $suppressed_issues, $template_did_fail, $is_equality - ); + )); $did_remove_type = true; @@ -1619,19 +1606,18 @@ private static function reconcileResource( int &$failed_reconciliation, bool $is_equality ): Union { + $types = $existing_var_type->getAtomicTypes(); $old_var_type_string = $existing_var_type->getId(); $did_remove_type = false; - if ($existing_var_type->hasType('resource')) { + if (isset($types['resource'])) { $did_remove_type = true; - $existing_var_type = $existing_var_type->getBuilder(); - $existing_var_type->removeType('resource'); - $existing_var_type = $existing_var_type->freeze(); + unset($types['resource']); } - foreach ($existing_var_type->getAtomicTypes() as $type) { + foreach ($types as &$type) { if ($type instanceof TTemplateParam) { - $type->as = self::reconcileResource( + $new = $type->replaceAs(self::reconcileResource( $assertion, $type->as, null, @@ -1640,14 +1626,14 @@ private static function reconcileResource( $suppressed_issues, $failed_reconciliation, $is_equality - ); + )); - $did_remove_type = true; - $existing_var_type->bustCache(); + $did_remove_type = $new !== $type; + $type = $new; } } - if (!$did_remove_type || $existing_var_type->isUnionEmpty()) { + if (!$did_remove_type || !$types) { if ($key && $code_location && !$is_equality) { self::triggerIssueForImpossible( $existing_var_type, @@ -1666,8 +1652,8 @@ private static function reconcileResource( } } - if (!$existing_var_type->isUnionEmpty()) { - return $existing_var_type; + if ($types) { + return $existing_var_type->setTypes($types); } $failed_reconciliation = Reconciler::RECONCILIATION_EMPTY; @@ -1711,14 +1697,17 @@ private static function reconcileIsLessThanOrEqualTo( $did_remove_type = true; $existing_var_type->removeType($atomic_type->getKey()); if ($atomic_type->max_bound === null) { - $atomic_type->max_bound = $assertion_value; + $max_bound = $assertion_value; } else { - $atomic_type->max_bound = TIntRange::getNewLowestBound( + $max_bound = TIntRange::getNewLowestBound( $assertion_value, $atomic_type->max_bound ); } - $existing_var_type->addType($atomic_type); + $existing_var_type->addType(new TIntRange( + $atomic_type->min_bound, + $max_bound + )); } elseif ($atomic_type->isLesserThan($assertion_value)) { // if the range is lesser than the assertion, the check is redundant } elseif ($atomic_type->isGreaterThan($assertion_value)) { @@ -1815,12 +1804,16 @@ private static function reconcileIsGreaterThanOrEqualTo( // if the range contains the assertion, the range must be adapted $did_remove_type = true; $existing_var_type->removeType($atomic_type->getKey()); - if ($atomic_type->min_bound === null) { - $atomic_type->min_bound = $assertion_value; + $min_bound = $atomic_type->min_bound; + if ($min_bound === null) { + $min_bound = $assertion_value; } else { - $atomic_type->min_bound = max($atomic_type->min_bound, $assertion_value); + $min_bound = max($min_bound, $assertion_value); } - $existing_var_type->addType($atomic_type); + $existing_var_type->addType(new TIntRange( + $min_bound, + $atomic_type->max_bound + )); } elseif ($atomic_type->isLesserThan($assertion_value)) { // if the range is lesser than the assertion, the type must be removed $did_remove_type = true; diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index 5ccd43ec185..7e0e76f0597 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -42,6 +42,8 @@ class TemplateInferredTypeReplacer { /** * This replaces template types in unions with the inferred types they should be + * + * @psalm-external-mutation-free */ public static function replace( Union $union, @@ -206,7 +208,6 @@ public static function replace( $new_types[] = $class_template_atomic_type; } } - if ($should_set) { $types []= $atomic_type; } @@ -394,7 +395,7 @@ private static function replaceTemplatePropertiesOf( private static function replaceConditional( TemplateResult $template_result, Codebase $codebase, - TConditional $atomic_type, + TConditional &$atomic_type, array $inferred_lower_bounds ): Union { $template_type = isset($inferred_lower_bounds[$atomic_type->param_name][$atomic_type->defining_class]) @@ -407,16 +408,19 @@ private static function replaceConditional( $if_template_type = null; $else_template_type = null; - $atomic_type = clone $atomic_type; + $as_type = $atomic_type->as_type; + $conditional_type = $atomic_type->conditional_type; + $if_type = $atomic_type->if_type; + $else_type = $atomic_type->else_type; if ($template_type) { - $atomic_type->as_type = self::replace( - $atomic_type->as_type, + $as_type = self::replace( + $as_type, $template_result, $codebase ); - if ($atomic_type->as_type->isNullable() && $template_type->isVoid()) { + if ($as_type->isNullable() && $template_type->isVoid()) { $template_type = Type::getNull(); } @@ -427,7 +431,7 @@ private static function replaceConditional( if (UnionTypeComparator::isContainedBy( $codebase, new Union([$candidate_atomic_type]), - $atomic_type->conditional_type, + $conditional_type, false, false, null, @@ -435,12 +439,12 @@ private static function replaceConditional( false ) && (!$candidate_atomic_type instanceof TInt - || $atomic_type->conditional_type->getId() !== 'float') + || $conditional_type->getId() !== 'float') ) { $matching_if_types[] = $candidate_atomic_type; } elseif (!UnionTypeComparator::isContainedBy( $codebase, - $atomic_type->conditional_type, + $conditional_type, new Union([$candidate_atomic_type]), false, false, @@ -459,7 +463,7 @@ private static function replaceConditional( && UnionTypeComparator::isContainedBy( $codebase, $if_candidate_type, - $atomic_type->conditional_type, + $conditional_type, false, false, null, @@ -467,7 +471,7 @@ private static function replaceConditional( false ) ) { - $if_template_type = clone $atomic_type->if_type; + $if_template_type = clone $if_type; $refined_template_result = clone $template_result; @@ -489,7 +493,7 @@ private static function replaceConditional( && UnionTypeComparator::isContainedBy( $codebase, $else_candidate_type, - $atomic_type->as_type, + $as_type, false, false, null, @@ -497,7 +501,7 @@ private static function replaceConditional( false ) ) { - $else_template_type = clone $atomic_type->else_type; + $else_template_type = clone $else_type; $refined_template_result = clone $template_result; @@ -517,21 +521,21 @@ private static function replaceConditional( } if (!$if_template_type && !$else_template_type) { - $atomic_type->if_type = self::replace( - $atomic_type->if_type, + $if_type = self::replace( + $if_type, $template_result, $codebase ); - $atomic_type->else_type = self::replace( - $atomic_type->else_type, + $else_type = self::replace( + $else_type, $template_result, $codebase ); $class_template_type = Type::combineUnionTypes( - $atomic_type->if_type, - $atomic_type->else_type, + $if_type, + $else_type, $codebase ); } else { @@ -542,6 +546,13 @@ private static function replaceConditional( ); } + $atomic_type = $atomic_type->replaceTypes( + $as_type, + $conditional_type, + $if_type, + $else_type + ); + return $class_template_type; } } diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 69292371375..7f6dcd07dc0 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -599,7 +599,7 @@ private static function findMatchingAtomicTypesForTemplate( * @return list */ private static function handleTemplateParamStandin( - TTemplateParam $atomic_type, + TTemplateParam &$atomic_type, string $key, ?Union $input_type, ?int $input_arg_offset, @@ -762,7 +762,7 @@ private static function handleTemplateParamStandin( $matching_input_keys = []; - $atomic_type->as = TypeExpander::expandUnion( + $as = TypeExpander::expandUnion( $codebase, $atomic_type->as, $calling_class, @@ -770,8 +770,8 @@ private static function handleTemplateParamStandin( null ); - $atomic_type->as = self::replace( - $atomic_type->as, + $as = self::replace( + $as, $template_result, $codebase, $statements_analyzer, @@ -785,6 +785,8 @@ private static function handleTemplateParamStandin( $depth + 1 ); + $atomic_type = $atomic_type->replaceAs($as); + if ($input_type && !$template_result->readonly && ( @@ -810,13 +812,11 @@ private static function handleTemplateParamStandin( } } } - $generic_param = $generic_param->freeze(); - if ($add_lower_bound) { return array_values($generic_param->getAtomicTypes()); } - $generic_param->setFromDocblock(); + $generic_param = $generic_param->setFromDocblock()->freeze(); if (isset( $template_result->lower_bounds[$param_name_key][$atomic_type->defining_class] @@ -970,13 +970,18 @@ public static function handleTemplateParamClassStandin( $atomic_types = []; + $as_type = $atomic_type->as_type; if ($input_type && !$template_result->readonly) { $valid_input_atomic_types = []; foreach ($input_type->getAtomicTypes() as $input_atomic_type) { if ($input_atomic_type instanceof TLiteralClassString) { $valid_input_atomic_types[] = new TNamedObject( - $input_atomic_type->value + $input_atomic_type->value, + false, + false, + [], + true ); } elseif ($input_atomic_type instanceof TTemplateParamClass) { $valid_input_atomic_types[] = new TTemplateParam( @@ -986,20 +991,26 @@ public static function handleTemplateParamClassStandin( : ($input_atomic_type->as === 'object' ? Type::getObject() : Type::getMixed()), - $input_atomic_type->defining_class + $input_atomic_type->defining_class, + [], + true ); } elseif ($input_atomic_type instanceof TClassString) { if ($input_atomic_type->as_type) { - $valid_input_atomic_types[] = clone $input_atomic_type->as_type; + $valid_input_atomic_types[] = $input_atomic_type->as_type->setFromDocblock(true); } elseif ($input_atomic_type->as !== 'object') { $valid_input_atomic_types[] = new TNamedObject( - $input_atomic_type->as + $input_atomic_type->as, + false, + false, + [], + true ); } else { - $valid_input_atomic_types[] = new TObject(); + $valid_input_atomic_types[] = new TObject(true); } } elseif ($input_atomic_type instanceof TDependentGetClass) { - $valid_input_atomic_types[] = new TObject(); + $valid_input_atomic_types[] = new TObject(true); } } @@ -1007,16 +1018,15 @@ public static function handleTemplateParamClassStandin( if ($valid_input_atomic_types) { $generic_param = new Union($valid_input_atomic_types); - $generic_param->setFromDocblock(); } elseif ($was_single) { $generic_param = Type::getMixed(); } - if ($atomic_type->as_type) { + if ($as_type) { // sometimes templated class-strings can contain nested templates // in the as type that need to be resolved as well. $as_type_union = self::replace( - new Union([$atomic_type->as_type]), + new Union([$as_type]), $template_result, $codebase, $statements_analyzer, @@ -1033,9 +1043,9 @@ public static function handleTemplateParamClassStandin( $first = $as_type_union->getSingleAtomic(); if (count($as_type_union->getAtomicTypes()) === 1 && $first instanceof TNamedObject) { - $atomic_type->as_type = $first; + $as_type = $first; } else { - $atomic_type->as_type = null; + $as_type = null; } } @@ -1080,7 +1090,7 @@ public static function handleTemplateParamClassStandin( } } - $class_string = new TClassString($atomic_type->as, $atomic_type->as_type); + $class_string = new TClassString($atomic_type->as, $as_type); if (!$atomic_types) { $atomic_types[] = $class_string; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 843673f894d..0aaac93c9e2 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -85,6 +85,8 @@ class TypeCombiner * - and `array + array = array` * - and `array + array = array` * + * @psalm-external-mutation-free + * * @param non-empty-list $types * @param int $literal_limit any greater number of literal types than this * will be merged to a scalar @@ -97,13 +99,7 @@ public static function combine( int $literal_limit = 500 ): Union { if (count($types) === 1) { - $union_type = new Union([$types[0]]); - - if ($types[0]->from_docblock) { - $union_type->from_docblock = true; - } - - return $union_type; + return new Union([$types[0]], $types[0]->from_docblock); } $combination = new TypeCombination(); @@ -143,23 +139,11 @@ public static function combine( && !$combination->floats ) { if (isset($combination->value_types['false'])) { - $union_type = Type::getFalse(); - - if ($from_docblock) { - $union_type->from_docblock = true; - } - - return $union_type; + return Type::getFalse($from_docblock); } if (isset($combination->value_types['true'])) { - $union_type = Type::getTrue(); - - if ($from_docblock) { - $union_type->from_docblock = true; - } - - return $union_type; + return Type::getTrue($from_docblock); } } elseif (isset($combination->value_types['void'])) { unset($combination->value_types['void']); @@ -252,7 +236,6 @@ public static function combine( if ($generic_type === 'iterable') { $new_types[] = new TIterable($generic_type_params); } else { - /** @psalm-suppress ArgumentTypeCoercion Caused by the above assignment */ $generic_object = new TGenericObject( $generic_type, $generic_type_params, @@ -271,7 +254,6 @@ public static function combine( foreach ($combination->object_type_params as $generic_type => $generic_type_params) { $generic_type = substr($generic_type, 0, (int) strpos($generic_type, '<')); - /** @psalm-suppress ArgumentTypeCoercion */ $generic_object = new TGenericObject( $generic_type, $generic_type_params, @@ -799,11 +781,12 @@ private static function scrapeTypeProperties( $existing_template_type = $combination->value_types[$type_key]; if (!$existing_template_type->as->equals($type->as)) { - $existing_template_type->as = Type::combineUnionTypes( - clone $type->as, + $existing_template_type = $existing_template_type->replaceAs(Type::combineUnionTypes( + $type->as, $existing_template_type->as, $codebase - ); + )); + $combination->value_types[$type_key] = $existing_template_type; } return null; @@ -1184,18 +1167,28 @@ private static function scrapeIntProperties( if (isset($combination->value_types['int'])) { $current_int_type = $combination->value_types['int']; if ($current_int_type instanceof TIntRange) { + $min_bound = $current_int_type->min_bound; + $max_bound = $current_int_type->max_bound; foreach ($combination->ints as $int) { if (!$current_int_type->contains($int->value)) { - $current_int_type->min_bound = TIntRange::getNewLowestBound( - $current_int_type->min_bound, + $min_bound = TIntRange::getNewLowestBound( + $min_bound, $int->value ); - $current_int_type->max_bound = TIntRange::getNewHighestBound( - $current_int_type->max_bound, + $max_bound = TIntRange::getNewHighestBound( + $max_bound, $int->value ); } } + if ($min_bound !== $current_int_type->min_bound + || $max_bound !== $current_int_type->max_bound + ) { + $combination->value_types['int'] = new TIntRange( + $min_bound, + $max_bound + ); + } } } @@ -1218,14 +1211,16 @@ private static function scrapeIntProperties( $combination->value_types['int'] = new TInt(); } } elseif ($type instanceof TIntRange) { - $type = clone $type; + $min_bound = $type->min_bound; + $max_bound = $type->max_bound; if ($combination->ints) { foreach ($combination->ints as $int) { if (!$type->contains($int->value)) { - $type->min_bound = TIntRange::getNewLowestBound($type->min_bound, $int->value); - $type->max_bound = TIntRange::getNewHighestBound($type->max_bound, $int->value); + $min_bound = TIntRange::getNewLowestBound($min_bound, $int->value); + $max_bound = TIntRange::getNewHighestBound($max_bound, $int->value); } } + $type = new TIntRange($min_bound, $max_bound); $combination->value_types['int'] = $type; } elseif (!isset($combination->value_types['int'])) { @@ -1233,8 +1228,9 @@ private static function scrapeIntProperties( } else { $old_type = $combination->value_types['int']; if ($old_type instanceof TIntRange) { - $type->min_bound = TIntRange::getNewLowestBound($old_type->min_bound, $type->min_bound); - $type->max_bound = TIntRange::getNewHighestBound($old_type->max_bound, $type->max_bound); + $min_bound = TIntRange::getNewLowestBound($old_type->min_bound, $min_bound); + $max_bound = TIntRange::getNewHighestBound($old_type->max_bound, $max_bound); + $type = new TIntRange($min_bound, $max_bound); } else { $type = new TInt(); } @@ -1360,34 +1356,42 @@ private static function handleKeyedArrayEntries( } if ($combination->objectlike_entries) { - if ($combination->all_arrays_callable) { - $objectlike = new TCallableKeyedArray($combination->objectlike_entries); - } else { - $objectlike = new TKeyedArray($combination->objectlike_entries); - } - - if ($combination->objectlike_sealed && !$combination->array_type_params) { - $objectlike->sealed = true; - } - + $previous_key_type = null; if ($combination->objectlike_key_type) { - $objectlike->previous_key_type = $combination->objectlike_key_type; + $previous_key_type = $combination->objectlike_key_type; } elseif ($combination->array_type_params && $combination->array_type_params[0]->isArrayKey() ) { - $objectlike->previous_key_type = $combination->array_type_params[0]; + $previous_key_type = $combination->array_type_params[0]; } + $previous_value_type = null; if ($combination->objectlike_value_type) { - $objectlike->previous_value_type = $combination->objectlike_value_type; + $previous_value_type = $combination->objectlike_value_type; } elseif ($combination->array_type_params && $combination->array_type_params[1]->isMixed() ) { - $objectlike->previous_value_type = $combination->array_type_params[1]; + $previous_value_type = $combination->array_type_params[1]; } - if ($combination->all_arrays_lists) { - $objectlike->is_list = true; + if ($combination->all_arrays_callable) { + $objectlike = new TCallableKeyedArray( + $combination->objectlike_entries, + null, + $combination->objectlike_sealed && !$combination->array_type_params, + $previous_key_type, + $previous_value_type, + (bool)$combination->all_arrays_lists + ); + } else { + $objectlike = new TKeyedArray( + $combination->objectlike_entries, + null, + $combination->objectlike_sealed && !$combination->array_type_params, + $previous_key_type, + $previous_value_type, + (bool)$combination->all_arrays_lists + ); } $new_types[] = $objectlike; @@ -1492,35 +1496,37 @@ private static function getArrayTypeFromGenericParams( if ($combination->objectlike_entries && $combination->objectlike_sealed ) { - $array_type = new TKeyedArray([$generic_type_params[1]]); - $array_type->previous_key_type = Type::getInt(); - $array_type->previous_value_type = $combination->array_type_params[1]; - $array_type->is_list = true; + $array_type = new TKeyedArray( + [$generic_type_params[1]], + null, + false, + Type::getInt(), + $combination->array_type_params[1], + true + ); } else { - $array_type = new TNonEmptyList($generic_type_params[1]); - - if ($combination->array_counts && count($combination->array_counts) === 1) { - /** @psalm-suppress PropertyTypeCoercion */ - $array_type->count = array_keys($combination->array_counts)[0]; - } - - if ($combination->array_min_counts) { - /** @psalm-suppress PropertyTypeCoercion */ - $array_type->min_count = min(array_keys($combination->array_min_counts)); - } + /** @psalm-suppress ArgumentTypeCoercion */ + $array_type = new TNonEmptyList( + $generic_type_params[1], + $combination->array_counts && count($combination->array_counts) === 1 + ? array_keys($combination->array_counts)[0] + : null, + $combination->array_min_counts + ? min(array_keys($combination->array_min_counts)) + : null + ); } } else { - $array_type = new TNonEmptyArray($generic_type_params); - - if ($combination->array_counts && count($combination->array_counts) === 1) { - /** @psalm-suppress PropertyTypeCoercion */ - $array_type->count = array_keys($combination->array_counts)[0]; - } - - if ($combination->array_min_counts) { - /** @psalm-suppress PropertyTypeCoercion */ - $array_type->min_count = min(array_keys($combination->array_min_counts)); - } + /** @psalm-suppress ArgumentTypeCoercion */ + $array_type = new TNonEmptyArray( + $generic_type_params, + $combination->array_counts && count($combination->array_counts) === 1 + ? array_keys($combination->array_counts)[0] + : null, + $combination->array_min_counts + ? min(array_keys($combination->array_min_counts)) + : null + ); } } else { if ($combination->all_arrays_class_string_maps diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index ec40872af57..f1e2a6c4232 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -58,7 +58,7 @@ class TypeExpander { /** - * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param string|TNamedObject|TTemplateParam|null $static_class_type */ public static function expandUnion( Codebase $codebase, @@ -126,8 +126,8 @@ public static function expandUnion( } /** - * @param string|TNamedObject|TTemplateParam|null $static_class_type - * + * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param-out Atomic $return_type * @return non-empty-list * * @psalm-suppress ConflictingReferenceConstraint Ultimately, the output type is always an Atomic @@ -197,7 +197,7 @@ public static function expandAtomic( if ($return_type instanceof TClassString && $return_type->as_type ) { - $new_as_type = clone $return_type->as_type; + $new_as_type = $return_type->as_type; self::expandAtomic( $codebase, @@ -213,9 +213,14 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); - if ($new_as_type instanceof TNamedObject) { - $return_type->as_type = $new_as_type; - $return_type->as = $return_type->as_type->value; + if ($new_as_type instanceof TNamedObject && $new_as_type !== $return_type->as_type) { + $return_type = new TClassString( + $new_as_type->value, + $new_as_type, + $return_type->is_loaded, + $return_type->is_interface, + $return_type->is_enum, + ); } } elseif ($return_type instanceof TTemplateParam) { $new_as_type = self::expandUnion( @@ -236,12 +241,15 @@ public static function expandAtomic( return array_values($new_as_type->getAtomicTypes()); } - $return_type->as = $new_as_type; + $return_type = $return_type->replaceAs($new_as_type); } if ($return_type instanceof TClassConstant) { if ($self_class) { - $return_type = $return_type->replaceClassLike('self', $self_class); + $return_type = $return_type->replaceClassLike( + 'self', + $self_class + ); } if (is_string($static_class_type) || $self_class) { $return_type = $return_type->replaceClassLike( @@ -473,9 +481,9 @@ public static function expandAtomic( || $return_type instanceof TGenericObject || $return_type instanceof TIterable ) { - foreach ($return_type->type_params as $k => $type_param) { - /** @psalm-suppress PropertyTypeCoercion */ - $return_type->type_params[$k] = self::expandUnion( + $type_params = $return_type->type_params; + foreach ($type_params as &$type_param) { + $type_param = self::expandUnion( $codebase, $type_param, $self_class, @@ -489,8 +497,11 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + /** @psalm-suppress ArgumentTypeCoercion */ + $return_type = $return_type->replaceTypeParams($type_params); } elseif ($return_type instanceof TKeyedArray) { - foreach ($return_type->properties as &$property_type) { + $properties = $return_type->properties; + foreach ($properties as &$property_type) { $property_type = self::expandUnion( $codebase, $property_type, @@ -505,8 +516,9 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + $return_type = $return_type->setProperties($properties); } elseif ($return_type instanceof TList) { - $return_type->type_param = self::expandUnion( + $return_type = $return_type->replaceTypeParam(self::expandUnion( $codebase, $return_type->type_param, $self_class, @@ -518,11 +530,12 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, - ); + )); } if ($return_type instanceof TObjectWithProperties) { - foreach ($return_type->properties as &$property_type) { + $properties = $return_type->properties; + foreach ($properties as &$property_type) { $property_type = self::expandUnion( $codebase, $property_type, @@ -537,15 +550,17 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + $return_type = $return_type->setProperties($properties); } if ($return_type instanceof TCallable || $return_type instanceof TClosure ) { - if ($return_type->params) { - foreach ($return_type->params as $param) { + $params = $return_type->params; + if ($params) { + foreach ($params as &$param) { if ($param->type) { - $param->type = self::expandUnion( + $param = $param->replaceType(self::expandUnion( $codebase, $param->type, $self_class, @@ -557,14 +572,15 @@ public static function expandAtomic( $expand_generic, $expand_templates, $throw_on_unresolvable_constant, - ); + )); } } } - if ($return_type->return_type) { - $return_type->return_type = self::expandUnion( + $sub_return_type = $return_type->return_type; + if ($sub_return_type) { + $sub_return_type = self::expandUnion( $codebase, - $return_type->return_type, + $sub_return_type, $self_class, $static_class_type, $parent_class, @@ -576,18 +592,26 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + + if ($sub_return_type !== $return_type->return_type || $params !== $return_type->params) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty We just cloned this */ + $return_type->return_type = $sub_return_type; + /** @psalm-suppress InaccessibleProperty We just cloned this */ + $return_type->params = $params; + } } return [$return_type]; } /** - * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param string|TNamedObject|TTemplateParam|null $static_class_type * @return TNamedObject|TTemplateParam */ private static function expandNamedObject( Codebase $codebase, - TNamedObject $return_type, + TNamedObject &$return_type, ?string $self_class, $static_class_type, ?string $parent_class, @@ -629,11 +653,15 @@ private static function expandNamedObject( if ($static_class_type && ($return_type_lc === 'static' || $return_type_lc === '$this')) { if (is_string($static_class_type)) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $static_class_type; } else { if ($return_type instanceof TGenericObject && $static_class_type instanceof TGenericObject ) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $static_class_type->value; } else { $return_type = clone $static_class_type; @@ -641,6 +669,7 @@ private static function expandNamedObject( } if (!$final && $return_type instanceof TNamedObject) { + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->is_static = true; } } elseif ($return_type->is_static @@ -662,21 +691,33 @@ private static function expandNamedObject( } $return_type = $return_type->setIntersectionTypes($return_type_types); } elseif ($return_type->is_static && is_string($static_class_type) && $final) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $static_class_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->is_static = false; } elseif ($self_class && $return_type_lc === 'self') { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $self_class; } elseif ($parent_class && $return_type_lc === 'parent') { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ $return_type->value = $parent_class; } else { - $return_type->value = $codebase->classlikes->getUnAliasedName($return_type->value); + $new_value = $codebase->classlikes->getUnAliasedName($return_type->value); + if ($return_type->value !== $new_value) { + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on a clone */ + $return_type->value = $new_value; + } } return $return_type; } /** - * @param string|TNamedObject|TTemplateParam|null $static_class_type + * @param string|TNamedObject|TTemplateParam|null $static_class_type * * @return non-empty-list */ @@ -827,9 +868,7 @@ private static function expandConditional( ); if (count($all_conditional_return_types) !== $number_of_types) { - $null_type = new TNull(); - $null_type->from_docblock = true; - $all_conditional_return_types[] = $null_type; + $all_conditional_return_types[] = new TNull(true); } } @@ -901,7 +940,10 @@ private static function expandPropertiesOf( $static_class_type ): array { if ($self_class) { - $return_type = $return_type->replaceClassLike('self', $self_class); + $return_type = $return_type->replaceClassLike( + 'self', + $self_class + ); $return_type = $return_type->replaceClassLike( 'static', is_string($static_class_type) ? $static_class_type : $self_class diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index d658852d891..e810718075d 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -114,7 +114,8 @@ public static function parseTokens( array $type_tokens, ?int $analysis_php_version_id = null, array $template_type_map = [], - array $type_aliases = [] + array $type_aliases = [], + bool $from_docblock = false ): Union { if (count($type_tokens) === 1) { $only_token = $type_tokens[0]; @@ -137,10 +138,11 @@ public static function parseTokens( $type_aliases, 0, strlen($only_token[0]), - isset($only_token[2]) && $only_token[2] !== $only_token[0] ? $only_token[2] : null + isset($only_token[2]) && $only_token[2] !== $only_token[0] ? $only_token[2] : null, + $from_docblock ); - return new Union([$atomic]); + return new Union([$atomic], $from_docblock); } } @@ -151,11 +153,12 @@ public static function parseTokens( $codebase, $analysis_php_version_id, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if (!($parsed_type instanceof Union)) { - $parsed_type = new Union([$parsed_type]); + $parsed_type = new Union([$parsed_type], $from_docblock); } return $parsed_type; @@ -172,27 +175,47 @@ public static function getTypeFromTree( Codebase $codebase, ?int $analysis_php_version_id = null, array $template_type_map = [], - array $type_aliases = [] + array $type_aliases = [], + bool $from_docblock = false ): TypeNode { if ($parse_tree instanceof GenericTree) { return self::getTypeFromGenericTree( $parse_tree, $codebase, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } if ($parse_tree instanceof UnionTree) { - return self::getTypeFromUnionTree($parse_tree, $codebase, $template_type_map, $type_aliases); + return self::getTypeFromUnionTree( + $parse_tree, + $codebase, + $template_type_map, + $type_aliases, + $from_docblock + ); } if ($parse_tree instanceof IntersectionTree) { - return self::getTypeFromIntersectionTree($parse_tree, $codebase, $template_type_map, $type_aliases); + return self::getTypeFromIntersectionTree( + $parse_tree, + $codebase, + $template_type_map, + $type_aliases, + $from_docblock + ); } if ($parse_tree instanceof KeyedArrayTree) { - return self::getTypeFromKeyedArrayTree($parse_tree, $codebase, $template_type_map, $type_aliases); + return self::getTypeFromKeyedArrayTree( + $parse_tree, + $codebase, + $template_type_map, + $type_aliases, + $from_docblock + ); } if ($parse_tree instanceof CallableWithReturnTypeTree) { @@ -201,7 +224,8 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if (!$callable_type instanceof TCallable && !$callable_type instanceof TClosure) { @@ -217,16 +241,26 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); - $callable_type->return_type = $return_type instanceof Union ? $return_type : new Union([$return_type]); + $callable_type->return_type = $return_type instanceof Union + ? $return_type + : new Union([$return_type], $from_docblock) + ; return $callable_type; } if ($parse_tree instanceof CallableTree) { - return self::getTypeFromCallableTree($parse_tree, $codebase, $template_type_map, $type_aliases); + return self::getTypeFromCallableTree( + $parse_tree, + $codebase, + $template_type_map, + $type_aliases, + $from_docblock + ); } if ($parse_tree instanceof EncapsulationTree) { @@ -243,7 +277,8 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } @@ -257,17 +292,18 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if ($non_nullable_type instanceof Union) { - $non_nullable_type = $non_nullable_type->getBuilder()->addType(new TNull)->freeze(); + $non_nullable_type = $non_nullable_type->getBuilder()->addType(new TNull($from_docblock))->freeze(); return $non_nullable_type; } return TypeCombiner::combine([ - new TNull, + new TNull($from_docblock), $non_nullable_type, ]); } @@ -279,15 +315,18 @@ public static function getTypeFromTree( } if ($parse_tree instanceof IndexedAccessTree) { - return self::getTypeFromIndexAccessTree($parse_tree, $template_type_map); + return self::getTypeFromIndexAccessTree($parse_tree, $template_type_map, $from_docblock); } if ($parse_tree instanceof TemplateAsTree) { - return new TTemplateParam( + $result = new TTemplateParam( $parse_tree->param_name, new Union([new TNamedObject($parse_tree->as)]), - 'class-string-map' + 'class-string-map', + [], + $from_docblock ); + return $result; } if ($parse_tree instanceof ConditionalTree) { @@ -308,7 +347,8 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $if_type = self::getTypeFromTree( @@ -316,7 +356,8 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $else_type = self::getTypeFromTree( @@ -324,19 +365,20 @@ public static function getTypeFromTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if ($conditional_type instanceof Atomic) { - $conditional_type = new Union([$conditional_type]); + $conditional_type = new Union([$conditional_type], $from_docblock); } if ($if_type instanceof Atomic) { - $if_type = new Union([$if_type]); + $if_type = new Union([$if_type], $from_docblock); } if ($else_type instanceof Atomic) { - $else_type = new Union([$else_type]); + $else_type = new Union([$else_type], $from_docblock); } return new TConditional( @@ -345,7 +387,8 @@ public static function getTypeFromTree( $template_type_map[$template_param_name][$first_class], $conditional_type, $if_type, - $else_type + $else_type, + $from_docblock ); } @@ -354,7 +397,7 @@ public static function getTypeFromTree( } if ($parse_tree->value[0] === '"' || $parse_tree->value[0] === '\'') { - return new TLiteralString(substr($parse_tree->value, 1, -1)); + return new TLiteralString(substr($parse_tree->value, 1, -1), $from_docblock); } if (strpos($parse_tree->value, '::')) { @@ -366,23 +409,24 @@ public static function getTypeFromTree( return self::getGenericParamClass( $fq_classlike_name, $template_type_map[$fq_classlike_name][$first_class], - $first_class + $first_class, + $from_docblock ); } if ($const_name === 'class') { - return new TLiteralClassString($fq_classlike_name); + return new TLiteralClassString($fq_classlike_name, false, $from_docblock); } - return new TClassConstant($fq_classlike_name, $const_name); + return new TClassConstant($fq_classlike_name, $const_name, $from_docblock); } if (preg_match('/^\-?(0|[1-9][0-9]*)(\.[0-9]{1,})$/', $parse_tree->value)) { - return new TLiteralFloat((float) $parse_tree->value); + return new TLiteralFloat((float) $parse_tree->value, $from_docblock); } if (preg_match('/^\-?(0|[1-9][0-9]*)$/', $parse_tree->value)) { - return new TLiteralInt((int) $parse_tree->value); + return new TLiteralInt((int) $parse_tree->value, $from_docblock); } if (!preg_match('@^(\$this|\\\\?[a-zA-Z_\x7f-\xff][\\\\\-0-9a-zA-Z_\x7f-\xff]*)$@', $parse_tree->value)) { @@ -398,21 +442,24 @@ public static function getTypeFromTree( $type_aliases, $parse_tree->offset_start, $parse_tree->offset_end, - $parse_tree->text + $parse_tree->text, + $from_docblock ); } private static function getGenericParamClass( string $param_name, Union &$as, - string $defining_class + string $defining_class, + bool $from_docblock = false ): TTemplateParamClass { if ($as->hasMixed()) { return new TTemplateParamClass( $param_name, 'object', null, - $defining_class + $defining_class, + $from_docblock ); } @@ -428,14 +475,19 @@ private static function getGenericParamClass( $param_name, 'object', null, - $defining_class + $defining_class, + $from_docblock ); } if ($t instanceof TIterable) { $traversable = new TGenericObject( 'Traversable', - $t->type_params + $t->type_params, + false, + false, + [], + $from_docblock ); $as = $as->getBuilder()->substitute(new Union([$t]), new Union([$traversable]))->freeze(); @@ -444,7 +496,8 @@ private static function getGenericParamClass( $param_name, $traversable->value, $traversable, - $defining_class + $defining_class, + $from_docblock ); } @@ -459,7 +512,8 @@ private static function getGenericParamClass( $t->param_name, $t_atomic_type->value ?? 'object', $t_atomic_type, - $t->defining_class + $t->defining_class, + $from_docblock ); } @@ -473,7 +527,8 @@ private static function getGenericParamClass( $param_name, $t->value, $t, - $defining_class + $defining_class, + $from_docblock ); } @@ -484,7 +539,7 @@ private static function getGenericParamClass( * @param non-empty-list $potential_ints * @return non-empty-list */ - public static function getComputedIntsFromMask(array $potential_ints): array + public static function getComputedIntsFromMask(array $potential_ints, bool $from_docblock = false): array { /** @var list */ $potential_values = []; @@ -507,7 +562,7 @@ public static function getComputedIntsFromMask(array $potential_ints): array $potential_values = array_unique($potential_values); return array_map( - static fn($int): TLiteralInt => new TLiteralInt($int), + static fn($int): TLiteralInt => new TLiteralInt($int, $from_docblock), array_values($potential_values) ); } @@ -523,7 +578,8 @@ private static function getTypeFromGenericTree( GenericTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock = false ) { $generic_type = $parse_tree->value; @@ -535,7 +591,8 @@ private static function getTypeFromGenericTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if ($generic_type === 'class-string-map' @@ -548,7 +605,7 @@ private static function getTypeFromGenericTree( } } - $generic_params[] = $tree_type instanceof Union ? $tree_type : new Union([$tree_type]); + $generic_params[] = $tree_type instanceof Union ? $tree_type : new Union([$tree_type], $from_docblock); } $generic_type_value = TypeTokenizer::fixScalarTerms($generic_type); @@ -558,7 +615,7 @@ private static function getTypeFromGenericTree( || $generic_type_value === 'associative-array') && count($generic_params) === 1 ) { - array_unshift($generic_params, new Union([new TArrayKey])); + array_unshift($generic_params, new Union([new TArrayKey($from_docblock)])); } elseif (count($generic_params) === 1 && in_array( $generic_type_value, @@ -566,14 +623,14 @@ private static function getTypeFromGenericTree( true ) ) { - array_unshift($generic_params, new Union([new TMixed])); + array_unshift($generic_params, new Union([new TMixed(false, $from_docblock)])); } elseif ($generic_type_value === 'Generator') { if (count($generic_params) === 1) { - array_unshift($generic_params, new Union([new TMixed])); + array_unshift($generic_params, new Union([new TMixed(false, $from_docblock)])); } for ($i = 0, $l = 4 - count($generic_params); $i < $l; ++$i) { - $generic_params[] = new Union([new TMixed]); + $generic_params[] = new Union([new TMixed(false, $from_docblock)]); } } @@ -583,19 +640,19 @@ private static function getTypeFromGenericTree( if ($generic_type_value === 'array' || $generic_type_value === 'associative-array') { if ($generic_params[0]->isMixed()) { - $generic_params[0] = Type::getArrayKey(); + $generic_params[0] = Type::getArrayKey($from_docblock); } if (count($generic_params) !== 2) { throw new TypeParseTreeException('Too many template parameters for array'); } - return new TArray($generic_params); + return new TArray($generic_params, $from_docblock); } if ($generic_type_value === 'arraylike-object') { - $array_acccess = new TGenericObject('ArrayAccess', $generic_params); - $countable = new TNamedObject('Countable'); + $array_acccess = new TGenericObject('ArrayAccess', $generic_params, false, false, [], $from_docblock); + $countable = new TNamedObject('Countable', false, false, [], $from_docblock); return new TGenericObject( 'Traversable', $generic_params, @@ -604,32 +661,33 @@ private static function getTypeFromGenericTree( [ $array_acccess->getKey() => $array_acccess, $countable->getKey() => $countable - ] + ], + $from_docblock ); } if ($generic_type_value === 'non-empty-array') { if ($generic_params[0]->isMixed()) { - $generic_params[0] = Type::getArrayKey(); + $generic_params[0] = Type::getArrayKey($from_docblock); } if (count($generic_params) !== 2) { throw new TypeParseTreeException('Too many template parameters for non-empty-array'); } - return new TNonEmptyArray($generic_params); + return new TNonEmptyArray($generic_params, null, null, 'non-empty-array', $from_docblock); } if ($generic_type_value === 'iterable') { - return new TIterable($generic_params); + return new TIterable($generic_params, [], $from_docblock); } if ($generic_type_value === 'list') { - return new TList($generic_params[0]); + return new TList($generic_params[0], $from_docblock); } if ($generic_type_value === 'non-empty-list') { - return new TNonEmptyList($generic_params[0]); + return new TNonEmptyList($generic_params[0], null, null, $from_docblock); } if ($generic_type_value === 'class-string' @@ -644,7 +702,8 @@ private static function getTypeFromGenericTree( return self::getGenericParamClass( $class_name, $template_type_map[$class_name][$first_class], - $first_class + $first_class, + $from_docblock ); } @@ -658,7 +717,7 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Class string param should be a named object'); } - return new TClassString($class_name, $param_union_types[0]); + return new TClassString($class_name, $param_union_types[0], false, false, false, $from_docblock); } if ($generic_type_value === 'class-string-map') { @@ -695,7 +754,8 @@ private static function getTypeFromGenericTree( return new TClassStringMap( $template_param_name, $template_as_type, - $generic_params[1] + $generic_params[1], + $from_docblock ); } @@ -718,7 +778,7 @@ private static function getTypeFromGenericTree( if ($template_param->getIntersectionTypes()) { throw new TypeParseTreeException( $generic_type_value . '<' . $param_name . '> must be a TTemplateParam' - . ' with no intersection types.' + . ' with no intersection types.' ); } @@ -726,7 +786,8 @@ private static function getTypeFromGenericTree( $param_name, $defining_class, $template_param, - TPropertiesOf::filterForTokenName($generic_type_value) + TPropertiesOf::filterForTokenName($generic_type_value), + $from_docblock ); } @@ -742,7 +803,8 @@ private static function getTypeFromGenericTree( return new TPropertiesOf( $param_union_types[0], - TPropertiesOf::filterForTokenName($generic_type_value) + TPropertiesOf::filterForTokenName($generic_type_value), + $from_docblock ); } @@ -755,7 +817,8 @@ private static function getTypeFromGenericTree( return new TTemplateKeyOf( $param_name, $defining_class, - $generic_params[0] + $generic_params[0], + $from_docblock ); } @@ -765,7 +828,7 @@ private static function getTypeFromGenericTree( ); } - return new TKeyOf($generic_params[0]); + return new TKeyOf($generic_params[0], $from_docblock); } if ($generic_type_value === 'value-of') { @@ -777,7 +840,8 @@ private static function getTypeFromGenericTree( return new TTemplateValueOf( $param_name, $defining_class, - $generic_params[0] + $generic_params[0], + $from_docblock ); } @@ -815,7 +879,7 @@ private static function getTypeFromGenericTree( ); } - $atomic_type = new TLiteralInt($constant_value); + $atomic_type = new TLiteralInt($constant_value, $from_docblock); } else { throw new TypeParseTreeException( 'int-mask types must all be integer values' @@ -839,13 +903,13 @@ private static function getTypeFromGenericTree( foreach ($atomic_types as $atomic_type) { if (!$atomic_type instanceof TLiteralInt) { - return new TIntMask($atomic_types); + return new TIntMask($atomic_types, $from_docblock); } $potential_ints[] = $atomic_type->value; } - return new Union(self::getComputedIntsFromMask($potential_ints)); + return new Union(self::getComputedIntsFromMask($potential_ints, $from_docblock)); } if ($generic_type_value === 'int-mask-of') { @@ -872,7 +936,7 @@ private static function getTypeFromGenericTree( ); } - return new TIntMaskOf($param_type); + return new TIntMaskOf($param_type, $from_docblock); } if ($generic_type_value === 'int') { @@ -902,7 +966,7 @@ private static function getTypeFromGenericTree( $max_bound = $get_int_range_bound($parse_tree->children[1], $generic_params[1], TIntRange::BOUND_MAX); if ($min_bound === null && $max_bound === null) { - return new TInt(); + return new TInt($from_docblock); } if (is_int($min_bound) && is_int($max_bound) && $min_bound > $max_bound) { @@ -917,7 +981,7 @@ private static function getTypeFromGenericTree( ); } - return new TIntRange($min_bound, $max_bound); + return new TIntRange($min_bound, $max_bound, $from_docblock); } if (isset(TypeTokenizer::PSALM_RESERVED_WORDS[$generic_type_value]) @@ -927,7 +991,7 @@ private static function getTypeFromGenericTree( throw new TypeParseTreeException('Cannot create generic object with reserved word'); } - return new TGenericObject($generic_type_value, $generic_params); + return new TGenericObject($generic_type_value, $generic_params, false, false, [], $from_docblock); } /** @@ -939,7 +1003,8 @@ private static function getTypeFromUnionTree( UnionTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock ): Union { $has_null = false; @@ -956,7 +1021,8 @@ private static function getTypeFromUnionTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $has_null = true; } else { @@ -965,7 +1031,8 @@ private static function getTypeFromUnionTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } @@ -981,7 +1048,7 @@ private static function getTypeFromUnionTree( } if ($has_null) { - $atomic_types[] = new TNull; + $atomic_types[] = new TNull($from_docblock); } if (!$atomic_types) { @@ -1002,7 +1069,8 @@ private static function getTypeFromIntersectionTree( IntersectionTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock ): Atomic { $intersection_types = []; @@ -1012,7 +1080,8 @@ private static function getTypeFromIntersectionTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); if (!$atomic_type instanceof Atomic) { @@ -1076,17 +1145,25 @@ private static function getTypeFromIntersectionTree( } } - $keyed_array = new TKeyedArray($properties); - + $previous_key_type = null; + $previous_value_type = null; if ($first_type instanceof TArray) { - $keyed_array->previous_key_type = $first_type->type_params[0]; - $keyed_array->previous_value_type = $first_type->type_params[1]; + $previous_key_type = $first_type->type_params[0]; + $previous_value_type = $first_type->type_params[1]; } elseif ($last_type instanceof TArray) { - $keyed_array->previous_key_type = $last_type->type_params[0]; - $keyed_array->previous_value_type = $last_type->type_params[1]; + $previous_key_type = $last_type->type_params[0]; + $previous_value_type = $last_type->type_params[1]; } - return $keyed_array; + return new TKeyedArray( + $properties, + null, + false, + $previous_key_type ?? null, + $previous_value_type ?? null, + false, + $from_docblock + ); } $keyed_intersection_types = []; @@ -1134,7 +1211,7 @@ private static function getTypeFromIntersectionTree( } if (!$keyed_intersection_types && $intersect_static) { - return new TNamedObject('static'); + return new TNamedObject('static', false, false, [], $from_docblock); } $first_type = array_shift($keyed_intersection_types); @@ -1163,7 +1240,8 @@ private static function getTypeFromCallableTree( CallableTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock ) { $params = []; @@ -1178,10 +1256,11 @@ private static function getTypeFromCallableTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } else { - $tree_type = new TMixed(); + $tree_type = new TMixed(false, $from_docblock); } $is_variadic = $child_tree->variadic; @@ -1196,7 +1275,8 @@ private static function getTypeFromCallableTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); } @@ -1218,10 +1298,10 @@ private static function getTypeFromCallableTree( $pure = strpos($parse_tree->value, 'pure-') === 0 ? true : null; if (in_array(strtolower($parse_tree->value), ['closure', '\closure', 'pure-closure'], true)) { - return new TClosure('Closure', $params, null, $pure); + return new TClosure('Closure', $params, null, $pure, [], [], $from_docblock); } - return new TCallable('callable', $params, null, $pure); + return new TCallable('callable', $params, null, $pure, $from_docblock); } /** @@ -1230,7 +1310,8 @@ private static function getTypeFromCallableTree( */ private static function getTypeFromIndexAccessTree( IndexedAccessTree $parse_tree, - array $template_type_map + array $template_type_map, + bool $from_docblock ): TTemplateIndexedAccess { if (!isset($parse_tree->children[0]) || !$parse_tree->children[0] instanceof Value) { throw new TypeParseTreeException('Unrecognised indexed access'); @@ -1273,7 +1354,8 @@ private static function getTypeFromIndexAccessTree( return new TTemplateIndexedAccess( $array_param_name, $offset_param_name, - $array_defining_class + $array_defining_class, + $from_docblock ); } @@ -1287,7 +1369,8 @@ private static function getTypeFromKeyedArrayTree( KeyedArrayTree $parse_tree, Codebase $codebase, array $template_type_map, - array $type_aliases + array $type_aliases, + bool $from_docblock ) { $properties = []; $class_strings = []; @@ -1305,7 +1388,8 @@ private static function getTypeFromKeyedArrayTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $property_maybe_undefined = false; $property_key = (string)$i; @@ -1315,7 +1399,8 @@ private static function getTypeFromKeyedArrayTree( $codebase, null, $template_type_map, - $type_aliases + $type_aliases, + $from_docblock ); $property_maybe_undefined = $property_branch->possibly_undefined; if (strpos($property_branch->value, '::')) { @@ -1341,7 +1426,7 @@ private static function getTypeFromKeyedArrayTree( } if (!$property_type instanceof Union) { - $property_type = new Union([$property_type]); + $property_type = new Union([$property_type], $from_docblock); } if ($property_maybe_undefined) { @@ -1359,24 +1444,26 @@ private static function getTypeFromKeyedArrayTree( } if (!$properties) { - return new TArray([Type::getNever(), Type::getNever()]); + return new TArray([Type::getNever($from_docblock), Type::getNever($from_docblock)], $from_docblock); } if ($type === 'object') { - return new TObjectWithProperties($properties); + return new TObjectWithProperties($properties, [], [], $from_docblock); } + $class = TKeyedArray::class; if ($type === 'callable-array') { - return new TCallableKeyedArray($properties); + $class = TCallableKeyedArray::class; } - return new TKeyedArray( + return new $class( $properties, $class_strings, $is_tuple, null, null, - $is_tuple + $is_tuple, + $from_docblock ); } } diff --git a/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php b/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php new file mode 100644 index 00000000000..c1bc1280fd4 --- /dev/null +++ b/src/Psalm/Internal/TypeVisitor/ClasslikeReplacer.php @@ -0,0 +1,53 @@ +old = strtolower($old); + $this->new = $new; + } + + /** + * @psalm-suppress InaccessibleProperty Acting on clones + */ + protected function enterNode(TypeNode &$type): ?int + { + if ($type instanceof TClassConstant) { + if (strtolower($type->fq_classlike_name) === $this->old) { + $type = clone $type; + $type->fq_classlike_name = $this->new; + } + } elseif ($type instanceof TClassString) { + if ($type->as !== 'object' && strtolower($type->as) === $this->old) { + $type = clone $type; + $type->as = $this->new; + } + } elseif ($type instanceof TNamedObject || $type instanceof TLiteralClassString) { + if (strtolower($type->value) === $this->old) { + $type = clone $type; + $type->value = $this->new; + } + } + return null; + } +} diff --git a/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php index fb57c5c2059..472f61f93d6 100644 --- a/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/ContainsClassLikeVisitor.php @@ -5,15 +5,16 @@ use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; use function strtolower; /** * @internal */ -class ContainsClassLikeVisitor extends NodeVisitor +class ContainsClassLikeVisitor extends ImmutableTypeVisitor { /** * @var lowercase-string @@ -26,6 +27,7 @@ class ContainsClassLikeVisitor extends NodeVisitor private $contains_classlike = false; /** + * @psalm-external-mutation-free * @param lowercase-string $fq_classlike_name */ public function __construct(string $fq_classlike_name) @@ -33,32 +35,38 @@ public function __construct(string $fq_classlike_name) $this->fq_classlike_name = $fq_classlike_name; } + /** + * @psalm-external-mutation-free + */ protected function enterNode(TypeNode $type): ?int { if ($type instanceof TNamedObject) { if (strtolower($type->value) === $this->fq_classlike_name) { $this->contains_classlike = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } } if ($type instanceof TClassConstant) { if (strtolower($type->fq_classlike_name) === $this->fq_classlike_name) { $this->contains_classlike = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } } if ($type instanceof TLiteralClassString) { if (strtolower($type->value) === $this->fq_classlike_name) { $this->contains_classlike = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } } return null; } + /** + * @psalm-mutation-free + */ public function matches(): bool { return $this->contains_classlike; diff --git a/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php index 8fb36b79c2b..c340207e57d 100644 --- a/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php +++ b/src/Psalm/Internal/TypeVisitor/ContainsLiteralVisitor.php @@ -8,13 +8,14 @@ use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Atomic\TTrue; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; /** * @internal */ -class ContainsLiteralVisitor extends NodeVisitor +class ContainsLiteralVisitor extends ImmutableTypeVisitor { /** * @var bool @@ -30,12 +31,12 @@ protected function enterNode(TypeNode $type): ?int || $type instanceof TFalse ) { $this->contains_literal = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } if ($type instanceof TArray && $type->isEmptyArray()) { $this->contains_literal = true; - return NodeVisitor::STOP_TRAVERSAL; + return TypeVisitor::STOP_TRAVERSAL; } return null; diff --git a/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php b/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php index 2f4a7b7cff0..ce7250a7150 100644 --- a/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php +++ b/src/Psalm/Internal/TypeVisitor/FromDocblockSetter.php @@ -4,29 +4,40 @@ use Psalm\Type\Atomic; use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\NodeVisitor; +use Psalm\Type\MutableUnion; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; use Psalm\Type\Union; /** * @internal */ -class FromDocblockSetter extends NodeVisitor +class FromDocblockSetter extends TypeVisitor { + private bool $from_docblock; + public function __construct(bool $from_docblock) + { + $this->from_docblock = $from_docblock; + } /** - * @psalm-suppress MoreSpecificImplementedParamType - * - * @param Atomic|Union $type * @return self::STOP_TRAVERSAL|self::DONT_TRAVERSE_CHILDREN|null */ - protected function enterNode(TypeNode $type): ?int + protected function enterNode(TypeNode &$type): ?int { - $type->from_docblock = true; + if (!$type instanceof Atomic && !$type instanceof Union && !$type instanceof MutableUnion) { + return null; + } + if ($type->from_docblock === $this->from_docblock) { + return null; + } + $type = clone $type; + /** @psalm-suppress InaccessibleProperty Acting on clone */ + $type->from_docblock = $this->from_docblock; if ($type instanceof TTemplateParam && $type->as->isMixed() ) { - return NodeVisitor::DONT_TRAVERSE_CHILDREN; + return TypeVisitor::DONT_TRAVERSE_CHILDREN; } return null; diff --git a/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php b/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php index 5806638342d..de02f7b70f5 100644 --- a/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php +++ b/src/Psalm/Internal/TypeVisitor/TemplateTypeCollector.php @@ -6,14 +6,14 @@ use Psalm\Type\Atomic\TConditional; use Psalm\Type\Atomic\TTemplateParam; use Psalm\Type\Atomic\TTemplateParamClass; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; use Psalm\Type\Union; /** * @internal */ -class TemplateTypeCollector extends NodeVisitor +class TemplateTypeCollector extends ImmutableTypeVisitor { /** * @var list diff --git a/src/Psalm/Internal/TypeVisitor/TypeChecker.php b/src/Psalm/Internal/TypeVisitor/TypeChecker.php index af0ad30c33c..dcf43591cd2 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeChecker.php +++ b/src/Psalm/Internal/TypeVisitor/TypeChecker.php @@ -25,8 +25,10 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Atomic\TResource; use Psalm\Type\Atomic\TTemplateParam; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; +use Psalm\Type\MutableUnion; use Psalm\Type\TypeNode; +use Psalm\Type\TypeVisitor; use Psalm\Type\Union; use ReflectionProperty; @@ -40,7 +42,7 @@ /** * @internal */ -class TypeChecker extends NodeVisitor +class TypeChecker extends ImmutableTypeVisitor { /** * @var StatementsSource @@ -107,15 +109,16 @@ public function __construct( } /** - * @psalm-suppress MoreSpecificImplementedParamType - * - * @param Atomic|Union $type * @return self::STOP_TRAVERSAL|self::DONT_TRAVERSE_CHILDREN|null */ protected function enterNode(TypeNode $type): ?int { + if (!$type instanceof Atomic && !$type instanceof Union && !$type instanceof MutableUnion) { + return null; + } + if ($type->checked) { - return NodeVisitor::DONT_TRAVERSE_CHILDREN; + return TypeVisitor::DONT_TRAVERSE_CHILDREN; } if ($type instanceof TNamedObject) { @@ -128,6 +131,7 @@ protected function enterNode(TypeNode $type): ?int $this->checkResource($type); } + /** @psalm-suppress InaccessibleProperty Doesn't affect anything else */ $type->checked = true; return null; diff --git a/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php b/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php new file mode 100644 index 00000000000..658cf6c9da0 --- /dev/null +++ b/src/Psalm/Internal/TypeVisitor/TypeLocalizer.php @@ -0,0 +1,98 @@ +> + */ + private array $extends; + private string $base_fq_class_name; + + /** + * @param array> $extends + */ + public function __construct( + array $extends, + string $base_fq_class_name + ) { + $this->extends = $extends; + $this->base_fq_class_name = $base_fq_class_name; + } + + /** + * @psalm-suppress InaccessibleProperty Acting on clones + */ + protected function enterNode(TypeNode &$type): ?int + { + if ($type instanceof TTemplateParamClass) { + if ($type->defining_class === $this->base_fq_class_name) { + if (isset($this->extends[$this->base_fq_class_name][$type->param_name])) { + $extended_param = $this->extends[$this->base_fq_class_name][$type->param_name]; + + $types = array_values($extended_param->getAtomicTypes()); + + if (count($types) === 1 && $types[0] instanceof TNamedObject) { + $type = clone $type; + $type->as_type = $types[0]; + } elseif ($type->as_type !== null) { + $type = clone $type; + $type->as_type = null; + } + } + } + } + + if ($type instanceof Union) { + $union = $type->getBuilder(); + } elseif ($type instanceof MutableUnion) { + $union = $type; + } else { + return null; + } + + foreach ($union->getAtomicTypes() as $key => $atomic_type) { + if ($atomic_type instanceof TTemplateParam + && ($atomic_type->defining_class === $this->base_fq_class_name + || isset($this->extends[$atomic_type->defining_class])) + ) { + $types_to_add = Methods::getExtendedTemplatedTypes( + $atomic_type, + $this->extends + ); + + if ($types_to_add) { + $union->removeType($key); + + foreach ($types_to_add as $extra_added_type) { + $union->addType($extra_added_type); + } + } + } + } + + if ($type instanceof Union) { + $type = $union->freeze(); + } else { + $type = $union; + } + + return null; + } +} diff --git a/src/Psalm/Internal/TypeVisitor/TypeScanner.php b/src/Psalm/Internal/TypeVisitor/TypeScanner.php index 48f6796ef28..73b2687124c 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeScanner.php +++ b/src/Psalm/Internal/TypeVisitor/TypeScanner.php @@ -7,7 +7,7 @@ use Psalm\Type\Atomic\TClassConstant; use Psalm\Type\Atomic\TLiteralClassString; use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\NodeVisitor; +use Psalm\Type\ImmutableTypeVisitor; use Psalm\Type\TypeNode; use function strtolower; @@ -15,13 +15,16 @@ /** * @internal */ -class TypeScanner extends NodeVisitor +class TypeScanner extends ImmutableTypeVisitor { - private $scanner; + private Scanner $scanner; - private $file_storage; + private ?FileStorage $file_storage; - private $phantom_classes; + /** + * @var array + */ + private array $phantom_classes; /** * @param array $phantom_classes diff --git a/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php b/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php index 1f39a9d398e..db782f4d69d 100644 --- a/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php +++ b/src/Psalm/Plugin/EventHandler/Event/StringInterpreterEvent.php @@ -12,6 +12,8 @@ final class StringInterpreterEvent /** * Called after a statement has been checked * + * @psalm-external-mutation-free + * * @internal */ public function __construct(string $value) diff --git a/src/Psalm/Storage/FunctionLikeParameter.php b/src/Psalm/Storage/FunctionLikeParameter.php index d854c16f2f3..f7bc9ca0662 100644 --- a/src/Psalm/Storage/FunctionLikeParameter.php +++ b/src/Psalm/Storage/FunctionLikeParameter.php @@ -4,9 +4,10 @@ use Psalm\CodeLocation; use Psalm\Internal\Scanner\UnresolvedConstantComponent; +use Psalm\Type\TypeNode; use Psalm\Type\Union; -final class FunctionLikeParameter implements HasAttributesInterface +final class FunctionLikeParameter implements HasAttributesInterface, TypeNode { use CustomMetadataTrait; @@ -111,6 +112,7 @@ final class FunctionLikeParameter implements HasAttributesInterface public $description; /** + * @psalm-external-mutation-free * @param Union|UnresolvedConstantComponent|null $default_type */ public function __construct( @@ -140,6 +142,7 @@ public function __construct( $this->out_type = $out_type; } + /** @psalm-mutation-free */ public function getId(): string { return ($this->type ? $this->type->getId() : 'mixed') @@ -147,6 +150,7 @@ public function getId(): string . ($this->is_optional ? '=' : ''); } + /** @psalm-mutation-free */ public function replaceType(Union $type): self { if ($this->type === $type) { @@ -157,14 +161,18 @@ public function replaceType(Union $type): self return $cloned; } - public function __clone() + /** @psalm-mutation-free */ + public function getChildNodeKeys(): array { - if ($this->type) { - $this->type = clone $this->type; + $result = ['type', 'signature_type', 'out_type']; + if ($this->default_type instanceof Union) { + $result []= 'default_type'; } + return $result; } /** + * @psalm-mutation-free * @return list */ public function getAttributeStorages(): array diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 28e7f75ada8..6d66ab19407 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -175,6 +175,9 @@ public static function getStringFromFQCLN( return '\\' . $value; } + /** + * @psalm-pure + */ public static function getInt(bool $from_calculation = false, ?int $value = null): Union { if ($value !== null) { @@ -183,11 +186,15 @@ public static function getInt(bool $from_calculation = false, ?int $value = null $union = new Union([new TInt()]); } + /** @psalm-suppress ImpurePropertyAssignment We just created this object */ $union->from_calculation = $from_calculation; return $union; } + /** + * @psalm-pure + */ public static function getLowercaseString(): Union { $type = new TLowercaseString(); @@ -195,6 +202,9 @@ public static function getLowercaseString(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNonEmptyLowercaseString(): Union { $type = new TNonEmptyLowercaseString(); @@ -202,6 +212,9 @@ public static function getNonEmptyLowercaseString(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNonEmptyString(): Union { $type = new TNonEmptyString(); @@ -209,6 +222,9 @@ public static function getNonEmptyString(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNumeric(): Union { $type = new TNumeric; @@ -216,6 +232,9 @@ public static function getNumeric(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNumericString(): Union { $type = new TNumericString; @@ -250,6 +269,9 @@ public static function getString(?string $value = null): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getSingleLetter(): Union { $type = new TSingleLetter; @@ -257,6 +279,9 @@ public static function getSingleLetter(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getClassString(string $extends = 'object'): Union { return new Union([ @@ -269,6 +294,9 @@ public static function getClassString(string $extends = 'object'): Union ]); } + /** + * @psalm-pure + */ public static function getLiteralClassString(string $class_type, bool $definite_class = false): Union { $type = new TLiteralClassString($class_type, $definite_class); @@ -276,41 +304,59 @@ public static function getLiteralClassString(string $class_type, bool $definite_ return new Union([$type]); } - public static function getNull(): Union + /** + * @psalm-pure + */ + public static function getNull(bool $from_docblock = false): Union { - $type = new TNull; + $type = new TNull($from_docblock); return new Union([$type]); } - public static function getMixed(bool $from_loop_isset = false): Union + /** + * @psalm-pure + */ + public static function getMixed(bool $from_loop_isset = false, bool $from_docblock = false): Union { - $type = new TMixed($from_loop_isset); + $type = new TMixed($from_loop_isset, $from_docblock); return new Union([$type]); } - public static function getScalar(): Union + /** + * @psalm-pure + */ + public static function getScalar(bool $from_docblock = false): Union { - $type = new TScalar(); + $type = new TScalar($from_docblock); return new Union([$type]); } - public static function getNever(): Union + /** + * @psalm-pure + */ + public static function getNever(bool $from_docblock = false): Union { - $type = new TNever(); + $type = new TNever($from_docblock); return new Union([$type]); } - public static function getBool(): Union + /** + * @psalm-pure + */ + public static function getBool(bool $from_docblock = false): Union { - $type = new TBool; + $type = new TBool($from_docblock); return new Union([$type]); } + /** + * @psalm-pure + */ public static function getFloat(?float $value = null): Union { if ($value !== null) { @@ -322,6 +368,9 @@ public static function getFloat(?float $value = null): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getObject(): Union { $type = new TObject; @@ -329,6 +378,9 @@ public static function getObject(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getClosure(): Union { $type = new TClosure('Closure'); @@ -336,13 +388,19 @@ public static function getClosure(): Union return new Union([$type]); } - public static function getArrayKey(): Union + /** + * @psalm-pure + */ + public static function getArrayKey(bool $from_docblock = false): Union { - $type = new TArrayKey(); + $type = new TArrayKey($from_docblock); return new Union([$type]); } + /** + * @psalm-pure + */ public static function getArray(): Union { $type = new TArray( @@ -355,6 +413,9 @@ public static function getArray(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getEmptyArray(): Union { $array_type = new TArray( @@ -369,6 +430,9 @@ public static function getEmptyArray(): Union ]); } + /** + * @psalm-pure + */ public static function getList(): Union { $type = new TList(new Union([new TMixed])); @@ -376,6 +440,9 @@ public static function getList(): Union return new Union([$type]); } + /** + * @psalm-pure + */ public static function getNonEmptyList(): Union { $type = new TNonEmptyList(new Union([new TMixed])); @@ -383,33 +450,46 @@ public static function getNonEmptyList(): Union return new Union([$type]); } - public static function getVoid(): Union + /** + * @psalm-pure + */ + public static function getVoid(bool $from_docblock = false): Union { - $type = new TVoid; + $type = new TVoid($from_docblock); return new Union([$type]); } - public static function getFalse(): Union + /** + * @psalm-pure + */ + public static function getFalse(bool $from_docblock = false): Union { - $type = new TFalse; + $type = new TFalse($from_docblock); return new Union([$type]); } - public static function getTrue(): Union + /** + * @psalm-pure + */ + public static function getTrue(bool $from_docblock = false): Union { - $type = new TTrue; + $type = new TTrue($from_docblock); return new Union([$type]); } - public static function getResource(): Union + /** + * @psalm-pure + */ + public static function getResource(bool $from_docblock = false): Union { - return new Union([new TResource]); + return new Union([new TResource($from_docblock)]); } /** + * @psalm-external-mutation-free * @param non-empty-list $union_types */ public static function combineUnionTypeArray(array $union_types, ?Codebase $codebase): Union @@ -429,6 +509,7 @@ public static function combineUnionTypeArray(array $union_types, ?Codebase $code * @param int $literal_limit any greater number of literal types than this * will be merged to a scalar * + * @psalm-external-mutation-free */ public static function combineUnionTypes( ?Union $type_1, diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index b95b789f0cb..2a37fbeaed8 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -10,6 +10,7 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Internal\Type\TypeAlias; use Psalm\Internal\Type\TypeAlias\LinkableTypeAlias; +use Psalm\Internal\TypeVisitor\ClasslikeReplacer; use Psalm\Type; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TArrayKey; @@ -75,8 +76,15 @@ use function strpos; use function strtolower; +/** + * @psalm-immutable + */ abstract class Atomic implements TypeNode { + public function __construct(bool $from_docblock = false) + { + $this->from_docblock = $from_docblock; + } /** * Whether or not the type has been checked yet * @@ -107,6 +115,34 @@ abstract class Atomic implements TypeNode public $text; /** + * @return static + */ + public function setFromDocblock(bool $from_docblock): self + { + if ($from_docblock === $this->from_docblock) { + return $this; + } + $cloned = clone $this; + $cloned->from_docblock = $from_docblock; + return $cloned; + } + + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type = $this; + (new ClasslikeReplacer( + $old, + $new + ))->traverse($type); + return $type; + } + + /** + * @psalm-suppress InaccessibleProperty Allowed during construction + * * @param int $analysis_php_version_id contains php version when the type comes from signature * @param array> $template_type_map * @param array $type_aliases @@ -118,15 +154,19 @@ public static function create( array $type_aliases = [], ?int $offset_start = null, ?int $offset_end = null, - ?string $text = null + ?string $text = null, + bool $from_docblock = false ): Atomic { $result = self::createInner($value, $analysis_php_version_id, $template_type_map, $type_aliases); $result->offset_start = $offset_start; $result->offset_end = $offset_end; $result->text = $text; + $result->from_docblock = $from_docblock; return $result; } /** + * @psalm-suppress InaccessibleProperty Allowed during construction + * * @param int $analysis_php_version_id contains php version when the type comes from signature * @param array> $template_type_map * @param array $type_aliases @@ -135,7 +175,8 @@ private static function createInner( string $value, ?int $analysis_php_version_id = null, array $template_type_map = [], - array $type_aliases = [] + array $type_aliases = [], + bool $from_docblock = false ): Atomic { switch ($value) { case 'int': @@ -197,19 +238,28 @@ private static function createInner( case 'array': case 'associative-array': - return new TArray([new Union([new TArrayKey]), new Union([new TMixed])]); + return new TArray([ + new Union([new TArrayKey($from_docblock)]), + new Union([new TMixed(false, $from_docblock)]) + ]); case 'non-empty-array': - return new TNonEmptyArray([new Union([new TArrayKey]), new Union([new TMixed])]); + return new TNonEmptyArray([ + new Union([new TArrayKey($from_docblock)]), + new Union([new TMixed(false, $from_docblock)]) + ]); case 'callable-array': - return new TCallableArray([new Union([new TArrayKey]), new Union([new TMixed])]); + return new TCallableArray([ + new Union([new TArrayKey($from_docblock)]), + new Union([new TMixed(false, $from_docblock)]) + ]); case 'list': - return new TList(Type::getMixed()); + return new TList(Type::getMixed(false, $from_docblock)); case 'non-empty-list': - return new TNonEmptyList(Type::getMixed()); + return new TNonEmptyList(Type::getMixed(false, $from_docblock)); case 'non-empty-string': return new TNonEmptyString(); @@ -531,43 +581,16 @@ public function hasArrayAccessInterface(Codebase $codebase): bool ); } - public function getChildNodes(): array + public function getChildNodeKeys(): array { return []; } - /** - * @return static - */ - public function replaceClassLike(string $old, string $new): self - { - return $this; - } - final public function __toString(): string { return $this->getId(); } - public function __clone() - { - if ($this instanceof TNamedObject - || $this instanceof TTemplateParam - || $this instanceof TIterable - || $this instanceof TObjectWithProperties - ) { - if ($this->extra_types) { - foreach ($this->extra_types as &$type) { - $type = clone $type; - } - } - } - - if ($this instanceof TTemplateParam) { - $this->as = clone $this->as; - } - } - /** * This is the true identifier for the type. It defaults to self::getKey() but can be overrided to be more precise */ diff --git a/src/Psalm/Type/Atomic/CallableTrait.php b/src/Psalm/Type/Atomic/CallableTrait.php index 328c4a01ce2..17980ae840e 100644 --- a/src/Psalm/Type/Atomic/CallableTrait.php +++ b/src/Psalm/Type/Atomic/CallableTrait.php @@ -9,12 +9,14 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Storage\FunctionLikeParameter; use Psalm\Type\Atomic; -use Psalm\Type\TypeNode; use Psalm\Type\Union; use function count; use function implode; +/** + * @psalm-immutable + */ trait CallableTrait { /** @@ -41,23 +43,14 @@ public function __construct( string $value = 'callable', ?array $params = null, ?Union $return_type = null, - ?bool $is_pure = null + ?bool $is_pure = null, + bool $from_docblock = false ) { $this->value = $value; $this->params = $params; $this->return_type = $return_type; $this->is_pure = $is_pure; - } - - public function __clone() - { - if ($this->params) { - foreach ($this->params as &$param) { - $param = clone $param; - } - } - - $this->return_type = $this->return_type ? clone $this->return_type : null; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -205,7 +198,7 @@ protected function replaceCallableTemplateTypesWithStandins( $replaced = false; $params = $this->params; if ($params) { - foreach ($params as $offset => &$param) { + foreach ($params as $offset => $param) { if (!$param->type) { continue; } @@ -233,7 +226,7 @@ protected function replaceCallableTemplateTypesWithStandins( $depth )); $replaced = $replaced || $new_param !== $param; - $param = $new_param; + $params[$offset] = $new_param; } } @@ -274,7 +267,7 @@ protected function replaceCallableTemplateTypesWithArgTypes( $params = $this->params; if ($params) { - foreach ($params as &$param) { + foreach ($params as $k => $param) { if ($param->type) { $new_param = $param->replaceType(TemplateInferredTypeReplacer::replace( $param->type, @@ -282,7 +275,7 @@ protected function replaceCallableTemplateTypesWithArgTypes( $codebase )); $replaced = $replaced || $new_param !== $param; - $param = $new_param; + $params[$k] = $new_param; } } } @@ -303,53 +296,10 @@ protected function replaceCallableTemplateTypesWithArgTypes( } /** - * @return array{list|null, Union|null}|null + * @return list */ - protected function replaceCallableClassLike(string $old, string $new): ?array + protected function getCallableChildNodeKeys(): array { - $replaced = false; - - $params = $this->params; - if ($params) { - foreach ($params as &$param) { - if ($param->type) { - $new_param = $param->replaceType($param->type->replaceClassLike($old, $new)); - $replaced = $replaced || $new_param !== $param; - $param = $new_param; - } - } - } - - $return_type = $this->return_type; - if ($return_type) { - $return_type = $return_type->replaceClassLike($old, $new); - $replaced = $replaced || $return_type !== $this->return_type; - } - if ($replaced) { - return [$params, $return_type]; - } - return null; - } - - /** - * @return list - */ - protected function getCallableChildNodes(): array - { - $child_nodes = []; - - if ($this->params) { - foreach ($this->params as $param) { - if ($param->type) { - $child_nodes[] = $param->type; - } - } - } - - if ($this->return_type) { - $child_nodes[] = $this->return_type; - } - - return $child_nodes; + return ['params', 'return_type']; } } diff --git a/src/Psalm/Type/Atomic/DependentType.php b/src/Psalm/Type/Atomic/DependentType.php index 4ebefe03b38..98cf7a5749c 100644 --- a/src/Psalm/Type/Atomic/DependentType.php +++ b/src/Psalm/Type/Atomic/DependentType.php @@ -4,6 +4,9 @@ use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ interface DependentType { public function getVarId(): string; diff --git a/src/Psalm/Type/Atomic/GenericTrait.php b/src/Psalm/Type/Atomic/GenericTrait.php index d92861d1355..87767a38fec 100644 --- a/src/Psalm/Type/Atomic/GenericTrait.php +++ b/src/Psalm/Type/Atomic/GenericTrait.php @@ -20,14 +20,10 @@ /** * @template TTypeParams as array + * @psalm-immutable */ trait GenericTrait { - /** - * @var TTypeParams - */ - public array $type_params; - /** * @param TTypeParams $type_params * @@ -161,13 +157,6 @@ public function toNamespacedString( '>' . $extra_types; } - public function __clone() - { - foreach ($this->type_params as &$type_param) { - $type_param = clone $type_param; - } - } - /** * @return TTypeParams|null */ @@ -255,7 +244,7 @@ protected function replaceTypeParamsTemplateTypesWithArgTypes( ?Codebase $codebase ): ?array { $type_params = $this->type_params; - foreach ($type_params as $offset => &$type_param) { + foreach ($type_params as $offset => $type_param) { $type_param = TemplateInferredTypeReplacer::replace( $type_param, $template_result, @@ -265,20 +254,10 @@ protected function replaceTypeParamsTemplateTypesWithArgTypes( if ($this instanceof TArray && $offset === 0 && $type_param->isMixed()) { $type_param = Type::getArrayKey(); } - } - return $type_params === $this->type_params ? null : $type_params; - } - - /** - * @return TTypeParams|null - */ - protected function replaceTypeParamsClassLike(string $old, string $new): ?array - { - $type_params = $this->type_params; - foreach ($type_params as &$type_param) { - $type_param = $type_param->replaceClassLike($old, $new); + $type_params[$offset] = $type_param; } + return $type_params === $this->type_params ? null : $type_params; } } diff --git a/src/Psalm/Type/Atomic/HasIntersectionTrait.php b/src/Psalm/Type/Atomic/HasIntersectionTrait.php index 3f19aa9b9bd..7889a0111a8 100644 --- a/src/Psalm/Type/Atomic/HasIntersectionTrait.php +++ b/src/Psalm/Type/Atomic/HasIntersectionTrait.php @@ -12,6 +12,9 @@ use function array_merge; use function implode; +/** + * @psalm-immutable + */ trait HasIntersectionTrait { /** @@ -160,20 +163,4 @@ protected function replaceIntersectionTemplateTypesWithStandins( return $new_types === $this->extra_types ? null : $new_types; } - - /** - * @return array|null - */ - protected function replaceIntersectionClassLike(string $old, string $new): ?array - { - if (!$this->extra_types) { - return null; - } - $new_types = []; - foreach ($this->extra_types as $extra_type) { - $extra_type = $extra_type->replaceClassLike($old, $new); - $new_types[$extra_type->getKey()] = $extra_type; - } - return $new_types === $this->extra_types ? null : $new_types; - } } diff --git a/src/Psalm/Type/Atomic/Scalar.php b/src/Psalm/Type/Atomic/Scalar.php index dbc2f1a4691..764b34af86a 100644 --- a/src/Psalm/Type/Atomic/Scalar.php +++ b/src/Psalm/Type/Atomic/Scalar.php @@ -4,6 +4,9 @@ use Psalm\Type\Atomic; +/** + * @psalm-immutable + */ abstract class Scalar extends Atomic { public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool diff --git a/src/Psalm/Type/Atomic/TAnonymousClassInstance.php b/src/Psalm/Type/Atomic/TAnonymousClassInstance.php index 1d77db1e5ef..a6da68a042c 100644 --- a/src/Psalm/Type/Atomic/TAnonymousClassInstance.php +++ b/src/Psalm/Type/Atomic/TAnonymousClassInstance.php @@ -4,6 +4,7 @@ /** * Denotes an anonymous class (i.e. `new class{}`) with potential methods + * @psalm-immutable */ final class TAnonymousClassInstance extends TNamedObject { diff --git a/src/Psalm/Type/Atomic/TArray.php b/src/Psalm/Type/Atomic/TArray.php index 208ab12da29..0b51e220fbf 100644 --- a/src/Psalm/Type/Atomic/TArray.php +++ b/src/Psalm/Type/Atomic/TArray.php @@ -13,6 +13,7 @@ /** * Denotes a simple array of the form `array`. It expects an array with two elements, both union types. + * @psalm-immutable */ class TArray extends Atomic { @@ -21,6 +22,11 @@ class TArray extends Atomic */ use GenericTrait; + /** + * @var array{Union, Union} + */ + public array $type_params; + /** * @var string */ @@ -31,9 +37,10 @@ class TArray extends Atomic * * @param array{Union, Union} $type_params */ - public function __construct(array $type_params) + public function __construct(array $type_params, bool $from_docblock = false) { $this->type_params = $type_params; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -98,20 +105,6 @@ public function isEmptyArray(): bool return $this->type_params[1]->isNever(); } - /** - * @return static - */ - public function replaceClassLike(string $old, string $new): self - { - $type_params = $this->replaceTypeParamsClassLike($old, $new); - if ($type_params) { - $cloned = clone $this; - $cloned->type_params = $type_params; - return $cloned; - } - return $this; - } - /** * @return static */ @@ -164,8 +157,8 @@ public function replaceTemplateTypesWithArgTypes(TemplateResult $template_result return $this; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return $this->type_params; + return ['type_params']; } } diff --git a/src/Psalm/Type/Atomic/TArrayKey.php b/src/Psalm/Type/Atomic/TArrayKey.php index bf317292958..9938d6a7082 100644 --- a/src/Psalm/Type/Atomic/TArrayKey.php +++ b/src/Psalm/Type/Atomic/TArrayKey.php @@ -4,6 +4,7 @@ /** * Denotes the `array-key` type, used for something that could be the offset of an `array`. + * @psalm-immutable */ class TArrayKey extends Scalar { diff --git a/src/Psalm/Type/Atomic/TBool.php b/src/Psalm/Type/Atomic/TBool.php index ec7cbc06258..2312152afbd 100644 --- a/src/Psalm/Type/Atomic/TBool.php +++ b/src/Psalm/Type/Atomic/TBool.php @@ -4,6 +4,7 @@ /** * Denotes the `bool` type where the exact value is unknown. + * @psalm-immutable */ class TBool extends Scalar { diff --git a/src/Psalm/Type/Atomic/TCallable.php b/src/Psalm/Type/Atomic/TCallable.php index 8f4420bd1b6..18c62ed283e 100644 --- a/src/Psalm/Type/Atomic/TCallable.php +++ b/src/Psalm/Type/Atomic/TCallable.php @@ -9,6 +9,7 @@ /** * Denotes the `callable` type. Can result from an `is_callable` check. + * @psalm-immutable */ final class TCallable extends Atomic { @@ -89,25 +90,9 @@ public function replaceTemplateTypesWithStandins( $this->is_pure ); } - /** - * @return static - */ - public function replaceClassLike(string $old, string $new): self - { - $replaced = $this->replaceCallableClassLike($old, $new); - if (!$replaced) { - return $this; - } - return new static( - $this->value, - $replaced[0], - $replaced[1], - $this->is_pure - ); - } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return $this->getCallableChildNodes(); + return $this->getCallableChildNodeKeys(); } } diff --git a/src/Psalm/Type/Atomic/TCallableArray.php b/src/Psalm/Type/Atomic/TCallableArray.php index 4dba9674653..332dfb1118c 100644 --- a/src/Psalm/Type/Atomic/TCallableArray.php +++ b/src/Psalm/Type/Atomic/TCallableArray.php @@ -4,6 +4,7 @@ /** * Denotes an array that is _also_ `callable`. + * @psalm-immutable */ final class TCallableArray extends TNonEmptyArray { diff --git a/src/Psalm/Type/Atomic/TCallableKeyedArray.php b/src/Psalm/Type/Atomic/TCallableKeyedArray.php index 91c89e0600e..21a774e454e 100644 --- a/src/Psalm/Type/Atomic/TCallableKeyedArray.php +++ b/src/Psalm/Type/Atomic/TCallableKeyedArray.php @@ -4,6 +4,7 @@ /** * Denotes an object-like array that is _also_ `callable`. + * @psalm-immutable */ final class TCallableKeyedArray extends TKeyedArray { diff --git a/src/Psalm/Type/Atomic/TCallableList.php b/src/Psalm/Type/Atomic/TCallableList.php index 1429d36ed0e..67c490e173a 100644 --- a/src/Psalm/Type/Atomic/TCallableList.php +++ b/src/Psalm/Type/Atomic/TCallableList.php @@ -4,6 +4,7 @@ /** * Denotes a list that is _also_ `callable`. + * @psalm-immutable */ final class TCallableList extends TNonEmptyList { diff --git a/src/Psalm/Type/Atomic/TCallableObject.php b/src/Psalm/Type/Atomic/TCallableObject.php index 2bb7e3b2791..9114a313de2 100644 --- a/src/Psalm/Type/Atomic/TCallableObject.php +++ b/src/Psalm/Type/Atomic/TCallableObject.php @@ -4,6 +4,7 @@ /** * Denotes an object that is also `callable` (i.e. it has `__invoke` defined). + * @psalm-immutable */ final class TCallableObject extends TObject { diff --git a/src/Psalm/Type/Atomic/TCallableString.php b/src/Psalm/Type/Atomic/TCallableString.php index a3dc8036333..c95456a0ede 100644 --- a/src/Psalm/Type/Atomic/TCallableString.php +++ b/src/Psalm/Type/Atomic/TCallableString.php @@ -4,7 +4,7 @@ /** * Denotes the `callable-string` type, used to represent an unknown string that is also `callable`. - * + * @psalm-immutable */ final class TCallableString extends TNonFalsyString { diff --git a/src/Psalm/Type/Atomic/TClassConstant.php b/src/Psalm/Type/Atomic/TClassConstant.php index d85492f69a9..dea31daa0c9 100644 --- a/src/Psalm/Type/Atomic/TClassConstant.php +++ b/src/Psalm/Type/Atomic/TClassConstant.php @@ -5,10 +5,9 @@ use Psalm\Type; use Psalm\Type\Atomic; -use function strtolower; - /** * Denotes a class constant whose value might not yet be known. + * @psalm-immutable */ final class TClassConstant extends Atomic { @@ -18,24 +17,11 @@ final class TClassConstant extends Atomic /** @var string */ public $const_name; - public function __construct(string $fq_classlike_name, string $const_name) + public function __construct(string $fq_classlike_name, string $const_name, bool $from_docblock = false) { $this->fq_classlike_name = $fq_classlike_name; $this->const_name = $const_name; - } - - /** - * @return static - */ - public function replaceClassLike(string $old, string $new): self - { - if (strtolower($this->fq_classlike_name) === $old) { - return new TClassConstant( - $new, - $this->const_name - ); - } - return $this; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TClassString.php b/src/Psalm/Type/Atomic/TClassString.php index e040bf4a54e..b8eec740027 100644 --- a/src/Psalm/Type/Atomic/TClassString.php +++ b/src/Psalm/Type/Atomic/TClassString.php @@ -20,6 +20,7 @@ /** * Denotes the `class-string` type, used to describe a string representing a valid PHP class. * The parent type from which the classes descend may or may not be specified in the constructor. + * @psalm-immutable */ class TClassString extends TString { @@ -28,10 +29,7 @@ class TClassString extends TString */ public $as; - /** - * @var ?TNamedObject - */ - public $as_type; + public ?TNamedObject $as_type; /** @var bool */ public $is_loaded = false; @@ -47,25 +45,15 @@ public function __construct( ?TNamedObject $as_type = null, bool $is_loaded = false, bool $is_interface = false, - bool $is_enum = false + bool $is_enum = false, + bool $from_docblock = false ) { $this->as = $as; $this->as_type = $as_type; $this->is_loaded = $is_loaded; $this->is_interface = $is_interface; $this->is_enum = $is_enum; - } - /** - * @return static - */ - public function replaceClassLike(string $old, string $new): self - { - if ($this->as !== 'object' && strtolower($this->as) === $old) { - $cloned = clone $this; - $cloned->as = $new; - return $cloned; - } - return $this; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string { @@ -147,9 +135,9 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return $this->as_type ? [$this->as_type] : []; + return $this->as_type ? ['as_type'] : []; } /** diff --git a/src/Psalm/Type/Atomic/TClassStringMap.php b/src/Psalm/Type/Atomic/TClassStringMap.php index 63e4faa96a1..fe3e1c3e39c 100644 --- a/src/Psalm/Type/Atomic/TClassStringMap.php +++ b/src/Psalm/Type/Atomic/TClassStringMap.php @@ -16,6 +16,7 @@ /** * Represents an array where the type of each value * is a function of its string key value + * @psalm-immutable */ final class TClassStringMap extends Atomic { @@ -24,10 +25,7 @@ final class TClassStringMap extends Atomic */ public $param_name; - /** - * @var ?TNamedObject - */ - public $as_type; + public ?TNamedObject $as_type; /** * @var Union @@ -37,11 +35,16 @@ final class TClassStringMap extends Atomic /** * Constructs a new instance of a list */ - public function __construct(string $param_name, ?TNamedObject $as_type, Union $value_param) - { + public function __construct( + string $param_name, + ?TNamedObject $as_type, + Union $value_param, + bool $from_docblock = false + ) { $this->param_name = $param_name; $this->as_type = $as_type; $this->value_param = $value_param; + $this->from_docblock = $from_docblock; } public function getId(bool $exact = true, bool $nested = false): string @@ -56,11 +59,6 @@ public function getId(bool $exact = true, bool $nested = false): string . '>'; } - public function __clone() - { - $this->value_param = clone $this->value_param; - } - /** * @param array $aliased_classes * @@ -118,6 +116,7 @@ public function getKey(bool $include_extra = true): string } /** + * @psalm-suppress InaccessibleProperty We're only acting on cloned instances * @return static */ public function replaceTemplateTypesWithStandins( @@ -204,9 +203,9 @@ public function replaceTemplateTypesWithArgTypes( ); } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return [$this->value_param]; + return ['value_param']; } public function equals(Atomic $other_type, bool $ensure_source_equality): bool diff --git a/src/Psalm/Type/Atomic/TClosedResource.php b/src/Psalm/Type/Atomic/TClosedResource.php index 5d4828fbf25..3d867d73818 100644 --- a/src/Psalm/Type/Atomic/TClosedResource.php +++ b/src/Psalm/Type/Atomic/TClosedResource.php @@ -6,6 +6,7 @@ /** * Denotes the `resource` type that has been closed (e.g. a file handle through `fclose()`). + * @psalm-immutable */ final class TClosedResource extends Atomic { diff --git a/src/Psalm/Type/Atomic/TClosure.php b/src/Psalm/Type/Atomic/TClosure.php index f21afe32c09..7b0345e9eb3 100644 --- a/src/Psalm/Type/Atomic/TClosure.php +++ b/src/Psalm/Type/Atomic/TClosure.php @@ -10,10 +10,10 @@ use Psalm\Type\Union; use function array_merge; -use function strtolower; /** * Represents a closure where we know the return type and params + * @psalm-immutable */ final class TClosure extends TNamedObject { @@ -33,7 +33,8 @@ public function __construct( ?Union $return_type = null, ?bool $is_pure = null, array $byref_uses = [], - array $extra_types = [] + array $extra_types = [], + bool $from_docblock = false ) { $this->value = $value; $this->params = $params; @@ -41,6 +42,7 @@ public function __construct( $this->is_pure = $is_pure; $this->byref_uses = $byref_uses; $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; } public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool @@ -48,27 +50,6 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } - /** - * @return static - */ - public function replaceClassLike(string $old, string $new): self - { - $replaced = $this->replaceCallableClassLike($old, $new); - $intersection = $this->replaceIntersectionClassLike($old, $new); - if (!$replaced && !$intersection) { - return $this; - } - return new static( - strtolower($this->value) === $old ? $new : $this->value, - $replaced[0] ?? $this->params, - $replaced[1] ?? $this->return_type, - $this->is_pure, - $this->byref_uses, - $intersection ?? $this->extra_types - ); - } - - /** * @return static */ @@ -143,8 +124,8 @@ public function replaceTemplateTypesWithStandins( ); } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return array_merge(parent::getChildNodes(), $this->getCallableChildNodes()); + return array_merge(parent::getChildNodeKeys(), $this->getCallableChildNodeKeys()); } } diff --git a/src/Psalm/Type/Atomic/TConditional.php b/src/Psalm/Type/Atomic/TConditional.php index b24192b33ee..b1c2d6b9794 100644 --- a/src/Psalm/Type/Atomic/TConditional.php +++ b/src/Psalm/Type/Atomic/TConditional.php @@ -10,6 +10,7 @@ /** * Internal representation of a conditional return type in phpdoc. For example ($param1 is int ? int : string) + * @psalm-immutable */ final class TConditional extends Atomic { @@ -49,7 +50,8 @@ public function __construct( Union $as_type, Union $conditional_type, Union $if_type, - Union $else_type + Union $else_type, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->defining_class = $defining_class; @@ -57,6 +59,7 @@ public function __construct( $this->conditional_type = $conditional_type; $this->if_type = $if_type; $this->else_type = $else_type; + $this->from_docblock = $from_docblock; } public function replaceTypes( @@ -85,14 +88,6 @@ public function replaceTypes( return $cloned; } - public function __clone() - { - $this->conditional_type = clone $this->conditional_type; - $this->if_type = clone $this->if_type; - $this->else_type = clone $this->else_type; - $this->as_type = clone $this->as_type; - } - public function getKey(bool $include_extra = true): string { return 'TConditional<' . $this->param_name . '>'; @@ -140,9 +135,9 @@ public function toNamespacedString( return ''; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return [$this->conditional_type, $this->if_type, $this->else_type]; + return ['conditional_type', 'if_type', 'else_type']; } public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool diff --git a/src/Psalm/Type/Atomic/TDependentGetClass.php b/src/Psalm/Type/Atomic/TDependentGetClass.php index c1b1b91fd41..bde7432c18b 100644 --- a/src/Psalm/Type/Atomic/TDependentGetClass.php +++ b/src/Psalm/Type/Atomic/TDependentGetClass.php @@ -6,6 +6,7 @@ /** * Represents a string whose value is a fully-qualified class found by get_class($var) + * @psalm-immutable */ final class TDependentGetClass extends TString implements DependentType { diff --git a/src/Psalm/Type/Atomic/TDependentGetDebugType.php b/src/Psalm/Type/Atomic/TDependentGetDebugType.php index e732cd3dbbe..fd56a78cbc9 100644 --- a/src/Psalm/Type/Atomic/TDependentGetDebugType.php +++ b/src/Psalm/Type/Atomic/TDependentGetDebugType.php @@ -4,6 +4,7 @@ /** * Represents a string whose value is that of a type found by get_debug_type($var) + * @psalm-immutable */ final class TDependentGetDebugType extends TString implements DependentType { diff --git a/src/Psalm/Type/Atomic/TDependentGetType.php b/src/Psalm/Type/Atomic/TDependentGetType.php index 6d53ef62bbd..44181f9d573 100644 --- a/src/Psalm/Type/Atomic/TDependentGetType.php +++ b/src/Psalm/Type/Atomic/TDependentGetType.php @@ -4,6 +4,7 @@ /** * Represents a string whose value is that of a type found by gettype($var) + * @psalm-immutable */ final class TDependentGetType extends TString { diff --git a/src/Psalm/Type/Atomic/TDependentListKey.php b/src/Psalm/Type/Atomic/TDependentListKey.php index 076217fe8c0..c18109e6ae7 100644 --- a/src/Psalm/Type/Atomic/TDependentListKey.php +++ b/src/Psalm/Type/Atomic/TDependentListKey.php @@ -4,6 +4,7 @@ /** * Represents a list key created from foreach ($list as $key => $value) + * @psalm-immutable */ final class TDependentListKey extends TInt implements DependentType { diff --git a/src/Psalm/Type/Atomic/TEmptyMixed.php b/src/Psalm/Type/Atomic/TEmptyMixed.php index adbe375cfbd..fb4b5c297b6 100644 --- a/src/Psalm/Type/Atomic/TEmptyMixed.php +++ b/src/Psalm/Type/Atomic/TEmptyMixed.php @@ -5,6 +5,7 @@ /** * Denotes the `mixed` type, but empty. * Generated for `$x` inside the `if` statement `if (!$x) {...}` when `$x` is `mixed` outside. + * @psalm-immutable */ final class TEmptyMixed extends TMixed { diff --git a/src/Psalm/Type/Atomic/TEmptyNumeric.php b/src/Psalm/Type/Atomic/TEmptyNumeric.php index 7199f6821e2..1a3ce1f67c8 100644 --- a/src/Psalm/Type/Atomic/TEmptyNumeric.php +++ b/src/Psalm/Type/Atomic/TEmptyNumeric.php @@ -4,6 +4,7 @@ /** * Denotes the `numeric` type that's also empty (which can also result from an `is_numeric` and `empty` check). + * @psalm-immutable */ final class TEmptyNumeric extends TNumeric { diff --git a/src/Psalm/Type/Atomic/TEmptyScalar.php b/src/Psalm/Type/Atomic/TEmptyScalar.php index 036d28bef1d..cb2543810d9 100644 --- a/src/Psalm/Type/Atomic/TEmptyScalar.php +++ b/src/Psalm/Type/Atomic/TEmptyScalar.php @@ -4,6 +4,7 @@ /** * Denotes a `scalar` type that is also empty. + * @psalm-immutable */ final class TEmptyScalar extends TScalar { diff --git a/src/Psalm/Type/Atomic/TEnumCase.php b/src/Psalm/Type/Atomic/TEnumCase.php index 5522714948e..614a59c1d5d 100644 --- a/src/Psalm/Type/Atomic/TEnumCase.php +++ b/src/Psalm/Type/Atomic/TEnumCase.php @@ -4,6 +4,7 @@ /** * Denotes an enum with a specific value + * @psalm-immutable */ final class TEnumCase extends TNamedObject { diff --git a/src/Psalm/Type/Atomic/TFalse.php b/src/Psalm/Type/Atomic/TFalse.php index dbcb4ec8a6b..2ccd395d002 100644 --- a/src/Psalm/Type/Atomic/TFalse.php +++ b/src/Psalm/Type/Atomic/TFalse.php @@ -4,6 +4,7 @@ /** * Denotes the `false` value type + * @psalm-immutable */ final class TFalse extends TBool { diff --git a/src/Psalm/Type/Atomic/TFloat.php b/src/Psalm/Type/Atomic/TFloat.php index f30592dd8fd..d856df6e2cc 100644 --- a/src/Psalm/Type/Atomic/TFloat.php +++ b/src/Psalm/Type/Atomic/TFloat.php @@ -4,6 +4,7 @@ /** * Denotes the `float` type, where the exact value is unknown. + * @psalm-immutable */ class TFloat extends Scalar { diff --git a/src/Psalm/Type/Atomic/TGenericObject.php b/src/Psalm/Type/Atomic/TGenericObject.php index ea0d00d343f..7e78bd1324e 100644 --- a/src/Psalm/Type/Atomic/TGenericObject.php +++ b/src/Psalm/Type/Atomic/TGenericObject.php @@ -12,11 +12,11 @@ use function count; use function implode; use function strrpos; -use function strtolower; use function substr; /** * Denotes an object type that has generic parameters e.g. `ArrayObject` + * @psalm-immutable */ final class TGenericObject extends TNamedObject { @@ -25,6 +25,11 @@ final class TGenericObject extends TNamedObject */ use GenericTrait; + /** + * @var non-empty-list + */ + public array $type_params; + /** @var bool if the parameters have been remapped to another class */ public $remapped_params = false; @@ -38,7 +43,8 @@ public function __construct( array $type_params, bool $remapped_params = false, bool $is_static = false, - array $extra_types = [] + array $extra_types = [], + bool $from_docblock = false ) { if ($value[0] === '\\') { $value = substr($value, 1); @@ -49,6 +55,7 @@ public function __construct( $this->remapped_params = $remapped_params; $this->is_static = $is_static; $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -114,28 +121,9 @@ public function getAssertionString(): string return $this->value; } - public function getChildNodes(): array - { - return array_merge(parent::getChildNodes(), $this->type_params); - } - - /** - * @return static - */ - public function replaceClassLike(string $old, string $new): self + public function getChildNodeKeys(): array { - $type_params = $this->replaceTypeParamsClassLike($old, $new); - $intersection = $this->replaceIntersectionClassLike($old, $new); - if (!$type_params && !$intersection) { - return $this; - } - return new static( - strtolower($this->value) === $old ? $new : $this->value, - $type_params ?? $this->type_params, - $this->remapped_params, - $this->is_static, - $intersection ?? $this->extra_types - ); + return array_merge(parent::getChildNodeKeys(), ['type_params']); } /** diff --git a/src/Psalm/Type/Atomic/TInt.php b/src/Psalm/Type/Atomic/TInt.php index 95f6506509a..fd7ad05cb7d 100644 --- a/src/Psalm/Type/Atomic/TInt.php +++ b/src/Psalm/Type/Atomic/TInt.php @@ -4,6 +4,7 @@ /** * Denotes the `int` type, where the exact value is unknown. + * @psalm-immutable */ class TInt extends Scalar { diff --git a/src/Psalm/Type/Atomic/TIntMask.php b/src/Psalm/Type/Atomic/TIntMask.php index 4d3dca8340a..a77babf1062 100644 --- a/src/Psalm/Type/Atomic/TIntMask.php +++ b/src/Psalm/Type/Atomic/TIntMask.php @@ -7,6 +7,7 @@ /** * Represents the type that is the result of a bitmask combination of its parameters. * `int-mask<1, 2, 4>` corresponds to `0|1|2|3|4|5|6|7` + * @psalm-immutable */ final class TIntMask extends TInt { @@ -14,9 +15,10 @@ final class TIntMask extends TInt public $values; /** @param non-empty-array $values */ - public function __construct(array $values) + public function __construct(array $values, bool $from_docblock = false) { $this->values = $values; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TIntMaskOf.php b/src/Psalm/Type/Atomic/TIntMaskOf.php index 3e210e409e8..b1fd8c987f0 100644 --- a/src/Psalm/Type/Atomic/TIntMaskOf.php +++ b/src/Psalm/Type/Atomic/TIntMaskOf.php @@ -8,6 +8,7 @@ * Represents the type that is the result of a bitmask combination of its parameters. * This is the same concept as TIntMask but TIntMaskOf is used with a reference to constants in code * `int-mask-of` will corresponds to `0|1|2|3|4|5|6|7` if there are three constant 1, 2 and 4 + * @psalm-immutable */ final class TIntMaskOf extends TInt { @@ -17,9 +18,10 @@ final class TIntMaskOf extends TInt /** * @param TClassConstant|TKeyOf|TValueOf $value */ - public function __construct(Atomic $value) + public function __construct(Atomic $value, bool $from_docblock = false) { $this->value = $value; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -45,6 +47,11 @@ public function toNamespacedString( . '>'; } + public function getChildNodeKeys(): array + { + return ['value']; + } + public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { return false; diff --git a/src/Psalm/Type/Atomic/TIntRange.php b/src/Psalm/Type/Atomic/TIntRange.php index eec4e671f65..57fe237f520 100644 --- a/src/Psalm/Type/Atomic/TIntRange.php +++ b/src/Psalm/Type/Atomic/TIntRange.php @@ -7,6 +7,7 @@ /** * Denotes an interval of integers between two bounds + * @psalm-immutable */ final class TIntRange extends TInt { @@ -22,10 +23,11 @@ final class TIntRange extends TInt */ public $max_bound; - public function __construct(?int $min_bound, ?int $max_bound) + public function __construct(?int $min_bound, ?int $max_bound, bool $from_docblock = false) { $this->min_bound = $min_bound; $this->max_bound = $max_bound; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TIterable.php b/src/Psalm/Type/Atomic/TIterable.php index eca66a1b89f..91f75bc9fb8 100644 --- a/src/Psalm/Type/Atomic/TIterable.php +++ b/src/Psalm/Type/Atomic/TIterable.php @@ -9,14 +9,13 @@ use Psalm\Type\Atomic; use Psalm\Type\Union; -use function array_merge; -use function array_values; use function count; use function implode; use function substr; /** * denotes the `iterable` type(which can also result from an `is_iterable` check). + * @psalm-immutable */ final class TIterable extends Atomic { @@ -26,6 +25,11 @@ final class TIterable extends Atomic */ use GenericTrait; + /** + * @var array{Union, Union} + */ + public array $type_params; + /** * @var string */ @@ -40,7 +44,7 @@ final class TIterable extends Atomic * @param array{Union, Union}|array $type_params * @param array $extra_types */ - public function __construct(array $type_params = [], array $extra_types = []) + public function __construct(array $type_params = [], array $extra_types = [], bool $from_docblock = false) { if (isset($type_params[0], $type_params[1])) { $this->has_docblock_params = true; @@ -49,6 +53,7 @@ public function __construct(array $type_params = [], array $extra_types = []) $this->type_params = [Type::getMixed(), Type::getMixed()]; } $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string @@ -117,32 +122,11 @@ public function equals(Atomic $other_type, bool $ensure_source_equality): bool return true; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return array_merge($this->type_params, array_values($this->extra_types)); + return ['type_params', 'extra_types']; } - /** - * @return static - */ - public function replaceClassLike(string $old, string $new): self - { - $type_params = $this->replaceTypeParamsClassLike( - $old, - $new - ); - $intersection = $this->replaceIntersectionClassLike( - $old, - $new - ); - if (!$type_params && !$intersection) { - return $this; - } - return new static( - $type_params ?? $this->type_params, - $intersection ?? $this->extra_types - ); - } /** * @return static */ diff --git a/src/Psalm/Type/Atomic/TKeyOf.php b/src/Psalm/Type/Atomic/TKeyOf.php index 027a0e04fb5..b8a4b33f854 100644 --- a/src/Psalm/Type/Atomic/TKeyOf.php +++ b/src/Psalm/Type/Atomic/TKeyOf.php @@ -10,15 +10,17 @@ /** * Represents an offset of an array. + * @psalm-immutable */ final class TKeyOf extends TArrayKey { /** @var Union */ public $type; - public function __construct(Union $type) + public function __construct(Union $type, bool $from_docblock = false) { $this->type = $type; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TKeyedArray.php b/src/Psalm/Type/Atomic/TKeyedArray.php index 0235fba89b3..99e14a64deb 100644 --- a/src/Psalm/Type/Atomic/TKeyedArray.php +++ b/src/Psalm/Type/Atomic/TKeyedArray.php @@ -31,6 +31,7 @@ /** * Represents an 'object-like array' - an array with known keys. + * @psalm-immutable */ class TKeyedArray extends Atomic { @@ -83,7 +84,8 @@ public function __construct( bool $sealed = false, ?Union $previous_key_type = null, ?Union $previous_value_type = null, - bool $is_list = false + bool $is_list = false, + bool $from_docblock = false ) { $this->properties = $properties; $this->class_strings = $class_strings; @@ -91,6 +93,7 @@ public function __construct( $this->previous_key_type = $previous_key_type; $this->previous_value_type = $previous_value_type; $this->is_list = $is_list; + $this->from_docblock = $from_docblock; } /** @@ -108,18 +111,6 @@ public function setProperties(array $properties): self return $cloned; } - /** - * @return static - */ - public function replaceClassLike(string $old, string $new): self - { - $properties = $this->properties; - foreach ($properties as &$property_type) { - $property_type = $property_type->replaceClassLike($old, $new); - } - return $this->setProperties($properties); - } - public function getId(bool $exact = true, bool $nested = false): string { $property_strings = []; @@ -250,6 +241,9 @@ public function getGenericValueType(): Union return $value_type; } + /** + * @return TArray|TNonEmptyArray + */ public function getGenericArrayType(bool $allow_non_empty = true): TArray { $key_types = []; @@ -300,13 +294,6 @@ public function isNonEmpty(): bool return false; } - public function __clone() - { - foreach ($this->properties as &$property) { - $property = clone $property; - } - } - public function getKey(bool $include_extra = true): string { /** @var string */ @@ -330,7 +317,7 @@ public function replaceTemplateTypesWithStandins( ): self { $properties = $this->properties; - foreach ($properties as $offset => &$property) { + foreach ($properties as $offset => $property) { $input_type_param = null; if ($input_type instanceof TKeyedArray @@ -339,7 +326,7 @@ public function replaceTemplateTypesWithStandins( $input_type_param = $input_type->properties[$offset]; } - $property = TemplateStandinTypeReplacer::replace( + $properties[$offset] = TemplateStandinTypeReplacer::replace( $property, $template_result, $codebase, @@ -371,8 +358,8 @@ public function replaceTemplateTypesWithArgTypes( ?Codebase $codebase ): self { $properties = $this->properties; - foreach ($properties as &$property) { - $property = TemplateInferredTypeReplacer::replace( + foreach ($properties as $offset => $property) { + $properties[$offset] = TemplateInferredTypeReplacer::replace( $property, $template_result, $codebase @@ -386,9 +373,9 @@ public function replaceTemplateTypesWithArgTypes( return $this; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return $this->properties; + return ['properties']; } public function equals(Atomic $other_type, bool $ensure_source_equality): bool diff --git a/src/Psalm/Type/Atomic/TList.php b/src/Psalm/Type/Atomic/TList.php index 63c334cf6a1..78beb798b34 100644 --- a/src/Psalm/Type/Atomic/TList.php +++ b/src/Psalm/Type/Atomic/TList.php @@ -19,6 +19,7 @@ * - they start at 0 * - they are consecutive and go upwards (no negative int) * + * @psalm-immutable */ class TList extends Atomic { @@ -33,9 +34,10 @@ class TList extends Atomic /** * Constructs a new instance of a list */ - public function __construct(Union $type_param) + public function __construct(Union $type_param, bool $from_docblock = false) { $this->type_param = $type_param; + $this->from_docblock = $from_docblock; } /** @@ -56,11 +58,6 @@ public function getId(bool $exact = true, bool $nested = false): string return static::KEY . '<' . $this->type_param->getId($exact) . '>'; } - public function __clone() - { - $this->type_param = clone $this->type_param; - } - /** * @param array $aliased_classes * @@ -115,6 +112,7 @@ public function getKey(bool $include_extra = true): string } /** + * @psalm-suppress InaccessibleProperty We're only acting on cloned instances * @return static */ public function replaceTemplateTypesWithStandins( @@ -215,8 +213,8 @@ public function getAssertionString(): string return $this->getId(); } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return [$this->type_param]; + return ['type_param']; } } diff --git a/src/Psalm/Type/Atomic/TLiteralClassString.php b/src/Psalm/Type/Atomic/TLiteralClassString.php index 735d45ba3ac..cc17a77d61e 100644 --- a/src/Psalm/Type/Atomic/TLiteralClassString.php +++ b/src/Psalm/Type/Atomic/TLiteralClassString.php @@ -10,6 +10,7 @@ /** * Denotes a specific class string, generated by expressions like `A::class`. + * @psalm-immutable */ final class TLiteralClassString extends TLiteralString { @@ -19,9 +20,9 @@ final class TLiteralClassString extends TLiteralString */ public $definite_class = false; - public function __construct(string $value, bool $definite_class = false) + public function __construct(string $value, bool $definite_class = false, bool $from_docblock = false) { - parent::__construct($value); + parent::__construct($value, $from_docblock); $this->definite_class = $definite_class; } @@ -61,17 +62,6 @@ public function getAssertionString(): string return $this->getKey(); } - /** - * @return static - */ - public function replaceClassLike(string $old, string $new): self - { - if (strtolower($this->value) === $old) { - return new static($new, $this->definite_class); - } - return $this; - } - /** * @param array $aliased_classes */ diff --git a/src/Psalm/Type/Atomic/TLiteralFloat.php b/src/Psalm/Type/Atomic/TLiteralFloat.php index 4e11468304f..991625ec328 100644 --- a/src/Psalm/Type/Atomic/TLiteralFloat.php +++ b/src/Psalm/Type/Atomic/TLiteralFloat.php @@ -4,15 +4,17 @@ /** * Denotes a floating point value where the exact numeric value is known. + * @psalm-immutable */ final class TLiteralFloat extends TFloat { /** @var float */ public $value; - public function __construct(float $value) + public function __construct(float $value, bool $from_docblock = false) { $this->value = $value; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TLiteralInt.php b/src/Psalm/Type/Atomic/TLiteralInt.php index 390606ebb1b..6a506061d3f 100644 --- a/src/Psalm/Type/Atomic/TLiteralInt.php +++ b/src/Psalm/Type/Atomic/TLiteralInt.php @@ -4,15 +4,17 @@ /** * Denotes an integer value where the exact numeric value is known. + * @psalm-immutable */ final class TLiteralInt extends TInt { /** @var int */ public $value; - public function __construct(int $value) + public function __construct(int $value, bool $from_docblock = false) { $this->value = $value; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TLiteralString.php b/src/Psalm/Type/Atomic/TLiteralString.php index 71ae7f73551..9f000b90aa9 100644 --- a/src/Psalm/Type/Atomic/TLiteralString.php +++ b/src/Psalm/Type/Atomic/TLiteralString.php @@ -8,15 +8,17 @@ /** * Denotes a string whose value is known. + * @psalm-immutable */ class TLiteralString extends TString { /** @var string */ public $value; - public function __construct(string $value) + public function __construct(string $value, bool $from_docblock = false) { $this->value = $value; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TLowercaseString.php b/src/Psalm/Type/Atomic/TLowercaseString.php index 92afd724299..b65ac5e9dcb 100644 --- a/src/Psalm/Type/Atomic/TLowercaseString.php +++ b/src/Psalm/Type/Atomic/TLowercaseString.php @@ -3,6 +3,7 @@ namespace Psalm\Type\Atomic; /** + * @psalm-immutable */ final class TLowercaseString extends TString { diff --git a/src/Psalm/Type/Atomic/TMixed.php b/src/Psalm/Type/Atomic/TMixed.php index 1fb883003b1..d122a432b16 100644 --- a/src/Psalm/Type/Atomic/TMixed.php +++ b/src/Psalm/Type/Atomic/TMixed.php @@ -7,15 +7,17 @@ /** * Denotes the `mixed` type, used when you don’t know the type of an expression. * + * @psalm-immutable */ class TMixed extends Atomic { /** @var bool */ public $from_loop_isset = false; - public function __construct(bool $from_loop_isset = false) + public function __construct(bool $from_loop_isset = false, bool $from_docblock = false) { $this->from_loop_isset = $from_loop_isset; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TNamedObject.php b/src/Psalm/Type/Atomic/TNamedObject.php index 15c64b9d7d0..0323952391f 100644 --- a/src/Psalm/Type/Atomic/TNamedObject.php +++ b/src/Psalm/Type/Atomic/TNamedObject.php @@ -9,14 +9,13 @@ use Psalm\Type\Atomic; use function array_map; -use function array_values; use function implode; use function strrpos; -use function strtolower; use function substr; /** * Denotes an object type where the type of the object is known e.g. `Exception`, `Throwable`, `Foo\Bar` + * @psalm-immutable */ class TNamedObject extends Atomic { @@ -46,7 +45,8 @@ public function __construct( string $value, bool $is_static = false, bool $definite_class = false, - array $extra_types = [] + array $extra_types = [], + bool $from_docblock = false ) { if ($value[0] === '\\') { $value = substr($value, 1); @@ -56,6 +56,17 @@ public function __construct( $this->is_static = $is_static; $this->definite_class = $definite_class; $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; + } + + public function setIsStatic(bool $is_static): self + { + if ($this->is_static === $is_static) { + return $this; + } + $cloned = clone $this; + $cloned->is_static = $is_static; + return $cloned; } public function getKey(bool $include_extra = true): string @@ -143,23 +154,6 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return ($this->value !== 'static' && $this->is_static === false) || $analysis_php_version_id >= 8_00_00; } - /** - * @return static - */ - public function replaceClassLike(string $old, string $new): self - { - $intersection = $this->replaceIntersectionClassLike($old, $new); - if (!$intersection && strtolower($this->value) !== $old) { - return $this; - } - $cloned = clone $this; - if (strtolower($cloned->value) === $old) { - $cloned->value = $new; - } - $cloned->extra_types = $intersection ?? $this->extra_types; - return $cloned; - } - /** * @return static */ @@ -179,8 +173,18 @@ public function replaceTemplateTypesWithArgTypes( /** * @return static */ - public function replaceTemplateTypesWithStandins(TemplateResult $template_result, Codebase $codebase, ?StatementsAnalyzer $statements_analyzer = null, ?Atomic $input_type = null, ?int $input_arg_offset = null, ?string $calling_class = null, ?string $calling_function = null, bool $replace = true, bool $add_lower_bound = false, int $depth = 0): self - { + public function replaceTemplateTypesWithStandins( + TemplateResult $template_result, + Codebase $codebase, + ?StatementsAnalyzer $statements_analyzer = null, + ?Atomic $input_type = null, + ?int $input_arg_offset = null, + ?string $calling_class = null, + ?string $calling_function = null, + bool $replace = true, + bool $add_lower_bound = false, + int $depth = 0 + ): self { $intersection = $this->replaceIntersectionTemplateTypesWithStandins( $template_result, $codebase, @@ -200,8 +204,8 @@ public function replaceTemplateTypesWithStandins(TemplateResult $template_result } return $this; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return array_values($this->extra_types); + return ['extra_types']; } } diff --git a/src/Psalm/Type/Atomic/TNever.php b/src/Psalm/Type/Atomic/TNever.php index b10bfc10cf6..9c468445de7 100644 --- a/src/Psalm/Type/Atomic/TNever.php +++ b/src/Psalm/Type/Atomic/TNever.php @@ -7,6 +7,7 @@ /** * Denotes the `no-return`/`never-return` type for functions that never return, either throwing an exception or * terminating (like the builtin `exit()`). + * @psalm-immutable */ final class TNever extends Atomic { diff --git a/src/Psalm/Type/Atomic/TNonEmptyArray.php b/src/Psalm/Type/Atomic/TNonEmptyArray.php index 061e180c87c..2c2fba076c8 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyArray.php +++ b/src/Psalm/Type/Atomic/TNonEmptyArray.php @@ -7,6 +7,7 @@ /** * Denotes array known to be non-empty of the form `non-empty-array`. * It expects an array with two elements, both union types. + * @psalm-immutable */ class TNonEmptyArray extends TArray { @@ -34,12 +35,14 @@ public function __construct( array $type_params, ?int $count = null, ?int $min_count = null, - string $value = 'non-empty-array' + string $value = 'non-empty-array', + bool $from_docblock = false ) { $this->type_params = $type_params; $this->count = $count; $this->min_count = $min_count; $this->value = $value; + $this->from_docblock = $from_docblock; } /** diff --git a/src/Psalm/Type/Atomic/TNonEmptyList.php b/src/Psalm/Type/Atomic/TNonEmptyList.php index ef9dc0c05e2..c56bd0b5672 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyList.php +++ b/src/Psalm/Type/Atomic/TNonEmptyList.php @@ -6,6 +6,7 @@ /** * Represents a non-empty list + * @psalm-immutable */ class TNonEmptyList extends TList { @@ -28,11 +29,16 @@ class TNonEmptyList extends TList * @param positive-int|null $count * @param positive-int|null $min_count */ - public function __construct(Union $type_param, ?int $count = null, ?int $min_count = null) - { + public function __construct( + Union $type_param, + ?int $count = null, + ?int $min_count = null, + bool $from_docblock = false + ) { $this->type_param = $type_param; $this->count = $count; $this->min_count = $min_count; + $this->from_docblock = $from_docblock; } /** diff --git a/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php b/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php index 52812e75783..ab39bc3d497 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php +++ b/src/Psalm/Type/Atomic/TNonEmptyLowercaseString.php @@ -4,6 +4,7 @@ /** * Denotes a non-empty-string where every character is lowercased. (which can also result from a `strtolower` call). + * @psalm-immutable */ final class TNonEmptyLowercaseString extends TNonEmptyString { diff --git a/src/Psalm/Type/Atomic/TNonEmptyMixed.php b/src/Psalm/Type/Atomic/TNonEmptyMixed.php index 1b96eb47f88..b7c829e079c 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyMixed.php +++ b/src/Psalm/Type/Atomic/TNonEmptyMixed.php @@ -5,6 +5,7 @@ /** * Denotes the `mixed` type, but not empty. * Generated for `$x` inside the `if` statement `if ($x) {...}` when `$x` is `mixed` outside. + * @psalm-immutable */ final class TNonEmptyMixed extends TMixed { diff --git a/src/Psalm/Type/Atomic/TNonEmptyNonspecificLiteralString.php b/src/Psalm/Type/Atomic/TNonEmptyNonspecificLiteralString.php index 4802a72a261..6cfd2526f34 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyNonspecificLiteralString.php +++ b/src/Psalm/Type/Atomic/TNonEmptyNonspecificLiteralString.php @@ -5,6 +5,7 @@ /** * Denotes the `literal-string` type, where the exact value is unknown but * we know that the string is not from user input + * @psalm-immutable */ final class TNonEmptyNonspecificLiteralString extends TNonspecificLiteralString { diff --git a/src/Psalm/Type/Atomic/TNonEmptyScalar.php b/src/Psalm/Type/Atomic/TNonEmptyScalar.php index ec3d56d6fb7..7de002334cd 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyScalar.php +++ b/src/Psalm/Type/Atomic/TNonEmptyScalar.php @@ -4,6 +4,7 @@ /** * Denotes a `scalar` type that is also non-empty. + * @psalm-immutable */ final class TNonEmptyScalar extends TScalar { diff --git a/src/Psalm/Type/Atomic/TNonEmptyString.php b/src/Psalm/Type/Atomic/TNonEmptyString.php index 2a1dbdcf8c4..5aeedb303ba 100644 --- a/src/Psalm/Type/Atomic/TNonEmptyString.php +++ b/src/Psalm/Type/Atomic/TNonEmptyString.php @@ -4,6 +4,7 @@ /** * Denotes a string, that is also non-empty (every string except '') + * @psalm-immutable */ class TNonEmptyString extends TString { diff --git a/src/Psalm/Type/Atomic/TNonFalsyString.php b/src/Psalm/Type/Atomic/TNonFalsyString.php index 467951c1848..e352d1d9f71 100644 --- a/src/Psalm/Type/Atomic/TNonFalsyString.php +++ b/src/Psalm/Type/Atomic/TNonFalsyString.php @@ -4,6 +4,7 @@ /** * Denotes a string, that is also non-falsy (every string except '' and '0') + * @psalm-immutable */ class TNonFalsyString extends TNonEmptyString { diff --git a/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php b/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php index 08c2f4d7af8..fbc5e71f24f 100644 --- a/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php +++ b/src/Psalm/Type/Atomic/TNonspecificLiteralInt.php @@ -5,6 +5,7 @@ /** * Denotes the `literal-int` type, where the exact value is unknown but * we know that the int is not from user input + * @psalm-immutable */ final class TNonspecificLiteralInt extends TInt { diff --git a/src/Psalm/Type/Atomic/TNonspecificLiteralString.php b/src/Psalm/Type/Atomic/TNonspecificLiteralString.php index b946f0cc944..b6c73cc20ee 100644 --- a/src/Psalm/Type/Atomic/TNonspecificLiteralString.php +++ b/src/Psalm/Type/Atomic/TNonspecificLiteralString.php @@ -5,6 +5,7 @@ /** * Denotes the `literal-string` type, where the exact value is unknown but * we know that the string is not from user input + * @psalm-immutable */ class TNonspecificLiteralString extends TString { diff --git a/src/Psalm/Type/Atomic/TNull.php b/src/Psalm/Type/Atomic/TNull.php index 959a53d0e4a..685509d9678 100644 --- a/src/Psalm/Type/Atomic/TNull.php +++ b/src/Psalm/Type/Atomic/TNull.php @@ -6,6 +6,7 @@ /** * Denotes the `null` type + * @psalm-immutable */ final class TNull extends Atomic { diff --git a/src/Psalm/Type/Atomic/TNumeric.php b/src/Psalm/Type/Atomic/TNumeric.php index 2a0c70acf7f..942a1df0e2c 100644 --- a/src/Psalm/Type/Atomic/TNumeric.php +++ b/src/Psalm/Type/Atomic/TNumeric.php @@ -4,6 +4,7 @@ /** * Denotes the `numeric` type (which can also result from an `is_numeric` check). + * @psalm-immutable */ class TNumeric extends Scalar { diff --git a/src/Psalm/Type/Atomic/TNumericString.php b/src/Psalm/Type/Atomic/TNumericString.php index 8f2109ec0d0..ed1040a3cb4 100644 --- a/src/Psalm/Type/Atomic/TNumericString.php +++ b/src/Psalm/Type/Atomic/TNumericString.php @@ -4,6 +4,7 @@ /** * Denotes a string that's also a numeric value e.g. `"5"`. It can result from `is_string($s) && is_numeric($s)`. + * @psalm-immutable */ final class TNumericString extends TNonEmptyString { diff --git a/src/Psalm/Type/Atomic/TObject.php b/src/Psalm/Type/Atomic/TObject.php index 1725c136869..bffa66a5413 100644 --- a/src/Psalm/Type/Atomic/TObject.php +++ b/src/Psalm/Type/Atomic/TObject.php @@ -6,6 +6,7 @@ /** * Denotes the `object` type + * @psalm-immutable */ class TObject extends Atomic { diff --git a/src/Psalm/Type/Atomic/TObjectWithProperties.php b/src/Psalm/Type/Atomic/TObjectWithProperties.php index 032029bec6f..96f1e2d805e 100644 --- a/src/Psalm/Type/Atomic/TObjectWithProperties.php +++ b/src/Psalm/Type/Atomic/TObjectWithProperties.php @@ -12,13 +12,12 @@ use function array_keys; use function array_map; -use function array_merge; -use function array_values; use function count; use function implode; /** * Denotes an object with specified member variables e.g. `object{foo:int, bar:string}`. + * @psalm-immutable */ final class TObjectWithProperties extends TObject { @@ -41,11 +40,42 @@ final class TObjectWithProperties extends TObject * @param array $methods * @param array $extra_types */ - public function __construct(array $properties, array $methods = [], array $extra_types = []) - { + public function __construct( + array $properties, + array $methods = [], + array $extra_types = [], + bool $from_docblock = false + ) { $this->properties = $properties; $this->methods = $methods; $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; + } + + /** + * @param array $properties + */ + public function setProperties(array $properties): self + { + if ($properties === $this->properties) { + return $this; + } + $cloned = clone $this; + $cloned->properties = $properties; + return $cloned; + } + + /** + * @param array $methods + */ + public function setMethods(array $methods): self + { + if ($methods === $this->methods) { + return $this; + } + $cloned = clone $this; + $cloned->methods = $methods; + return $cloned; } public function getId(bool $exact = true, bool $nested = false): string @@ -60,6 +90,7 @@ public function getId(bool $exact = true, bool $nested = false): string ', ', array_map( /** + * @psalm-pure * @param string|int $name */ static fn($name, Union $type): string => $name . ($type->possibly_undefined ? '?' : '') . ':' @@ -72,6 +103,9 @@ public function getId(bool $exact = true, bool $nested = false): string $methods_string = implode( ', ', array_map( + /** + * @psalm-pure + */ static fn(string $name): string => $name . '()', array_keys($this->methods) ) @@ -102,6 +136,7 @@ public function toNamespacedString( ', ', array_map( /** + * @psalm-pure * @param string|int $name */ static fn($name, Union $type): string => @@ -138,13 +173,6 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } - public function __clone() - { - foreach ($this->properties as &$property) { - $property = clone $property; - } - } - public function equals(Atomic $other_type, bool $ensure_source_equality): bool { if (!$other_type instanceof self) { @@ -232,25 +260,6 @@ public function replaceTemplateTypesWithStandins( return new static($properties, $this->methods, $intersection ?? $this->extra_types); } - /** - * @return static - */ - public function replaceClassLike(string $old, string $new): self - { - $properties = $this->properties; - foreach ($properties as &$property) { - $property = $property->replaceClassLike($old, $new); - } - $intersection = $this->replaceIntersectionClassLike($old, $new); - if (!$intersection && $properties === $this->properties) { - return $this; - } - return new static( - $properties, - $this->methods, - $intersection ?? $this->extra_types - ); - } /** * @return static */ @@ -259,8 +268,8 @@ public function replaceTemplateTypesWithArgTypes( ?Codebase $codebase ): self { $properties = $this->properties; - foreach ($this->properties as &$property) { - $property = TemplateInferredTypeReplacer::replace( + foreach ($properties as $offset => $property) { + $properties[$offset] = TemplateInferredTypeReplacer::replace( $property, $template_result, $codebase @@ -280,9 +289,9 @@ public function replaceTemplateTypesWithArgTypes( ); } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return array_merge($this->properties, array_values($this->extra_types)); + return ['properties', 'extra_types']; } public function getAssertionString(): string diff --git a/src/Psalm/Type/Atomic/TPropertiesOf.php b/src/Psalm/Type/Atomic/TPropertiesOf.php index 7cbfbdee376..ffee4f946c5 100644 --- a/src/Psalm/Type/Atomic/TPropertiesOf.php +++ b/src/Psalm/Type/Atomic/TPropertiesOf.php @@ -10,6 +10,7 @@ * * @psalm-type TokenName = 'properties-of'|'public-properties-of'|'protected-properties-of'|'private-properties-of' * + * @psalm-immutable */ final class TPropertiesOf extends Atomic { @@ -20,10 +21,7 @@ final class TPropertiesOf extends Atomic public const VISIBILITY_PROTECTED = 2; public const VISIBILITY_PRIVATE = 3; - /** - * @var TNamedObject - */ - public $classlike_type; + public TNamedObject $classlike_type; /** * @var self::VISIBILITY_*|null */ @@ -47,10 +45,12 @@ public static function tokenNames(): array */ public function __construct( TNamedObject $classlike_type, - ?int $visibility_filter + ?int $visibility_filter, + bool $from_docblock = false ) { $this->classlike_type = $classlike_type; $this->visibility_filter = $visibility_filter; + $this->from_docblock = $from_docblock; } /** @@ -72,6 +72,7 @@ public static function filterForTokenName(string $token_name): ?int } /** + * @psalm-pure * @return TokenName */ public static function tokenNameForFilter(?int $visibility_filter): string @@ -88,19 +89,9 @@ public static function tokenNameForFilter(?int $visibility_filter): string } } - /** - * @return static - */ - public function replaceClassLike(string $old, string $new): self + public function getChildNodeKeys(): array { - $replaced = $this->classlike_type->replaceClassLike($old, $new); - if ($replaced === $this->classlike_type) { - return $this; - } - return new static( - $replaced, - $this->visibility_filter - ); + return ['classlike_type']; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TResource.php b/src/Psalm/Type/Atomic/TResource.php index e7ca229412c..bb7c8f3a95d 100644 --- a/src/Psalm/Type/Atomic/TResource.php +++ b/src/Psalm/Type/Atomic/TResource.php @@ -6,6 +6,7 @@ /** * Denotes the `resource` type (e.g. a file handle). + * @psalm-immutable */ final class TResource extends Atomic { diff --git a/src/Psalm/Type/Atomic/TScalar.php b/src/Psalm/Type/Atomic/TScalar.php index 5fc10c5b0a8..de32876a374 100644 --- a/src/Psalm/Type/Atomic/TScalar.php +++ b/src/Psalm/Type/Atomic/TScalar.php @@ -5,6 +5,7 @@ /** * Denotes the `scalar` super type (which can also result from an `is_scalar` check). * This type encompasses `float`, `int`, `bool` and `string`. + * @psalm-immutable */ class TScalar extends Scalar { diff --git a/src/Psalm/Type/Atomic/TSingleLetter.php b/src/Psalm/Type/Atomic/TSingleLetter.php index 80613302479..23e8a354b64 100644 --- a/src/Psalm/Type/Atomic/TSingleLetter.php +++ b/src/Psalm/Type/Atomic/TSingleLetter.php @@ -4,6 +4,7 @@ /** * Denotes a string that has a length of 1 + * @psalm-immutable */ final class TSingleLetter extends TString { diff --git a/src/Psalm/Type/Atomic/TString.php b/src/Psalm/Type/Atomic/TString.php index ee08f110f43..2cfd5f51474 100644 --- a/src/Psalm/Type/Atomic/TString.php +++ b/src/Psalm/Type/Atomic/TString.php @@ -4,6 +4,7 @@ /** * Denotes the `string` type, where the exact value is unknown. + * @psalm-immutable */ class TString extends Scalar { diff --git a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php index f408ba22b94..0ef28969bab 100644 --- a/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php +++ b/src/Psalm/Type/Atomic/TTemplateIndexedAccess.php @@ -5,6 +5,7 @@ use Psalm\Type\Atomic; /** + * @psalm-immutable */ final class TTemplateIndexedAccess extends Atomic { @@ -26,11 +27,13 @@ final class TTemplateIndexedAccess extends Atomic public function __construct( string $array_param_name, string $offset_param_name, - string $defining_class + string $defining_class, + bool $from_docblock = false ) { $this->array_param_name = $array_param_name; $this->offset_param_name = $offset_param_name; $this->defining_class = $defining_class; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TTemplateKeyOf.php b/src/Psalm/Type/Atomic/TTemplateKeyOf.php index 26ef36b955b..9f667a225ca 100644 --- a/src/Psalm/Type/Atomic/TTemplateKeyOf.php +++ b/src/Psalm/Type/Atomic/TTemplateKeyOf.php @@ -10,6 +10,7 @@ /** * Represents the type used when using TKeyOf when the type of the array is a template + * @psalm-immutable */ final class TTemplateKeyOf extends Atomic { @@ -31,11 +32,13 @@ final class TTemplateKeyOf extends Atomic public function __construct( string $param_name, string $defining_class, - Union $as + Union $as, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->defining_class = $defining_class; $this->as = $as; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TTemplateParam.php b/src/Psalm/Type/Atomic/TTemplateParam.php index af4e033f525..775f52993e2 100644 --- a/src/Psalm/Type/Atomic/TTemplateParam.php +++ b/src/Psalm/Type/Atomic/TTemplateParam.php @@ -8,12 +8,11 @@ use Psalm\Type\Union; use function array_map; -use function array_merge; -use function array_values; use function implode; /** * denotes a template parameter that has been previously specified in a `@template` tag. + * @psalm-immutable */ final class TTemplateParam extends Atomic { @@ -37,12 +36,18 @@ final class TTemplateParam extends Atomic /** * @param array $extra_types */ - public function __construct(string $param_name, Union $extends, string $defining_class, array $extra_types = []) - { + public function __construct( + string $param_name, + Union $extends, + string $defining_class, + array $extra_types = [], + bool $from_docblock = false + ) { $this->param_name = $param_name; $this->as = $extends; $this->defining_class = $defining_class; $this->extra_types = $extra_types; + $this->from_docblock = $from_docblock; } /** @@ -132,9 +137,9 @@ public function toNamespacedString( return $this->param_name . $intersection_types; } - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return array_merge([$this->as], array_values($this->extra_types)); + return ['as', 'extra_types']; } public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool @@ -142,22 +147,6 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool return false; } - /** - * @return static - */ - public function replaceClassLike(string $old, string $new): self - { - $intersection = $this->replaceIntersectionClassLike($old, $new); - $replaced = $this->as->replaceClassLike($old, $new); - if (!$intersection && $replaced === $this->as) { - return $this; - } - $cloned = clone $this; - $cloned->as = $replaced; - $cloned->extra_types = $intersection ?? $this->extra_types; - return $cloned; - } - /** * @return static */ diff --git a/src/Psalm/Type/Atomic/TTemplateParamClass.php b/src/Psalm/Type/Atomic/TTemplateParamClass.php index a64aadef247..62544ab6217 100644 --- a/src/Psalm/Type/Atomic/TTemplateParamClass.php +++ b/src/Psalm/Type/Atomic/TTemplateParamClass.php @@ -4,6 +4,7 @@ /** * Denotes a `class-string` corresponding to a template parameter previously specified in a `@template` tag. + * @psalm-immutable */ final class TTemplateParamClass extends TClassString { @@ -21,12 +22,14 @@ public function __construct( string $param_name, string $as, ?TNamedObject $as_type, - string $defining_class + string $defining_class, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->as = $as; $this->as_type = $as_type; $this->defining_class = $defining_class; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php index 8caf397ee0a..59fbb539153 100644 --- a/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php +++ b/src/Psalm/Type/Atomic/TTemplatePropertiesOf.php @@ -10,6 +10,7 @@ /** * Represents the type used when using TPropertiesOf when the type of the array is a template + * @psalm-immutable */ final class TTemplatePropertiesOf extends Atomic { @@ -37,12 +38,14 @@ public function __construct( string $param_name, string $defining_class, TTemplateParam $as, - ?int $visibility_filter + ?int $visibility_filter, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->defining_class = $defining_class; $this->as = $as; $this->visibility_filter = $visibility_filter; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TTemplateValueOf.php b/src/Psalm/Type/Atomic/TTemplateValueOf.php index adcb287d559..bcae2992e87 100644 --- a/src/Psalm/Type/Atomic/TTemplateValueOf.php +++ b/src/Psalm/Type/Atomic/TTemplateValueOf.php @@ -10,6 +10,7 @@ /** * Represents the type used when using TValueOf when the type of the array or enum is a template + * @psalm-immutable */ final class TTemplateValueOf extends Atomic { @@ -31,11 +32,13 @@ final class TTemplateValueOf extends Atomic public function __construct( string $param_name, string $defining_class, - Union $as + Union $as, + bool $from_docblock = false ) { $this->param_name = $param_name; $this->defining_class = $defining_class; $this->as = $as; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TTraitString.php b/src/Psalm/Type/Atomic/TTraitString.php index bb4cf93b684..eb0f4b44593 100644 --- a/src/Psalm/Type/Atomic/TTraitString.php +++ b/src/Psalm/Type/Atomic/TTraitString.php @@ -4,6 +4,7 @@ /** * Denotes the `trait-string` type, used to describe a string representing a valid PHP trait. + * @psalm-immutable */ final class TTraitString extends TString { diff --git a/src/Psalm/Type/Atomic/TTrue.php b/src/Psalm/Type/Atomic/TTrue.php index 64545045e17..fa0293dd01d 100644 --- a/src/Psalm/Type/Atomic/TTrue.php +++ b/src/Psalm/Type/Atomic/TTrue.php @@ -4,6 +4,7 @@ /** * Denotes the `true` value type + * @psalm-immutable */ final class TTrue extends TBool { diff --git a/src/Psalm/Type/Atomic/TTypeAlias.php b/src/Psalm/Type/Atomic/TTypeAlias.php index a9da4aa8834..8174ba7ac23 100644 --- a/src/Psalm/Type/Atomic/TTypeAlias.php +++ b/src/Psalm/Type/Atomic/TTypeAlias.php @@ -8,6 +8,7 @@ use function implode; /** + * @psalm-immutable */ final class TTypeAlias extends Atomic { diff --git a/src/Psalm/Type/Atomic/TValueOf.php b/src/Psalm/Type/Atomic/TValueOf.php index 7cabc8ffad2..486021b8257 100644 --- a/src/Psalm/Type/Atomic/TValueOf.php +++ b/src/Psalm/Type/Atomic/TValueOf.php @@ -15,15 +15,17 @@ /** * Represents a value of an array or enum. + * @psalm-immutable */ final class TValueOf extends Atomic { /** @var Union */ public $type; - public function __construct(Union $type) + public function __construct(Union $type, bool $from_docblock = false) { $this->type = $type; + $this->from_docblock = $from_docblock; } public function getKey(bool $include_extra = true): string diff --git a/src/Psalm/Type/Atomic/TVoid.php b/src/Psalm/Type/Atomic/TVoid.php index f885fe03370..dfd0657c8d7 100644 --- a/src/Psalm/Type/Atomic/TVoid.php +++ b/src/Psalm/Type/Atomic/TVoid.php @@ -6,6 +6,7 @@ /** * Denotes the `void` type, normally just used to annotate a function/method that returns nothing + * @psalm-immutable */ final class TVoid extends Atomic { diff --git a/src/Psalm/Type/NodeVisitor.php b/src/Psalm/Type/ImmutableTypeVisitor.php similarity index 50% rename from src/Psalm/Type/NodeVisitor.php rename to src/Psalm/Type/ImmutableTypeVisitor.php index d33325a5db0..7578277f2c7 100644 --- a/src/Psalm/Type/NodeVisitor.php +++ b/src/Psalm/Type/ImmutableTypeVisitor.php @@ -2,7 +2,9 @@ namespace Psalm\Type; -abstract class NodeVisitor +use function is_array; + +abstract class ImmutableTypeVisitor { public const STOP_TRAVERSAL = 1; public const DONT_TRAVERSE_CHILDREN = 2; @@ -27,8 +29,22 @@ public function traverse(TypeNode $node): bool return false; } - foreach ($node->getChildNodes() as $child_node) { - if ($this->traverse($child_node) === false) { + foreach ($node->getChildNodeKeys() as $key) { + if ($node instanceof Union || $node instanceof MutableUnion) { + $child_node = $node->getAtomicTypes(); + } else { + /** @var TypeNode|non-empty-array|null */ + $child_node = $node->{$key}; + } + if ($child_node === null) { + continue; + } + if (is_array($child_node)) { + $visitor_result = $this->traverseArray($child_node); + } else { + $visitor_result = $this->traverse($child_node); + } + if ($visitor_result === false) { return false; } } @@ -37,14 +53,15 @@ public function traverse(TypeNode $node): bool } /** - * @param array $nodes + * @param non-empty-array $nodes */ - public function traverseArray(array $nodes): void + public function traverseArray(array $nodes): bool { foreach ($nodes as $node) { if ($this->traverse($node) === false) { - return; + return false; } } + return true; } } diff --git a/src/Psalm/Type/MutableUnion.php b/src/Psalm/Type/MutableUnion.php index 85df70b29da..da26d538097 100644 --- a/src/Psalm/Type/MutableUnion.php +++ b/src/Psalm/Type/MutableUnion.php @@ -4,6 +4,7 @@ use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\Type\TypeCombiner; +use Psalm\Internal\TypeVisitor\FromDocblockSetter; use Psalm\Type; use Psalm\Type\Atomic\Scalar; use Psalm\Type\Atomic\TArray; @@ -212,6 +213,7 @@ final class MutableUnion implements TypeNode, Stringable public $different = false; /** + * @psalm-external-mutation-free * @param non-empty-array $types */ public function setTypes(array $types): self @@ -222,7 +224,6 @@ public function setTypes(array $types): self $this->typed_class_strings = []; $from_docblock = false; - $keyed_types = []; foreach ($types as $type) { @@ -250,6 +251,9 @@ public function setTypes(array $types): self return $this; } + /** + * @psalm-external-mutation-free + */ public function addType(Atomic $type): self { $this->types[$type->getKey()] = $type; @@ -290,6 +294,9 @@ public function addType(Atomic $type): self return $this; } + /** + * @psalm-external-mutation-free + */ public function removeType(string $type_string): bool { if (isset($this->types[$type_string])) { @@ -339,6 +346,21 @@ public function removeType(string $type_string): bool return false; } + /** + * @psalm-external-mutation-free + */ + public function setFromDocblock(bool $fromDocblock = true): self + { + $this->from_docblock = $fromDocblock; + + (new FromDocblockSetter($fromDocblock))->traverseArray($this->types); + + return $this; + } + + /** + * @psalm-external-mutation-free + */ public function bustCache(): void { $this->id = null; @@ -346,6 +368,7 @@ public function bustCache(): void } /** + * @psalm-external-mutation-free * @param Union|MutableUnion $old_type * @param Union|MutableUnion|null $new_type */ @@ -436,23 +459,17 @@ public function substitute($old_type, $new_type = null): self return $this; } - - public function replaceClassLike(string $old, string $new): self - { - foreach ($this->types as $key => $atomic_type) { - $atomic_type = $atomic_type->replaceClassLike($old, $new); - - $this->removeType($key); - $this->addType($atomic_type); - } - return $this; - } - + /** + * @psalm-mutation-free + */ public function getBuilder(): self { return $this; } + /** + * @psalm-mutation-free + */ public function freeze(): Union { $union = new Union($this->getAtomicTypes()); diff --git a/src/Psalm/Type/TypeNode.php b/src/Psalm/Type/TypeNode.php index 6bdc054c28c..f8d8cf4a506 100644 --- a/src/Psalm/Type/TypeNode.php +++ b/src/Psalm/Type/TypeNode.php @@ -5,7 +5,7 @@ interface TypeNode { /** - * @return array + * @return list */ - public function getChildNodes(): array; + public function getChildNodeKeys(): array; } diff --git a/src/Psalm/Type/TypeVisitor.php b/src/Psalm/Type/TypeVisitor.php new file mode 100644 index 00000000000..9feded61512 --- /dev/null +++ b/src/Psalm/Type/TypeVisitor.php @@ -0,0 +1,102 @@ +enterNode($node); + + if ($visitor_result === self::DONT_TRAVERSE_CHILDREN) { + return true; + } + + if ($visitor_result === self::STOP_TRAVERSAL) { + return false; + } + + $cloned = $node !== $old; + foreach ($node->getChildNodeKeys() as $key) { + if ($node instanceof Union || $node instanceof MutableUnion) { + $child_node = $node->getAtomicTypes(); + } else { + /** @var TypeNode|non-empty-array|null */ + $child_node = $node->{$key}; + } + if ($child_node === null) { + continue; + } + $orig = $child_node; + if (is_array($child_node)) { + $visitor_result = $this->traverseArray($child_node); + } else { + $visitor_result = $this->traverse($child_node); + } + if ($child_node !== $orig) { + if ($node instanceof Union) { + /** @var non-empty-array $child_node */ + $node = $node->getBuilder()->setTypes($child_node)->freeze(); + } elseif ($node instanceof MutableUnion) { + // This mutates in-place + /** @var non-empty-array $child_node */ + $node->setTypes($child_node); + } else { + if (!$cloned) { + $cloned = true; + $node = clone $node; + } + if ($key === 'extra_types' && is_array($child_node)) { + $new = []; + /** @var Union */ + foreach ($child_node as $value) { + $new[$value->getKey()] = $value; + } + $child_node = $new; + } + $node->{$key} = $child_node; + } + } + if ($visitor_result === false) { + return false; + } + } + + return true; + } + + /** + * @template T as array + * @param T $nodes + * @param-out T $nodes + */ + public function traverseArray(array &$nodes): bool + { + foreach ($nodes as &$node) { + if ($this->traverse($node) === false) { + return false; + } + } + return true; + } +} diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 61efdd1cc9e..451a6d86b8c 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -3,6 +3,7 @@ namespace Psalm\Type; use Psalm\Internal\DataFlow\DataFlowNode; +use Psalm\Internal\TypeVisitor\FromDocblockSetter; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TLiteralFloat; use Psalm\Type\Atomic\TLiteralInt; @@ -196,13 +197,24 @@ final class Union implements TypeNode, Stringable */ public $different = false; - public function getBuilder(): MutableUnion + /** + * @psalm-mutation-free + * @param non-empty-array $types + */ + public function setTypes(array $types): self { - $types = []; - foreach ($this->getAtomicTypes() as $type) { - $types []= clone $type; + if ($types === $this->types) { + return $this; } - $union = new MutableUnion($types); + return $this->getBuilder()->setTypes($types)->freeze(); + } + + /** + * @psalm-mutation-free + */ + public function getBuilder(): MutableUnion + { + $union = new MutableUnion($this->getAtomicTypes()); foreach (get_object_vars($this) as $key => $value) { if ($key === 'types') { continue; @@ -230,12 +242,13 @@ public function getBuilder(): MutableUnion return $union; } - public function replaceClassLike(string $old, string $new): self + /** + * @psalm-mutation-free + */ + public function setFromDocblock(bool $fromDocblock = true): self { - $types = $this->types; - foreach ($types as &$atomic_type) { - $atomic_type = $atomic_type->replaceClassLike($old, $new); - } - return $types === $this->types ? $this : $this->getBuilder()->setTypes($types)->freeze(); + $cloned = clone $this; + (new FromDocblockSetter($fromDocblock))->traverse($cloned); + return $cloned; } } diff --git a/src/Psalm/Type/UnionTrait.php b/src/Psalm/Type/UnionTrait.php index cbf55c95dd7..7c0f834e7d0 100644 --- a/src/Psalm/Type/UnionTrait.php +++ b/src/Psalm/Type/UnionTrait.php @@ -5,9 +5,9 @@ use InvalidArgumentException; use Psalm\CodeLocation; use Psalm\Codebase; +use Psalm\Internal\TypeVisitor\ClasslikeReplacer; use Psalm\Internal\TypeVisitor\ContainsClassLikeVisitor; use Psalm\Internal\TypeVisitor\ContainsLiteralVisitor; -use Psalm\Internal\TypeVisitor\FromDocblockSetter; use Psalm\Internal\TypeVisitor\TemplateTypeCollector; use Psalm\Internal\TypeVisitor\TypeChecker; use Psalm\Internal\TypeVisitor\TypeScanner; @@ -54,12 +54,12 @@ trait UnionTrait /** * Constructs an Union instance * + * @psalm-external-mutation-free + * * @param non-empty-array $types */ - public function __construct(array $types) + public function __construct(array $types, bool $from_docblock = false) { - $from_docblock = false; - $keyed_types = []; foreach ($types as $type) { @@ -86,30 +86,6 @@ public function __construct(array $types) $this->from_docblock = $from_docblock; } - public function __clone() - { - $this->literal_string_types = []; - $this->literal_int_types = []; - $this->literal_float_types = []; - $this->typed_class_strings = []; - - foreach ($this->types as $key => &$type) { - $type = clone $type; - - if ($type instanceof TLiteralInt) { - $this->literal_int_types[$key] = $type; - } elseif ($type instanceof TLiteralString) { - $this->literal_string_types[$key] = $type; - } elseif ($type instanceof TLiteralFloat) { - $this->literal_float_types[$key] = $type; - } elseif ($type instanceof TClassString - && ($type->as_type || $type instanceof TTemplateParamClass) - ) { - $this->typed_class_strings[$key] = $type; - } - } - } - /** * @psalm-mutation-free * @return non-empty-array @@ -119,6 +95,9 @@ public function getAtomicTypes(): array return $this->types; } + /** + * @psalm-mutation-free + */ public function __toString(): string { $types = []; @@ -155,6 +134,9 @@ public function __toString(): string return implode('|', $types); } + /** + * @psalm-mutation-free + */ public function getKey(): string { $types = []; @@ -194,12 +176,9 @@ public function getKey(): string return implode('|', $types); } - public function bustCache(): void - { - $this->id = null; - $this->exact_id = null; - } - + /** + * @psalm-mutation-free + */ public function getId(bool $exact = true): string { if ($exact && $this->exact_id) { @@ -226,8 +205,10 @@ public function getId(bool $exact = true): string $id = implode('|', $types); if ($exact) { + /** @psalm-suppress ImpurePropertyAssignment Cache */ $this->exact_id = $id; } else { + /** @psalm-suppress ImpurePropertyAssignment Cache */ $this->id = $id; } @@ -236,7 +217,7 @@ public function getId(bool $exact = true): string /** * @param array $aliased_classes - * + * @psalm-mutation-free */ public function toNamespacedString( ?string $namespace, @@ -285,6 +266,7 @@ public function toNamespacedString( } /** + * @psalm-mutation-free * @param array $aliased_classes */ public function toPhpString( @@ -356,6 +338,9 @@ public function toPhpString( return implode('|', array_unique($php_types)); } + /** + * @psalm-mutation-free + */ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool { if (!$this->isSingleAndMaybeNullable() && $analysis_php_version_id < 8_00_00) { @@ -378,31 +363,49 @@ public function canBeFullyExpressedInPhp(int $analysis_php_version_id): bool ); } + /** + * @psalm-mutation-free + */ public function hasType(string $type_string): bool { return isset($this->types[$type_string]); } + /** + * @psalm-mutation-free + */ public function hasArray(): bool { return isset($this->types['array']); } + /** + * @psalm-mutation-free + */ public function hasIterable(): bool { return isset($this->types['iterable']); } + /** + * @psalm-mutation-free + */ public function hasList(): bool { return isset($this->types['array']) && $this->types['array'] instanceof TList; } + /** + * @psalm-mutation-free + */ public function hasClassStringMap(): bool { return isset($this->types['array']) && $this->types['array'] instanceof TClassStringMap; } + /** + * @psalm-mutation-free + */ public function isTemplatedClassString(): bool { return $this->isSingle() @@ -414,6 +417,9 @@ public function isTemplatedClassString(): bool ) === 1; } + /** + * @psalm-mutation-free + */ public function hasArrayAccessInterface(Codebase $codebase): bool { return (bool)array_filter( @@ -422,12 +428,16 @@ public function hasArrayAccessInterface(Codebase $codebase): bool ); } + /** + * @psalm-mutation-free + */ public function hasCallableType(): bool { return $this->getCallableTypes() || $this->getClosureTypes(); } /** + * @psalm-mutation-free * @return array */ public function getCallableTypes(): array @@ -439,6 +449,7 @@ public function getCallableTypes(): array } /** + * @psalm-mutation-free * @return array */ public function getClosureTypes(): array @@ -449,11 +460,17 @@ public function getClosureTypes(): array ); } + /** + * @psalm-mutation-free + */ public function hasObject(): bool { return isset($this->types['object']); } + /** + * @psalm-mutation-free + */ public function hasObjectType(): bool { foreach ($this->types as $type) { @@ -465,6 +482,9 @@ public function hasObjectType(): bool return false; } + /** + * @psalm-mutation-free + */ public function isObjectType(): bool { foreach ($this->types as $type) { @@ -476,6 +496,9 @@ public function isObjectType(): bool return true; } + /** + * @psalm-mutation-free + */ public function hasNamedObjectType(): bool { foreach ($this->types as $type) { @@ -487,6 +510,9 @@ public function hasNamedObjectType(): bool return false; } + /** + * @psalm-mutation-free + */ public function isStaticObject(): bool { foreach ($this->types as $type) { @@ -500,6 +526,9 @@ public function isStaticObject(): bool return true; } + /** + * @psalm-mutation-free + */ public function hasStaticObject(): bool { foreach ($this->types as $type) { @@ -513,6 +542,9 @@ public function hasStaticObject(): bool return false; } + /** + * @psalm-mutation-free + */ public function isNullable(): bool { if (isset($this->types['null'])) { @@ -528,6 +560,9 @@ public function isNullable(): bool return false; } + /** + * @psalm-mutation-free + */ public function isFalsable(): bool { if (isset($this->types['false'])) { @@ -543,11 +578,17 @@ public function isFalsable(): bool return false; } + /** + * @psalm-mutation-free + */ public function hasBool(): bool { return isset($this->types['bool']) || isset($this->types['false']) || isset($this->types['true']); } + /** + * @psalm-mutation-free + */ public function hasString(): bool { return isset($this->types['string']) @@ -560,6 +601,9 @@ public function hasString(): bool || $this->typed_class_strings; } + /** + * @psalm-mutation-free + */ public function hasLowercaseString(): bool { return isset($this->types['string']) @@ -567,37 +611,58 @@ public function hasLowercaseString(): bool || $this->types['string'] instanceof TNonEmptyLowercaseString); } + /** + * @psalm-mutation-free + */ public function hasLiteralClassString(): bool { return count($this->typed_class_strings) > 0; } + /** + * @psalm-mutation-free + */ public function hasInt(): bool { return isset($this->types['int']) || isset($this->types['array-key']) || $this->literal_int_types || array_filter($this->types, static fn(Atomic $type): bool => $type instanceof TIntRange); } + /** + * @psalm-mutation-free + */ public function hasArrayKey(): bool { return isset($this->types['array-key']); } + /** + * @psalm-mutation-free + */ public function hasFloat(): bool { return isset($this->types['float']) || $this->literal_float_types; } + /** + * @psalm-mutation-free + */ public function hasScalar(): bool { return isset($this->types['scalar']); } + /** + * @psalm-mutation-free + */ public function hasNumeric(): bool { return isset($this->types['numeric']); } + /** + * @psalm-mutation-free + */ public function hasScalarType(): bool { return isset($this->types['int']) @@ -616,6 +681,9 @@ public function hasScalarType(): bool || $this->typed_class_strings; } + /** + * @psalm-mutation-free + */ public function hasTemplate(): bool { return (bool) array_filter( @@ -631,6 +699,9 @@ public function hasTemplate(): bool ); } + /** + * @psalm-mutation-free + */ public function hasConditional(): bool { return (bool) array_filter( @@ -639,6 +710,9 @@ public function hasConditional(): bool ); } + /** + * @psalm-mutation-free + */ public function hasTemplateOrStatic(): bool { return (bool) array_filter( @@ -657,16 +731,25 @@ public function hasTemplateOrStatic(): bool ); } + /** + * @psalm-mutation-free + */ public function hasMixed(): bool { return isset($this->types['mixed']); } + /** + * @psalm-mutation-free + */ public function isMixed(): bool { return isset($this->types['mixed']) && count($this->types) === 1; } + /** + * @psalm-mutation-free + */ public function isEmptyMixed(): bool { return isset($this->types['mixed']) @@ -674,6 +757,9 @@ public function isEmptyMixed(): bool && count($this->types) === 1; } + /** + * @psalm-mutation-free + */ public function isVanillaMixed(): bool { return isset($this->types['mixed']) @@ -682,21 +768,33 @@ public function isVanillaMixed(): bool && count($this->types) === 1; } + /** + * @psalm-mutation-free + */ public function isArrayKey(): bool { return isset($this->types['array-key']) && count($this->types) === 1; } + /** + * @psalm-mutation-free + */ public function isNull(): bool { return count($this->types) === 1 && isset($this->types['null']); } + /** + * @psalm-mutation-free + */ public function isFalse(): bool { return count($this->types) === 1 && isset($this->types['false']); } + /** + * @psalm-mutation-free + */ public function isAlwaysFalsy(): bool { foreach ($this->getAtomicTypes() as $atomic_type) { @@ -708,11 +806,17 @@ public function isAlwaysFalsy(): bool return true; } + /** + * @psalm-mutation-free + */ public function isTrue(): bool { return count($this->types) === 1 && isset($this->types['true']); } + /** + * @psalm-mutation-free + */ public function isAlwaysTruthy(): bool { if ($this->possibly_undefined || $this->possibly_undefined_from_try) { @@ -728,16 +832,25 @@ public function isAlwaysTruthy(): bool return true; } + /** + * @psalm-mutation-free + */ public function isVoid(): bool { return isset($this->types['void']) && count($this->types) === 1; } + /** + * @psalm-mutation-free + */ public function isNever(): bool { return isset($this->types['never']) && count($this->types) === 1; } + /** + * @psalm-mutation-free + */ public function isGenerator(): bool { return count($this->types) === 1 @@ -745,6 +858,9 @@ public function isGenerator(): bool && ($single_type->value === 'Generator'); } + /** + * @psalm-mutation-free + */ public function isSingle(): bool { $type_count = count($this->types); @@ -767,6 +883,9 @@ public function isSingle(): bool return $type_count === 1; } + /** + * @psalm-mutation-free + */ public function isSingleAndMaybeNullable(): bool { $is_nullable = isset($this->types['null']); @@ -796,6 +915,7 @@ public function isSingleAndMaybeNullable(): bool } /** + * @psalm-mutation-free * @return bool true if this is an int */ public function isInt(bool $check_templates = false): bool @@ -813,6 +933,7 @@ public function isInt(bool $check_templates = false): bool } /** + * @psalm-mutation-free * @return bool true if this is a float */ public function isFloat(): bool @@ -825,6 +946,7 @@ public function isFloat(): bool } /** + * @psalm-mutation-free * @return bool true if this is a string */ public function isString(bool $check_templates = false): bool @@ -842,6 +964,7 @@ public function isString(bool $check_templates = false): bool } /** + * @psalm-mutation-free * @return bool true if this is a boolean */ public function isBool(): bool @@ -854,6 +977,7 @@ public function isBool(): bool } /** + * @psalm-mutation-free * @return bool true if this is an array */ public function isArray(): bool @@ -866,6 +990,7 @@ public function isArray(): bool } /** + * @psalm-mutation-free * @return bool true if this is a string literal with only one possible value */ public function isSingleStringLiteral(): bool @@ -875,7 +1000,7 @@ public function isSingleStringLiteral(): bool /** * @throws InvalidArgumentException if isSingleStringLiteral is false - * + * @psalm-mutation-free * @return TLiteralString the only string literal represented by this union type */ public function getSingleStringLiteral(): TLiteralString @@ -887,6 +1012,9 @@ public function getSingleStringLiteral(): TLiteralString return reset($this->literal_string_types); } + /** + * @psalm-mutation-free + */ public function allStringLiterals(): bool { foreach ($this->types as $atomic_key_type) { @@ -898,6 +1026,9 @@ public function allStringLiterals(): bool return true; } + /** + * @psalm-mutation-free + */ public function allIntLiterals(): bool { foreach ($this->types as $atomic_key_type) { @@ -909,6 +1040,9 @@ public function allIntLiterals(): bool return true; } + /** + * @psalm-mutation-free + */ public function allFloatLiterals(): bool { foreach ($this->types as $atomic_key_type) { @@ -921,6 +1055,7 @@ public function allFloatLiterals(): bool } /** + * @psalm-mutation-free * @psalm-assert-if-true array< * array-key, * TLiteralString|TLiteralInt|TLiteralFloat|TFalse|TTrue @@ -943,6 +1078,7 @@ public function allSpecificLiterals(): bool } /** + * @psalm-mutation-free * @psalm-assert-if-true array< * array-key, * TLiteralString|TLiteralInt|TLiteralFloat|TNonspecificLiteralString|TNonSpecificLiteralInt|TFalse|TTrue @@ -966,6 +1102,9 @@ public function allLiterals(): bool return true; } + /** + * @psalm-mutation-free + */ public function hasLiteralValue(): bool { return $this->literal_int_types @@ -975,6 +1114,9 @@ public function hasLiteralValue(): bool || isset($this->types['true']); } + /** + * @psalm-mutation-free + */ public function isSingleLiteral(): bool { return count($this->types) === 1 @@ -985,6 +1127,7 @@ public function isSingleLiteral(): bool } /** + * @psalm-mutation-free * @return TLiteralInt|TLiteralString|TLiteralFloat */ public function getSingleLiteral() @@ -1001,17 +1144,24 @@ public function getSingleLiteral() ; } + /** + * @psalm-mutation-free + */ public function hasLiteralString(): bool { return count($this->literal_string_types) > 0; } + /** + * @psalm-mutation-free + */ public function hasLiteralInt(): bool { return count($this->literal_int_types) > 0; } /** + * @psalm-mutation-free * @return bool true if this is a int literal with only one possible value */ public function isSingleIntLiteral(): bool @@ -1021,7 +1171,7 @@ public function isSingleIntLiteral(): bool /** * @throws InvalidArgumentException if isSingleIntLiteral is false - * + * @psalm-mutation-free * @return TLiteralInt the only int literal represented by this union type */ public function getSingleIntLiteral(): TLiteralInt @@ -1036,7 +1186,6 @@ public function getSingleIntLiteral(): TLiteralInt /** * @param array $suppressed_issues * @param array $phantom_classes - * */ public function check( StatementsSource $source, @@ -1072,7 +1221,6 @@ public function check( /** * @param array $phantom_classes - * */ public function queueClassLikesForScanning( Codebase $codebase, @@ -1090,6 +1238,7 @@ public function queueClassLikesForScanning( /** * @param lowercase-string $fq_class_like_name + * @psalm-mutation-free */ public function containsClassLike(string $fq_class_like_name): bool { @@ -1100,6 +1249,20 @@ public function containsClassLike(string $fq_class_like_name): bool return $classlike_visitor->matches(); } + /** + * @return static + */ + public function replaceClassLike(string $old, string $new): self + { + $type = $this; + (new ClasslikeReplacer( + $old, + $new + ))->traverse($type); + return $type; + } + + /** @psalm-mutation-free */ public function containsAnyLiteral(): bool { $literal_visitor = new ContainsLiteralVisitor(); @@ -1110,6 +1273,7 @@ public function containsAnyLiteral(): bool } /** + * @psalm-mutation-free * @return list */ public function getTemplateTypes(): array @@ -1121,13 +1285,9 @@ public function getTemplateTypes(): array return $template_type_collector->getTemplateTypes(); } - public function setFromDocblock(): void - { - $this->from_docblock = true; - - (new FromDocblockSetter())->traverseArray($this->types); - } - + /** + * @psalm-mutation-free + */ public function equals(self $other_type, bool $ensure_source_equality = true): bool { if ($other_type === $this) { @@ -1194,6 +1354,7 @@ public function equals(self $other_type, bool $ensure_source_equality = true): b } /** + * @psalm-mutation-free * @return array */ public function getLiteralStrings(): array @@ -1202,6 +1363,7 @@ public function getLiteralStrings(): array } /** + * @psalm-mutation-free * @return array */ public function getLiteralInts(): array @@ -1210,6 +1372,7 @@ public function getLiteralInts(): array } /** + * @psalm-mutation-free * @return array */ public function getRangeInts(): array @@ -1225,6 +1388,7 @@ public function getRangeInts(): array } /** + * @psalm-mutation-free * @return array */ public function getLiteralFloats(): array @@ -1233,14 +1397,16 @@ public function getLiteralFloats(): array } /** - * @return array + * @psalm-mutation-free + * @return list */ - public function getChildNodes(): array + public function getChildNodeKeys(): array { - return $this->types; + return ['types']; } /** + * @psalm-mutation-free * @return bool true if this is a float literal with only one possible value */ public function isSingleFloatLiteral(): bool @@ -1249,6 +1415,7 @@ public function isSingleFloatLiteral(): bool } /** + * @psalm-mutation-free * @throws InvalidArgumentException if isSingleFloatLiteral is false * * @return TLiteralFloat the only float literal represented by this union type @@ -1262,16 +1429,25 @@ public function getSingleFloatLiteral(): TLiteralFloat return reset($this->literal_float_types); } + /** + * @psalm-mutation-free + */ public function hasLiteralFloat(): bool { return count($this->literal_float_types) > 0; } + /** + * @psalm-mutation-free + */ public function getSingleAtomic(): Atomic { return reset($this->types); } + /** + * @psalm-mutation-free + */ public function isEmptyArray(): bool { return count($this->types) === 1 @@ -1280,6 +1456,9 @@ public function isEmptyArray(): bool && $this->types['array']->isEmptyArray(); } + /** + * @psalm-mutation-free + */ public function isUnionEmpty(): bool { return $this->types === []; diff --git a/tests/ArrayAccessTest.php b/tests/ArrayAccessTest.php index f1d3e16bb63..fdf1f43914d 100644 --- a/tests/ArrayAccessTest.php +++ b/tests/ArrayAccessTest.php @@ -564,11 +564,20 @@ function f(array $p): void ], 'unsetTKeyedArrayOffset' => [ 'code' => ' "value"]; - unset($x["a"]); - $x[] = 5; - takesInt($x[0]);', + $x1 = ["a" => "value"]; + unset($x1["a"]); + + $x2 = ["a" => "value", "b" => "value"]; + unset($x2["a"]); + + $x3 = ["a" => "value", "b" => "value"]; + $k = "a"; + unset($x3[$k]);', + 'assertions' => [ + '$x1===' => 'array', + '$x2===' => "array{b: 'value'}", + '$x3===' => "array{b: 'value'}", + ] ], 'domNodeListAccessible' => [ 'code' => ' [ 'code' => ' */ + /** @var SplObjectStorage<\stdClass, mixed> */ $storage = new SplObjectStorage(); new SomeService($storage); $c = new \stdClass(); $storage[$c] = "hello"; + /** @psalm-suppress MixedAssignment */ $b = $storage->offsetGet($c);', 'assertions' => [ - '$b' => 'string', + '$b' => 'mixed', ], ], 'extendsArrayIterator' => [ diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index 147ac2ab2be..02a699964a9 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -24,6 +24,7 @@ public function testValidTypeCombination($expected, $types): void foreach ($types as $type) { $converted_type = self::getAtomic($type); + /** @psalm-suppress InaccessibleProperty */ $converted_type->from_docblock = true; $converted_types[] = $converted_type; } From 58fd83a01f05d5ff23a8c2917cf5b385f96153e1 Mon Sep 17 00:00:00 2001 From: "William Owen O. Ponce" Date: Tue, 4 Oct 2022 09:47:40 +0800 Subject: [PATCH 144/194] Add int type aliases based on existing codes --- src/Psalm/Type/Atomic.php | 24 +++++++++++++ src/Psalm/Type/Atomic/TNegativeInt .php | 41 +++++++++++++++++++++++ src/Psalm/Type/Atomic/TNonNegativeInt.php | 41 +++++++++++++++++++++++ src/Psalm/Type/Atomic/TNonPositiveInt.php | 41 +++++++++++++++++++++++ 4 files changed, 147 insertions(+) create mode 100644 src/Psalm/Type/Atomic/TNegativeInt .php create mode 100644 src/Psalm/Type/Atomic/TNonNegativeInt.php create mode 100644 src/Psalm/Type/Atomic/TNonPositiveInt.php diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index b0d456b65dd..47b54162c4b 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -62,6 +62,9 @@ use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TPositiveInt; +use Psalm\Type\Atomic\TNonPositiveInt; +use Psalm\Type\Atomic\TNegativeInt; +use Psalm\Type\Atomic\TNonNegativeInt; use Psalm\Type\Atomic\TResource; use Psalm\Type\Atomic\TScalar; use Psalm\Type\Atomic\TString; @@ -229,6 +232,15 @@ public static function create( case 'positive-int': return new TPositiveInt(); + + case 'non-positive-int': + return new TNonPositiveInt(); + + case 'negative-int': + return new TNegativeInt(); + + case 'non-negative-int': + return new TNonNegativeInt(); case 'numeric': return $php_version !== null ? new TNamedObject($value) : new TNumeric(); @@ -720,6 +732,18 @@ public function isTruthy(): bool return true; } + if ($this instanceof TNonPositiveInt) { + return true; + } + + if ($this instanceof TNegativeInt) { + return true; + } + + if ($this instanceof TNonNegativeInt) { + return true; + } + if ($this instanceof TLiteralClassString) { return true; } diff --git a/src/Psalm/Type/Atomic/TNegativeInt .php b/src/Psalm/Type/Atomic/TNegativeInt .php new file mode 100644 index 00000000000..1325492cdac --- /dev/null +++ b/src/Psalm/Type/Atomic/TNegativeInt .php @@ -0,0 +1,41 @@ + $aliased_classes + * + */ + public function toNamespacedString( + ?string $namespace, + array $aliased_classes, + ?string $this_class, + bool $use_phpdoc_format + ): string { + return $use_phpdoc_format ? 'int' : 'negative-int'; + } +} diff --git a/src/Psalm/Type/Atomic/TNonNegativeInt.php b/src/Psalm/Type/Atomic/TNonNegativeInt.php new file mode 100644 index 00000000000..10c16d638a6 --- /dev/null +++ b/src/Psalm/Type/Atomic/TNonNegativeInt.php @@ -0,0 +1,41 @@ + 0) + * @deprecated will be removed in Psalm 5 + */ +class TNonNegativeInt extends TInt +{ + public function getId(bool $nested = false): string + { + return 'non-negative-int'; + } + + public function __toString(): string + { + return 'non-negative-int'; + } + + /** + * @return false + */ + public function canBeFullyExpressedInPhp(int $php_major_version, int $php_minor_version): bool + { + return false; + } + + /** + * @param array $aliased_classes + * + */ + public function toNamespacedString( + ?string $namespace, + array $aliased_classes, + ?string $this_class, + bool $use_phpdoc_format + ): string { + return $use_phpdoc_format ? 'int' : 'non-negative-int'; + } +} diff --git a/src/Psalm/Type/Atomic/TNonPositiveInt.php b/src/Psalm/Type/Atomic/TNonPositiveInt.php new file mode 100644 index 00000000000..dbc66be77a4 --- /dev/null +++ b/src/Psalm/Type/Atomic/TNonPositiveInt.php @@ -0,0 +1,41 @@ + $aliased_classes + * + */ + public function toNamespacedString( + ?string $namespace, + array $aliased_classes, + ?string $this_class, + bool $use_phpdoc_format + ): string { + return $use_phpdoc_format ? 'int' : 'non-positive-int'; + } +} From 6b6c320fe62e75db20fe41289e7fefd9880effe5 Mon Sep 17 00:00:00 2001 From: "William Owen O. Ponce" Date: Tue, 4 Oct 2022 09:53:22 +0800 Subject: [PATCH 145/194] Arrange use statements alphabetically --- src/Psalm/Type/Atomic.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 47b54162c4b..2672139322b 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -45,6 +45,7 @@ use Psalm\Type\Atomic\TLowercaseString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; +use Psalm\Type\Atomic\TNegativeInt; use Psalm\Type\Atomic\TNever; use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNonEmptyList; @@ -56,15 +57,14 @@ use Psalm\Type\Atomic\TNonFalsyString; use Psalm\Type\Atomic\TNonspecificLiteralInt; use Psalm\Type\Atomic\TNonspecificLiteralString; +use Psalm\Type\Atomic\TNonNegativeInt; +use Psalm\Type\Atomic\TNonPositiveInt; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TNumeric; use Psalm\Type\Atomic\TNumericString; use Psalm\Type\Atomic\TObject; use Psalm\Type\Atomic\TObjectWithProperties; use Psalm\Type\Atomic\TPositiveInt; -use Psalm\Type\Atomic\TNonPositiveInt; -use Psalm\Type\Atomic\TNegativeInt; -use Psalm\Type\Atomic\TNonNegativeInt; use Psalm\Type\Atomic\TResource; use Psalm\Type\Atomic\TScalar; use Psalm\Type\Atomic\TString; From 04c9fe89c1225dd6dff975d20b6fea4ef3a173aa Mon Sep 17 00:00:00 2001 From: "William Owen O. Ponce" Date: Tue, 4 Oct 2022 09:59:34 +0800 Subject: [PATCH 146/194] Arrange use statements alphabetically again --- src/Psalm/Type/Atomic.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 2672139322b..657657bae41 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -55,10 +55,10 @@ use Psalm\Type\Atomic\TNonEmptyScalar; use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Atomic\TNonFalsyString; -use Psalm\Type\Atomic\TNonspecificLiteralInt; -use Psalm\Type\Atomic\TNonspecificLiteralString; use Psalm\Type\Atomic\TNonNegativeInt; use Psalm\Type\Atomic\TNonPositiveInt; +use Psalm\Type\Atomic\TNonspecificLiteralInt; +use Psalm\Type\Atomic\TNonspecificLiteralString; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TNumeric; use Psalm\Type\Atomic\TNumericString; From ae426a00cec386680801c6973e27c8caa0d08003 Mon Sep 17 00:00:00 2001 From: "William Owen O. Ponce" Date: Wed, 5 Oct 2022 09:09:56 +0800 Subject: [PATCH 147/194] Remove irrelevant types, use keep aliases --- src/Psalm/Type/Atomic.php | 21 ++---------- src/Psalm/Type/Atomic/TNegativeInt .php | 41 ----------------------- src/Psalm/Type/Atomic/TNonNegativeInt.php | 41 ----------------------- src/Psalm/Type/Atomic/TNonPositiveInt.php | 41 ----------------------- 4 files changed, 3 insertions(+), 141 deletions(-) delete mode 100644 src/Psalm/Type/Atomic/TNegativeInt .php delete mode 100644 src/Psalm/Type/Atomic/TNonNegativeInt.php delete mode 100644 src/Psalm/Type/Atomic/TNonPositiveInt.php diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 657657bae41..7e5a258ea61 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -45,7 +45,6 @@ use Psalm\Type\Atomic\TLowercaseString; use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; -use Psalm\Type\Atomic\TNegativeInt; use Psalm\Type\Atomic\TNever; use Psalm\Type\Atomic\TNonEmptyArray; use Psalm\Type\Atomic\TNonEmptyList; @@ -55,8 +54,6 @@ use Psalm\Type\Atomic\TNonEmptyScalar; use Psalm\Type\Atomic\TNonEmptyString; use Psalm\Type\Atomic\TNonFalsyString; -use Psalm\Type\Atomic\TNonNegativeInt; -use Psalm\Type\Atomic\TNonPositiveInt; use Psalm\Type\Atomic\TNonspecificLiteralInt; use Psalm\Type\Atomic\TNonspecificLiteralString; use Psalm\Type\Atomic\TNull; @@ -234,13 +231,13 @@ public static function create( return new TPositiveInt(); case 'non-positive-int': - return new TNonPositiveInt(); + return new TIntRange(null, -1); case 'negative-int': - return new TNegativeInt(); + return new TIntRange(null, -1); case 'non-negative-int': - return new TNonNegativeInt(); + return new TPositiveInt(); case 'numeric': return $php_version !== null ? new TNamedObject($value) : new TNumeric(); @@ -732,18 +729,6 @@ public function isTruthy(): bool return true; } - if ($this instanceof TNonPositiveInt) { - return true; - } - - if ($this instanceof TNegativeInt) { - return true; - } - - if ($this instanceof TNonNegativeInt) { - return true; - } - if ($this instanceof TLiteralClassString) { return true; } diff --git a/src/Psalm/Type/Atomic/TNegativeInt .php b/src/Psalm/Type/Atomic/TNegativeInt .php deleted file mode 100644 index 1325492cdac..00000000000 --- a/src/Psalm/Type/Atomic/TNegativeInt .php +++ /dev/null @@ -1,41 +0,0 @@ - $aliased_classes - * - */ - public function toNamespacedString( - ?string $namespace, - array $aliased_classes, - ?string $this_class, - bool $use_phpdoc_format - ): string { - return $use_phpdoc_format ? 'int' : 'negative-int'; - } -} diff --git a/src/Psalm/Type/Atomic/TNonNegativeInt.php b/src/Psalm/Type/Atomic/TNonNegativeInt.php deleted file mode 100644 index 10c16d638a6..00000000000 --- a/src/Psalm/Type/Atomic/TNonNegativeInt.php +++ /dev/null @@ -1,41 +0,0 @@ - 0) - * @deprecated will be removed in Psalm 5 - */ -class TNonNegativeInt extends TInt -{ - public function getId(bool $nested = false): string - { - return 'non-negative-int'; - } - - public function __toString(): string - { - return 'non-negative-int'; - } - - /** - * @return false - */ - public function canBeFullyExpressedInPhp(int $php_major_version, int $php_minor_version): bool - { - return false; - } - - /** - * @param array $aliased_classes - * - */ - public function toNamespacedString( - ?string $namespace, - array $aliased_classes, - ?string $this_class, - bool $use_phpdoc_format - ): string { - return $use_phpdoc_format ? 'int' : 'non-negative-int'; - } -} diff --git a/src/Psalm/Type/Atomic/TNonPositiveInt.php b/src/Psalm/Type/Atomic/TNonPositiveInt.php deleted file mode 100644 index dbc66be77a4..00000000000 --- a/src/Psalm/Type/Atomic/TNonPositiveInt.php +++ /dev/null @@ -1,41 +0,0 @@ - $aliased_classes - * - */ - public function toNamespacedString( - ?string $namespace, - array $aliased_classes, - ?string $this_class, - bool $use_phpdoc_format - ): string { - return $use_phpdoc_format ? 'int' : 'non-positive-int'; - } -} From 0c3a62bc4882dab8e057c58c9f720db891fd085b Mon Sep 17 00:00:00 2001 From: William Owen Ponce <31012084+hamburnyog@users.noreply.github.com> Date: Wed, 5 Oct 2022 14:29:24 +0800 Subject: [PATCH 148/194] Update args --- src/Psalm/Type/Atomic.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 7e5a258ea61..ff0868a23b5 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -231,13 +231,13 @@ public static function create( return new TPositiveInt(); case 'non-positive-int': - return new TIntRange(null, -1); + return new TIntRange(null, 0); case 'negative-int': return new TIntRange(null, -1); case 'non-negative-int': - return new TPositiveInt(); + return new TIntRange(0, null); case 'numeric': return $php_version !== null ? new TNamedObject($value) : new TNumeric(); From f1d1721fa7c3063a4a8bfd2ea1cfa3b8b758fe32 Mon Sep 17 00:00:00 2001 From: Peter de Blieck Date: Wed, 5 Oct 2022 14:36:33 +0200 Subject: [PATCH 149/194] Fixed function signatures of imap_delete and imap_undelete --- dictionaries/CallMap.php | 4 ++-- dictionaries/CallMap_81_delta.php | 4 ++-- dictionaries/CallMap_historical.php | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index df1739c6609..0f0f20cc941 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -6017,7 +6017,7 @@ 'imap_close' => ['bool', 'imap'=>'IMAP\Connection', 'flags='=>'int'], 'imap_create' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_createmailbox' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], -'imap_delete' => ['bool', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], +'imap_delete' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], 'imap_deletemailbox' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_errors' => ['array|false'], 'imap_expunge' => ['bool', 'imap'=>'IMAP\Connection'], @@ -6074,7 +6074,7 @@ 'imap_thread' => ['array|false', 'imap'=>'IMAP\Connection', 'flags='=>'int'], 'imap_timeout' => ['int|bool', 'timeout_type'=>'int', 'timeout='=>'int'], 'imap_uid' => ['int|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int'], -'imap_undelete' => ['bool', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], +'imap_undelete' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], 'imap_unsubscribe' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], 'imap_utf7_decode' => ['string|false', 'string'=>'string'], 'imap_utf7_encode' => ['string', 'string'=>'string'], diff --git a/dictionaries/CallMap_81_delta.php b/dictionaries/CallMap_81_delta.php index 622c86614d0..2a1dffdc194 100644 --- a/dictionaries/CallMap_81_delta.php +++ b/dictionaries/CallMap_81_delta.php @@ -270,8 +270,8 @@ 'new' => ['bool', 'imap'=>'IMAP\Connection', 'mailbox'=>'string'], ], 'imap_delete' => [ - 'old' => ['bool', 'imap'=>'resource', 'message_num'=>'int', 'flags='=>'int'], - 'new' => ['bool', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], + 'old' => ['bool', 'imap'=>'resource', 'message_nums'=>'string', 'flags='=>'int'], + 'new' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], ], 'imap_deletemailbox' => [ 'old' => ['bool', 'imap'=>'resource', 'mailbox'=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 0fc9a97fbd0..d67a99b4e82 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -12228,7 +12228,7 @@ 'imap_close' => ['bool', 'imap'=>'resource', 'flags='=>'int'], 'imap_create' => ['bool', 'imap'=>'resource', 'mailbox'=>'string'], 'imap_createmailbox' => ['bool', 'imap'=>'resource', 'mailbox'=>'string'], - 'imap_delete' => ['bool', 'imap'=>'resource', 'message_num'=>'int', 'flags='=>'int'], + 'imap_delete' => ['bool', 'imap'=>'resource', 'message_nums'=>'string', 'flags='=>'int'], 'imap_deletemailbox' => ['bool', 'imap'=>'resource', 'mailbox'=>'string'], 'imap_errors' => ['array|false'], 'imap_expunge' => ['bool', 'imap'=>'resource'], @@ -12285,7 +12285,7 @@ 'imap_thread' => ['array|false', 'imap'=>'resource', 'flags='=>'int'], 'imap_timeout' => ['int|bool', 'timeout_type'=>'int', 'timeout='=>'int'], 'imap_uid' => ['int|false', 'imap'=>'resource', 'message_num'=>'int'], - 'imap_undelete' => ['bool', 'imap'=>'resource', 'message_num'=>'int', 'flags='=>'int'], + 'imap_undelete' => ['bool', 'imap'=>'resource', 'message_nums'=>'string', 'flags='=>'int'], 'imap_unsubscribe' => ['bool', 'imap'=>'resource', 'mailbox'=>'string'], 'imap_utf7_decode' => ['string|false', 'string'=>'string'], 'imap_utf7_encode' => ['string', 'string'=>'string'], From ea5c2a1302b177595221600164eef31c05b63ed7 Mon Sep 17 00:00:00 2001 From: Peter de Blieck Date: Thu, 6 Oct 2022 08:52:36 +0200 Subject: [PATCH 150/194] Changed signature of imap_undelete in 8.1 delta --- dictionaries/CallMap_81_delta.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dictionaries/CallMap_81_delta.php b/dictionaries/CallMap_81_delta.php index 2a1dffdc194..d86cc48cf0e 100644 --- a/dictionaries/CallMap_81_delta.php +++ b/dictionaries/CallMap_81_delta.php @@ -450,8 +450,8 @@ 'new' => ['int|false', 'imap'=>'IMAP\Connection', 'message_num'=>'int'], ], 'imap_undelete' => [ - 'old' => ['bool', 'imap'=>'resource', 'message_num'=>'int', 'flags='=>'int'], - 'new' => ['bool', 'imap'=>'IMAP\Connection', 'message_num'=>'int', 'flags='=>'int'], + 'old' => ['bool', 'imap'=>'resource', 'message_nums'=>'string', 'flags='=>'int'], + 'new' => ['bool', 'imap'=>'IMAP\Connection', 'message_nums'=>'string', 'flags='=>'int'], ], 'imap_unsubscribe' => [ 'old' => ['bool', 'imap'=>'resource', 'mailbox'=>'string'], From 15453d4306b2be2202086b37905c46164846f5d1 Mon Sep 17 00:00:00 2001 From: Peter de Blieck Date: Thu, 6 Oct 2022 14:44:49 +0200 Subject: [PATCH 151/194] Removed imap_delete and imap_undelete from the ignoredFunctions list. --- tests/Internal/Codebase/InternalCallMapHandlerTest.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index 0821cd9cae5..1fcac4e865b 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -119,11 +119,9 @@ class InternalCallMapHandlerTest extends TestCase 'imagettfbbox', 'imagettftext', 'imagexbm', - 'imap_delete', 'imap_open', 'imap_rfc822_write_address', 'imap_sort', - 'imap_undelete', 'inflate_add', 'inflate_get_read_len', 'inflate_get_status', From 41a6afda32b7f3bbedf7afce07e85801fdead603 Mon Sep 17 00:00:00 2001 From: Gregory Hargreaves Date: Fri, 7 Oct 2022 09:44:10 +0100 Subject: [PATCH 152/194] Add check for const with reserved word class --- src/Psalm/Internal/Analyzer/ClassAnalyzer.php | 10 ++++++++++ tests/ClassTest.php | 12 ++++++++++++ 2 files changed, 22 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index f55fbaaa90d..eca4f0f498b 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -508,6 +508,16 @@ public function analyze( $member_stmts[] = $stmt; foreach ($stmt->consts as $const) { + if ($const->name->toLowerString() === 'class') { + IssueBuffer::maybeAdd( + new ReservedWord( + 'A class constant cannot be named \'class\'', + new CodeLocation($this, $this->class), + $this->fq_class_name + ) + ); + } + $const_id = strtolower($this->fq_class_name) . '::' . $const->name; foreach ($codebase->class_constants_to_rename as $original_const_id => $new_const_name) { diff --git a/tests/ClassTest.php b/tests/ClassTest.php index 342fe78b0a3..55a82d3f76c 100644 --- a/tests/ClassTest.php +++ b/tests/ClassTest.php @@ -925,6 +925,18 @@ final class C implements I {} ', 'error_message' => 'InvalidTraversableImplementation', ], + 'cannotNameClassConstantClass' => [ + ' */ + protected const CLASS = Bar::class; + } + + class Bar {} + ', + 'error_message' => 'ReservedWord', + ] ]; } } From f06c459415ee48893ca35e4e534130855497f16e Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Fri, 7 Oct 2022 13:18:57 +0200 Subject: [PATCH 153/194] Bugfix --- src/Psalm/Internal/Type/TypeExpander.php | 12 ++--- .../FunctionClassStringTemplateTest.php | 49 +++++++++++++++++++ 2 files changed, 54 insertions(+), 7 deletions(-) diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index f1e2a6c4232..dbd73bca632 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -214,13 +214,11 @@ public static function expandAtomic( ); if ($new_as_type instanceof TNamedObject && $new_as_type !== $return_type->as_type) { - $return_type = new TClassString( - $new_as_type->value, - $new_as_type, - $return_type->is_loaded, - $return_type->is_interface, - $return_type->is_enum, - ); + $return_type = clone $return_type; + /** @psalm-suppress InaccessibleProperty Acting on clone */ + $return_type->as_type = $new_as_type; + /** @psalm-suppress InaccessibleProperty Acting on clone */ + $return_type->as = $new_as_type->value; } } elseif ($return_type instanceof TTemplateParam) { $new_as_type = self::expandUnion( diff --git a/tests/Template/FunctionClassStringTemplateTest.php b/tests/Template/FunctionClassStringTemplateTest.php index 223a291e794..34edd8fc5df 100644 --- a/tests/Template/FunctionClassStringTemplateTest.php +++ b/tests/Template/FunctionClassStringTemplateTest.php @@ -727,6 +727,55 @@ function bar(string $_fooClass): void {} bar(Bar::class); ', ], + 'classStringNestedTemplate' => [ + 'code' => ' + */ + final class AObject extends MyObject {} + /** + * @extends MyMapper + */ + final class AMapper extends MyMapper {} + + + /** + * @extends MyObject + */ + final class BObject extends MyObject {} + /** + * @extends MyMapper + */ + final class BMapper extends MyMapper {} + + + /** + * Get source, asserting class type + * + * @template T as MyObject + * + * @param class-string $class + * @param AObject|BObject $source + * + * @return T + */ + function getSourceAssertType(string $class, MyObject $source): MyObject { + if (!$source instanceof $class) { + throw new RuntimeException("Invalid class!"); + } + return $source; + }' + ] ]; } From dec8d0edc2e94913a7abaee5e25aac29e9637dd3 Mon Sep 17 00:00:00 2001 From: Alies Lapatsin Date: Sun, 9 Oct 2022 17:49:17 +0400 Subject: [PATCH 154/194] Mark hash functions as non-false See https://github.com/php/php-src/issues/7759 and PR https://github.com/phpstan/phpstan-src/pull/822/files# --- dictionaries/CallMap_80_delta.php | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index 4f659c915f3..3e2a9b17e5a 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -381,6 +381,14 @@ 'old' => ['string|false', 'format'=>'string', 'timestamp='=>'int'], 'new' => ['string|false', 'format'=>'string', 'timestamp='=>'?int'], ], + 'hash' => [ + 'old' => ['string|false', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool'], + 'new' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool', 'options='=>'array'], + ], + 'hash_hmac' => [ + 'old' => ['string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], + 'new' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], + ], 'hash_init' => [ 'old' => ['HashContext|false', 'algo'=>'string', 'flags='=>'int', 'key='=>'string'], 'new' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string'], From 3c3839b5cb81ceb5601da0cd1b9461502b428901 Mon Sep 17 00:00:00 2001 From: Alies Lapatsin Date: Sun, 9 Oct 2022 18:01:27 +0400 Subject: [PATCH 155/194] Update hash(), hash_file() and hash_init() types see https://github.com/php/php-src/blob/php-8.1.5/ext/hash/hash.stub.php --- dictionaries/CallMap_81_delta.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dictionaries/CallMap_81_delta.php b/dictionaries/CallMap_81_delta.php index d86cc48cf0e..70aa34add06 100644 --- a/dictionaries/CallMap_81_delta.php +++ b/dictionaries/CallMap_81_delta.php @@ -222,16 +222,16 @@ 'new' => ['mixed|false', 'ftp' => 'FTP\Connection', 'option' => 'int'], ], 'hash' => [ - 'old' => ['string|false', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool'], - 'new' => ['string|false', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool', 'options='=>'array'], + 'old' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool'], + 'new' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool', 'options='=>'array{seed:scalar}'], ], 'hash_file' => [ 'old' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool'], - 'new' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool', 'options='=>'array'], + 'new' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool', 'options='=>'array{seed:scalar}'], ], 'hash_init' => [ 'old' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string'], - 'new' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string', 'options='=>'array'], + 'new' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string', 'options='=>'array{seed:scalar}'], ], 'imageinterlace' => [ 'old' => ['int|bool', 'image'=>'GdImage', 'enable='=>'bool|null'], From ee86d6360c31d3c4cde4031151cd7755acf77047 Mon Sep 17 00:00:00 2001 From: Alies Lapatsin Date: Sun, 9 Oct 2022 18:05:35 +0400 Subject: [PATCH 156/194] Remove extra changes --- dictionaries/CallMap_80_delta.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index 3e2a9b17e5a..aa5084ff451 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -383,7 +383,7 @@ ], 'hash' => [ 'old' => ['string|false', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool'], - 'new' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool', 'options='=>'array'], + 'new' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool'], ], 'hash_hmac' => [ 'old' => ['string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], From 198a4ba9b0f8001a67e26763c2563f02f66effcc Mon Sep 17 00:00:00 2001 From: Alies Lapatsin Date: Sun, 9 Oct 2022 17:31:14 +0300 Subject: [PATCH 157/194] Return non-empty-string by hash functions --- dictionaries/CallMap.php | 16 ++++++++-------- dictionaries/CallMap_71_delta.php | 2 +- dictionaries/CallMap_72_delta.php | 4 ++-- dictionaries/CallMap_80_delta.php | 2 +- dictionaries/CallMap_81_delta.php | 4 ++-- dictionaries/CallMap_historical.php | 10 +++++----- 6 files changed, 19 insertions(+), 19 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 0f0f20cc941..5284fd27ea7 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -4346,18 +4346,18 @@ 'HaruPage::stroke' => ['bool', 'close_path='=>'bool'], 'HaruPage::textOut' => ['bool', 'x'=>'float', 'y'=>'float', 'text'=>'string'], 'HaruPage::textRect' => ['bool', 'left'=>'float', 'top'=>'float', 'right'=>'float', 'bottom'=>'float', 'text'=>'string', 'align='=>'int'], -'hash' => ['string|false', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool', 'options='=>'array'], +'hash' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool', 'options='=>'array{seed:scalar}'], 'hash_algos' => ['list'], 'hash_copy' => ['HashContext', 'context'=>'HashContext'], 'hash_equals' => ['bool', 'known_string'=>'string', 'user_string'=>'string'], -'hash_file' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool', 'options='=>'array'], -'hash_final' => ['string', 'context'=>'HashContext', 'binary='=>'bool'], -'hash_hkdf' => ['string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], -'hash_hmac' => ['string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], +'hash_file' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool', 'options='=>'array{seed:scalar}'], +'hash_final' => ['non-empty-string', 'context'=>'HashContext', 'binary='=>'bool'], +'hash_hkdf' => ['non-empty-string', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], +'hash_hmac' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], 'hash_hmac_algos' => ['list'], -'hash_hmac_file' => ['string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], -'hash_init' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string', 'options='=>'array'], -'hash_pbkdf2' => ['string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'], +'hash_hmac_file' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], +'hash_init' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string', 'options='=>'array{seed:scalar}'], +'hash_pbkdf2' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'], 'hash_update' => ['bool', 'context'=>'HashContext', 'data'=>'string'], 'hash_update_file' => ['bool', 'context'=>'HashContext', 'filename'=>'string', 'stream_context='=>'?resource'], 'hash_update_stream' => ['int', 'context'=>'HashContext', 'stream'=>'resource', 'length='=>'int'], diff --git a/dictionaries/CallMap_71_delta.php b/dictionaries/CallMap_71_delta.php index 89451449402..2630ce958c1 100644 --- a/dictionaries/CallMap_71_delta.php +++ b/dictionaries/CallMap_71_delta.php @@ -21,7 +21,7 @@ 'curl_share_errno' => ['int|false', 'sh'=>'resource'], 'curl_share_strerror' => ['?string', 'error_code'=>'int'], 'getenv\'1' => ['array'], - 'hash_hkdf' => ['string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'hash_hkdf' => ['non-empty-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], 'is_iterable' => ['bool', 'value'=>'mixed'], 'openssl_get_curve_names' => ['list'], 'pcntl_async_signals' => ['bool', 'enable='=>'bool'], diff --git a/dictionaries/CallMap_72_delta.php b/dictionaries/CallMap_72_delta.php index fe0b3b249ed..9a7b5996ae7 100644 --- a/dictionaries/CallMap_72_delta.php +++ b/dictionaries/CallMap_72_delta.php @@ -141,8 +141,8 @@ 'new' => ['HashContext', 'context'=>'HashContext'], ], 'hash_final' => [ - 'old' => ['string', 'context'=>'resource', 'raw_output='=>'bool'], - 'new' => ['string', 'context'=>'HashContext', 'binary='=>'bool'], + 'old' => ['non-empty-string', 'context'=>'resource', 'raw_output='=>'bool'], + 'new' => ['non-empty-string', 'context'=>'HashContext', 'binary='=>'bool'], ], 'hash_init' => [ 'old' => ['resource', 'algo'=>'string', 'options='=>'int', 'key='=>'string'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index aa5084ff451..06308d48d2b 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -386,7 +386,7 @@ 'new' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool'], ], 'hash_hmac' => [ - 'old' => ['string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], + 'old' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], 'new' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], ], 'hash_init' => [ diff --git a/dictionaries/CallMap_81_delta.php b/dictionaries/CallMap_81_delta.php index 70aa34add06..5da78307225 100644 --- a/dictionaries/CallMap_81_delta.php +++ b/dictionaries/CallMap_81_delta.php @@ -226,8 +226,8 @@ 'new' => ['non-empty-string', 'algo'=>'string', 'data'=>'string', 'binary='=>'bool', 'options='=>'array{seed:scalar}'], ], 'hash_file' => [ - 'old' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool'], - 'new' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool', 'options='=>'array{seed:scalar}'], + 'old' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool'], + 'new' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool', 'options='=>'array{seed:scalar}'], ], 'hash_init' => [ 'old' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index d67a99b4e82..21df4530590 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -11369,12 +11369,12 @@ 'hash_algos' => ['list'], 'hash_copy' => ['resource', 'context'=>'resource'], 'hash_equals' => ['bool', 'known_string'=>'string', 'user_string'=>'string'], - 'hash_file' => ['string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool'], - 'hash_final' => ['string', 'context'=>'resource', 'raw_output='=>'bool'], - 'hash_hmac' => ['string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], - 'hash_hmac_file' => ['string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], + 'hash_file' => ['non-empty-string|false', 'algo'=>'string', 'filename'=>'string', 'binary='=>'bool'], + 'hash_final' => ['non-empty-string', 'context'=>'resource', 'raw_output='=>'bool'], + 'hash_hmac' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], + 'hash_hmac_file' => ['non-empty-string|false', 'algo'=>'string', 'data'=>'string', 'key'=>'string', 'binary='=>'bool'], 'hash_init' => ['resource', 'algo'=>'string', 'options='=>'int', 'key='=>'string'], - 'hash_pbkdf2' => ['string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'], + 'hash_pbkdf2' => ['non-empty-string', 'algo'=>'string', 'password'=>'string', 'salt'=>'string', 'iterations'=>'int', 'length='=>'int', 'binary='=>'bool'], 'hash_update' => ['bool', 'context'=>'resource', 'data'=>'string'], 'hash_update_file' => ['bool', 'hcontext'=>'resource', 'filename'=>'string', 'scontext='=>'resource'], 'hash_update_stream' => ['int', 'context'=>'resource', 'handle'=>'resource', 'length='=>'int'], From 30dc46528deaeb1ec5b66d2f06ed836422e7709b Mon Sep 17 00:00:00 2001 From: Alies Lapatsin Date: Sun, 9 Oct 2022 17:40:36 +0300 Subject: [PATCH 158/194] Cleaup $ignoredFunctions --- tests/Internal/Codebase/InternalCallMapHandlerTest.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Internal/Codebase/InternalCallMapHandlerTest.php b/tests/Internal/Codebase/InternalCallMapHandlerTest.php index 1fcac4e865b..16eb66e9a3a 100644 --- a/tests/Internal/Codebase/InternalCallMapHandlerTest.php +++ b/tests/Internal/Codebase/InternalCallMapHandlerTest.php @@ -371,9 +371,6 @@ class InternalCallMapHandlerTest extends TestCase 'gzeof', 'gzopen', 'gzpassthru', - 'hash', - 'hash_hkdf', - 'hash_hmac', 'iconv_get_encoding', 'igbinary_serialize', 'imagecolorclosest', From ea5f4cb5691cb0fad1917a9fd5ed0a8d6f5606d6 Mon Sep 17 00:00:00 2001 From: Alies Lapatsin Date: Sun, 9 Oct 2022 17:40:52 +0300 Subject: [PATCH 159/194] Add PHP 8.0 delta for hash_hkdf() --- dictionaries/CallMap_80_delta.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index 06308d48d2b..a16d4967141 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -393,6 +393,10 @@ 'old' => ['HashContext|false', 'algo'=>'string', 'flags='=>'int', 'key='=>'string'], 'new' => ['HashContext', 'algo'=>'string', 'flags='=>'int', 'key='=>'string'], ], + 'hash_hkdf' => [ + 'old' => ['non-empty-string|false', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + 'new' => ['non-empty-string', 'algo'=>'string', 'key'=>'string', 'length='=>'int', 'info='=>'string', 'salt='=>'string'], + ], 'hash_update_file' => [ 'old' => ['bool', 'context'=>'HashContext', 'filename'=>'string', 'stream_context='=>'resource'], 'new' => ['bool', 'context'=>'HashContext', 'filename'=>'string', 'stream_context='=>'?resource'], From dfa82366d723a1b5d42f52e9a8f8f96bc6409473 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 9 Oct 2022 16:44:34 +0200 Subject: [PATCH 160/194] add false return type to additional phpredis functions address https://github.com/phpredis/phpredis/pull/2120#issuecomment-1166644919 - weedwacker method, as I don't have time to check it all one by one --- stubs/phpredis.phpstub | 102 ++++++++++++++++++++--------------------- 1 file changed, 51 insertions(+), 51 deletions(-) diff --git a/stubs/phpredis.phpstub b/stubs/phpredis.phpstub index 0687320eff0..8831306420f 100644 --- a/stubs/phpredis.phpstub +++ b/stubs/phpredis.phpstub @@ -31,7 +31,7 @@ class Redis { */ public function acl(string $subcmd, ...$args); - /** @return int|Redis */ + /** @return false|int|Redis */ public function append(string $key, mixed $value); public function auth(mixed $credentials): bool; @@ -40,15 +40,15 @@ class Redis { public function bgrewriteaof(): bool; - /** @return int|Redis */ + /** @return false|int|Redis */ public function bitcount(string $key, int $start = 0, int $end = -1); /** - * @return int|Redis + * @return false|int|Redis */ public function bitop(string $operation, string $deskey, string $srckey, string ...$other_keys): int; - /** @return int|Redis */ + /** @return false|int|Redis */ public function bitpos(string $key, int $bit, int $start = 0, int $end = -1); public function blPop(string|array $key, string|int $timeout_or_key, mixed ...$extra_args): array|null|false; @@ -57,9 +57,9 @@ class Redis { public function brpoplpush(string $src, string $dst, int $timeout): Redis|string|false; - public function bzPopMax(string|array $key, string|int $timeout_or_key, mixed ...$extra_args): array; + public function bzPopMax(string|array $key, string|int $timeout_or_key, mixed ...$extra_args): array|false; - public function bzPopMin(string|array $key, string|int $timeout_or_key, mixed ...$extra_args): array; + public function bzPopMin(string|array $key, string|int $timeout_or_key, mixed ...$extra_args): array|false; public function clearLastError(): bool; @@ -79,21 +79,21 @@ class Redis { public function debug(string $key): string; - /** @return int|Redis */ + /** @return false|int|Redis */ public function decr(string $key, int $by = 1); - /** @return int|Redis */ + /** @return false|int|Redis */ public function decrBy(string $key, int $value); /** - * @return int|Redis + * @return false|int|Redis */ public function del(array|string $key, string ...$other_keys); /** * @deprecated * @alias Redis::del - * @return int|Redis + * @return false|int|Redis */ public function delete(array|string $key, string ...$other_keys); @@ -101,7 +101,7 @@ class Redis { public function dump(string $key): string; - /** @return string|Redis */ + /** @return false|string|Redis */ public function echo(string $str); public function eval(string $script, array $keys = null, int $num_keys = 0): mixed; @@ -125,7 +125,7 @@ class Redis { public function geodist(string $key, string $src, string $dst, ?string $unit = null): Redis|float|false; - public function geohash(string $key, string $member, string ...$other_members): array; + public function geohash(string $key, string $member, string ...$other_members): array|false; public function geopos(string $key, string $member, string ...$other_members): Redis|array|false; @@ -137,16 +137,16 @@ class Redis { public function georadiusbymember_ro(string $key, string $member, float $radius, string $unit, array $options = []): Redis|mixed|false; - public function geosearch(string $key, array|string $position, array|int|float $shape, string $unit, array $options = []): array; + public function geosearch(string $key, array|string $position, array|int|float $shape, string $unit, array $options = []): array|false; - public function geosearchstore(string $dst, string $src, array|string $position, array|int|float $shape, string $unit, array $options = []): array; + public function geosearchstore(string $dst, string $src, array|string $position, array|int|float $shape, string $unit, array $options = []): array|false; /** @return false|string|Redis */ public function get(string $key); public function getAuth(): mixed; - /** @return int|Redis */ + /** @return false|int|Redis */ public function getBit(string $key, int $idx); public function getDBNum(): int; @@ -163,12 +163,12 @@ class Redis { public function getPort(): int; - /** @return string|Redis */ + /** @return false|string|Redis */ public function getRange(string $key, int $start, int $end); public function getReadTimeout(): int; - /** @return string|Redis */ + /** @return false|string|Redis */ public function getset(string $key, mixed $value); public function getTimeout(): int; @@ -203,25 +203,25 @@ class Redis { public function hscan(string $key, ?int &$iterator, ?string $pattern = null, int $count = 0): bool|array; - /** @return int|Redis */ + /** @return false|int|Redis */ public function incr(string $key, int $by = 1); - /** @return int|Redis */ + /** @return false|int|Redis */ public function incrBy(string $key, int $value); - /** @return int|Redis */ + /** @return false|int|Redis */ public function incrByFloat(string $key, float $value); public function info(string $opt = null): Redis|array|false; public function isConnected(): bool; - /** @return array|Redis */ + /** @return false|array|Redis */ public function keys(string $pattern); /** * @param mixed $elements - * @return int|Redis + * @return false|int|Redis */ public function lInsert(string $key, string $pos, mixed $pivot, mixed $value); @@ -230,25 +230,25 @@ class Redis { public function lMove(string $src, string $dst, string $wherefrom, string $whereto): string; - /** @return string|Redis */ + /** @return false|string|Redis */ public function lPop(string $key); /** * @param mixed $elements - * @return int|Redis + * @return false|int|Redis */ public function lPush(string $key, ...$elements); /** * @param mixed $elements - * @return int|Redis + * @return false|int|Redis */ public function rPush(string $key, ...$elements); - /** @return int|Redis */ + /** @return false|int|Redis */ public function lPushx(string $key, mixed $value); - /** @return int|Redis */ + /** @return false|int|Redis */ public function rPushx(string $key, mixed $value); public function lSet(string $key, int $index, mixed $value): Redis|bool; @@ -266,7 +266,7 @@ class Redis { public function ltrim(string $key, int $start , int $end): Redis|bool; - /** @return array|Redis */ + /** @return false|array|Redis */ public function mget(array $keys); public function migrate(string $host, int $port, string $key, string $dst, int $timeout, bool $copy = false, bool $replace = false): bool; @@ -301,7 +301,7 @@ public function persist(string $key): bool; public function pfmerge(string $dst, array $keys): bool; - /** @return string|Redis */ + /** @return false|string|Redis */ public function ping(string $key = NULL); public function pipeline(): bool|Redis; @@ -323,12 +323,12 @@ public function persist(string $key): bool; public function pubsub(string $command, mixed $arg = null): mixed; - public function punsubscribe(array $patterns): array; + public function punsubscribe(array $patterns): array|false; - /** @return string|Redis */ + /** @return false|string|Redis */ public function rPop(string $key); - /** @return string|Redis */ + /** @return false|string|Redis */ public function randomKey(); public function rawcommand(string $command, mixed ...$args): mixed; @@ -359,7 +359,7 @@ public function persist(string $key): bool; public function sMembers(string $key): Redis|array|false; - public function sMisMember(string $key, string $member, string ...$other_members): array; + public function sMisMember(string $key, string $member, string ...$other_members): array|false; public function sMove(string $src, string $dst, mixed $value): Redis|bool; @@ -384,10 +384,10 @@ public function persist(string $key): bool; /** @return bool|Redis */ public function set(string $key, mixed $value, mixed $opt = NULL); - /** @return int|Redis */ + /** @return false|int|Redis */ public function setBit(string $key, int $idx, bool $value); - /** @return int|Redis */ + /** @return false|int|Redis */ public function setRange(string $key, int $start, string $value); @@ -431,26 +431,26 @@ public function persist(string $key): bool; public function sscan(string $key, int &$iterator, ?string $pattern = null, int $count = 0): array|false; - /** @return int|Redis */ + /** @return false|int|Redis */ public function strlen(string $key); - public function subscribe(string $channel, string ...$other_channels): array; + public function subscribe(string $channel, string ...$other_channels): array|false; public function swapdb(string $src, string $dst): bool; - public function time(): array; + public function time(): array|false; public function ttl(string $key): Redis|int|false; - /** @return int|Redis */ + /** @return false|int|Redis */ public function type(string $key); /** - * @return int|Redis + * @return false|int|Redis */ public function unlink(array|string $key, string ...$other_keys); - public function unsubscribe(string $channel, string ...$other_channels): array; + public function unsubscribe(string $channel, string ...$other_channels): array|false; /** @return bool|Redis */ public function unwatch(); @@ -466,7 +466,7 @@ public function persist(string $key): bool; public function xadd(string $key, string $id, array $values, int $maxlen = 0, bool $approx = false): string|false; - public function xclaim(string $key, string $group, string $consumer, int $min_iddle, array $ids, array $options): string|array; + public function xclaim(string $key, string $group, string $consumer, int $min_iddle, array $ids, array $options): string|array|false; public function xdel(string $key, array $ids): Redis|int|false; @@ -498,25 +498,25 @@ public function persist(string $key): bool; public function zLexCount(string $key, string $min, string $max): Redis|int|false; - public function zMscore(string $key, string $member, string ...$other_members): array; + public function zMscore(string $key, string $member, string ...$other_members): array|false; - public function zPopMax(string $key, int $value = null): array; + public function zPopMax(string $key, int $value = null): array|false; - public function zPopMin(string $key, int $value = null): array; + public function zPopMin(string $key, int $value = null): array|false; public function zRange(string $key, int $start, int $end, mixed $scores = null): Redis|array|false; - public function zRangeByLex(string $key, string $min, string $max, int $offset = -1, int $count = -1): array; + public function zRangeByLex(string $key, string $min, string $max, int $offset = -1, int $count = -1): array|false; public function zRangeByScore(string $key, string $start, string $end, array $options = []): Redis|array|false; - public function zRandMember(string $key, array $options = null): string|array; + public function zRandMember(string $key, array $options = null): string|array|false; public function zRank(string $key, mixed $member): Redis|int|false; public function zRem(mixed $key, mixed $member, mixed ...$other_members): Redis|int|false; - public function zRemRangeByLex(string $key, string $min, string $max): int; + public function zRemRangeByLex(string $key, string $min, string $max): int|false; public function zRemRangeByRank(string $key, int $start, int $end): Redis|int|false; @@ -524,15 +524,15 @@ public function persist(string $key): bool; public function zRevRange(string $key, int $start, int $end, mixed $scores = null): Redis|array|false; - public function zRevRangeByLex(string $key, string $min, string $max, int $offset = -1, int $count = -1): array; + public function zRevRangeByLex(string $key, string $min, string $max, int $offset = -1, int $count = -1): array|false; - public function zRevRangeByScore(string $key, string $start, string $end, array $options = []): array; + public function zRevRangeByScore(string $key, string $start, string $end, array $options = []): array|false; public function zRevRank(string $key, mixed $member): Redis|int|false; public function zScore(string $key, mixed $member): Redis|float|false; - public function zdiff(array $keys, array $options = null): array; + public function zdiff(array $keys, array $options = null): array|false; public function zdiffstore(string $dst, array $keys, array $options = null): int; From 47317205c19039cc8c8bb653e1646c7bffacc717 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 9 Oct 2022 16:51:26 +0200 Subject: [PATCH 161/194] small improvement for return type of mGet --- stubs/phpredis.phpstub | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/stubs/phpredis.phpstub b/stubs/phpredis.phpstub index 8831306420f..93c65ab4611 100644 --- a/stubs/phpredis.phpstub +++ b/stubs/phpredis.phpstub @@ -266,7 +266,7 @@ class Redis { public function ltrim(string $key, int $start , int $end): Redis|bool; - /** @return false|array|Redis */ + /** @return false|list|Redis */ public function mget(array $keys); public function migrate(string $host, int $port, string $key, string $dst, int $timeout, bool $copy = false, bool $replace = false): bool; From 5bfc0f960be71093c4b4dc754aa94c6142b44bd9 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 9 Oct 2022 16:53:30 +0200 Subject: [PATCH 162/194] force $value to be string technically all stringable types work https://github.com/phpredis/phpredis/issues/1735#event-7529843256 however they're all cast to string implicitly, which unevitably leads to unexpected results (see riskyCast,...) --- stubs/phpredis.phpstub | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/stubs/phpredis.phpstub b/stubs/phpredis.phpstub index 93c65ab4611..147287dddfb 100644 --- a/stubs/phpredis.phpstub +++ b/stubs/phpredis.phpstub @@ -273,9 +273,15 @@ class Redis { public function move(string $key, int $index): bool; - public function mset(array $key_values): Redis|bool; + /** + * @param array + */ + public function mset($key_values): Redis|bool; - public function msetnx(array $key_values): Redis|bool; + /** + * @param array + */ + public function msetnx($key_values): Redis|bool; public function multi(int $value = Redis::MULTI): bool|Redis; @@ -382,7 +388,7 @@ public function persist(string $key): bool; public function select(int $db): bool; /** @return bool|Redis */ - public function set(string $key, mixed $value, mixed $opt = NULL); + public function set(string $key, string $value, mixed $opt = NULL); /** @return false|int|Redis */ public function setBit(string $key, int $idx, bool $value); @@ -394,10 +400,10 @@ public function persist(string $key): bool; public function setOption(int $option, mixed $value): bool; /** @return bool|Redis */ - public function setex(string $key, int $expire, mixed $value); + public function setex(string $key, int $expire, string $value); /** @return bool|array|Redis */ - public function setnx(string $key, mixed $value); + public function setnx(string $key, string $value); public function sismember(string $key, mixed $value): Redis|bool; From 88ba8452c4dcdfb83e8f63c4bd68e54d8e2f5846 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Sun, 9 Oct 2022 17:01:38 +0200 Subject: [PATCH 163/194] some more string values --- stubs/phpredis.phpstub | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/stubs/phpredis.phpstub b/stubs/phpredis.phpstub index 147287dddfb..0ad1780d22d 100644 --- a/stubs/phpredis.phpstub +++ b/stubs/phpredis.phpstub @@ -32,7 +32,7 @@ class Redis { public function acl(string $subcmd, ...$args); /** @return false|int|Redis */ - public function append(string $key, mixed $value); + public function append(string $key, string $value); public function auth(mixed $credentials): bool; @@ -169,7 +169,7 @@ class Redis { public function getReadTimeout(): int; /** @return false|string|Redis */ - public function getset(string $key, mixed $value); + public function getset(string $key, string $value); public function getTimeout(): int; @@ -193,7 +193,7 @@ class Redis { public function hMset(string $key, array $keyvals): Redis|bool|false; - public function hSet(string $key, string $member, mixed $value): Redis|int|false; + public function hSet(string $key, string $member, string $value): Redis|int|false; public function hSetNx(string $key, string $member, string $value): Redis|bool; @@ -246,12 +246,12 @@ class Redis { public function rPush(string $key, ...$elements); /** @return false|int|Redis */ - public function lPushx(string $key, mixed $value); + public function lPushx(string $key, string $value); /** @return false|int|Redis */ - public function rPushx(string $key, mixed $value); + public function rPushx(string $key, string $value); - public function lSet(string $key, int $index, mixed $value): Redis|bool; + public function lSet(string $key, int $index, string $value): Redis|bool; public function lastSave(): int; @@ -262,7 +262,7 @@ class Redis { /** * @return int|Redis|false */ - public function lrem(string $key, mixed $value, int $count = 0); + public function lrem(string $key, string $value, int $count = 0); public function ltrim(string $key, int $start , int $end): Redis|bool; @@ -319,7 +319,7 @@ public function persist(string $key): bool; public function popen(string $host, int $port = 6379, float $timeout = 0, string $persistent_id = NULL, int $retry_interval = 0, float $read_timeout = 0, array $context = NULL): bool; /** @return bool|Redis */ - public function psetex(string $key, int $expire, mixed $value); + public function psetex(string $key, int $expire, string $value); public function psubscribe(array $patterns): void; @@ -351,7 +351,7 @@ public function persist(string $key): bool; public function rpoplpush(string $src, string $dst): Redis|string|false; - public function sAdd(string $key, mixed $value, mixed ...$other_values): Redis|int|false; + public function sAdd(string $key, string $value, mixed ...$other_values): Redis|int|false; public function sAddArray(string $key, array $values): int; @@ -367,7 +367,7 @@ public function persist(string $key): bool; public function sMisMember(string $key, string $member, string ...$other_members): array|false; - public function sMove(string $src, string $dst, mixed $value): Redis|bool; + public function sMove(string $src, string $dst, string $value): Redis|bool; public function sPop(string $key, int $count = 0): Redis|string|array|false; @@ -405,7 +405,7 @@ public function persist(string $key): bool; /** @return bool|array|Redis */ public function setnx(string $key, string $value); - public function sismember(string $key, mixed $value): Redis|bool; + public function sismember(string $key, string $value): Redis|bool; public function slaveof(string $host = null, int $port = 6379): bool; @@ -433,7 +433,7 @@ public function persist(string $key): bool; */ public function sortDescAlpha(string $key, ?string $pattern = null, mixed $get = null, int $offset = -1, int $count = -1, ?string $store = null): array; - public function srem(string $key, mixed $value, mixed ...$other_values): Redis|int|false; + public function srem(string $key, string $value, mixed ...$other_values): Redis|int|false; public function sscan(string $key, int &$iterator, ?string $pattern = null, int $count = 0): array|false; From fe17720441245c004dd1f896e9deb943a3af632c Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Oct 2022 11:43:04 +0200 Subject: [PATCH 164/194] Revert --- src/Psalm/Internal/LanguageServer/Client/TextDocument.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php index 380b5e0d18e..c2b7f8cc704 100644 --- a/src/Psalm/Internal/LanguageServer/Client/TextDocument.php +++ b/src/Psalm/Internal/LanguageServer/Client/TextDocument.php @@ -57,6 +57,8 @@ public function publishDiagnostics(string $uri, array $diagnostics): void * @param TextDocumentIdentifier $textDocument The document to get the content for * * @return Promise The document's current content + * + * @psalm-suppress MixedReturnTypeCoercion due to Psalm bug */ public function xcontent(TextDocumentIdentifier $textDocument): Promise { From 2e8fd6fc1d3c3443262ceac09f9957953dd618cf Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Oct 2022 11:46:47 +0200 Subject: [PATCH 165/194] Add flag --- src/Psalm/Type.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 6d66ab19407..26c8a883e73 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -357,12 +357,12 @@ public static function getBool(bool $from_docblock = false): Union /** * @psalm-pure */ - public static function getFloat(?float $value = null): Union + public static function getFloat(?float $value = null, bool $from_docblock = false): Union { if ($value !== null) { - $type = new TLiteralFloat($value); + $type = new TLiteralFloat($value, $from_docblock); } else { - $type = new TFloat(); + $type = new TFloat($from_docblock); } return new Union([$type]); From 8a7b201b2cd86a57e3408a327b7e684d24f52ae1 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Oct 2022 12:38:57 +0200 Subject: [PATCH 166/194] Update --- .../InstancePropertyAssignmentAnalyzer.php | 2 ++ .../BinaryOp/ArithmeticOpAnalyzer.php | 2 ++ .../Expression/Fetch/ArrayFetchAnalyzer.php | 2 ++ .../Fetch/AtomicPropertyFetchAnalyzer.php | 2 ++ .../Analyzer/Statements/UnsetAnalyzer.php | 1 + .../Type/Comparator/CallableTypeComparator.php | 1 + .../Type/Comparator/GenericTypeComparator.php | 1 + .../Type/SimpleNegatedAssertionReconciler.php | 1 + .../Type/TemplateStandinTypeReplacer.php | 18 ++++++++++-------- src/Psalm/Internal/Type/TypeCombiner.php | 7 +++++++ src/Psalm/Internal/Type/TypeExpander.php | 2 ++ 11 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php index cbbf381a410..ea9bf51f9ff 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Assignment/InstancePropertyAssignmentAnalyzer.php @@ -875,6 +875,8 @@ private static function analyzeRegularAssignment( /** * @param list $invalid_assignment_types + * + * @psalm-suppress ComplexMethod Unavoidably complex method */ private static function analyzeAtomicAssignment( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index 0ce8d3c2d48..4e4668dd4ac 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -292,6 +292,8 @@ private static function getNumericalType($result): Union /** * @param string[] $invalid_left_messages * @param string[] $invalid_right_messages + * + * @psalm-suppress ComplexMethod Unavoidably complex method. */ private static function analyzeOperands( ?StatementsSource $statements_source, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index b7ab06bd180..5b45cdc1d74 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -1086,6 +1086,8 @@ public static function handleMixedArrayAccess( * @param TArray|TKeyedArray|TList|TClassStringMap $type * @param-out TArray|TKeyedArray|TList|TClassStringMap $type * @param list $key_values + * + * @psalm-suppress ConflictingReferenceConstraint Ignore */ private static function handleArrayAccessOnArray( bool $in_assignment, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index 6136d708155..ef24481d64e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -77,6 +77,8 @@ class AtomicPropertyFetchAnalyzer { /** * @param array $invalid_fetch_types $invalid_fetch_types + * + * @psalm-suppress ComplexMethod Unavoidably complex method. */ public static function analyze( StatementsAnalyzer $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php index f6f5bd73729..3916fcccbb5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/UnsetAnalyzer.php @@ -81,6 +81,7 @@ public static function analyze( unset($properties[$key_value]); } + /** @psalm-suppress DocblockTypeContradiction https://github.com/vimeo/psalm/issues/8518 */ if (!$properties) { if ($atomic_root_type->previous_value_type) { $root_types [] = diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 643c11d0e75..d67e7087d41 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -498,6 +498,7 @@ public static function getCallableMethodIdFromTKeyedArray( } if ($member_id) { + /** @psalm-suppress PossiblyNullArgument Psalm bug */ $codebase->analyzer->addMixedMemberName( strtolower($member_id) . '::', $calling_method_id ?: $file_name diff --git a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php index 4c563f7c0a9..c6de9ca06be 100644 --- a/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/GenericTypeComparator.php @@ -177,6 +177,7 @@ public static function isContainedBy( && $atomic_comparison_result->replacement_atomic_type instanceof TGenericObject && $atomic_comparison_result_type_params ) { + /** @psalm-suppress ArgumentTypeCoercion Psalm bug */ $atomic_comparison_result->replacement_atomic_type = $atomic_comparison_result->replacement_atomic_type ->replaceTypeParams($atomic_comparison_result_type_params); diff --git a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php index 439d887907c..e744f456ad1 100644 --- a/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleNegatedAssertionReconciler.php @@ -899,6 +899,7 @@ private static function reconcileFalsyOrEmpty( } } + /** @psalm-suppress RedundantCondition Psalm bug */ assert(!$existing_var_type->isUnionEmpty()); return $existing_var_type->freeze(); } diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 7f6dcd07dc0..a7d26df2d91 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -374,6 +374,7 @@ private static function handleAtomicStandin( return [$atomic_type]; } + /** @psalm-suppress ReferenceConstraintViolation Psalm bug */ $atomic_type = new TPropertiesOf( $classlike_type, $atomic_type->visibility_filter @@ -394,6 +395,7 @@ private static function handleAtomicStandin( } if (!$matching_atomic_types) { + /** @psalm-suppress ReferenceConstraintViolation Psalm bug */ $atomic_type = $atomic_type->replaceTemplateTypesWithStandins( $template_result, $codebase, @@ -864,15 +866,15 @@ private static function handleTemplateParamStandin( } } - foreach ($atomic_types as &$atomic_type) { - if ($atomic_type instanceof TNamedObject - || $atomic_type instanceof TTemplateParam - || $atomic_type instanceof TIterable - || $atomic_type instanceof TObjectWithProperties + foreach ($atomic_types as &$t) { + if ($t instanceof TNamedObject + || $t instanceof TTemplateParam + || $t instanceof TIterable + || $t instanceof TObjectWithProperties ) { - $atomic_type = $atomic_type->setIntersectionTypes($extra_types); - } elseif ($atomic_type instanceof TObject && $extra_types) { - $atomic_type = reset($extra_types)->setIntersectionTypes(array_slice($extra_types, 1)); + $t = $t->setIntersectionTypes($extra_types); + } elseif ($t instanceof TObject && $extra_types) { + $t = reset($extra_types)->setIntersectionTypes(array_slice($extra_types, 1)); } } diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 0aaac93c9e2..b0348134cdf 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -87,6 +87,8 @@ class TypeCombiner * * @psalm-external-mutation-free * + * @psalm-suppress ImpurePropertyAssignment We're not actually mutating any external instance + * * @param non-empty-list $types * @param int $literal_limit any greater number of literal types than this * will be merged to a scalar @@ -236,6 +238,7 @@ public static function combine( if ($generic_type === 'iterable') { $new_types[] = new TIterable($generic_type_params); } else { + /** @psalm-suppress ArgumentTypeCoercion Caused by the PropertyTypeCoercion above */ $generic_object = new TGenericObject( $generic_type, $generic_type_params, @@ -254,6 +257,7 @@ public static function combine( foreach ($combination->object_type_params as $generic_type => $generic_type_params) { $generic_type = substr($generic_type, 0, (int) strpos($generic_type, '<')); + /** @psalm-suppress ArgumentTypeCoercion Caused by the PropertyTypeCoercion above */ $generic_object = new TGenericObject( $generic_type, $generic_type_params, @@ -362,6 +366,9 @@ public static function combine( return $union_type; } + /** + * @psalm-suppress ComplexMethod Unavoidably complex method + */ private static function scrapeTypeProperties( Atomic $type, TypeCombination $combination, diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index dbd73bca632..01a1f5c93e0 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -182,6 +182,7 @@ public static function expandAtomic( } if ($return_type instanceof TNamedObject) { + /** @psalm-suppress ReferenceConstraintViolation */ $return_type = self::expandNamedObject( $codebase, $return_type, @@ -214,6 +215,7 @@ public static function expandAtomic( ); if ($new_as_type instanceof TNamedObject && $new_as_type !== $return_type->as_type) { + /** @psalm-suppress ReferenceConstraintViolation Psalm bug */ $return_type = clone $return_type; /** @psalm-suppress InaccessibleProperty Acting on clone */ $return_type->as_type = $new_as_type; From deaa6bd2ead0bfa0b78a42026415a5f36093636f Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Oct 2022 12:53:13 +0200 Subject: [PATCH 167/194] Add purity issues to baseline --- psalm-baseline.xml | 146 ++++++++++++++++-- .../Type/TemplateStandinTypeReplacer.php | 1 + src/Psalm/Internal/Type/TypeExpander.php | 5 +- src/Psalm/Type.php | 2 + src/Psalm/Type/Atomic.php | 1 + src/Psalm/Type/MutableUnion.php | 4 +- src/Psalm/Type/TypeVisitor.php | 2 + src/Psalm/Type/Union.php | 2 + src/Psalm/Type/UnionTrait.php | 3 + 9 files changed, 144 insertions(+), 22 deletions(-) diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 09f1f3a3587..61a039758b6 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + $comment_block->tags['variablesfrom'][0] @@ -122,9 +122,8 @@ - + $arg_function_params[$argument_offset][0] - $array_type->getGenericArrayType()->getChildNodes()[0] @@ -221,9 +220,16 @@ + + !$this->is_accepting_new_requests + $this->is_accepting_new_requests + $parts[1] + + TypeDoesNotContainType + @@ -256,10 +262,9 @@ - + $l[4] $r[4] - $var_line_parts[0] @@ -300,6 +305,17 @@ TCallable|TClosure|null + + + get + get + get + getClassTemplateTypes + + + $candidate_param_type->from_template_default + + $combination->array_type_params[1] @@ -323,28 +339,123 @@ + + classExtendsOrImplements + classExtendsOrImplements + classExtendsOrImplements + classOrInterfaceExists + classOrInterfaceExists + classOrInterfaceExists + getMappedGenericTypeParams + interfaceExtends + interfaceExtends + interfaceExtends + array_keys($template_type_map[$value])[0] + + + replace + replace + replace + replace + + + + getMappedGenericTypeParams + replace + replace + $this->type_params[1] - - replaceTypeParams - replaceTypeParams - replaceTypeParams - - - - replaceAs - + + + getMostSpecificTypeFromBounds + + + + + replace + + + + + getString + getString + replace + replace + + + $cloned->value_param + + + + + replace + + + + + combine + combine + combineUnionTypes + combineUnionTypes + combineUnionTypes + combineUnionTypes + combineUnionTypes + combineUnionTypes + replace + replace + + + $key_type->possibly_undefined + $value_type->possibly_undefined + $value_type->possibly_undefined + + + + + replace + replace + + + $cloned->type_param + + + + + replace + replace + + + $type->possibly_undefined + $type->possibly_undefined + + + + + replace + + + + + replace + + + + + replace + - + $allow_mutations + $by_ref $failed_reconciliation $from_template_default $has_mutations @@ -369,6 +480,11 @@ allFloatLiterals + + + UndefinedMethod + + $subNodes['expr'] diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index a7d26df2d91..7938e84516f 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -1201,6 +1201,7 @@ public static function getMostSpecificTypeFromBounds(array $lower_bounds, ?Codeb /** * @param TGenericObject|TNamedObject|TIterable $input_type_part * @param TGenericObject|TIterable $container_type_part + * @psalm-external-mutation-free * @return list */ public static function getMappedGenericTypeParams( diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 01a1f5c93e0..2d9ccd188b7 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -130,7 +130,7 @@ public static function expandUnion( * @param-out Atomic $return_type * @return non-empty-list * - * @psalm-suppress ConflictingReferenceConstraint Ultimately, the output type is always an Atomic + * @psalm-suppress ConflictingReferenceConstraint, ReferenceConstraintViolation Ultimately, the output type is always an Atomic * @psalm-suppress ComplexMethod */ public static function expandAtomic( @@ -182,7 +182,6 @@ public static function expandAtomic( } if ($return_type instanceof TNamedObject) { - /** @psalm-suppress ReferenceConstraintViolation */ $return_type = self::expandNamedObject( $codebase, $return_type, @@ -215,7 +214,6 @@ public static function expandAtomic( ); if ($new_as_type instanceof TNamedObject && $new_as_type !== $return_type->as_type) { - /** @psalm-suppress ReferenceConstraintViolation Psalm bug */ $return_type = clone $return_type; /** @psalm-suppress InaccessibleProperty Acting on clone */ $return_type->as_type = $new_as_type; @@ -497,7 +495,6 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } - /** @psalm-suppress ArgumentTypeCoercion */ $return_type = $return_type->replaceTypeParams($type_params); } elseif ($return_type instanceof TKeyedArray) { $properties = $return_type->properties; diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 26c8a883e73..1b19595ab34 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -510,6 +510,8 @@ public static function combineUnionTypeArray(array $union_types, ?Codebase $code * will be merged to a scalar * * @psalm-external-mutation-free + * + * @psalm-suppress ImpurePropertyAssignment We're not mutating external instances */ public static function combineUnionTypes( ?Union $type_1, diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 2a37fbeaed8..9c37d9baed6 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -133,6 +133,7 @@ public function setFromDocblock(bool $from_docblock): self public function replaceClassLike(string $old, string $new): self { $type = $this; + /** @psalm-suppress ImpureMethodCall ClasslikeReplacer will always clone */ (new ClasslikeReplacer( $old, $new diff --git a/src/Psalm/Type/MutableUnion.php b/src/Psalm/Type/MutableUnion.php index da26d538097..a187a3be98e 100644 --- a/src/Psalm/Type/MutableUnion.php +++ b/src/Psalm/Type/MutableUnion.php @@ -346,9 +346,6 @@ public function removeType(string $type_string): bool return false; } - /** - * @psalm-external-mutation-free - */ public function setFromDocblock(bool $fromDocblock = true): self { $this->from_docblock = $fromDocblock; @@ -495,6 +492,7 @@ public function freeze(): Union if ($key === 'literal_float_types') { continue; } + /** @psalm-suppress ImpurePropertyAssignment Acting on clone */ $union->{$key} = $value; } return $union; diff --git a/src/Psalm/Type/TypeVisitor.php b/src/Psalm/Type/TypeVisitor.php index 9feded61512..c4e98bcf517 100644 --- a/src/Psalm/Type/TypeVisitor.php +++ b/src/Psalm/Type/TypeVisitor.php @@ -22,6 +22,8 @@ abstract protected function enterNode(TypeNode &$type): ?int; * @param T $node * @param-out T $node * @return bool - true if we want to continue traversal, false otherwise + * + * @psalm-suppress ReferenceConstraintViolation, ConflictingReferenceConstraint */ public function traverse(TypeNode &$node): bool { diff --git a/src/Psalm/Type/Union.php b/src/Psalm/Type/Union.php index 451a6d86b8c..cdcfc5d9ed0 100644 --- a/src/Psalm/Type/Union.php +++ b/src/Psalm/Type/Union.php @@ -237,6 +237,7 @@ public function getBuilder(): MutableUnion if ($key === 'literal_float_types') { continue; } + /** @psalm-suppress ImpurePropertyAssignment Acting on clone */ $union->{$key} = $value; } return $union; @@ -248,6 +249,7 @@ public function getBuilder(): MutableUnion public function setFromDocblock(bool $fromDocblock = true): self { $cloned = clone $this; + /** @psalm-suppress ImpureMethodCall Acting on clone */ (new FromDocblockSetter($fromDocblock))->traverse($cloned); return $cloned; } diff --git a/src/Psalm/Type/UnionTrait.php b/src/Psalm/Type/UnionTrait.php index 7c0f834e7d0..ce7ff78965f 100644 --- a/src/Psalm/Type/UnionTrait.php +++ b/src/Psalm/Type/UnionTrait.php @@ -1244,6 +1244,7 @@ public function containsClassLike(string $fq_class_like_name): bool { $classlike_visitor = new ContainsClassLikeVisitor($fq_class_like_name); + /** @psalm-suppress ImpureMethodCall Actually mutation-free */ $classlike_visitor->traverseArray($this->types); return $classlike_visitor->matches(); @@ -1267,6 +1268,7 @@ public function containsAnyLiteral(): bool { $literal_visitor = new ContainsLiteralVisitor(); + /** @psalm-suppress ImpureMethodCall Actually mutation-free */ $literal_visitor->traverseArray($this->types); return $literal_visitor->matches(); @@ -1280,6 +1282,7 @@ public function getTemplateTypes(): array { $template_type_collector = new TemplateTypeCollector(); + /** @psalm-suppress ImpureMethodCall Actually mutation-free */ $template_type_collector->traverseArray($this->types); return $template_type_collector->getTemplateTypes(); From 26f9b2cd6ad2ac7d796550dadead659e084f7460 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Oct 2022 13:00:46 +0200 Subject: [PATCH 168/194] Add suppresses --- src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php | 4 ++-- src/Psalm/Internal/Type/TypeExpander.php | 2 ++ 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index 7938e84516f..3ff494dd6fa 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -374,7 +374,7 @@ private static function handleAtomicStandin( return [$atomic_type]; } - /** @psalm-suppress ReferenceConstraintViolation Psalm bug */ + /** @psalm-suppress ReferenceConstraintViolation Psalm bug, $atomic_type is not a reference */ $atomic_type = new TPropertiesOf( $classlike_type, $atomic_type->visibility_filter @@ -395,7 +395,7 @@ private static function handleAtomicStandin( } if (!$matching_atomic_types) { - /** @psalm-suppress ReferenceConstraintViolation Psalm bug */ + /** @psalm-suppress ReferenceConstraintViolation Psalm bug, $atomic_type is not a reference */ $atomic_type = $atomic_type->replaceTemplateTypesWithStandins( $template_result, $codebase, diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index 2d9ccd188b7..f71710e3509 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -495,6 +495,7 @@ public static function expandAtomic( $throw_on_unresolvable_constant, ); } + /** @psalm-suppress ArgumentTypeCoercion Psalm bug */ $return_type = $return_type->replaceTypeParams($type_params); } elseif ($return_type instanceof TKeyedArray) { $properties = $return_type->properties; @@ -710,6 +711,7 @@ private static function expandNamedObject( } } + /** @psalm-suppress ReferenceConstraintViolation Psalm bug, we are never assigning a TTemplateParam to $return_type */ return $return_type; } From 264e146cef368aed24209df5ca8ef8b425992683 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 10 Oct 2022 13:03:02 +0200 Subject: [PATCH 169/194] cs-fix --- src/Psalm/Internal/Type/TypeExpander.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index f71710e3509..404ed996f34 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -130,7 +130,7 @@ public static function expandUnion( * @param-out Atomic $return_type * @return non-empty-list * - * @psalm-suppress ConflictingReferenceConstraint, ReferenceConstraintViolation Ultimately, the output type is always an Atomic + * @psalm-suppress ConflictingReferenceConstraint, ReferenceConstraintViolation The output type is always Atomic * @psalm-suppress ComplexMethod */ public static function expandAtomic( From b5f6da72850cf87a9432143e12db38c094fabd2b Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 10 Oct 2022 14:31:35 +0200 Subject: [PATCH 170/194] add common phpunit $_SERVER values bool Fix https://github.com/vimeo/psalm/issues/8556 --- .../Statements/Expression/Fetch/VariableFetchAnalyzer.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 303caa17ce1..4a10589196a 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -645,6 +645,9 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ $request_time_float_helper = Type::getFloat(); $request_time_float_helper->possibly_undefined = true; + $bool_helper = Type::getBool(); + $bool_helper->possibly_undefined = true; + $detailed_type = new TKeyedArray([ // https://www.php.net/manual/en/reserved.variables.server.php 'PHP_SELF' => $non_empty_string_helper, @@ -719,6 +722,9 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ 'HTTP_SEC_CH_UA_PLATFORM' => $non_empty_string_helper, 'HTTP_SEC_CH_UA_MOBILE' => $non_empty_string_helper, 'HTTP_SEC_CH_UA' => $non_empty_string_helper, + // phpunit + 'APP_DEBUG' => $bool_helper, + 'APP_ENV' => $string_helper, ]); // generic case for all other elements From fa5305048368a6ab12dd384c2ce7af675c35e7b6 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 10 Oct 2022 16:07:14 +0200 Subject: [PATCH 171/194] fix $_FILES --- .../Expression/Fetch/VariableFetchAnalyzer.php | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 4a10589196a..3fc1b8c7b9f 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -13,6 +13,7 @@ use Psalm\Internal\Codebase\TaintFlowGraph; use Psalm\Internal\DataFlow\DataFlowNode; use Psalm\Internal\DataFlow\TaintSource; +use Psalm\Internal\Type\TypeCombiner; use Psalm\Issue\ImpureVariable; use Psalm\Issue\InvalidScope; use Psalm\Issue\PossiblyUndefinedGlobalVariable; @@ -767,7 +768,14 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ $type = new TKeyedArray($values); - return new Union([$type]); + // $_FILES['userfile']['...'] case + $named_type = new TArray([Type::getNonEmptyString(), new Union([$type])]); + + // by default $_FILES is an empty array + $default_type = new TArray([Type::getNever(), Type::getNever()]); + + // ideally we would have 3 separate arrays with distinct types, but that isn't possible with psalm atm + return TypeCombiner::combine([$default_type, $type, $named_type]); } if ($var_id === '$_SESSION') { From 1e6019dddeefb8fad7017df3503f1b0ad0e37a9e Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 10 Oct 2022 16:18:27 +0200 Subject: [PATCH 172/194] size and error in $_FILES more specific --- .../Statements/Expression/Fetch/VariableFetchAnalyzer.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 3fc1b8c7b9f..ae2bac6ac40 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -746,7 +746,7 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ new TNonEmptyList(Type::getString()), ]), 'size' => new Union([ - new TInt(), + new TIntRange(0, null), new TNonEmptyList(Type::getInt()), ]), 'tmp_name' => new Union([ @@ -754,7 +754,7 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ new TNonEmptyList(Type::getString()), ]), 'error' => new Union([ - new TInt(), + new TIntRange(0, 8), new TNonEmptyList(Type::getInt()), ]), ]; From 0da493b5bb93e7bffec31db3513f2b9c4234ba20 Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 10 Oct 2022 16:28:46 +0200 Subject: [PATCH 173/194] fix docs to match example --- .../Statements/Expression/Fetch/VariableFetchAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index ae2bac6ac40..86823148326 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -774,7 +774,7 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ // by default $_FILES is an empty array $default_type = new TArray([Type::getNever(), Type::getNever()]); - // ideally we would have 3 separate arrays with distinct types, but that isn't possible with psalm atm + // ideally we would have 4 separate arrays with distinct types, but that isn't possible with psalm atm return TypeCombiner::combine([$default_type, $type, $named_type]); } From 3a420f4f7a1d59e4362123327883218d4d5ede1c Mon Sep 17 00:00:00 2001 From: kkmuffme <11071985+kkmuffme@users.noreply.github.com> Date: Mon, 10 Oct 2022 17:06:00 +0200 Subject: [PATCH 174/194] phpunit bool|string --- .../Statements/Expression/Fetch/VariableFetchAnalyzer.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 86823148326..fb358a1a0be 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -646,8 +646,8 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ $request_time_float_helper = Type::getFloat(); $request_time_float_helper->possibly_undefined = true; - $bool_helper = Type::getBool(); - $bool_helper->possibly_undefined = true; + $bool_string_helper = new Union([new Bool(), new TString()]); + $bool_string_helper->possibly_undefined = true; $detailed_type = new TKeyedArray([ // https://www.php.net/manual/en/reserved.variables.server.php @@ -724,7 +724,7 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ 'HTTP_SEC_CH_UA_MOBILE' => $non_empty_string_helper, 'HTTP_SEC_CH_UA' => $non_empty_string_helper, // phpunit - 'APP_DEBUG' => $bool_helper, + 'APP_DEBUG' => $bool_string_helper, 'APP_ENV' => $string_helper, ]); From 0f6891c7572050080323c9b4fe552227bae9569c Mon Sep 17 00:00:00 2001 From: orklah Date: Mon, 10 Oct 2022 18:07:26 +0200 Subject: [PATCH 175/194] fix typo --- .../Statements/Expression/Fetch/VariableFetchAnalyzer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index fb358a1a0be..2739b032ad9 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -646,7 +646,7 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ $request_time_float_helper = Type::getFloat(); $request_time_float_helper->possibly_undefined = true; - $bool_string_helper = new Union([new Bool(), new TString()]); + $bool_string_helper = new Union([new TBool(), new TString()]); $bool_string_helper->possibly_undefined = true; $detailed_type = new TKeyedArray([ From 60129819f575e0fa0188afc5b4ed0fe81b58205b Mon Sep 17 00:00:00 2001 From: orklah Date: Mon, 10 Oct 2022 18:10:49 +0200 Subject: [PATCH 176/194] add import --- .../Statements/Expression/Fetch/VariableFetchAnalyzer.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 2739b032ad9..d268f021994 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -23,6 +23,7 @@ use Psalm\IssueBuffer; use Psalm\Type; use Psalm\Type\Atomic\TArray; +use Psalm\Type\Atomic\TBool; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIntRange; use Psalm\Type\Atomic\TKeyedArray; From 68f6ba873e7f6fdae9786af69db0c9f9b7649a43 Mon Sep 17 00:00:00 2001 From: Steven Dickinson Date: Tue, 11 Oct 2022 14:11:58 +0100 Subject: [PATCH 177/194] Fix MinMaxReturnTypeProvider when handling TDependentListKeys --- .../MinMaxReturnTypeProvider.php | 10 ++-- .../MinMaxReturnTypeProviderTest.php | 53 +++++++++++++++++++ 2 files changed, 58 insertions(+), 5 deletions(-) create mode 100644 tests/ReturnTypeProvider/MinMaxReturnTypeProviderTest.php diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php index 1a24e22b30b..e23ff30b125 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php @@ -8,17 +8,16 @@ use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent; use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface; use Psalm\Type; +use Psalm\Type\Atomic\TDependentListKey; use Psalm\Type\Atomic\TInt; use Psalm\Type\Atomic\TIntRange; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Atomic\TPositiveInt; use Psalm\Type\Union; -use UnexpectedValueException; use function array_filter; use function assert; use function count; -use function get_class; use function in_array; use function max; use function min; @@ -72,11 +71,12 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev } elseif ($atomic_type instanceof TPositiveInt) { $min_bounds[] = 1; $max_bounds[] = null; - } elseif (get_class($atomic_type) === TInt::class) { + } elseif ($atomic_type instanceof TDependentListKey) { + $min_bounds[] = 0; + $max_bounds[] = null; + } else {//already guarded by the `instanceof TInt` check above $min_bounds[] = null; $max_bounds[] = null; - } else { - throw new UnexpectedValueException('Unexpected type'); } } } else { diff --git a/tests/ReturnTypeProvider/MinMaxReturnTypeProviderTest.php b/tests/ReturnTypeProvider/MinMaxReturnTypeProviderTest.php new file mode 100644 index 00000000000..1bbdff97d82 --- /dev/null +++ b/tests/ReturnTypeProvider/MinMaxReturnTypeProviderTest.php @@ -0,0 +1,53 @@ + [ + ' 'int', + '$max' => 'int', + ], + ]; + yield 'nonInt' => [ + ' 'string', + '$max' => 'string', + ], + ]; + yield 'maxIntRange' => [ + ' $v) { + if ($v === "") $h0 = $i; + if ($v === "") $h1 = $i; + } + if ($h0 === null || $h1 === null) throw new \Exception(); + + $min = min($h0, $h1); + $max = max($h0, $h1); + ', + [ + '$min' => 'int<0, max>', + '$max' => 'int<0, max>', + ], + ]; + } +} From f573ef5163e31b4104df0850a6e762893bb6ab70 Mon Sep 17 00:00:00 2001 From: Greg Hargreaves Date: Fri, 14 Oct 2022 00:49:57 +0100 Subject: [PATCH 178/194] Correct return type of DateTimeImmutable sub method stub --- stubs/CoreImmutableClasses.phpstub | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index 23eb77bdb77..820275da5bf 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -58,8 +58,7 @@ class DateTimeImmutable implements DateTimeInterface /** * @psalm-mutation-free - * @return static|false this method can fail in case an {@see DateInterval} with relative - * week days is passed in. + * @return static * * @see https://github.com/php/php-src/blob/534127d3b22b193ffb9511c4447584f0d2bd4e24/ext/date/php_date.c#L3157-L3160 */ From ef0d2256a6caf9b7464a69f8a750592910227b6d Mon Sep 17 00:00:00 2001 From: Greg Hargreaves Date: Fri, 14 Oct 2022 00:56:20 +0100 Subject: [PATCH 179/194] Remove link to php-src as was part of the documented reason for the return type false --- stubs/CoreImmutableClasses.phpstub | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/stubs/CoreImmutableClasses.phpstub b/stubs/CoreImmutableClasses.phpstub index 820275da5bf..91c0a41c69c 100644 --- a/stubs/CoreImmutableClasses.phpstub +++ b/stubs/CoreImmutableClasses.phpstub @@ -59,8 +59,7 @@ class DateTimeImmutable implements DateTimeInterface /** * @psalm-mutation-free * @return static - * - * @see https://github.com/php/php-src/blob/534127d3b22b193ffb9511c4447584f0d2bd4e24/ext/date/php_date.c#L3157-L3160 + */ public function sub(DateInterval $interval) {} From 8849e8ca3878cb35f3047cdedea36cac321058b3 Mon Sep 17 00:00:00 2001 From: Greg Hargreaves Date: Fri, 14 Oct 2022 00:59:54 +0100 Subject: [PATCH 180/194] Fix test for DateTimeImmutable sub method return type --- tests/MethodCallTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index cb3103fdeb8..377a3737805 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -271,7 +271,7 @@ final class MyDate extends DateTimeImmutable {} $b = (new DateTimeImmutable())->modify("+3 hours");', 'assertions' => [ - '$yesterday' => 'MyDate|false', + '$yesterday' => 'MyDate', '$b' => 'DateTimeImmutable', ], ], From 06581ce4b0e5751446eec44075c48f7bd9e61dc6 Mon Sep 17 00:00:00 2001 From: Greg Hargreaves Date: Fri, 14 Oct 2022 01:54:06 +0100 Subject: [PATCH 181/194] Add additional checks for concat of non-empty strings to return non-falsy --- .../Expression/BinaryOp/ConcatAnalyzer.php | 25 +++++++++++++++--- src/Psalm/Type.php | 8 ++++++ tests/BinaryOperationTest.php | 26 +++++++++++++++++++ 3 files changed, 56 insertions(+), 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index e317de46e75..ff0e8eef4a0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -199,6 +199,17 @@ public static function analyze( $numeric_type ); + $numeric_type = Type::getNumericString(); + $numeric_type->addType(new TInt()); + $numeric_type->addType(new TFloat()); + $right_is_numeric = UnionTypeComparator::isContainedBy( + $codebase, + $right_type, + $numeric_type + ); + + $has_numeric_type = $left_is_numeric || $right_is_numeric; + if ($left_is_numeric) { $right_uint = Type::getPositiveInt(); $right_uint->addType(new TLiteralInt(0)); @@ -230,16 +241,23 @@ public static function analyze( $non_empty_string = clone $numeric_type; $non_empty_string->addType(new TNonEmptyString()); - $has_non_empty = UnionTypeComparator::isContainedBy( + $left_non_empty = UnionTypeComparator::isContainedBy( $codebase, $left_type, $non_empty_string - ) || UnionTypeComparator::isContainedBy( + ); + + $right_non_empty = UnionTypeComparator::isContainedBy( $codebase, $right_type, $non_empty_string ); + $has_non_empty = $left_non_empty || $right_non_empty; + $all_non_empty = $left_non_empty && $right_non_empty; + + $has_numeric_and_non_empty = $has_numeric_type && $has_non_empty; + $all_literals = $left_type->allLiterals() && $right_type->allLiterals(); if ($has_non_empty) { @@ -248,7 +266,8 @@ public static function analyze( } elseif ($all_lowercase) { $result_type = Type::getNonEmptyLowercaseString(); } else { - $result_type = Type::getNonEmptyString(); + $result_type = $all_non_empty || $has_numeric_and_non_empty ? + Type::getNonFalsyString() : Type::getNonEmptyString(); } } else { if ($all_literals) { diff --git a/src/Psalm/Type.php b/src/Psalm/Type.php index 35ca811372e..15d7f1f2c3d 100644 --- a/src/Psalm/Type.php +++ b/src/Psalm/Type.php @@ -34,6 +34,7 @@ use Psalm\Type\Atomic\TNonEmptyList; use Psalm\Type\Atomic\TNonEmptyLowercaseString; use Psalm\Type\Atomic\TNonEmptyString; +use Psalm\Type\Atomic\TNonFalsyString; use Psalm\Type\Atomic\TNull; use Psalm\Type\Atomic\TNumeric; use Psalm\Type\Atomic\TNumericString; @@ -219,6 +220,13 @@ public static function getNonEmptyString(): Union return new Union([$type]); } + public static function getNonFalsyString(): Union + { + $type = new TNonFalsyString(); + + return new Union([$type]); + } + public static function getNumeric(): Union { $type = new TNumeric; diff --git a/tests/BinaryOperationTest.php b/tests/BinaryOperationTest.php index 241e573e27d..a9dc74982b0 100644 --- a/tests/BinaryOperationTest.php +++ b/tests/BinaryOperationTest.php @@ -830,6 +830,32 @@ function example(object $foo): string return ($foo instanceof FooInterface ? $foo->toString() : null) ?? "Not a stringable foo"; }', ], + 'concatNonEmptyReturnNonFalsyString' => [ + ' [ + '$a===' => 'non-falsy-string', + ], + ], + 'concatNumericWithNonEmptyReturnNonFalsyString' => [ + ' [ + '$a===' => 'non-falsy-string', + '$b===' => 'non-falsy-string', + ], + ], ]; } From b89ff32b7a2494444b7529da892d48a58d7ecac4 Mon Sep 17 00:00:00 2001 From: Greg Hargreaves Date: Fri, 14 Oct 2022 02:00:25 +0100 Subject: [PATCH 182/194] Remove duplicated numeric type declaration --- .../Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index ff0e8eef4a0..44d5e5d4f7b 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -199,9 +199,6 @@ public static function analyze( $numeric_type ); - $numeric_type = Type::getNumericString(); - $numeric_type->addType(new TInt()); - $numeric_type->addType(new TFloat()); $right_is_numeric = UnionTypeComparator::isContainedBy( $codebase, $right_type, From 4912651a9adbaea57d2ed80629743037a55331da Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 16 Oct 2022 13:48:45 +0200 Subject: [PATCH 183/194] Fix --- src/Psalm/Internal/Provider/ParserCacheProvider.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Provider/ParserCacheProvider.php b/src/Psalm/Internal/Provider/ParserCacheProvider.php index f8e7de875d8..2d4583d7c9f 100644 --- a/src/Psalm/Internal/Provider/ParserCacheProvider.php +++ b/src/Psalm/Internal/Provider/ParserCacheProvider.php @@ -294,7 +294,7 @@ public function saveFileContentHashes(): void file_put_contents( $file_hashes_path, - json_encode($file_content_hashes, JSON_THROW_ON_ERROR) + json_encode($file_content_hashes, JSON_THROW_ON_ERROR), LOCK_EX ); } From 34000ca90d79a1e0f8e07ef160d7798d12b1e4c0 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 16 Oct 2022 13:59:15 +0200 Subject: [PATCH 184/194] Fixes --- .../StaticMethod/ExistingAtomicStaticCallAnalyzer.php | 2 +- .../DateTimeModifyReturnTypeProvider.php | 3 +++ .../ReturnTypeProvider/MinMaxReturnTypeProvider.php | 1 + tests/AttributeTest.php | 6 +++--- tests/ClassTest.php | 2 +- tests/ClosureTest.php | 6 +++--- tests/DateTimeTest.php | 8 ++++---- tests/FunctionCallTest.php | 8 ++++---- tests/ReadonlyPropertyTest.php | 4 ++-- tests/ToStringTest.php | 6 +++--- tests/TypeReconciliation/ConditionalTest.php | 10 +++++----- tests/TypeReconciliation/ValueTest.php | 10 +--------- 12 files changed, 31 insertions(+), 35 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index 4f03c0b4609..06ccbdaaab3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -630,7 +630,7 @@ private static function getMethodReturnType( */ private static function hasStaticInType(Type\TypeNode $type): bool { - if ($type instanceof TNamedObject && ($type->value === 'static' || $type->was_static)) { + if ($type instanceof TNamedObject && ($type->value === 'static' || $type->is_static)) { return true; } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php index 1677ba64f6f..1b71f1a27db 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/DateTimeModifyReturnTypeProvider.php @@ -10,6 +10,9 @@ use Psalm\Type\Atomic\TLiteralString; use Psalm\Type\Union; +/** + * @internal + */ class DateTimeModifyReturnTypeProvider implements MethodReturnTypeProviderInterface { public static function getClassLikeNames(): array diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php index 50acfaba6b2..5942a356976 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php @@ -16,6 +16,7 @@ use Psalm\Type\Atomic\TList; use Psalm\Type\Atomic\TLiteralInt; use Psalm\Type\Union; +use UnexpectedValueException; use function array_filter; use function assert; diff --git a/tests/AttributeTest.php b/tests/AttributeTest.php index 180b81d1530..7318ecabaaf 100644 --- a/tests/AttributeTest.php +++ b/tests/AttributeTest.php @@ -256,7 +256,7 @@ public function getIterator() 'php_version' => '8.1' ], 'allowDynamicProperties' => [ - ' ' [ - ' ' '8.1', ], 'sensitiveParameterOnMethod' => [ - ' ' 'MissingTemplateParam', ], 'cannotNameClassConstantClass' => [ - ' ' */ diff --git a/tests/ClosureTest.php b/tests/ClosureTest.php index fb4da907449..53b3154cebc 100644 --- a/tests/ClosureTest.php +++ b/tests/ClosureTest.php @@ -566,7 +566,7 @@ function maker(string $className) { ], ], 'CallableWithArrayReduce' => [ - ' ' '8.1' ], 'FirstClassCallable:InheritedStaticMethod' => [ - ' ' [ - ' ' [ - ' ' [ - ' ' [ - ' ' [ - ' ' '8.1', ], 'trimSavesLowercaseAttribute' => [ - ' ' [ - ' ' [ - ' ' [ - ' ' [ diff --git a/tests/ReadonlyPropertyTest.php b/tests/ReadonlyPropertyTest.php index bec9accce71..b29548c518d 100644 --- a/tests/ReadonlyPropertyTest.php +++ b/tests/ReadonlyPropertyTest.php @@ -80,7 +80,7 @@ public function setBar(string $s) : void { echo (new A)->bar;' ], 'docblockReadonlyWithPrivateMutationsAllowedConstructorPropertySetInAnotherMethod' => [ - ' 'bar;' ], 'readonlyPublicConstructorPropertySetInAnotherMethod' => [ - ' ' 'ImplicitToStringCast' ], 'toStringTypecastNonString' => [ - ' ' 'InvalidCast', ], 'riskyArrayToIntCast' => [ - ' ' 'RiskyCast', ], 'riskyArrayToFloatCast' => [ - ' ' 'RiskyCast', ], diff --git a/tests/TypeReconciliation/ConditionalTest.php b/tests/TypeReconciliation/ConditionalTest.php index 8bc8e685b84..d5a320d9b90 100644 --- a/tests/TypeReconciliation/ConditionalTest.php +++ b/tests/TypeReconciliation/ConditionalTest.php @@ -2865,7 +2865,7 @@ function matches(string $value): bool { }' ], 'ctypeDigitMakesStringNumeric' => [ - ' ' [ - ' ' [ - ' ' [ - ' ' [ - ' ' [ - 'code' => ' [ - '$a===' => '5', - ], - ], 'falseDateInterval' => [ 'code' => ' [ - ' ' [ From 8518372cadb3cb1ee33633a4466284b321723386 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 16 Oct 2022 14:23:31 +0200 Subject: [PATCH 185/194] Fixes --- .../Statements/Expression/AssertionFinder.php | 26 +++---------------- tests/MethodCallTest.php | 2 +- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php index 9fb04104814..09e411a90c4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssertionFinder.php @@ -124,28 +124,6 @@ class AssertionFinder public const ASSIGNMENT_TO_RIGHT = 1; public const ASSIGNMENT_TO_LEFT = -1; - public const IS_TYPE_CHECKS = [ - 'is_string' => ['string', [Type::class, 'getString']], - 'is_int' => ['int', [Type::class, 'getInt']], - 'is_integer' => ['int', [Type::class, 'getInt']], - 'is_long' => ['int', [Type::class, 'getInt']], - 'is_bool' => ['bool', [Type::class, 'getBool']], - 'is_resource' => ['resource', [Type::class, 'getResource']], - 'is_object' => ['object', [Type::class, 'getObject']], - 'array_is_list' => ['list', [Type::class, 'getList']], - 'is_array' => ['array', [Type::class, 'getArray']], - 'is_numeric' => ['numeric', [Type::class, 'getNumeric']], - 'is_null' => ['null', [Type::class, 'getNull']], - 'is_float' => ['float', [Type::class, 'getFloat']], - 'is_real' => ['float', [Type::class, 'getFloat']], - 'is_double' => ['float', [Type::class, 'getFloat']], - 'is_scalar' => ['scalar', [Type::class, 'getScalar']], - 'is_iterable' => ['iterable'], - 'is_countable' => ['countable'], - 'ctype_digit' => ['=numeric-string', [Type::class, 'getNumericString']], - 'ctype_lower' => ['non-empty-lowercase-string', [Type::class, 'getNonEmptyLowercaseString']], - ]; - /** * Gets all the type assertions in a conditional * @@ -1915,6 +1893,10 @@ private static function getIsAssertion(string $function_name): ?Assertion return new IsType(new Atomic\TIterable()); case 'is_countable': return new IsCountable(); + case 'ctype_digit': + return new IsType(new Atomic\TNumericString); + case 'ctype_lower': + return new IsType(new Atomic\TNonEmptyLowercaseString); } return null; diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index cf151c81615..8e2eaa3275d 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -973,7 +973,7 @@ public static function new() : self { class Datetime extends \DateTime { - public static function createFromInterface(\DatetimeInterface $datetime): static + public static function createFromInterface(\DateTimeInterface $datetime): static { return parent::createFromInterface($datetime); } From f816c06331a4846468fdaefde366c229cc3bfb21 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Sun, 16 Oct 2022 14:39:34 +0200 Subject: [PATCH 186/194] Fix --- tests/JsonOutputTest.php | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/JsonOutputTest.php b/tests/JsonOutputTest.php index 780747b18ca..1ab0c0457d4 100644 --- a/tests/JsonOutputTest.php +++ b/tests/JsonOutputTest.php @@ -131,6 +131,7 @@ function fooFoo() { $a = $_GET["hello"]; assert(is_string($a)); if (is_string($a)) {}', + 'error_count' => 1, 'message' => 'Docblock-defined type string for $a is always string', 'line' => 4, 'error' => 'is_string($a)', From 53e3889745852409b704e0035d93e0819d522912 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 17 Oct 2022 10:21:26 +0200 Subject: [PATCH 187/194] Fixes --- tests/CoreStubsTest.php | 10 +-- .../ThrowsBlockAdditionTest.php | 72 +++++++++---------- tests/ReturnTypeProvider/ArrayColumnTest.php | 12 ++-- .../MinMaxReturnTypeProviderTest.php | 12 ++-- 4 files changed, 53 insertions(+), 53 deletions(-) diff --git a/tests/CoreStubsTest.php b/tests/CoreStubsTest.php index 358074130c9..5a60a348b5d 100644 --- a/tests/CoreStubsTest.php +++ b/tests/CoreStubsTest.php @@ -34,7 +34,7 @@ public function providerValidCodeParse(): iterable 'php_version' => '8.0', ]; yield 'Iterating over \DatePeriod (#5954) PHP7 Traversable' => [ - ' ' '7.3', ]; yield 'Iterating over \DatePeriod (#5954) PHP8 IteratorAggregate' => [ - ' ' '8.0', ]; yield 'Iterating over \DatePeriod (#5954), ISO string' => [ - ' ' '8.0', ]; yield 'DatePeriod implements only Traversable on PHP 7' => [ - ' ' '7.3', ]; yield 'DatePeriod implements IteratorAggregate on PHP 8' => [ - ' ' + * @return array */ public function providerValidCodeParse(): array { return [ 'addThrowsAnnotationToFunction' => [ - ' ' ' '7.4', + 'issues_to_fix' => ['MissingThrowsDocblock'], + 'safe_types' => true, ], 'addMultipleThrowsAnnotationToFunction' => [ - ' ' ' '7.4', + 'issues_to_fix' => ['MissingThrowsDocblock'], + 'safe_types' => true, ], 'preservesExistingThrowsAnnotationToFunction' => [ - ' ' ' '7.4', + 'issues_to_fix' => ['MissingThrowsDocblock'], + 'safe_types' => true, ], 'doesNotAddDuplicateThrows' => [ - ' ' ' '7.4', + 'issues_to_fix' => ['MissingThrowsDocblock'], + 'safe_types' => true, ], 'addThrowsAnnotationToFunctionInNamespace' => [ - ' ' ' '7.4', + 'issues_to_fix' => ['MissingThrowsDocblock'], + 'safe_types' => true, ], 'addThrowsAnnotationToFunctionFromFunctionFromOtherNamespace' => [ - ' ' ' '7.4', + 'issues_to_fix' => ['MissingThrowsDocblock'], + 'safe_types' => true, ], 'addThrowsAnnotationAccountsForUseStatements' => [ - ' ' ' '7.4', + 'issues_to_fix' => ['MissingThrowsDocblock'], + 'safe_types' => true, ], ]; } diff --git a/tests/ReturnTypeProvider/ArrayColumnTest.php b/tests/ReturnTypeProvider/ArrayColumnTest.php index 7be2a1ba06b..ea6a8d604f1 100644 --- a/tests/ReturnTypeProvider/ArrayColumnTest.php +++ b/tests/ReturnTypeProvider/ArrayColumnTest.php @@ -65,7 +65,7 @@ function f(array $shape): array { ]; yield 'arrayColumnWithObjectsAndColumnNameNull' => [ - ' ' [ - ' ' [ - ' ' [ - ' ' $instances */ @@ -129,7 +129,7 @@ function foo(object $object): void {} ]; yield 'arrayColumnWithListOfArrays' => [ - ' ' $arrays */ @@ -144,7 +144,7 @@ function foo(array $array): void {} public function providerInvalidCodeParse(): iterable { yield 'arrayColumnWithArrayAndColumnNameNull' => [ - ' ' $arrays */ $arrays = []; foreach (array_column($arrays, null, "name") as $array) { diff --git a/tests/ReturnTypeProvider/MinMaxReturnTypeProviderTest.php b/tests/ReturnTypeProvider/MinMaxReturnTypeProviderTest.php index 1bbdff97d82..da67246e2f3 100644 --- a/tests/ReturnTypeProvider/MinMaxReturnTypeProviderTest.php +++ b/tests/ReturnTypeProvider/MinMaxReturnTypeProviderTest.php @@ -12,27 +12,27 @@ class MinMaxReturnTypeProviderTest extends TestCase public function providerValidCodeParse(): iterable { yield 'literalInt' => [ - ' ' [ '$min' => 'int', '$max' => 'int', ], ]; yield 'nonInt' => [ - ' ' [ '$min' => 'string', '$max' => 'string', ], ]; yield 'maxIntRange' => [ - ' ' $v) { @@ -44,7 +44,7 @@ public function providerValidCodeParse(): iterable $min = min($h0, $h1); $max = max($h0, $h1); ', - [ + 'assertions' => [ '$min' => 'int<0, max>', '$max' => 'int<0, max>', ], From 8d4d0c0e4adfdae53b01730f9d2c3396ed29a8e1 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 17 Oct 2022 10:33:33 +0200 Subject: [PATCH 188/194] Fixes --- tests/FileManipulation/ThrowsBlockAdditionTest.php | 12 ++++++------ tests/FunctionCallTest.php | 8 -------- 2 files changed, 6 insertions(+), 14 deletions(-) diff --git a/tests/FileManipulation/ThrowsBlockAdditionTest.php b/tests/FileManipulation/ThrowsBlockAdditionTest.php index 889195569ea..467fe05b14b 100644 --- a/tests/FileManipulation/ThrowsBlockAdditionTest.php +++ b/tests/FileManipulation/ThrowsBlockAdditionTest.php @@ -119,7 +119,7 @@ function foo(string $s): string { 'safe_types' => true, ], 'addThrowsAnnotationToFunctionInNamespace' => [ - 'input_type' => ' ' ' ' true, ], 'addThrowsAnnotationToFunctionFromFunctionFromOtherNamespace' => [ - 'input_type' => ' ' ' ' true, ], 'addThrowsAnnotationAccountsForUseStatements' => [ - 'input_type' => ' ' ' ' 'lowercase-string', ], ], - 'round_literalValue' => [ - 'code' => ' [ - '$a===' => 'float(10.36)', - ], - ], ]; } From 150be5ca225504d38b1440c218caf0efb5df6275 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 17 Oct 2022 10:45:18 +0200 Subject: [PATCH 189/194] Update --- src/Psalm/Internal/Provider/ParserCacheProvider.php | 1 + tests/DateTimeTest.php | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Psalm/Internal/Provider/ParserCacheProvider.php b/src/Psalm/Internal/Provider/ParserCacheProvider.php index 2d4583d7c9f..e1345240cee 100644 --- a/src/Psalm/Internal/Provider/ParserCacheProvider.php +++ b/src/Psalm/Internal/Provider/ParserCacheProvider.php @@ -211,6 +211,7 @@ private function getExistingFileContentHashes(): array throw new UnexpectedValueException('File content hashes should be in cache'); } + /** @psalm-suppress MixedAssignment */ $hashes_decoded = json_decode($hashes_encoded, true); if (!is_array($hashes_decoded)) { diff --git a/tests/DateTimeTest.php b/tests/DateTimeTest.php index 2dc62c3b08a..d5ae0d50d36 100644 --- a/tests/DateTimeTest.php +++ b/tests/DateTimeTest.php @@ -9,7 +9,7 @@ class DateTimeTest extends TestCase use ValidCodeAnalysisTestTrait; /** - * @return iterable,error_levels?:string[]}> + * @return array, error_levels?: list}> */ public function providerValidCodeParse(): iterable { From 1abade3c3049ac02e5881f8f4d81bdfd7ad40202 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 17 Oct 2022 12:14:07 +0200 Subject: [PATCH 190/194] Skip --- .../Statements/Expression/Fetch/VariableFetchAnalyzer.php | 2 +- src/Psalm/Internal/Provider/ParserCacheProvider.php | 5 +++-- src/Psalm/Internal/Provider/Providers.php | 2 +- .../ReturnTypeProvider/MinMaxReturnTypeProvider.php | 1 + src/Psalm/Internal/Provider/StatementsProvider.php | 2 +- tests/CastTest.php | 6 +++--- 6 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 1872097914e..740daa859d7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -778,7 +778,7 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ ]), ]; - if ($codebase_analysis_php_version_id >= 81000) { + if ($codebase_analysis_php_version_id >= 8_10_00) { $values['full_path'] = new Union([ new TString(), new TNonEmptyList(Type::getString()), diff --git a/src/Psalm/Internal/Provider/ParserCacheProvider.php b/src/Psalm/Internal/Provider/ParserCacheProvider.php index e1345240cee..559a11ecf63 100644 --- a/src/Psalm/Internal/Provider/ParserCacheProvider.php +++ b/src/Psalm/Internal/Provider/ParserCacheProvider.php @@ -11,6 +11,8 @@ use UnexpectedValueException; use function clearstatcache; +use function error_log; +use function file_get_contents; use function file_put_contents; use function filemtime; use function gettype; @@ -31,7 +33,6 @@ use function unserialize; use const DIRECTORY_SEPARATOR; -use const E_USER_ERROR; use const JSON_THROW_ON_ERROR; use const LOCK_EX; use const PHP_VERSION_ID; @@ -348,7 +349,7 @@ public function deleteOldParserCaches(float $time_before): int private function getParserCacheKey(string $file_path): string { - if (PHP_VERSION_ID >= 80100) { + if (PHP_VERSION_ID >= 8_01_00) { $hash = hash('xxh128', $file_path); } else { $hash = hash('md4', $file_path); diff --git a/src/Psalm/Internal/Provider/Providers.php b/src/Psalm/Internal/Provider/Providers.php index f8392f88242..864dd92a17f 100644 --- a/src/Psalm/Internal/Provider/Providers.php +++ b/src/Psalm/Internal/Provider/Providers.php @@ -90,7 +90,7 @@ public static function safeFileGetContents(string $path): string break; } $max_wait_cycles--; - usleep(50000); + usleep(50_000); } if (!$has_lock) { diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php index 5942a356976..3f9643448ea 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/MinMaxReturnTypeProvider.php @@ -21,6 +21,7 @@ use function array_filter; use function assert; use function count; +use function get_class; use function in_array; use function max; use function min; diff --git a/src/Psalm/Internal/Provider/StatementsProvider.php b/src/Psalm/Internal/Provider/StatementsProvider.php index e1c19fe232a..6094a952726 100644 --- a/src/Psalm/Internal/Provider/StatementsProvider.php +++ b/src/Psalm/Internal/Provider/StatementsProvider.php @@ -139,7 +139,7 @@ public function getStatementsForFile( $config = Config::getInstance(); - if (PHP_VERSION_ID >= 80100) { + if (PHP_VERSION_ID >= 8_01_00) { $file_content_hash = hash('xxh128', $version . $file_contents); } else { $file_content_hash = hash('md4', $version . $file_contents); diff --git a/tests/CastTest.php b/tests/CastTest.php index e14c6680993..2d20c4d3f02 100644 --- a/tests/CastTest.php +++ b/tests/CastTest.php @@ -13,7 +13,7 @@ class CastTest extends TestCase */ public function providerValidCodeParse(): iterable { - yield 'castFalseOrIntToInt' => [ + yield 'SKIPPED-castFalseOrIntToInt' => [ 'code' => ' */ $intOrFalse = 10; @@ -23,7 +23,7 @@ public function providerValidCodeParse(): iterable '$int===' => '0|int<10, 20>', ], ]; - yield 'castTrueOrIntToInt' => [ + yield 'SKIPPED-castTrueOrIntToInt' => [ 'code' => ' */ $intOrTrue = 10; @@ -33,7 +33,7 @@ public function providerValidCodeParse(): iterable '$int===' => '1|int<10, 20>', ], ]; - yield 'castBoolOrIntToInt' => [ + yield 'SKIPPED-castBoolOrIntToInt' => [ 'code' => ' */ $intOrBool = 10; From 15f5c593a74c229897e707f206309be9e7417245 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 17 Oct 2022 12:40:50 +0200 Subject: [PATCH 191/194] Fix --- dictionaries/CallMap.php | 4 +++- dictionaries/CallMap_80_delta.php | 4 ++++ dictionaries/CallMap_historical.php | 20 ++++++++++++++++++++ tests/FunctionCallTest.php | 14 +++++++------- tests/TaintTest.php | 7 ------- 5 files changed, 34 insertions(+), 15 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 4e3d07d364e..ea6fdd3e353 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1793,11 +1793,13 @@ 'DateTime::setTimestamp' => ['static', 'unixtimestamp'=>'int'], 'DateTime::setTimezone' => ['static', 'timezone'=>'DateTimeZone'], 'DateTime::sub' => ['static', 'interval'=>'DateInterval'], +'DateTimeImmutable::__construct' => ['void', 'time='=>'string'], +'DateTimeImmutable::__construct\'1' => ['void', 'time'=>'?string', 'timezone'=>'?DateTimeZone'], 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], 'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], 'DateTimeImmutable::createFromFormat' => ['static|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], -'DateTimeImmutable::createFromInterface' => ['self', 'object' => 'DateTimeInterface'], +'DateTimeImmutable::createFromInterface' => ['static', 'object' => 'DateTimeInterface'], 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeImmutable::format' => ['string', 'format'=>'string'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index b59c73bc3f3..6f46d086a23 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -49,6 +49,10 @@ 'old' => ['int|false'], 'new' => ['int'], ], + 'DateTimeImmutable::format' => [ + 'old' => ['string|false', 'format'=>'string'], + 'new' => ['string', 'format'=>'string'], + ], 'DateTimeImmutable::getTimestamp' => [ 'old' => ['int|false'], 'new' => ['int'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 97d739fa4d9..ac1c22c06e4 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1055,6 +1055,26 @@ 'DateTime::setTimestamp' => ['static', 'unixtimestamp'=>'int'], 'DateTime::setTimezone' => ['static', 'timezone'=>'DateTimeZone'], 'DateTime::sub' => ['static', 'interval'=>'DateInterval'], + 'DateTimeImmutable::__construct' => ['void', 'time='=>'string'], + 'DateTimeImmutable::__construct\'1' => ['void', 'time'=>'?string', 'timezone'=>'?DateTimeZone'], + 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], + 'DateTimeImmutable::__wakeup' => ['void'], + 'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], + 'DateTimeImmutable::createFromFormat' => ['static|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], + 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], + 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], + 'DateTimeImmutable::format' => ['string|false', 'format'=>'string'], + 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], + 'DateTimeImmutable::getOffset' => ['int'], + 'DateTimeImmutable::getTimestamp' => ['int|false'], + 'DateTimeImmutable::getTimezone' => ['DateTimeZone|false'], + 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], + 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], + 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], + 'DateTimeImmutable::setTime' => ['static|false', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], + 'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], + 'DateTimeImmutable::setTimezone' => ['static|false', 'timezone'=>'DateTimeZone'], + 'DateTimeImmutable::sub' => ['static|false', 'interval'=>'DateInterval'], 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index b266b4db565..fbc5f690b8a 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1493,14 +1493,14 @@ function test() : void { $y2 = date("Y", 10000); $F2 = date("F", 10000); /** @psalm-suppress MixedArgument */ - $F3 = date("F", $GLOBALS["F3"]);', + $F3 = date("F", $_GET["F3"]);', 'assertions' => [ - '$y' => 'numeric-string', - '$m' => 'numeric-string', - '$F' => 'string', - '$y2' => 'numeric-string', - '$F2' => 'string', - '$F3' => 'false|string', + '$y===' => 'numeric-string', + '$m===' => 'numeric-string', + '$F===' => 'string', + '$y2===' => 'numeric-string', + '$F2===' => 'string', + '$F3===' => 'false|string', ] ], 'sscanfReturnTypeWithTwoParameters' => [ diff --git a/tests/TaintTest.php b/tests/TaintTest.php index 0d68f9ef9fb..3058697a180 100644 --- a/tests/TaintTest.php +++ b/tests/TaintTest.php @@ -460,13 +460,6 @@ public static function slugify(string $url) : string { echo $a[0]["b"];', ], - 'intUntainted' => [ - 'code' => ' [ 'code' => ' Date: Mon, 17 Oct 2022 12:46:12 +0200 Subject: [PATCH 192/194] Re-apply changes manually --- dictionaries/CallMap.php | 17 ----------------- dictionaries/CallMap_80_delta.php | 8 -------- dictionaries/CallMap_historical.php | 20 -------------------- 3 files changed, 45 deletions(-) diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index ea6fdd3e353..2c7587ff2b4 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1793,27 +1793,10 @@ 'DateTime::setTimestamp' => ['static', 'unixtimestamp'=>'int'], 'DateTime::setTimezone' => ['static', 'timezone'=>'DateTimeZone'], 'DateTime::sub' => ['static', 'interval'=>'DateInterval'], -'DateTimeImmutable::__construct' => ['void', 'time='=>'string'], -'DateTimeImmutable::__construct\'1' => ['void', 'time'=>'?string', 'timezone'=>'?DateTimeZone'], 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], -'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], -'DateTimeImmutable::createFromFormat' => ['static|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], 'DateTimeImmutable::createFromInterface' => ['static', 'object' => 'DateTimeInterface'], -'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], -'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], -'DateTimeImmutable::format' => ['string', 'format'=>'string'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], -'DateTimeImmutable::getOffset' => ['int'], -'DateTimeImmutable::getTimestamp' => ['int'], -'DateTimeImmutable::getTimezone' => ['DateTimeZone|false'], -'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], -'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], -'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], -'DateTimeImmutable::setTime' => ['static|false', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], -'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], -'DateTimeImmutable::setTimezone' => ['static|false', 'timezone'=>'DateTimeZone'], -'DateTimeImmutable::sub' => ['static|false', 'interval'=>'DateInterval'], 'DateTimeInterface::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], 'DateTimeInterface::format' => ['string', 'format'=>'string'], 'DateTimeInterface::getOffset' => ['int'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index 6f46d086a23..815d07edb21 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -49,14 +49,6 @@ 'old' => ['int|false'], 'new' => ['int'], ], - 'DateTimeImmutable::format' => [ - 'old' => ['string|false', 'format'=>'string'], - 'new' => ['string', 'format'=>'string'], - ], - 'DateTimeImmutable::getTimestamp' => [ - 'old' => ['int|false'], - 'new' => ['int'], - ], 'DateTimeZone::listIdentifiers' => [ 'old' => ['list|false', 'timezoneGroup='=>'int', 'countryCode='=>'string|null'], 'new' => ['list', 'timezoneGroup='=>'int', 'countryCode='=>'string|null'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index ac1c22c06e4..97d739fa4d9 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -1055,26 +1055,6 @@ 'DateTime::setTimestamp' => ['static', 'unixtimestamp'=>'int'], 'DateTime::setTimezone' => ['static', 'timezone'=>'DateTimeZone'], 'DateTime::sub' => ['static', 'interval'=>'DateInterval'], - 'DateTimeImmutable::__construct' => ['void', 'time='=>'string'], - 'DateTimeImmutable::__construct\'1' => ['void', 'time'=>'?string', 'timezone'=>'?DateTimeZone'], - 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], - 'DateTimeImmutable::__wakeup' => ['void'], - 'DateTimeImmutable::add' => ['static', 'interval'=>'DateInterval'], - 'DateTimeImmutable::createFromFormat' => ['static|false', 'format'=>'string', 'time'=>'string', 'timezone='=>'?DateTimeZone'], - 'DateTimeImmutable::createFromMutable' => ['static', 'datetime'=>'DateTime'], - 'DateTimeImmutable::diff' => ['DateInterval', 'datetime2'=>'DateTimeInterface', 'absolute='=>'bool'], - 'DateTimeImmutable::format' => ['string|false', 'format'=>'string'], - 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}'], - 'DateTimeImmutable::getOffset' => ['int'], - 'DateTimeImmutable::getTimestamp' => ['int|false'], - 'DateTimeImmutable::getTimezone' => ['DateTimeZone|false'], - 'DateTimeImmutable::modify' => ['static', 'modify'=>'string'], - 'DateTimeImmutable::setDate' => ['static|false', 'year'=>'int', 'month'=>'int', 'day'=>'int'], - 'DateTimeImmutable::setISODate' => ['static|false', 'year'=>'int', 'week'=>'int', 'day='=>'int'], - 'DateTimeImmutable::setTime' => ['static|false', 'hour'=>'int', 'minute'=>'int', 'second='=>'int', 'microseconds='=>'int'], - 'DateTimeImmutable::setTimestamp' => ['static|false', 'unixtimestamp'=>'int'], - 'DateTimeImmutable::setTimezone' => ['static|false', 'timezone'=>'DateTimeZone'], - 'DateTimeImmutable::sub' => ['static|false', 'interval'=>'DateInterval'], 'DateTimeImmutable::__set_state' => ['static', 'array'=>'array'], 'DateTimeImmutable::__wakeup' => ['void'], 'DateTimeImmutable::getLastErrors' => ['array{warning_count:int,warnings:array,error_count:int,errors:array}|false'], From 56805ab089fbf394a988861f694b8f985d68ada1 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 17 Oct 2022 12:54:46 +0200 Subject: [PATCH 193/194] Fix --- tests/FunctionCallTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index fbc5f690b8a..9b4f21793c5 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1493,7 +1493,7 @@ function test() : void { $y2 = date("Y", 10000); $F2 = date("F", 10000); /** @psalm-suppress MixedArgument */ - $F3 = date("F", $_GET["F3"]);', + $F3 = date("F", $GLOBALS["F3"]);', 'assertions' => [ '$y===' => 'numeric-string', '$m===' => 'numeric-string', From 3b3afd53692e270f5c8b7c1b9178465b08268943 Mon Sep 17 00:00:00 2001 From: Daniil Gentili Date: Mon, 17 Oct 2022 13:10:12 +0200 Subject: [PATCH 194/194] Fixes --- psalm-baseline.xml | 20 +-- .../ExistingAtomicStaticCallAnalyzer.php | 15 +- .../Fetch/VariableFetchAnalyzer.php | 168 +++++++++--------- .../TypeVisitor/ContainsStaticVisitor.php | 30 ++++ 4 files changed, 123 insertions(+), 110 deletions(-) create mode 100644 src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 61a039758b6..a1dcc3a49a3 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + $comment_block->tags['variablesfrom'][0] @@ -68,7 +68,7 @@ - + $assertion->rule[0] $assertion->rule[0] $assertion->rule[0] @@ -90,12 +90,6 @@ $expr->getArgs()[0] $expr->getArgs()[0] $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] - $expr->getArgs()[0] $expr->getArgs()[1] $expr->getArgs()[1] $get_debug_type_expr->getArgs()[0] @@ -220,16 +214,9 @@ - - !$this->is_accepting_new_requests - $this->is_accepting_new_requests - $parts[1] - - TypeDoesNotContainType - @@ -306,11 +293,12 @@ - + get get get getClassTemplateTypes + has $candidate_param_type->from_template_default diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php index a4d502bd61a..8cdfd6c7560 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/ExistingAtomicStaticCallAnalyzer.php @@ -21,6 +21,7 @@ use Psalm\Internal\Type\TemplateInferredTypeReplacer; use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TypeExpander; +use Psalm\Internal\TypeVisitor\ContainsStaticVisitor; use Psalm\Issue\AbstractMethodCall; use Psalm\Issue\ImpureMethodCall; use Psalm\IssueBuffer; @@ -630,16 +631,8 @@ private static function getMethodReturnType( */ private static function hasStaticInType(Type\TypeNode $type): bool { - if ($type instanceof TNamedObject && ($type->value === 'static' || $type->is_static)) { - return true; - } - - foreach ($type->getChildNodes() as $child_type) { - if (self::hasStaticInType($child_type)) { - return true; - } - } - - return false; + $visitor = new ContainsStaticVisitor; + $visitor->traverse($type); + return $visitor->matches(); } } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php index 740daa859d7..ee327d72eeb 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/VariableFetchAnalyzer.php @@ -668,89 +668,91 @@ public static function getGlobalType(string $var_id, int $codebase_analysis_php_ $bool_string_helper = new Union([new TBool(), new TString()]); $bool_string_helper->possibly_undefined = true; - $detailed_type = new TKeyedArray([ - // https://www.php.net/manual/en/reserved.variables.server.php - 'PHP_SELF' => $non_empty_string_helper, - 'argv' => $argv_helper, - 'argc' => $argc_helper, - 'GATEWAY_INTERFACE' => $non_empty_string_helper, - 'SERVER_ADDR' => $non_empty_string_helper, - 'SERVER_NAME' => $non_empty_string_helper, - 'SERVER_SOFTWARE' => $non_empty_string_helper, - 'SERVER_PROTOCOL' => $non_empty_string_helper, - 'REQUEST_METHOD' => $non_empty_string_helper, - 'REQUEST_TIME' => $request_time_helper, - 'REQUEST_TIME_FLOAT' => $request_time_float_helper, - 'QUERY_STRING' => $string_helper, - 'DOCUMENT_ROOT' => $non_empty_string_helper, - 'HTTP_ACCEPT' => $non_empty_string_helper, - 'HTTP_ACCEPT_CHARSET' => $non_empty_string_helper, - 'HTTP_ACCEPT_ENCODING' => $non_empty_string_helper, - 'HTTP_ACCEPT_LANGUAGE' => $non_empty_string_helper, - 'HTTP_CONNECTION' => $non_empty_string_helper, - 'HTTP_HOST' => $non_empty_string_helper, - 'HTTP_REFERER' => $non_empty_string_helper, - 'HTTP_USER_AGENT' => $non_empty_string_helper, - 'HTTPS' => $string_helper, - 'REMOTE_ADDR' => $non_empty_string_helper, - 'REMOTE_HOST' => $non_empty_string_helper, - 'REMOTE_PORT' => $string_helper, - 'REMOTE_USER' => $non_empty_string_helper, - 'REDIRECT_REMOTE_USER' => $non_empty_string_helper, - 'SCRIPT_FILENAME' => $non_empty_string_helper, - 'SERVER_ADMIN' => $non_empty_string_helper, - 'SERVER_PORT' => $non_empty_string_helper, - 'SERVER_SIGNATURE' => $non_empty_string_helper, - 'PATH_TRANSLATED' => $non_empty_string_helper, - 'SCRIPT_NAME' => $non_empty_string_helper, - 'REQUEST_URI' => $non_empty_string_helper, - 'PHP_AUTH_DIGEST' => $non_empty_string_helper, - 'PHP_AUTH_USER' => $non_empty_string_helper, - 'PHP_AUTH_PW' => $non_empty_string_helper, - 'AUTH_TYPE' => $non_empty_string_helper, - 'PATH_INFO' => $non_empty_string_helper, - 'ORIG_PATH_INFO' => $non_empty_string_helper, - // misc from RFC not included above already http://www.faqs.org/rfcs/rfc3875.html - 'CONTENT_LENGTH' => $string_helper, - 'CONTENT_TYPE' => $string_helper, - // common, misc stuff - 'FCGI_ROLE' => $non_empty_string_helper, - 'HOME' => $non_empty_string_helper, - 'HTTP_CACHE_CONTROL' => $non_empty_string_helper, - 'HTTP_COOKIE' => $non_empty_string_helper, - 'HTTP_PRIORITY' => $non_empty_string_helper, - 'PATH' => $non_empty_string_helper, - 'REDIRECT_STATUS' => $non_empty_string_helper, - 'REQUEST_SCHEME' => $non_empty_string_helper, - 'USER' => $non_empty_string_helper, - // common, misc headers - 'HTTP_UPGRADE_INSECURE_REQUESTS' => $non_empty_string_helper, - 'HTTP_X_FORWARDED_PROTO' => $non_empty_string_helper, - 'HTTP_CLIENT_IP' => $non_empty_string_helper, - 'HTTP_X_REAL_IP' => $non_empty_string_helper, - 'HTTP_X_FORWARDED_FOR' => $non_empty_string_helper, - 'HTTP_CF_CONNECTING_IP' => $non_empty_string_helper, - 'HTTP_CF_IPCOUNTRY' => $non_empty_string_helper, - 'HTTP_CF_VISITOR' => $non_empty_string_helper, - 'HTTP_CDN_LOOP' => $non_empty_string_helper, - // common, misc browser headers - 'HTTP_DNT' => $non_empty_string_helper, - 'HTTP_SEC_FETCH_DEST' => $non_empty_string_helper, - 'HTTP_SEC_FETCH_USER' => $non_empty_string_helper, - 'HTTP_SEC_FETCH_MODE' => $non_empty_string_helper, - 'HTTP_SEC_FETCH_SITE' => $non_empty_string_helper, - 'HTTP_SEC_CH_UA_PLATFORM' => $non_empty_string_helper, - 'HTTP_SEC_CH_UA_MOBILE' => $non_empty_string_helper, - 'HTTP_SEC_CH_UA' => $non_empty_string_helper, - // phpunit - 'APP_DEBUG' => $bool_string_helper, - 'APP_ENV' => $string_helper, - ]); - - // generic case for all other elements - $detailed_type->previous_key_type = Type::getNonEmptyString(); - $detailed_type->previous_value_type = Type::getString(); - + $detailed_type = new TKeyedArray( + [ + // https://www.php.net/manual/en/reserved.variables.server.php + 'PHP_SELF' => $non_empty_string_helper, + 'argv' => $argv_helper, + 'argc' => $argc_helper, + 'GATEWAY_INTERFACE' => $non_empty_string_helper, + 'SERVER_ADDR' => $non_empty_string_helper, + 'SERVER_NAME' => $non_empty_string_helper, + 'SERVER_SOFTWARE' => $non_empty_string_helper, + 'SERVER_PROTOCOL' => $non_empty_string_helper, + 'REQUEST_METHOD' => $non_empty_string_helper, + 'REQUEST_TIME' => $request_time_helper, + 'REQUEST_TIME_FLOAT' => $request_time_float_helper, + 'QUERY_STRING' => $string_helper, + 'DOCUMENT_ROOT' => $non_empty_string_helper, + 'HTTP_ACCEPT' => $non_empty_string_helper, + 'HTTP_ACCEPT_CHARSET' => $non_empty_string_helper, + 'HTTP_ACCEPT_ENCODING' => $non_empty_string_helper, + 'HTTP_ACCEPT_LANGUAGE' => $non_empty_string_helper, + 'HTTP_CONNECTION' => $non_empty_string_helper, + 'HTTP_HOST' => $non_empty_string_helper, + 'HTTP_REFERER' => $non_empty_string_helper, + 'HTTP_USER_AGENT' => $non_empty_string_helper, + 'HTTPS' => $string_helper, + 'REMOTE_ADDR' => $non_empty_string_helper, + 'REMOTE_HOST' => $non_empty_string_helper, + 'REMOTE_PORT' => $string_helper, + 'REMOTE_USER' => $non_empty_string_helper, + 'REDIRECT_REMOTE_USER' => $non_empty_string_helper, + 'SCRIPT_FILENAME' => $non_empty_string_helper, + 'SERVER_ADMIN' => $non_empty_string_helper, + 'SERVER_PORT' => $non_empty_string_helper, + 'SERVER_SIGNATURE' => $non_empty_string_helper, + 'PATH_TRANSLATED' => $non_empty_string_helper, + 'SCRIPT_NAME' => $non_empty_string_helper, + 'REQUEST_URI' => $non_empty_string_helper, + 'PHP_AUTH_DIGEST' => $non_empty_string_helper, + 'PHP_AUTH_USER' => $non_empty_string_helper, + 'PHP_AUTH_PW' => $non_empty_string_helper, + 'AUTH_TYPE' => $non_empty_string_helper, + 'PATH_INFO' => $non_empty_string_helper, + 'ORIG_PATH_INFO' => $non_empty_string_helper, + // misc from RFC not included above already http://www.faqs.org/rfcs/rfc3875.html + 'CONTENT_LENGTH' => $string_helper, + 'CONTENT_TYPE' => $string_helper, + // common, misc stuff + 'FCGI_ROLE' => $non_empty_string_helper, + 'HOME' => $non_empty_string_helper, + 'HTTP_CACHE_CONTROL' => $non_empty_string_helper, + 'HTTP_COOKIE' => $non_empty_string_helper, + 'HTTP_PRIORITY' => $non_empty_string_helper, + 'PATH' => $non_empty_string_helper, + 'REDIRECT_STATUS' => $non_empty_string_helper, + 'REQUEST_SCHEME' => $non_empty_string_helper, + 'USER' => $non_empty_string_helper, + // common, misc headers + 'HTTP_UPGRADE_INSECURE_REQUESTS' => $non_empty_string_helper, + 'HTTP_X_FORWARDED_PROTO' => $non_empty_string_helper, + 'HTTP_CLIENT_IP' => $non_empty_string_helper, + 'HTTP_X_REAL_IP' => $non_empty_string_helper, + 'HTTP_X_FORWARDED_FOR' => $non_empty_string_helper, + 'HTTP_CF_CONNECTING_IP' => $non_empty_string_helper, + 'HTTP_CF_IPCOUNTRY' => $non_empty_string_helper, + 'HTTP_CF_VISITOR' => $non_empty_string_helper, + 'HTTP_CDN_LOOP' => $non_empty_string_helper, + // common, misc browser headers + 'HTTP_DNT' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_DEST' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_USER' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_MODE' => $non_empty_string_helper, + 'HTTP_SEC_FETCH_SITE' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA_PLATFORM' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA_MOBILE' => $non_empty_string_helper, + 'HTTP_SEC_CH_UA' => $non_empty_string_helper, + // phpunit + 'APP_DEBUG' => $bool_string_helper, + 'APP_ENV' => $string_helper, + ], + null, + false, + Type::getNonEmptyString(), + Type::getString() + ); + return new Union([$detailed_type]); } diff --git a/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php b/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php new file mode 100644 index 00000000000..da0a266b0e7 --- /dev/null +++ b/src/Psalm/Internal/TypeVisitor/ContainsStaticVisitor.php @@ -0,0 +1,30 @@ +value === 'static' || $type->is_static)) { + $this->contains_static = true; + return TypeVisitor::STOP_TRAVERSAL; + } + return null; + } + + public function matches(): bool + { + return $this->contains_static; + } +}