diff --git a/UPGRADING.md b/UPGRADING.md index 55cb4f65119..2bf88c584a4 100644 --- a/UPGRADING.md +++ b/UPGRADING.md @@ -44,6 +44,14 @@ - [BC] `Psalm\CodeLocation\Raw`, `Psalm\CodeLocation\ParseErrorLocation`, `Psalm\CodeLocation\DocblockTypeLocation`, `Psalm\Report\CountReport`, `Psalm\Type\Atomic\TNonEmptyArray` are now all final. +- [BC] `Psalm\Config` is now final. + +- [BC] The return type of `Psalm\Plugin\ArgTypeInferer::infer` changed from `Union|false` to `Union|null` + +- [BC] The `extra_types` property and `setIntersectionTypes` method of `Psalm\Type\Atomic\TTypeAlias` were removed. + +- [BC] Methods `convertSeverity` and `calculateFingerprint` of `Psalm\Report\CodeClimateReport` were removed. + # Upgrading from Psalm 4 to Psalm 5 ## Changed diff --git a/composer.json b/composer.json index 0a56c517c06..0a36e057c1f 100644 --- a/composer.json +++ b/composer.json @@ -50,6 +50,7 @@ "amphp/phpunit-util": "^3", "bamarni/composer-bin-plugin": "^1.4", "brianium/paratest": "^6.9", + "dg/bypass-finals": "^1.5", "mockery/mockery": "^1.5", "nunomaduro/mock-final-classes": "^1.1", "php-parallel-lint/php-parallel-lint": "^1.2", diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index eedbac5dea9..782ed5d8fdf 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -12886,10 +12886,16 @@ 'str_contains' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'str_ends_with' => ['bool', 'haystack'=>'string', 'needle'=>'string'], 'str_getcsv' => ['non-empty-list', 'string'=>'string', 'separator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], -'str_ireplace' => ['string|string[]', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_count='=>'int'], +'str_ireplace' => ['string', 'search'=>'string', 'replace'=>'string', 'subject'=>'string', '&w_count='=>'int'], +'str_ireplace\'1' => ['string[]', 'search'=>'string', 'replace'=>'string', 'subject'=>'array', '&w_count='=>'int'], +'str_ireplace\'2' => ['string', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'string', '&w_count='=>'int'], +'str_ireplace\'3' => ['string[]', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'array', '&w_count='=>'int'], 'str_pad' => ['string', 'string'=>'string', 'length'=>'int', 'pad_string='=>'string', 'pad_type='=>'int'], 'str_repeat' => ['string', 'string'=>'string', 'times'=>'int'], -'str_replace' => ['string|string[]', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_count='=>'int'], +'str_replace' => ['string', 'search'=>'string', 'replace'=>'string', 'subject'=>'string', '&w_count='=>'int'], +'str_replace\'1' => ['string[]', 'search'=>'string', 'replace'=>'string', 'subject'=>'array', '&w_count='=>'int'], +'str_replace\'2' => ['string', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'string', '&w_count='=>'int'], +'str_replace\'3' => ['string[]', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'array', '&w_count='=>'int'], 'str_rot13' => ['string', 'string'=>'string'], 'str_shuffle' => ['string', 'string'=>'string'], 'str_split' => ['list', 'string'=>'string', 'length='=>'positive-int'], @@ -13015,7 +13021,8 @@ 'substr' => ['string', 'string'=>'string', 'offset'=>'int', 'length='=>'?int'], 'substr_compare' => ['int', 'haystack'=>'string', 'needle'=>'string', 'offset'=>'int', 'length='=>'?int', 'case_insensitive='=>'bool'], 'substr_count' => ['int', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'length='=>'?int'], -'substr_replace' => ['string|string[]', 'string'=>'string|string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]|null'], +'substr_replace' => ['string', 'string'=>'string', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]|null'], +'substr_replace\'1' => ['string[]', 'string'=>'string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]|null'], 'suhosin_encrypt_cookie' => ['string|false', 'name'=>'string', 'value'=>'string'], 'suhosin_get_raw_cookies' => ['array'], 'SVM::__construct' => ['void'], diff --git a/dictionaries/CallMap_80_delta.php b/dictionaries/CallMap_80_delta.php index a9cde9ff992..29227b632bc 100644 --- a/dictionaries/CallMap_80_delta.php +++ b/dictionaries/CallMap_80_delta.php @@ -2589,8 +2589,12 @@ 'new' => ['string', 'string'=>'string', 'offset'=>'int', 'length='=>'?int'], ], 'substr_replace' => [ - 'old' => ['string|string[]', 'string'=>'string|string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]'], - 'new' => ['string|string[]', 'string'=>'string|string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]|null'], + 'old' => ['string', 'string'=>'string', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]'], + 'new' => ['string', 'string'=>'string', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]|null'], + ], + 'substr_replace\'1' => [ + 'old' => ['string[]', 'string'=>'string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]'], + 'new' => ['string[]', 'string'=>'string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]|null'], ], 'tidy_parse_file' => [ 'old' => ['tidy', 'filename'=>'string', 'config='=>'array|string', 'encoding='=>'string', 'useIncludePath='=>'bool'], diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 9565bfa203d..5ff4033aff0 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -14303,10 +14303,16 @@ 'stomp_unsubscribe' => ['bool', 'link'=>'resource', 'destination'=>'string', 'headers='=>'?array'], 'stomp_version' => ['string'], 'str_getcsv' => ['non-empty-list', 'string'=>'string', 'separator='=>'string', 'enclosure='=>'string', 'escape='=>'string'], - 'str_ireplace' => ['string|string[]', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_count='=>'int'], + 'str_ireplace' => ['string', 'search'=>'string', 'replace'=>'string', 'subject'=>'string', '&w_count='=>'int'], + 'str_ireplace\'1' => ['string[]', 'search'=>'string', 'replace'=>'string', 'subject'=>'array', '&w_count='=>'int'], + 'str_ireplace\'2' => ['string', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'string', '&w_count='=>'int'], + 'str_ireplace\'3' => ['string[]', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'array', '&w_count='=>'int'], 'str_pad' => ['string', 'string'=>'string', 'length'=>'int', 'pad_string='=>'string', 'pad_type='=>'int'], 'str_repeat' => ['string', 'string'=>'string', 'times'=>'int'], - 'str_replace' => ['string|string[]', 'search'=>'string|array', 'replace'=>'string|array', 'subject'=>'string|array', '&w_count='=>'int'], + 'str_replace' => ['string', 'search'=>'string', 'replace'=>'string', 'subject'=>'string', '&w_count='=>'int'], + 'str_replace\'1' => ['string[]', 'search'=>'string', 'replace'=>'string', 'subject'=>'array', '&w_count='=>'int'], + 'str_replace\'2' => ['string', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'string', '&w_count='=>'int'], + 'str_replace\'3' => ['string[]', 'search'=>'array', 'replace'=>'string|string[]', 'subject'=>'array', '&w_count='=>'int'], 'str_rot13' => ['string', 'string'=>'string'], 'str_shuffle' => ['string', 'string'=>'string'], 'str_split' => ['non-empty-list', 'string'=>'string', 'length='=>'positive-int'], @@ -14430,7 +14436,8 @@ 'substr' => ['string|false', 'string'=>'string', 'offset'=>'int', 'length='=>'int'], 'substr_compare' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset'=>'int', 'length='=>'int', 'case_insensitive='=>'bool'], 'substr_count' => ['int', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'length='=>'int'], - 'substr_replace' => ['string|string[]', 'string'=>'string|string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]'], + 'substr_replace' => ['string', 'string'=>'string', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]'], + 'substr_replace\'1' => ['string[]', 'string'=>'string[]', 'replace'=>'string|string[]', 'offset'=>'int|int[]', 'length='=>'int|int[]'], 'suhosin_encrypt_cookie' => ['string|false', 'name'=>'string', 'value'=>'string'], 'suhosin_get_raw_cookies' => ['array'], 'svm::crossvalidate' => ['float', 'problem'=>'array', 'number_of_folds'=>'int'], diff --git a/dictionaries/ImpureFunctionsList.php b/dictionaries/ImpureFunctionsList.php index 70b1fad7d92..d3a3f7ce0a3 100644 --- a/dictionaries/ImpureFunctionsList.php +++ b/dictionaries/ImpureFunctionsList.php @@ -67,6 +67,8 @@ 'socket_set_block' => true, 'socket_set_nonblock' => true, 'socket_listen' => true, + 'stream_socket_shutdown' => true, + 'socket_shutdown' => true, // meta calls 'call_user_func' => true, 'call_user_func_array' => true, @@ -93,7 +95,6 @@ 'mcrypt_generic_deinit' => true, 'mcrypt_module_close' => true, // internal optimisation - 'opcache_compile_file' => true, 'clearstatcache' => true, // process-related 'pcntl_signal' => true, diff --git a/dictionaries/scripts/update_signaturemap_from_other_tool.php b/dictionaries/scripts/update_signaturemap_from_other_tool.php index 21fb61166d5..d50e88ee0a8 100644 --- a/dictionaries/scripts/update_signaturemap_from_other_tool.php +++ b/dictionaries/scripts/update_signaturemap_from_other_tool.php @@ -30,7 +30,7 @@ $removed_foreign_functions ); -uksort($new_local, fn($a, $b) => strtolower($a) <=> strtolower($b)); +uksort($new_local, static fn($a, $b) => strtolower($a) <=> strtolower($b)); foreach ($new_local as $name => $data) { if (!is_array($data)) { diff --git a/docs/security_analysis/custom_taint_sources.md b/docs/security_analysis/custom_taint_sources.md index b7b140706e6..d3bdb0a3205 100644 --- a/docs/security_analysis/custom_taint_sources.md +++ b/docs/security_analysis/custom_taint_sources.md @@ -68,6 +68,7 @@ class BadSqlTainter implements AfterExpressionAnalysisInterface ); } } + return null; } } ``` diff --git a/examples/plugins/ClassUnqualifier.php b/examples/plugins/ClassUnqualifier.php index 3f4f14f517a..57a9f86da08 100644 --- a/examples/plugins/ClassUnqualifier.php +++ b/examples/plugins/ClassUnqualifier.php @@ -44,7 +44,7 @@ public static function afterClassLikeExistenceCheck( $new_candidate_type = implode( '', array_map( - fn($f) => $f[0], + static fn($f) => $f[0], $type_tokens, ), ); diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 748be83439b..4f2c25cff79 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -1,7 +1,7 @@ - + tags['variablesfrom'][0]]]> @@ -16,6 +16,9 @@ $deprecated_element_xml + + $this + @@ -622,6 +625,16 @@ hasLowercaseString + + + Config + + + public function __construct() + public function getComposerFilePathForClassLike(string $fq_classlike_name): bool + public function getProjectDirectories(): array + + diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index 1e2e47498b7..7f09560b899 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -71,7 +71,6 @@ use UnexpectedValueException; use function array_combine; -use function array_merge; use function array_pop; use function array_reverse; use function array_values; @@ -1605,7 +1604,7 @@ public function getCompletionItemsForClassishThing( ); } - $completion_items = array_merge($completion_items, array_values($pseudo_property_types)); + $completion_items = [...$completion_items, ...array_values($pseudo_property_types)]; foreach ($class_storage->declaring_property_ids as $property_name => $declaring_class) { $property_storage = $this->properties->getStorage( diff --git a/src/Psalm/Config.php b/src/Psalm/Config.php index d86d787c73b..8759c0ddbcd 100644 --- a/src/Psalm/Config.php +++ b/src/Psalm/Config.php @@ -69,7 +69,6 @@ use function flock; use function fopen; use function function_exists; -use function get_class; use function get_defined_constants; use function get_defined_functions; use function getcwd; @@ -98,7 +97,9 @@ use function scandir; use function sha1; use function simplexml_import_dom; +use function str_contains; use function str_replace; +use function str_starts_with; use function strlen; use function strpos; use function strrpos; @@ -127,13 +128,13 @@ * @psalm-suppress PropertyNotSetInConstructor * @psalm-consistent-constructor */ -class Config +final class Config { private const DEFAULT_FILE_NAME = 'psalm.xml'; - public const CONFIG_NAMESPACE = 'https://getpsalm.org/schema/config'; - public const REPORT_INFO = 'info'; - public const REPORT_ERROR = 'error'; - public const REPORT_SUPPRESS = 'suppress'; + final public const CONFIG_NAMESPACE = 'https://getpsalm.org/schema/config'; + final public const REPORT_INFO = 'info'; + final public const REPORT_ERROR = 'error'; + final public const REPORT_SUPPRESS = 'suppress'; /** * @var array @@ -172,7 +173,7 @@ class Config * * @var array */ - protected array $universal_object_crates; + private array $universal_object_crates; /** * @var static|null @@ -222,7 +223,7 @@ class Config protected ?ProjectFileFilter $project_files = null; - protected ?ProjectFileFilter $extra_files = null; + private ?ProjectFileFilter $extra_files = null; /** * The base directory of this config file @@ -426,7 +427,7 @@ class Config private ?IncludeCollector $include_collector = null; - protected ?TaintAnalysisFileFilter $taint_analysis_ignored_files = null; + private ?TaintAnalysisFileFilter $taint_analysis_ignored_files = null; /** * @var bool whether to emit a backtrace of emitted issues to stderr @@ -874,7 +875,6 @@ private static function processConfigDeprecations( /** * @param non-empty-string $file_contents * @psalm-suppress MixedAssignment - * @psalm-suppress MixedArgument * @psalm-suppress MixedPropertyFetch * @throws ConfigException */ @@ -963,15 +963,15 @@ private static function fromXmlAndPaths( if (file_exists($composer_json_path)) { $composer_json_contents = file_get_contents($composer_json_path); assert($composer_json_contents !== false); - $composer_json = json_decode($composer_json_contents, true); + $composer_json = json_decode($composer_json_contents, true, 512, JSON_THROW_ON_ERROR); if (!is_array($composer_json)) { throw new UnexpectedValueException('Invalid composer.json at ' . $composer_json_path); } } $required_extensions = []; foreach (($composer_json["require"] ?? []) as $required => $_) { - if (strpos($required, "ext-") === 0) { - $required_extensions[strtolower(substr($required, 4))] = true; + if (str_starts_with((string) $required, "ext-")) { + $required_extensions[strtolower(substr((string) $required, 4))] = true; } } foreach ($required_extensions as $required_ext => $_) { @@ -1649,7 +1649,7 @@ public function reportIssueInFile(string $issue_type, string $file_path): bool try { $file_storage = $codebase->file_storage_provider->get($file_path); $dependent_files += $file_storage->required_by_file_paths; - } catch (InvalidArgumentException $e) { + } catch (InvalidArgumentException) { // do nothing } } @@ -1700,7 +1700,7 @@ public function trackTaintsInPath(string $file_path): bool public function getReportingLevelForIssue(CodeIssue $e): string { - $fqcn_parts = explode('\\', get_class($e)); + $fqcn_parts = explode('\\', $e::class); $issue_type = array_pop($fqcn_parts); $reporting_level = null; @@ -1765,17 +1765,17 @@ public static function getParentIssueType(string $issue_type): ?string return null; } - if (strpos($issue_type, 'Possibly') === 0) { + if (str_starts_with($issue_type, 'Possibly')) { $stripped_issue_type = (string) preg_replace('/^Possibly(False|Null)?/', '', $issue_type, 1); - if (strpos($stripped_issue_type, 'Invalid') === false && strpos($stripped_issue_type, 'Un') !== 0) { + if (!str_contains($stripped_issue_type, 'Invalid') && !str_starts_with($stripped_issue_type, 'Un')) { $stripped_issue_type = 'Invalid' . $stripped_issue_type; } return $stripped_issue_type; } - if (strpos($issue_type, 'Tainted') === 0) { + if (str_starts_with($issue_type, 'Tainted')) { return 'TaintedInput'; } @@ -2298,7 +2298,7 @@ public function visitComposerAutoloadFiles(ProjectAnalyzer $project_analyzer, ?P $codebase->classlikes->forgetMissingClassLikes(); $this->include_collector->runAndCollect( - [$this, 'requireAutoloader'], + $this->requireAutoloader(...), ); } @@ -2324,7 +2324,8 @@ public function visitComposerAutoloadFiles(ProjectAnalyzer $project_analyzer, ?P } } - public function getComposerFilePathForClassLike(string $fq_classlike_name): string|false + /** @return string|false */ + public function getComposerFilePathForClassLike(string $fq_classlike_name): string|bool { if (!$this->composer_class_loader) { return false; @@ -2502,7 +2503,7 @@ public function getPHPVersionFromComposerJson(): ?string $composer_json_contents = file_get_contents($composer_json_path); assert($composer_json_contents !== false); $composer_json = json_decode($composer_json_contents, true, 512, JSON_THROW_ON_ERROR); - } catch (JsonException $e) { + } catch (JsonException) { $composer_json = null; } diff --git a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php index cf54d72c3ab..2e89585d8f6 100644 --- a/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassLikeAnalyzer.php @@ -206,6 +206,7 @@ public static function checkFullyQualifiedClassLikeName( ?string $calling_method_id, array $suppressed_issues, ?ClassLikeNameOptions $options = null, + bool $check_classes = true, ): ?bool { if ($options === null) { $options = new ClassLikeNameOptions(); @@ -278,6 +279,9 @@ public static function checkFullyQualifiedClassLikeName( && !($interface_exists && $options->allow_interface) && !($enum_exists && $options->allow_enum) ) { + if (!$check_classes) { + return null; + } if (!$options->allow_trait || !$codebase->classlikes->traitExists($fq_class_name, $code_location)) { if ($options->from_docblock) { if (IssueBuffer::accepts( @@ -703,7 +707,7 @@ protected function checkTemplateParams( && $storage->template_types && $storage->template_covariants && ($local_offset - = array_search($t->param_name, array_keys($storage->template_types))) + = array_search($t->param_name, array_keys($storage->template_types), true)) !== false && !empty($storage->template_covariants[$local_offset]) ) { diff --git a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php index aabd1747028..efbcee99bad 100644 --- a/src/Psalm/Internal/Analyzer/CommentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/CommentAnalyzer.php @@ -27,7 +27,6 @@ use Psalm\Type\Union; use UnexpectedValueException; -use function array_merge; use function count; use function is_string; use function preg_match; @@ -402,7 +401,7 @@ public static function splitDocLine(string $return_block): array $remaining = trim((string) preg_replace('@^[ \t]*\* *@m', ' ', substr($return_block, $i + 1))); if ($remaining) { - return array_merge([rtrim($type)], preg_split('/\s+/', $remaining) ?: []); + return [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 bc571e03548..cca71e5b885 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLike/ReturnTypeAnalyzer.php @@ -199,6 +199,7 @@ public static function verifyReturnType( ) ) && !$return_type->isVoid() + && !$return_type->isNever() && !$inferred_yield_types && (!$function_like_storage || !$function_like_storage->has_yield) && $function_returns_implicitly @@ -224,7 +225,7 @@ public static function verifyReturnType( ) { if (IssueBuffer::accepts( new InvalidReturnType( - $cased_method_id . ' is not expected to return any values but it does, ' + $cased_method_id . ' is not expected to return, but it does, ' . 'either implicitly or explicitly', $return_type_location, ), diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 16ccc996926..80bf330b332 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -1262,6 +1262,17 @@ private function processParams( ); } + if ($param_type->isNever()) { + IssueBuffer::maybeAdd( + new ReservedWord( + 'Parameter cannot be never', + $function_param->type_location, + 'never', + ), + $this->suppressed_issues, + ); + } + if ($param_type->check( $this->source, $function_param->type_location, diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index 1a4b01f5fbd..a750940a27a 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -315,6 +315,16 @@ private static function checkForObviousMethodMismatches( ); } + if ($guide_method_storage->returns_by_ref && !$implementer_method_storage->returns_by_ref) { + IssueBuffer::maybeAdd( + new MethodSignatureMismatch( + 'Method ' . $cased_implementer_method_id . ' must return by-reference', + $code_location, + ), + $suppressed_issues + $implementer_classlike_storage->suppressed_issues, + ); + } + if ($guide_method_storage->external_mutation_free && !$implementer_method_storage->external_mutation_free && !$guide_method_storage->mutation_free_inferred diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php index 54fd5391bcd..d248ba90b57 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/ForeachAnalyzer.php @@ -1092,7 +1092,7 @@ private static function getExtendedType( ): ?Union { if ($calling_class === $template_class) { if (isset($class_template_types[$template_name]) && $calling_type_params) { - $offset = array_search($template_name, array_keys($class_template_types)); + $offset = array_search($template_name, array_keys($class_template_types), true); if ($offset !== false && isset($calling_type_params[$offset])) { return $calling_type_params[$offset]; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php index fa721397922..4f3668d129d 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/AssignmentAnalyzer.php @@ -671,16 +671,14 @@ private static function analyzeAssignment( return false; } - if ($context->check_classes) { - if (StaticPropertyAssignmentAnalyzer::analyze( - $statements_analyzer, - $assign_var, - $assign_value, - $assign_value_type, - $context, - ) === false) { - return false; - } + if (StaticPropertyAssignmentAnalyzer::analyze( + $statements_analyzer, + $assign_var, + $assign_value, + $assign_value_type, + $context, + ) === false) { + return false; } if ($var_id) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php index b7c3ab3969c..3086a7ee031 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOpAnalyzer.php @@ -34,6 +34,7 @@ use Psalm\Type\Union; use UnexpectedValueException; +use function array_merge; use function in_array; use function strlen; @@ -172,6 +173,14 @@ public static function analyze( $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); if ($stmt_left_type && $stmt_left_type->parent_nodes) { + // numeric types can't be tainted html or has_quotes, neither can bool + if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph + && $stmt_left_type->isSingle() + && ($stmt_left_type->isInt() || $stmt_left_type->isFloat() || $stmt_left_type->isBool()) + ) { + $removed_taints = array_merge($removed_taints, array('html', 'has_quotes')); + } + foreach ($stmt_left_type->parent_nodes as $parent_node) { $statements_analyzer->data_flow_graph->addPath( $parent_node, @@ -184,6 +193,14 @@ public static function analyze( } if ($stmt_right_type && $stmt_right_type->parent_nodes) { + // numeric types can't be tainted html or has_quotes, neither can bool + if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph + && $stmt_right_type->isSingle() + && ($stmt_right_type->isInt() || $stmt_right_type->isFloat() || $stmt_right_type->isBool()) + ) { + $removed_taints = array_merge($removed_taints, array('html', 'has_quotes')); + } + foreach ($stmt_right_type->parent_nodes as $parent_node) { $statements_analyzer->data_flow_graph->addPath( $parent_node, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 060ed42d1a7..6fc3a58d62e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -62,6 +62,7 @@ use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; +use function array_merge; use function count; use function explode; use function implode; @@ -1486,19 +1487,19 @@ private static function processTaintedness( return; } - // numeric types can't be tainted, neither can bool + $event = new AddRemoveTaintsEvent($expr, $context, $statements_analyzer, $codebase); + + $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); + $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); + + // numeric types can't be tainted html or has_quotes, neither can bool if ($statements_analyzer->data_flow_graph instanceof TaintFlowGraph && $input_type->isSingle() && ($input_type->isInt() || $input_type->isFloat() || $input_type->isBool()) ) { - return; + $removed_taints = array_merge($removed_taints, array('html', 'has_quotes')); } - $event = new AddRemoveTaintsEvent($expr, $context, $statements_analyzer, $codebase); - - $added_taints = $codebase->config->eventDispatcher->dispatchAddTaints($event); - $removed_taints = $codebase->config->eventDispatcher->dispatchRemoveTaints($event); - if ($function_param->type && $function_param->type->isString() && !$input_type->isString()) { $input_type = CastAnalyzer::castStringAttempt( $statements_analyzer, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalysisResult.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalysisResult.php index 3993d78e0e9..46927735f74 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalysisResult.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalysisResult.php @@ -28,7 +28,7 @@ final class AtomicMethodCallAnalysisResult public array $invalid_method_call_types = []; /** - * @var array + * @var array */ public array $existent_method_ids = []; 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 6ca257415ee..7dde48e6d9e 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/AtomicMethodCallAnalyzer.php @@ -186,6 +186,7 @@ public static function analyze( $context->calling_method_id, $statements_analyzer->getSuppressedIssues(), new ClassLikeNameOptions(true, false, true, true, $lhs_type_part->from_docblock), + $context->check_classes, ); } @@ -340,7 +341,7 @@ public static function analyze( $all_intersection_return_type = null; $all_intersection_existent_method_ids = []; - // insersection types are also fun, they also complicate matters + // intersection types are also fun, they also complicate matters if ($intersection_types) { [$all_intersection_return_type, $all_intersection_existent_method_ids] = self::getIntersectionReturnType( @@ -527,7 +528,7 @@ public static function analyze( /** * @param TNamedObject|TTemplateParam $lhs_type_part * @param array $intersection_types - * @return array{?Union, array} + * @return array{?Union, array} */ private static function getIntersectionReturnType( StatementsAnalyzer $statements_analyzer, @@ -648,7 +649,8 @@ private static function handleInvalidClass( && $stmt->name instanceof PhpParser\Node\Identifier && isset($lhs_type_part->methods[strtolower($stmt->name->name)]) ) { - $result->existent_method_ids[] = $lhs_type_part->methods[strtolower($stmt->name->name)]; + $method_id = $lhs_type_part->methods[strtolower($stmt->name->name)]; + $result->existent_method_ids[$method_id] = true; } elseif (!$is_intersection) { if ($stmt->name instanceof PhpParser\Node\Identifier) { $codebase->analyzer->addMixedMemberName( @@ -751,6 +753,7 @@ private static function handleTemplatedMixins( $param_position = array_search( $mixin->param_name, $template_type_keys, + true, ); if ($param_position !== false @@ -917,7 +920,7 @@ private static function handleCallableObject( ?TemplateResult $inferred_template_result = null, ): void { $method_id = 'object::__invoke'; - $result->existent_method_ids[] = $method_id; + $result->existent_method_ids[$method_id] = true; $result->has_valid_method_call_type = true; if ($lhs_type_part_callable !== null) { 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 d0de3e5995a..b1d1d0226cb 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/ExistingAtomicMethodCallAnalyzer.php @@ -89,7 +89,8 @@ public static function analyze( $cased_method_id = $fq_class_name . '::' . $stmt_name->name; - $result->existent_method_ids[] = $method_id->__toString(); + + $result->existent_method_ids[$method_id->__toString()] = true; if ($context->collect_initializations && $context->calling_method_id) { [$calling_method_class] = explode('::', $context->calling_method_id); 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 efa26eba6e4..6241127b579 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php @@ -54,7 +54,7 @@ public static function handleMagicMethod( if ($stmt->isFirstClassCallable()) { if (isset($class_storage->pseudo_methods[$method_name_lc])) { $result->has_valid_method_call_type = true; - $result->existent_method_ids[] = $method_id->__toString(); + $result->existent_method_ids[$method_id->__toString()] = true; $result->return_type = self::createFirstClassCallableReturnType( $class_storage->pseudo_methods[$method_name_lc], ); @@ -112,7 +112,7 @@ public static function handleMagicMethod( if ($found_method_and_class_storage) { $result->has_valid_method_call_type = true; - $result->existent_method_ids[] = $method_id->__toString(); + $result->existent_method_ids[$method_id->__toString()] = true; [$pseudo_method_storage, $defining_class_storage] = $found_method_and_class_storage; @@ -200,7 +200,7 @@ public static function handleMagicMethod( } $result->has_valid_method_call_type = true; - $result->existent_method_ids[] = $method_id->__toString(); + $result->existent_method_ids[$method_id->__toString()] = true; $array_values = array_map( static fn(PhpParser\Node\Arg $arg): PhpParser\Node\Expr\ArrayItem => new VirtualArrayItem( @@ -237,7 +237,7 @@ public static function handleMagicMethod( } /** - * @param array $all_intersection_existent_method_ids + * @param array $all_intersection_existent_method_ids */ public static function handleMissingOrMagicMethod( StatementsAnalyzer $statements_analyzer, @@ -269,7 +269,7 @@ public static function handleMissingOrMagicMethod( && $found_method_and_class_storage ) { $result->has_valid_method_call_type = true; - $result->existent_method_ids[] = $method_id->__toString(); + $result->existent_method_ids[$method_id->__toString()] = true; [$pseudo_method_storage, $defining_class_storage] = $found_method_and_class_storage; diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php index 0635bbafb32..3f44397e065 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/MethodCallAnalyzer.php @@ -124,21 +124,6 @@ public static function analyze( $statements_analyzer->node_data->setType($stmt, Type::getMixed()); } - if (!$context->check_classes) { - if (ArgumentsAnalyzer::analyze( - $statements_analyzer, - $stmt->getArgs(), - null, - null, - true, - $context, - ) === false) { - return false; - } - - return true; - } - if ($class_type && $stmt->name instanceof PhpParser\Node\Identifier && ($class_type->isNull() || $class_type->isVoid()) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index 5b5cc622874..5cd62f721c5 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -56,8 +56,6 @@ public static function analyze( $config = $codebase->config; if ($stmt->class instanceof PhpParser\Node\Name) { - $fq_class_name = null; - if (count($stmt->class->getParts()) === 1 && in_array(strtolower($stmt->class->getFirst()), ['self', 'static', 'parent'], true) ) { @@ -105,7 +103,7 @@ public static function analyze( if ($context->isPhantomClass($fq_class_name)) { return true; } - } elseif ($context->check_classes) { + } else { $aliases = $statements_analyzer->getAliases(); if ($context->calling_method_id @@ -155,6 +153,7 @@ public static function analyze( : null, $statements_analyzer->getSuppressedIssues(), new ClassLikeNameOptions(false, false, false, true), + $context->check_classes, ); } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php index 3856f18a1ad..2235b9501df 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/CastAnalyzer.php @@ -143,14 +143,9 @@ public static function analyze( } } - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph - ) { - $type = new Union([new TBool()], [ - 'parent_nodes' => $maybe_type->parent_nodes ?? [], - ]); - } else { - $type = Type::getBool(); - } + $type = new Union([new TBool()], [ + 'parent_nodes' => $maybe_type->parent_nodes ?? [], + ]); $statements_analyzer->node_data->setType($stmt, $type); @@ -323,11 +318,7 @@ public static function castIntAttempt( $atomic_types = $stmt_type->getAtomicTypes(); - $parent_nodes = []; - - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { - $parent_nodes = $stmt_type->parent_nodes; - } + $parent_nodes = $stmt_type->parent_nodes; while ($atomic_types) { $atomic_type = array_pop($atomic_types); @@ -476,7 +467,7 @@ public static function castIntAttempt( // todo: emit error here } - $valid_types = array_merge($valid_ints, $castable_types); + $valid_types = [...$valid_ints, ...$castable_types]; if (!$valid_types) { $int_type = Type::getInt(); @@ -509,11 +500,7 @@ public static function castFloatAttempt( $atomic_types = $stmt_type->getAtomicTypes(); - $parent_nodes = []; - - if ($statements_analyzer->data_flow_graph instanceof VariableUseGraph) { - $parent_nodes = $stmt_type->parent_nodes; - } + $parent_nodes = $stmt_type->parent_nodes; while ($atomic_types) { $atomic_type = array_pop($atomic_types); @@ -661,7 +648,7 @@ public static function castFloatAttempt( // todo: emit error here } - $valid_types = array_merge($valid_floats, $castable_types); + $valid_types = [...$valid_floats, ...$castable_types]; if (!$valid_types) { $float_type = Type::getFloat(); @@ -804,10 +791,7 @@ public static function castStringAttempt( $parent_nodes = array_merge($return_type->parent_nodes, $parent_nodes); } - $castable_types = array_merge( - $castable_types, - array_values($return_type->getAtomicTypes()), - ); + $castable_types = [...$castable_types, ...array_values($return_type->getAtomicTypes())]; continue 2; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php index 15723ccb0e5..48d2d5b71c7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/ClassConstAnalyzer.php @@ -387,7 +387,11 @@ public static function analyzeFetch( ); } - if ($first_part_lc !== 'static' || $const_class_storage->final || $class_constant_type->from_docblock) { + if ($first_part_lc !== 'static' || $const_class_storage->final || $class_constant_type->from_docblock + || (isset($const_class_storage->constants[$stmt->name->name]) + && $const_class_storage->constants[$stmt->name->name]->final + ) + ) { $stmt_type = $class_constant_type; $statements_analyzer->node_data->setType($stmt, $stmt_type); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php index 44d61115739..b9001192c98 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/AtomicPropertyFetchAnalyzer.php @@ -787,6 +787,7 @@ public static function localizePropertyType( $position = array_search( $param_name, array_keys($property_class_storage->template_types), + true, ); } @@ -999,7 +1000,7 @@ private static function handleEnumName( empty($relevant_enum_case_names) ? Type::getNonEmptyString() : new Union(array_map( - fn(string $name): TString => Type::getAtomicStringFromLiteral($name), + static fn(string $name): TString => Type::getAtomicStringFromLiteral($name), $relevant_enum_case_names, )), ); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php index 4402a27c048..daa1193c2f4 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/StaticPropertyFetchAnalyzer.php @@ -156,7 +156,6 @@ public static function analyze( } if (!$fq_class_name - || !$context->check_classes || !$context->check_variables || ExpressionAnalyzer::isMock($fq_class_name) ) { @@ -251,7 +250,7 @@ public static function analyze( : null, ) ) { - if ($context->inside_isset) { + if ($context->inside_isset || !$context->check_classes) { return true; } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index f12b0ebc553..101196a2ff0 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -339,14 +339,15 @@ public static function infer( } if ($existing_class_constants === null - && $file_source instanceof StatementsAnalyzer + || $existing_class_constants === [] + && $file_source !== null ) { try { $foreign_class_constant = $codebase->classlikes->getClassConstantType( $const_fq_class_name, $stmt->name->name, ReflectionProperty::IS_PRIVATE, - $file_source, + $file_source instanceof StatementsAnalyzer ? $file_source : null, ); if ($foreign_class_constant) { diff --git a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php index 183255e19ee..3306c6627ba 100644 --- a/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/StatementsAnalyzer.php @@ -680,7 +680,11 @@ private static function analyzeStatement( } else { try { $checked_type = $context->vars_in_scope[$checked_var_id]; - $check_type = Type::parseString($check_type_string); + $fq_check_type_string = Type::getFQCLNFromString( + $check_type_string, + $statements_analyzer->getAliases(), + ); + $check_type = Type::parseString($fq_check_type_string); /** @psalm-suppress InaccessibleProperty We just created this type */ $check_type->possibly_undefined = $possibly_undefined; @@ -868,7 +872,7 @@ public function checkUnreferencedVars(array $stmts, Context $context): void } if ($function_storage) { - $param_index = array_search(substr($var_id, 1), array_keys($function_storage->param_lookup)); + $param_index = array_search(substr($var_id, 1), array_keys($function_storage->param_lookup), true); if ($param_index !== false) { $param = $function_storage->params[$param_index]; diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php index 37688a034ea..d5ded4434a0 100644 --- a/src/Psalm/Internal/Codebase/Populator.php +++ b/src/Psalm/Internal/Codebase/Populator.php @@ -933,10 +933,10 @@ protected function inheritMethodsFromParent( if ($parent_storage->is_trait && $storage->trait_alias_map ) { - $aliased_method_names = array_merge( - $aliased_method_names, - array_keys($storage->trait_alias_map, $method_name_lc, true), - ); + $aliased_method_names = [ + ...$aliased_method_names, + ...array_keys($storage->trait_alias_map, $method_name_lc, true), + ]; } foreach ($aliased_method_names as $aliased_method_name) { @@ -1003,10 +1003,10 @@ protected function inheritMethodsFromParent( if ($parent_storage->is_trait && $storage->trait_alias_map ) { - $aliased_method_names = array_merge( - $aliased_method_names, - array_keys($storage->trait_alias_map, $method_name_lc, true), - ); + $aliased_method_names = [ + ...$aliased_method_names, + ...array_keys($storage->trait_alias_map, $method_name_lc, true), + ]; } foreach ($aliased_method_names as $aliased_method_name) { diff --git a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php index f28cbae4936..e672e5109b1 100644 --- a/src/Psalm/Internal/Diff/ClassStatementsDiffer.php +++ b/src/Psalm/Internal/Diff/ClassStatementsDiffer.php @@ -5,9 +5,11 @@ namespace Psalm\Internal\Diff; use PhpParser; +use UnexpectedValueException; use function count; use function get_class; +use function is_string; use function strpos; use function strtolower; use function substr; @@ -232,7 +234,19 @@ static function ( /** @var PhpParser\Node */ $affected_elem = $diff_elem->type === DiffElem::TYPE_REMOVE ? $diff_elem->old : $diff_elem->new; if ($affected_elem instanceof PhpParser\Node\Stmt\ClassMethod) { - $add_or_delete[] = $name_lc . '::' . strtolower((string) $affected_elem->name); + $method_name = strtolower((string) $affected_elem->name); + $add_or_delete[] = $name_lc . '::' . $method_name; + if ($method_name === '__construct') { + foreach ($affected_elem->getParams() as $param) { + if (!$param->flags || !$param->var instanceof PhpParser\Node\Expr\Variable) { + continue; + } + if ($param->var instanceof PhpParser\Node\Expr\Error || !is_string($param->var->name)) { + throw new UnexpectedValueException('Not expecting param name to be non-string'); + } + $add_or_delete[] = $name_lc . '::$' . $param->var->name; + } + } } elseif ($affected_elem instanceof PhpParser\Node\Stmt\Property) { foreach ($affected_elem->props as $prop) { $add_or_delete[] = $name_lc . '::$' . $prop->name; diff --git a/src/Psalm/Internal/Diff/FileStatementsDiffer.php b/src/Psalm/Internal/Diff/FileStatementsDiffer.php index c5928ae0a15..cd0b0c66c97 100644 --- a/src/Psalm/Internal/Diff/FileStatementsDiffer.php +++ b/src/Psalm/Internal/Diff/FileStatementsDiffer.php @@ -118,7 +118,11 @@ static function ( $b_code, ); - $keep = [...$keep, ...$class_keep[0]]; + if ($diff_elem->old->getDocComment() === $diff_elem->new->getDocComment()) { + $keep = [...$keep, ...$class_keep[0]]; + } else { + $add_or_delete = [...$add_or_delete, ...$class_keep[0]]; + } $keep_signature = [...$keep_signature, ...$class_keep[1]]; $add_or_delete = [...$add_or_delete, ...$class_keep[2]]; $diff_map = [...$diff_map, ...$class_keep[3]]; diff --git a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php index 12ac9be6ea4..7448b3b8e0d 100644 --- a/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php +++ b/src/Psalm/Internal/FileManipulation/FunctionDocblockManipulator.php @@ -416,7 +416,7 @@ private function getDocblock(): string $modified_docblock = true; $inferredThrowsClause = array_reduce( $this->throwsExceptions, - fn(string $throwsClause, string $exception) => $throwsClause === '' + static fn(string $throwsClause, string $exception) => $throwsClause === '' ? $exception : $throwsClause.'|'.$exception, '', diff --git a/src/Psalm/Internal/LanguageServer/LanguageServer.php b/src/Psalm/Internal/LanguageServer/LanguageServer.php index e155881c860..cb1804f7957 100644 --- a/src/Psalm/Internal/LanguageServer/LanguageServer.php +++ b/src/Psalm/Internal/LanguageServer/LanguageServer.php @@ -219,7 +219,7 @@ function (Message $msg): void { $this->protocolReader->on( 'readMessageGroup', - function (): void { + static function (): void { //$this->verboseLog('Received message group'); //$this->doAnalysis(); }, @@ -735,7 +735,7 @@ function (IssueData $issue_data): Diagnostic { return $diagnostic; }, array_filter( - array_map(function (IssueData $issue_data) use (&$issue_baseline) { + array_map(static function (IssueData $issue_data) use (&$issue_baseline) { if (empty($issue_baseline)) { return $issue_data; } diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php index a6ae833b414..4905aba9ccf 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/ArrayMapReturnTypeProvider.php @@ -115,9 +115,9 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev $array_arg_types = array_map(null, ...$array_arg_types); $array_arg_types = array_map( /** @param non-empty-array $sub */ - function (array $sub) use ($null) { + static function (array $sub) use ($null) { $sub = array_map( - fn(?Union $t) => $t ?? $null, + static fn(?Union $t) => $t ?? $null, $sub, ); return new Union([new TKeyedArray($sub, null, null, true)]); diff --git a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php index b916caa8e73..3dae08c292b 100644 --- a/src/Psalm/Internal/Type/NegatedAssertionReconciler.php +++ b/src/Psalm/Internal/Type/NegatedAssertionReconciler.php @@ -123,6 +123,11 @@ public static function reconcile( $existing_var_type->removeType('array'); } + if ($assertion instanceof IsNotType && $assertion_type instanceof TClassString) { + $existing_var_type->removeType(TClassString::class); + $existing_var_type->addType(new TString); + } + if (!$is_equality && isset($existing_var_atomic_types['int']) && $existing_var_type->from_calculation diff --git a/src/Psalm/Internal/Type/ParseTreeCreator.php b/src/Psalm/Internal/Type/ParseTreeCreator.php index 515838b95e8..fcd0c77aeb0 100644 --- a/src/Psalm/Internal/Type/ParseTreeCreator.php +++ b/src/Psalm/Internal/Type/ParseTreeCreator.php @@ -237,6 +237,46 @@ private function createMethodParam(array $current_token, ParseTree $current_pare $this->current_leaf = $new_parent_leaf; } + /** + * @param array{0: string, 1: int, 2?: string} $current_token + */ + private function parseCallableParam(array $current_token, ParseTree $current_parent): void + { + $variadic = false; + $has_default = false; + + if ($current_token[0] === '&') { + ++$this->t; + $current_token = $this->t < $this->type_token_count ? $this->type_tokens[$this->t] : null; + } elseif ($current_token[0] === '...') { + $variadic = true; + + ++$this->t; + $current_token = $this->t < $this->type_token_count ? $this->type_tokens[$this->t] : null; + } elseif ($current_token[0] === '=') { + $has_default = true; + + ++$this->t; + $current_token = $this->t < $this->type_token_count ? $this->type_tokens[$this->t] : null; + } + + if (!$current_token || $current_token[0][0] !== '$') { + throw new TypeParseTreeException('Unexpected token after space'); + } + + $new_leaf = new CallableParamTree($current_parent); + $new_leaf->has_default = $has_default; + $new_leaf->variadic = $variadic; + + if ($current_parent !== $this->current_leaf) { + $new_leaf->children = [$this->current_leaf]; + array_pop($current_parent->children); + } + $current_parent->children[] = $new_leaf; + + $this->current_leaf = $new_leaf; + } + private function handleLessThan(): void { if (!$this->current_leaf instanceof FieldEllipsis) { @@ -555,24 +595,27 @@ private function handleSpace(): void $current_parent = $this->current_leaf->parent; - if ($current_parent instanceof CallableTree) { - return; - } - - while ($current_parent && !$current_parent instanceof MethodTree) { + //while ($current_parent && !$method_or_callable_parent) { + while ($current_parent && !$current_parent instanceof MethodTree && !$current_parent instanceof CallableTree) { $this->current_leaf = $current_parent; $current_parent = $current_parent->parent; } $next_token = $this->t + 1 < $this->type_token_count ? $this->type_tokens[$this->t + 1] : null; - if (!$current_parent instanceof MethodTree || !$next_token) { + if (!($current_parent instanceof MethodTree || $current_parent instanceof CallableTree) || !$next_token) { throw new TypeParseTreeException('Unexpected space'); } - ++$this->t; - $this->createMethodParam($next_token, $current_parent); + if ($current_parent instanceof MethodTree) { + ++$this->t; + $this->createMethodParam($next_token, $current_parent); + } + if ($current_parent instanceof CallableTree) { + ++$this->t; + $this->parseCallableParam($next_token, $current_parent); + } } private function handleQuestionMark(): void diff --git a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php index 04125509fd3..3bf5ac4c80d 100644 --- a/src/Psalm/Internal/Type/SimpleAssertionReconciler.php +++ b/src/Psalm/Internal/Type/SimpleAssertionReconciler.php @@ -684,7 +684,7 @@ private static function reconcileNonEmptyCountable( $existing_var_type->removeType('array'); $existing_var_type->addType($array_atomic_type->setProperties( array_map( - fn(Union $union) => $union->setPossiblyUndefined(false), + static fn(Union $union) => $union->setPossiblyUndefined(false), $array_atomic_type->properties, ), )); @@ -804,7 +804,7 @@ private static function reconcileExactlyCountable( $existing_var_type->removeType('array'); $existing_var_type->addType($array_atomic_type->setProperties( array_map( - fn(Union $union) => $union->setPossiblyUndefined(false), + static fn(Union $union) => $union->setPossiblyUndefined(false), $array_atomic_type->properties, ), )); diff --git a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php index 3a872cefec1..36f8b6c8fe7 100644 --- a/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateInferredTypeReplacer.php @@ -229,7 +229,7 @@ public static function replace( )->freeze(); } - $atomic_types = array_merge($types, $new_types); + $atomic_types = [...$types, ...$new_types]; if (!$atomic_types) { throw new UnexpectedValueException('This array should be full'); } diff --git a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php index cc6281b151b..62b9bfbe068 100644 --- a/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php +++ b/src/Psalm/Internal/Type/TemplateStandinTypeReplacer.php @@ -1332,6 +1332,7 @@ public static function getMappedGenericTypeParams( $old_params_offset = (int) array_search( $template->param_name, array_keys($input_class_storage->template_types), + true, ); $candidate_param_types[] = ($input_type_params[$old_params_offset] ?? Type::getMixed()) diff --git a/src/Psalm/Internal/Type/TypeExpander.php b/src/Psalm/Internal/Type/TypeExpander.php index d4037652e8b..9c858d4af72 100644 --- a/src/Psalm/Internal/Type/TypeExpander.php +++ b/src/Psalm/Internal/Type/TypeExpander.php @@ -323,28 +323,6 @@ public static function expandAtomic( ]; } - /** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */ - foreach ($return_type->extra_types ?? [] as $alias) { - $more_recursively_fleshed_out_types = self::expandAtomic( - $codebase, - $alias, - $self_class, - $static_class_type, - $parent_class, - $evaluate_class_constants, - $evaluate_conditional_types, - $final, - $expand_generic, - $expand_templates, - $throw_on_unresolvable_constant, - ); - - $recursively_fleshed_out_types = [ - ...$more_recursively_fleshed_out_types, - ...$recursively_fleshed_out_types, - ]; - } - return $recursively_fleshed_out_types; } diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index eda058b9b55..5b3bb664fe7 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -959,7 +959,11 @@ private static function getTypeFromGenericTree( } assert(count($parse_tree->children) === 2); - $get_int_range_bound = function (ParseTree $parse_tree, Union $generic_param, string $bound_name): ?int { + $get_int_range_bound = static function ( + ParseTree $parse_tree, + Union $generic_param, + string $bound_name, + ): ?int { if (!$parse_tree instanceof Value || count($generic_param->getAtomicTypes()) > 1 || (!$generic_param->getSingleAtomic() instanceof TLiteralInt @@ -971,7 +975,6 @@ private static function getTypeFromGenericTree( "Invalid type \"{$generic_param->getId()}\" as int $bound_name boundary", ); } - $generic_param_atomic = $generic_param->getSingleAtomic(); return $generic_param_atomic instanceof TLiteralInt ? $generic_param_atomic->value : null; }; diff --git a/src/Psalm/Internal/TypeVisitor/TypeChecker.php b/src/Psalm/Internal/TypeVisitor/TypeChecker.php index f05faa9a05c..31847f19ced 100644 --- a/src/Psalm/Internal/TypeVisitor/TypeChecker.php +++ b/src/Psalm/Internal/TypeVisitor/TypeChecker.php @@ -11,6 +11,9 @@ use Psalm\Internal\Analyzer\ClassLikeNameOptions; use Psalm\Internal\Analyzer\MethodAnalyzer; use Psalm\Internal\Type\Comparator\UnionTypeComparator; +use Psalm\Internal\Type\TemplateBound; +use Psalm\Internal\Type\TemplateInferredTypeReplacer; +use Psalm\Internal\Type\TemplateResult; use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\DeprecatedClass; use Psalm\Issue\DeprecatedInterface; @@ -243,6 +246,7 @@ private function checkGenericParams(TGenericObject $atomic): void } $expected_type_param_keys = array_keys($expected_type_params); + $template_result = new TemplateResult($expected_type_params, []); foreach ($atomic->type_params as $i => $type_param) { $this->prevent_template_covariance = $this->source instanceof MethodAnalyzer @@ -253,12 +257,16 @@ private function checkGenericParams(TGenericObject $atomic): void $expected_template_name = $expected_type_param_keys[$i]; foreach ($expected_type_params[$expected_template_name] as $defining_class => $expected_type_param) { - $expected_type_param = TypeExpander::expandUnion( + $expected_type_param = TemplateInferredTypeReplacer::replace( + TypeExpander::expandUnion( + $codebase, + $expected_type_param, + $defining_class, + null, + null, + ), + $template_result, $codebase, - $expected_type_param, - $defining_class, - null, - null, ); $type_param = TypeExpander::expandUnion( @@ -281,6 +289,9 @@ private function checkGenericParams(TGenericObject $atomic): void ), $this->suppressed_issues, ); + } else { + $template_result->lower_bounds[$expected_template_name][$defining_class][] + = new TemplateBound($type_param); } } } diff --git a/src/Psalm/Issue/InternalClass.php b/src/Psalm/Issue/InternalClass.php index 3d48aade13b..8edb9a77029 100644 --- a/src/Psalm/Issue/InternalClass.php +++ b/src/Psalm/Issue/InternalClass.php @@ -5,6 +5,7 @@ namespace Psalm\Issue; use function array_pop; +use function array_unique; use function count; use function implode; use function reset; @@ -17,6 +18,7 @@ final class InternalClass extends ClassIssue /** @param non-empty-list $words */ public static function listToPhrase(array $words): string { + $words = array_unique($words); if (count($words) === 1) { return reset($words); } diff --git a/src/Psalm/IssueBuffer.php b/src/Psalm/IssueBuffer.php index fc04545fbea..5ca7bd6762d 100644 --- a/src/Psalm/IssueBuffer.php +++ b/src/Psalm/IssueBuffer.php @@ -188,7 +188,7 @@ public static function isSuppressed(CodeIssue $e, array $suppressed_issues = []) return true; } - $suppressed_issue_position = array_search($issue_type, $suppressed_issues); + $suppressed_issue_position = array_search($issue_type, $suppressed_issues, true); if ($suppressed_issue_position !== false) { if (is_int($suppressed_issue_position)) { @@ -201,7 +201,7 @@ public static function isSuppressed(CodeIssue $e, array $suppressed_issues = []) $parent_issue_type = Config::getParentIssueType($issue_type); if ($parent_issue_type) { - $suppressed_issue_position = array_search($parent_issue_type, $suppressed_issues); + $suppressed_issue_position = array_search($parent_issue_type, $suppressed_issues, true); if ($suppressed_issue_position !== false) { if (is_int($suppressed_issue_position)) { @@ -214,7 +214,7 @@ public static function isSuppressed(CodeIssue $e, array $suppressed_issues = []) $suppress_all_position = $config->disable_suppress_all ? false - : array_search('all', $suppressed_issues); + : array_search('all', $suppressed_issues, true); if ($suppress_all_position !== false) { if (is_int($suppress_all_position)) { @@ -829,7 +829,7 @@ public static function printSuccessMessage(ProjectAnalyzer $project_analyzer): v $foreground = "30"; // text style, 1 = bold - $style = "1"; + $style = "2"; if ($project_analyzer->stdout_report_options->use_color) { echo "\e[{$background};{$style}m{$paddingTop}\e[0m" . "\n"; diff --git a/src/Psalm/Plugin/ArgTypeInferer.php b/src/Psalm/Plugin/ArgTypeInferer.php index 0347eead24a..109769c8024 100644 --- a/src/Psalm/Plugin/ArgTypeInferer.php +++ b/src/Psalm/Plugin/ArgTypeInferer.php @@ -13,19 +13,16 @@ final class ArgTypeInferer { - private Context $context; - private StatementsAnalyzer $statements_analyzer; - /** * @internal */ - public function __construct(Context $context, StatementsAnalyzer $statements_analyzer) - { - $this->context = $context; - $this->statements_analyzer = $statements_analyzer; + public function __construct( + private readonly Context $context, + private readonly StatementsAnalyzer $statements_analyzer, + ) { } - public function infer(PhpParser\Node\Arg $arg): false|Union + public function infer(PhpParser\Node\Arg $arg): null|Union { $already_inferred_type = $this->statements_analyzer->node_data->getType($arg->value); @@ -34,7 +31,7 @@ public function infer(PhpParser\Node\Arg $arg): false|Union } if (ExpressionAnalyzer::analyze($this->statements_analyzer, $arg->value, $this->context) === false) { - return false; + return null; } return $this->statements_analyzer->node_data->getType($arg->value) ?? Type::getMixed(); diff --git a/src/Psalm/Report/ByIssueLevelAndTypeReport.php b/src/Psalm/Report/ByIssueLevelAndTypeReport.php index e3e4e54db5f..0d821cb4e30 100644 --- a/src/Psalm/Report/ByIssueLevelAndTypeReport.php +++ b/src/Psalm/Report/ByIssueLevelAndTypeReport.php @@ -181,7 +181,7 @@ private function sortIssuesByLevelAndType(): void { usort( $this->issues_data, - fn(IssueData $left, IssueData $right): int => [$left->error_level > 0, -$left->error_level, + static fn(IssueData $left, IssueData $right): int => [$left->error_level > 0, -$left->error_level, $left->type, $left->file_path, $left->file_name, $left->line_from] <=> [$right->error_level > 0, -$right->error_level, $right->type, $right->file_path, $right->file_name, $right->line_from], diff --git a/src/Psalm/Report/CodeClimateReport.php b/src/Psalm/Report/CodeClimateReport.php index fb0cbf28689..61027d6371b 100644 --- a/src/Psalm/Report/CodeClimateReport.php +++ b/src/Psalm/Report/CodeClimateReport.php @@ -28,7 +28,7 @@ public function create(): string $options = $this->pretty ? Json::PRETTY : Json::DEFAULT; $issues_data = array_map( - [$this, 'mapToNewStructure'], + $this->mapToNewStructure(...), $this->issues_data, ); @@ -39,7 +39,7 @@ public function create(): string * convert our own severity to CodeClimate format * Values can be : info, minor, major, critical, or blocker */ - protected function convertSeverity(string $input): string + private function convertSeverity(string $input): string { if (Config::REPORT_INFO === $input) { return 'info'; @@ -58,7 +58,7 @@ protected function convertSeverity(string $input): string /** * calculate a unique fingerprint for a given issue */ - protected function calculateFingerprint(IssueData $issue): string + private function calculateFingerprint(IssueData $issue): string { return md5($issue->type.$issue->message.$issue->file_name.$issue->from.$issue->to); } diff --git a/src/Psalm/Report/CountReport.php b/src/Psalm/Report/CountReport.php index a0ec59602ba..eb47b4c884b 100644 --- a/src/Psalm/Report/CountReport.php +++ b/src/Psalm/Report/CountReport.php @@ -21,7 +21,7 @@ public function create(): string $issue_type_counts[$issue_data->type] = 1; } } - uksort($issue_type_counts, function (string $a, string $b) use ($issue_type_counts): int { + uksort($issue_type_counts, static 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; diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index ef13057d9dc..81685917896 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -195,7 +195,7 @@ public function getHoverMarkdown(): string $params = count($this->params) > 0 ? "\n" . implode( ",\n", array_map( - function (FunctionLikeParameter $param): string { + static function (FunctionLikeParameter $param): string { $realType = $param->type ?: 'mixed'; return " {$realType} \${$param->name}"; }, @@ -230,7 +230,7 @@ public function getCompletionSignature(): string $symbol_text = 'function ' . $this->cased_name . '(' . implode( ',', array_map( - fn(FunctionLikeParameter $param): string => ($param->type ?: 'mixed') . ' $' . $param->name, + static fn(FunctionLikeParameter $param): string => ($param->type ?: 'mixed') . ' $' . $param->name, $this->params, ), ) . ') : ' . ($this->return_type ?: 'mixed'); diff --git a/src/Psalm/Type/Atomic/TClosure.php b/src/Psalm/Type/Atomic/TClosure.php index b556f503d29..d2548508f8d 100644 --- a/src/Psalm/Type/Atomic/TClosure.php +++ b/src/Psalm/Type/Atomic/TClosure.php @@ -11,8 +11,6 @@ use Psalm\Type\Atomic; use Psalm\Type\Union; -use function array_merge; - /** * Represents a closure where we know the return type and params * @@ -133,6 +131,6 @@ public function replaceTemplateTypesWithStandins( protected function getChildNodeKeys(): array { - return array_merge(parent::getChildNodeKeys(), $this->getCallableChildNodeKeys()); + return [...parent::getChildNodeKeys(), ...$this->getCallableChildNodeKeys()]; } } diff --git a/src/Psalm/Type/Atomic/TGenericObject.php b/src/Psalm/Type/Atomic/TGenericObject.php index 244a3218e40..d5f577e8af5 100644 --- a/src/Psalm/Type/Atomic/TGenericObject.php +++ b/src/Psalm/Type/Atomic/TGenericObject.php @@ -10,7 +10,6 @@ use Psalm\Type\Atomic; use Psalm\Type\Union; -use function array_merge; use function count; use function implode; use function strrpos; @@ -129,7 +128,7 @@ public function getAssertionString(): string protected function getChildNodeKeys(): array { - return array_merge(parent::getChildNodeKeys(), ['type_params']); + return [...parent::getChildNodeKeys(), 'type_params']; } /** diff --git a/src/Psalm/Type/Atomic/TTypeAlias.php b/src/Psalm/Type/Atomic/TTypeAlias.php index 52a2fe9037a..ebda8e4118b 100644 --- a/src/Psalm/Type/Atomic/TTypeAlias.php +++ b/src/Psalm/Type/Atomic/TTypeAlias.php @@ -6,55 +6,17 @@ use Psalm\Type\Atomic; -use function array_map; -use function implode; - /** * @psalm-immutable */ final class TTypeAlias extends Atomic { - /** - * @var array|null - * @deprecated type aliases are resolved within {@see TypeParser::resolveTypeAliases()} and therefore the - * referencing type(s) are part of other intersection types. The intersection types are not set anymore - * and with v6 this property along with its related methods will get removed. - */ - public ?array $extra_types = null; - - public string $declaring_fq_classlike_name; - - public 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; - /** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */ - $this->extra_types = $extra_types; + public function __construct( + public string $declaring_fq_classlike_name, + public string $alias_name, + ) { parent::__construct(true); } - /** - * @param array|null $extra_types - * @deprecated type aliases are resolved within {@see TypeParser::resolveTypeAliases()} and therefore the - * referencing type(s) are part of other intersection types. This method will get removed with v6. - * @psalm-suppress PossiblyUnusedMethod For backwards compatibility, we have to keep this here. - */ - public function setIntersectionTypes(?array $extra_types): self - { - /** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */ - 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 { @@ -63,17 +25,6 @@ public function getKey(bool $include_extra = true): string public function getId(bool $exact = true, bool $nested = false): string { - /** @psalm-suppress DeprecatedProperty For backwards compatibility, we have to keep this here. */ - if ($this->extra_types) { - return $this->getKey() . '&' . implode( - '&', - array_map( - static fn(Atomic $type): string => $type->getId($exact, true), - $this->extra_types, - ), - ); - } - return $this->getKey(); } diff --git a/src/Psalm/Type/Atomic/TValueOf.php b/src/Psalm/Type/Atomic/TValueOf.php index ad71cd03ce3..3a86ec720a7 100644 --- a/src/Psalm/Type/Atomic/TValueOf.php +++ b/src/Psalm/Type/Atomic/TValueOf.php @@ -42,7 +42,7 @@ private static function getValueTypeForNamedObject(array $cases, TNamedObject $a } return new Union(array_map( - function (EnumCaseStorage $case): Atomic { + static function (EnumCaseStorage $case): Atomic { assert($case->value !== null); // Backed enum must have a value return $case->value; diff --git a/src/Psalm/Type/UnionTrait.php b/src/Psalm/Type/UnionTrait.php index 8ae3600764e..114b09cc35c 100644 --- a/src/Psalm/Type/UnionTrait.php +++ b/src/Psalm/Type/UnionTrait.php @@ -44,7 +44,6 @@ use Psalm\Type\Atomic\TTrue; use function array_filter; -use function array_merge; use function array_unique; use function count; use function get_class; @@ -274,13 +273,13 @@ public function toNamespacedString( } if (count($literal_ints) <= 3 && !$has_non_literal_int) { - $other_types = array_merge($other_types, $literal_ints); + $other_types = [...$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); + $other_types = [...$other_types, ...$literal_strings]; } else { $other_types[] = 'string'; } diff --git a/stubs/CoreGenericFunctions.phpstub b/stubs/CoreGenericFunctions.phpstub index 026e90c65cb..3c7fbc7198d 100644 --- a/stubs/CoreGenericFunctions.phpstub +++ b/stubs/CoreGenericFunctions.phpstub @@ -1292,7 +1292,7 @@ function preg_quote(string $str, ?string $delimiter = null) : string {} /** * @psalm-pure * - * @param string|int|float $values + * @param string|stringable-object|int|float $values * @return (PHP_MAJOR_VERSION is 8 ? string : string|false) * @psalm-ignore-falsable-return * diff --git a/tests/AsyncTestCase.php b/tests/AsyncTestCase.php index 0e89d76594e..09c0eb81037 100644 --- a/tests/AsyncTestCase.php +++ b/tests/AsyncTestCase.php @@ -154,7 +154,7 @@ public static function assertArrayKeysAreStrings(array $array, string $message = */ public static function assertArrayKeysAreZeroOrString(array $array, string $message = ''): void { - $isZeroOrString = /** @param mixed $key */ fn($key): bool => $key === 0 || is_string($key); + $isZeroOrString = /** @param mixed $key */ static fn($key): bool => $key === 0 || is_string($key); $validKeys = array_filter($array, $isZeroOrString, ARRAY_FILTER_USE_KEY); self::assertTrue(count($array) === count($validKeys), $message); } diff --git a/tests/Cache/CacheTest.php b/tests/Cache/CacheTest.php index cc81d4a604f..5c081c34035 100644 --- a/tests/Cache/CacheTest.php +++ b/tests/Cache/CacheTest.php @@ -199,5 +199,113 @@ class B { ], ], ]; + + yield 'classDocblockChange' => [ + [ + [ + 'files' => [ + '/src/A.php' => <<<'PHP' + <<<'PHP' + foo(1); + } + } + PHP, + ], + 'issues' => [], + ], + [ + 'files' => [ + '/src/A.php' => <<<'PHP' + [ + '/src/A.php' => [ + "UndefinedDocblockClass: Docblock-defined class, interface or enum named T does not exist", + ], + '/src/B.php' => [ + "InvalidArgument: Argument 1 of A::foo expects T, but 1 provided", + ], + ], + ], + ], + ]; + + yield 'constructorPropertyPromotionChange' => [ + [ + [ + 'files' => [ + '/src/A.php' => <<<'PHP' + foo; + } + } + PHP, + ], + 'issues' => [], + ], + [ + 'files' => [ + '/src/A.php' => <<<'PHP' + foo; + } + } + PHP, + ], + 'issues' => [ + '/src/A.php' => [ + "UndefinedThisPropertyFetch: Instance property A::\$foo is not defined", + "MixedReturnStatement: Could not infer a return type", + "MixedInferredReturnType: Could not verify return type 'string' for A::bar", + ], + ], + ], + ], + ]; } } diff --git a/tests/CallableTest.php b/tests/CallableTest.php index ffdc5ba558a..4dc185ca6d2 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -1858,6 +1858,40 @@ function int_int_int_int(Closure $f): void {} '$c' => 'list{class-string|object, string}', ], ], + 'inferTypeWithNestedTemplatesAndExplicitTypeHint' => [ + 'code' => '> + */ + final class GetListOfNumbers implements Message {} + + /** + * @template TResult + * @template TMessage of Message + */ + final class Envelope {} + + /** + * @template TResult + * @template TMessage of Message + * @param class-string $_message + * @param callable(TMessage, Envelope): TResult $_handler + */ + function addHandler(string $_message, callable $_handler): void {} + + addHandler(GetListOfNumbers::class, function (Message $_message, Envelope $_envelope) { + /** + * @psalm-check-type-exact $_message = GetListOfNumbers + * @psalm-check-type-exact $_envelope = Envelope, GetListOfNumbers> + */ + return [1, 2, 3]; + });', + ], ]; } diff --git a/tests/CheckTypeTest.php b/tests/CheckTypeTest.php index bfd3f394839..937848b1edf 100644 --- a/tests/CheckTypeTest.php +++ b/tests/CheckTypeTest.php @@ -20,6 +20,26 @@ public function providerValidCodeParse(): iterable $foo = 1; ', ]; + yield 'allowNamespace' => [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + '$className===' => 'string', + ], + + ], 'createNewObjectFromGetClass' => [ 'code' => 'extends->getAttribute('resolvedName') : ''; $storage->custom_metadata['implements'] = array_map( - fn(Name $aspect): string => (string)$aspect->getAttribute('resolvedName'), + static fn(Name $aspect): string => (string)$aspect->getAttribute('resolvedName'), $stmt->implements, ); $storage->custom_metadata['a'] = 'b'; diff --git a/tests/Config/ConfigTest.php b/tests/Config/ConfigTest.php index 9fa8042c55c..34a5b06f976 100644 --- a/tests/Config/ConfigTest.php +++ b/tests/Config/ConfigTest.php @@ -1108,7 +1108,7 @@ public function testAllPossibleIssues(): void * @param string $issue_name * @return string */ - fn($issue_name): string => '<' . $issue_name . ' errorLevel="suppress" />' . "\n", + static fn($issue_name): string => '<' . $issue_name . ' errorLevel="suppress" />' . "\n", IssueHandler::getAllIssueTypes(), ), ); diff --git a/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php index fb2853f3530..cb3284754f8 100644 --- a/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php +++ b/tests/Config/Plugin/Hook/CustomArrayMapFunctionStorageProvider.php @@ -56,11 +56,10 @@ public static function getFunctionStorage(DynamicFunctionStorageProviderEvent $e $custom_array_map_storage->return_type = self::createReturnType($all_expected_callables); $custom_array_map_storage->params = [ ...array_map( - function (TCallable $expected, int $offset) { + static function (TCallable $expected, int $offset) { $t = new Union([$expected]); $param = new FunctionLikeParameter('fn' . $offset, false, $t, $t); $param->is_optional = false; - return $param; }, $all_expected_callables, diff --git a/tests/CoreStubsTest.php b/tests/CoreStubsTest.php index 87f26f859fe..5e47896abf7 100644 --- a/tests/CoreStubsTest.php +++ b/tests/CoreStubsTest.php @@ -127,6 +127,18 @@ function foo(string $foo): string '$a===' => 'string', ], ]; + yield 'sprintf accepts Stringable values' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.0', + ]; yield 'json_encode returns a non-empty-string provided JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE' => [ 'code' => ' count($issues) > 1 + static fn($issues): bool => count($issues) > 1 ); $this->assertEquals( diff --git a/tests/EnumTest.php b/tests/EnumTest.php index 354911c8875..ebc67a05e1e 100644 --- a/tests/EnumTest.php +++ b/tests/EnumTest.php @@ -632,6 +632,26 @@ function noop(string $s): string $foo = FooEnum::Foo->value; noop($foo); noop(FooEnum::Foo->value); + PHP, + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'backedEnumCaseValueFromClassConstant' => [ + 'code' => <<<'PHP' + [], 'ignored_issues' => [], @@ -1057,6 +1077,36 @@ function f(string $state): void {} 'ignored_issues' => [], 'php_version' => '8.1', ], + 'stringBackedEnumCaseValueFromClassConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'intBackedEnumCaseValueFromClassConstant' => [ + 'code' => ' 'InvalidEnumCaseValue', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } } diff --git a/tests/FileDiffTest.php b/tests/FileDiffTest.php index 0e7982552b9..2e206537b7b 100644 --- a/tests/FileDiffTest.php +++ b/tests/FileDiffTest.php @@ -64,7 +64,7 @@ public function testCode( * @param array{0: int, 1: int, 2: int, 3: int} $arr * @return array{0: int, 1: int} */ - fn(array $arr): array => [$arr[2], $arr[3]], + static fn(array $arr): array => [$arr[2], $arr[3]], $diff[3], ); @@ -135,7 +135,7 @@ public function testPartialAstDiff( * @param array{0: int, 1: int, 2: int, 3: int} $arr * @return array{0: int, 1: int} */ - fn(array $arr): array => [$arr[2], $arr[3]], + static fn(array $arr): array => [$arr[2], $arr[3]], $diff[3], ); diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index 23b4ae3bc17..d012e72fa9e 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -3033,6 +3033,17 @@ function hasZeroByteOffset(string $s) : bool { }', 'error_message' => 'InvalidScalarArgument', ], + 'disallowNeverTypeForParam' => [ + 'code' => ' 'ReservedWord', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index 3a808c99f19..f77eb36203e 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -558,6 +558,22 @@ function foo(DateTime $d1, DateTime $d2) : void { ); }', ], + 'methodExistsDoesntExhaustMemory' => [ + 'code' => 'a() : []; + method_exists($c, \'b\') ? $c->b() : []; + method_exists($c, \'c\') ? $c->c() : []; + method_exists($c, \'d\') ? $c->d() : []; + method_exists($c, \'e\') ? $c->e() : []; + method_exists($c, \'f\') ? $c->f() : []; + method_exists($c, \'g\') ? $c->g() : []; + method_exists($c, \'h\') ? $c->h() : []; + method_exists($c, \'i\') ? $c->i() : []; + }', + ], 'callMethodAfterCheckingExistence' => [ 'code' => ' [ + 'code' => 'x; + } + } + ', + ], + 'descendantAddsByRefReturn' => [ + 'code' => 'x; + } + } + ', + ], ]; } @@ -1588,6 +1616,20 @@ public function jsonSerialize() { 'ignored_issues' => [], 'php_version' => '8.1', ], + 'absentByRefReturnInDescendant' => [ + 'code' => ' 'MethodSignatureMismatch', + ], ]; } } diff --git a/tests/PsalmPluginTest.php b/tests/PsalmPluginTest.php index 64311a30122..5b97dc23136 100644 --- a/tests/PsalmPluginTest.php +++ b/tests/PsalmPluginTest.php @@ -25,9 +25,9 @@ class PsalmPluginTest extends TestCase { use MockeryPHPUnitIntegration; - private PluginList&MockInterface $plugin_list; + private MockInterface $plugin_list; - private PluginListFactory&MockInterface $plugin_list_factory; + private MockInterface $plugin_list_factory; private Application $app; diff --git a/tests/ReturnTypeTest.php b/tests/ReturnTypeTest.php index 2caf0208996..41d93d12453 100644 --- a/tests/ReturnTypeTest.php +++ b/tests/ReturnTypeTest.php @@ -1315,6 +1315,26 @@ function &foo(): array { } PHP, ], + 'neverReturnType' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } @@ -1874,6 +1894,36 @@ function &foo(): array { PHP, 'error_message' => 'NonVariableReferenceReturn', ], + 'implicitReturnFromFunctionWithNeverReturnType' => [ + 'code' => <<<'PHP' + 'InvalidReturnType', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], + 'implicitReturnFromFunctionWithNeverReturnType2' => [ + 'code' => <<<'PHP' + 'InvalidReturnType', + 'ignored_issues' => [], + 'php_version' => '8.1', + ], ]; } } diff --git a/tests/TaintTest.php b/tests/TaintTest.php index d685014c28b..4e7c2bda417 100644 --- a/tests/TaintTest.php +++ b/tests/TaintTest.php @@ -179,23 +179,6 @@ public function deleteUser(PDO $pdo, string $userId) : void { } }', ], - 'untaintedInputAfterIntCast' => [ - 'code' => 'getUserId(); - } - - public function deleteUser(PDO $pdo) : void { - $userId = $this->getAppendedUserId(); - $pdo->exec("delete from users where user_id = " . $userId); - } - }', - ], 'specializedCoreFunctionCall' => [ 'code' => ' [ - 'code' => ' [ 'code' => ' 'TaintedSql', ], + 'taintedInputAfterIntCast' => [ + 'code' => 'getUserId(); + } + + public function deleteUser(PDO $pdo) : void { + $userId = $this->getAppendedUserId(); + $pdo->exec("delete from users where user_id = " . $userId); + } + }', + 'error_message' => 'TaintedSql', + ], + 'TaintForIntTypeCastUsingAnnotatedSink' => [ + 'code' => ' 'TaintedSql', + ], 'taintedInputFromReturnTypeWithBranch' => [ 'code' => 'analyzeFile($filePath, new Context(), false); $actualIssueTypes = array_map( - fn(IssueData $issue): string => $issue->type . '{ ' . trim($issue->snippet) . ' }', + static fn(IssueData $issue): string => $issue->type . '{ ' . trim($issue->snippet) . ' }', IssueBuffer::getIssuesDataForFile($filePath), ); self::assertSame($expectedIssuesTypes, $actualIssueTypes); diff --git a/tests/Template/FunctionTemplateTest.php b/tests/Template/FunctionTemplateTest.php index d746d44588a..64210ed4bbc 100644 --- a/tests/Template/FunctionTemplateTest.php +++ b/tests/Template/FunctionTemplateTest.php @@ -1675,6 +1675,29 @@ function foo(string $t): string return $t; }', ], + 'typeWithNestedTemplates' => [ + 'code' => ' + */ + final class BType {} + + /** + * @param BType> $_value + */ + function test1(BType $_value): void {} + + /** + * @param BType> $_value + */ + function test2(BType $_value): void {}', + ], ]; } @@ -2270,6 +2293,30 @@ function jsonFromEntityCollection(Container $c): void { }', 'error_message' => 'InvalidArgument', ], + 'catchInvalidTemplateTypeWithNestedTemplates' => [ + 'code' => ' + */ + final class BType {} + + /** + * @param BType> $_value + */ + function test1(BType $_value): void {} + + /** + * @param BType> $_value + */ + function test2(BType $_value): void {}', + 'error_message' => 'InvalidTemplateParam', + ], ]; } } diff --git a/tests/TestCase.php b/tests/TestCase.php index 8e61664f870..5ddeebd3bfd 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -151,7 +151,7 @@ public static function assertArrayKeysAreStrings(array $array, string $message = public static function assertArrayKeysAreZeroOrString(array $array, string $message = ''): void { - $isZeroOrString = /** @param mixed $key */ fn($key): bool => $key === 0 || is_string($key); + $isZeroOrString = /** @param mixed $key */ static fn($key): bool => $key === 0 || is_string($key); $validKeys = array_filter($array, $isZeroOrString, ARRAY_FILTER_USE_KEY); self::assertTrue(count($array) === count($validKeys), $message); } diff --git a/tests/TestConfig.php b/tests/TestConfig.php index 572deabbce6..87433cc67c6 100644 --- a/tests/TestConfig.php +++ b/tests/TestConfig.php @@ -61,7 +61,8 @@ protected function getContents(): string '; } - public function getComposerFilePathForClassLike(string $fq_classlike_name): string|false + /** @return false */ + public function getComposerFilePathForClassLike(string $fq_classlike_name): bool { return false; } diff --git a/tests/ToStringTest.php b/tests/ToStringTest.php index f0c3a48929c..a3dff64f766 100644 --- a/tests/ToStringTest.php +++ b/tests/ToStringTest.php @@ -263,19 +263,6 @@ function fooFoo(string $b): void {} fooFoo(new A());', 'error_message' => 'InvalidArgument', ], - 'implicitCastWithStrictTypesToEchoOrSprintf' => [ - 'code' => ' 'ImplicitToStringCast', - ], 'implicitCast' => [ 'code' => ' 'callable():int', ], ], + 'callableFormats' => [ + 'code' => '): array + * @psalm-type I callable(array $e): array + * @psalm-type J callable(array ...): string + * @psalm-type K callable(array ...$e): string + * @psalm-type L \Closure(int, int): string + * + * @method ma(): A + * @method mb(): B + * @method mc(): C + * @method md(): D + * @method me(): E + * @method mf(): F + * @method mg(): G + * @method mh(): H + * @method mi(): I + * @method mj(): J + * @method mk(): K + * @method ml(): L + */ + class Foo { + public function __call(string $method, array $params) { return 1; } + } + + $foo = new \Foo(); + $output_ma = $foo->ma(); + $output_mb = $foo->mb(); + $output_mc = $foo->mc(); + $output_md = $foo->md(); + $output_me = $foo->me(); + $output_mf = $foo->mf(); + $output_mg = $foo->mg(); + $output_mh = $foo->mh(); + $output_mi = $foo->mi(); + $output_mj = $foo->mj(); + $output_mk = $foo->mk(); + $output_ml = $foo->ml(); + ', + 'assertions' => [ + '$output_ma===' => 'callable(int, int):string', + '$output_mb===' => 'callable(int, int=):string', + '$output_mc===' => 'callable(int, string):void', + '$output_md===' => 'callable(string):mixed', + '$output_me===' => 'callable(string):mixed', + '$output_mf===' => 'callable(float...):(int|null)', + '$output_mg===' => 'callable(float...):(int|null)', + '$output_mh===' => 'callable(array):array', + '$output_mi===' => 'callable(array):array', + '$output_mj===' => 'callable(array...):string', + '$output_mk===' => 'callable(array...):string', + '$output_ml===' => 'Closure(int, int):string', + ], + ], 'unionOfStringsContainingBraceChar' => [ 'code' => ' [$type], + static fn($type) => [$type], array_keys($basic_types), ); } diff --git a/tests/UnusedCodeTest.php b/tests/UnusedCodeTest.php index 87e18726536..d67669b05f6 100644 --- a/tests/UnusedCodeTest.php +++ b/tests/UnusedCodeTest.php @@ -469,6 +469,35 @@ public function __construct() {} new A(); }', ], + 'useMethodPropertiesAfterExtensionLoaded' => [ + 'code' => 'test(); + } + if (\extension_loaded("fdsfdsfd")) { + return a::$a; + } + if (\extension_loaded("fdsfdsfd")) { + return a::get(); + } + return $handler->test(); + }', + ], 'usedParamInIf' => [ 'code' => ' $_) { diff --git a/tests/fixtures/SuicidalAutoloader/autoloader.php b/tests/fixtures/SuicidalAutoloader/autoloader.php index 365fa7b723e..d506219c141 100644 --- a/tests/fixtures/SuicidalAutoloader/autoloader.php +++ b/tests/fixtures/SuicidalAutoloader/autoloader.php @@ -3,7 +3,7 @@ use React\Promise\PromiseInterface as ReactPromise; use Composer\InstalledVersions; -spl_autoload_register(function (string $className) { +spl_autoload_register(static function (string $className) { $knownBadClasses = [ ReactPromise::class, // amphp/amp ResourceBundle::class, // symfony/polyfill-php73 @@ -25,11 +25,9 @@ 'Symfony\Component\String\s', 'Symfony\Component\Translation\t', ]; - if (in_array($className, $knownBadClasses)) { return; } - $ex = new RuntimeException('Attempted to load ' . $className); echo $ex->__toString() . "\n\n" . $ex->getTraceAsString() . "\n\n"; exit(70);