diff --git a/config.xsd b/config.xsd index 55aedabe061..b58a3bbb9d7 100644 --- a/config.xsd +++ b/config.xsd @@ -315,6 +315,7 @@ + diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 80de3809a85..ad879c10104 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -1142,17 +1142,17 @@ 'crash' => [''], 'crc32' => ['int', 'string'=>'string'], 'crypt' => ['string', 'string'=>'string', 'salt'=>'string'], -'ctype_alnum' => ['bool', 'text'=>'string|int'], -'ctype_alpha' => ['bool', 'text'=>'string|int'], -'ctype_cntrl' => ['bool', 'text'=>'string|int'], -'ctype_digit' => ['bool', 'text'=>'string|int'], -'ctype_graph' => ['bool', 'text'=>'string|int'], -'ctype_lower' => ['bool', 'text'=>'string|int'], -'ctype_print' => ['bool', 'text'=>'string|int'], -'ctype_punct' => ['bool', 'text'=>'string|int'], -'ctype_space' => ['bool', 'text'=>'string|int'], -'ctype_upper' => ['bool', 'text'=>'string|int'], -'ctype_xdigit' => ['bool', 'text'=>'string|int'], +'ctype_alnum' => ['bool', 'text'=>'string'], +'ctype_alpha' => ['bool', 'text'=>'string'], +'ctype_cntrl' => ['bool', 'text'=>'string'], +'ctype_digit' => ['bool', 'text'=>'string'], +'ctype_graph' => ['bool', 'text'=>'string'], +'ctype_lower' => ['bool', 'text'=>'string'], +'ctype_print' => ['bool', 'text'=>'string'], +'ctype_punct' => ['bool', 'text'=>'string'], +'ctype_space' => ['bool', 'text'=>'string'], +'ctype_upper' => ['bool', 'text'=>'string'], +'ctype_xdigit' => ['bool', 'text'=>'string'], 'cubrid_affected_rows' => ['int', 'req_identifier='=>''], 'cubrid_bind' => ['bool', 'req_identifier'=>'resource', 'bind_param'=>'int', 'bind_value'=>'mixed', 'bind_value_type='=>'string'], 'cubrid_client_encoding' => ['string', 'conn_identifier='=>''], @@ -1293,7 +1293,7 @@ 'CURLFile::setMimeType' => ['void', 'mime_type'=>'string'], 'CURLFile::setPostFilename' => ['void', 'posted_filename'=>'string'], 'CURLStringFile::__construct' => ['void', 'data'=>'string', 'postname'=>'string', 'mime='=>'string'], -'current' => ['mixed|false', 'array'=>'array|object'], +'current' => ['mixed|false', 'array'=>'array'], 'cyrus_authenticate' => ['void', 'connection'=>'resource', 'mechlist='=>'string', 'service='=>'string', 'user='=>'string', 'minssf='=>'int', 'maxssf='=>'int', 'authname='=>'string', 'password='=>'string'], 'cyrus_bind' => ['bool', 'connection'=>'resource', 'callbacks'=>'array'], 'cyrus_close' => ['bool', 'connection'=>'resource'], @@ -3269,7 +3269,7 @@ 'get_call_stack' => [''], 'get_called_class' => ['class-string'], 'get_cfg_var' => ['string|false', 'option'=>'string'], -'get_class' => ['class-string', 'object='=>'object'], +'get_class' => ['class-string', 'object'=>'object'], 'get_class_methods' => ['list', 'object_or_class'=>'object|class-string'], 'get_class_vars' => ['array', 'class'=>'string'], 'get_current_user' => ['string'], @@ -3290,7 +3290,7 @@ 'get_magic_quotes_runtime' => ['int|false'], 'get_meta_tags' => ['array', 'filename'=>'string', 'use_include_path='=>'bool'], 'get_object_vars' => ['array', 'object'=>'object'], -'get_parent_class' => ['class-string|false', 'object_or_class='=>'object|class-string'], +'get_parent_class' => ['class-string|false', 'object_or_class'=>'object|class-string'], 'get_required_files' => ['list'], 'get_resource_id' => ['int', 'resource'=>'resource'], 'get_resource_type' => ['string', 'resource'=>'resource'], @@ -3307,7 +3307,7 @@ 'getimagesize' => ['array{0:int, 1: int, 2: int, 3: string, mime: string, channels?: 3|4, bits?: int}|false', 'filename'=>'string', '&w_image_info='=>'array'], 'getimagesizefromstring' => ['array{0:int, 1: int, 2: int, 3: string, mime: string, channels?: 3|4, bits?: int}|false', 'string'=>'string', '&w_image_info='=>'array'], 'getlastmod' => ['int|false'], -'getmxrr' => ['bool', 'hostname'=>'string', '&w_hosts'=>'array', '&w_weights='=>'array'], +'getmxrr' => ['bool', 'hostname'=>'string', '&w_hosts'=>'array', '&w_weights='=>'array'], 'getmygid' => ['int|false'], 'getmyinode' => ['int|false'], 'getmypid' => ['int|false'], @@ -6198,7 +6198,7 @@ 'kadm5_get_principals' => ['array', 'handle'=>'resource'], 'kadm5_init_with_password' => ['resource', 'admin_server'=>'string', 'realm'=>'string', 'principal'=>'string', 'password'=>'string'], 'kadm5_modify_principal' => ['bool', 'handle'=>'resource', 'principal'=>'string', 'options'=>'array'], -'key' => ['int|string|null', 'array'=>'array|object'], +'key' => ['int|string|null', 'array'=>'array'], 'key_exists' => ['bool', 'key'=>'string|int', 'array'=>'array'], 'krsort' => ['true', '&rw_array'=>'array', 'flags='=>'int'], 'ksort' => ['true', '&rw_array'=>'array', 'flags='=>'int'], @@ -6598,7 +6598,7 @@ 'mapObj::zoomScale' => ['int', 'nScaleDenom'=>'float', 'oPixelPos'=>'pointObj', 'nImageWidth'=>'int', 'nImageHeight'=>'int', 'oGeorefExt'=>'rectObj', 'oMaxGeorefExt'=>'rectObj'], 'max' => ['mixed', 'value'=>'non-empty-array'], 'max\'1' => ['mixed', 'value'=>'', 'values'=>'', '...args='=>''], -'mb_check_encoding' => ['bool', 'value='=>'array|string|null', 'encoding='=>'string|null'], +'mb_check_encoding' => ['bool', 'value'=>'array|string', 'encoding='=>'string|null'], 'mb_chr' => ['non-empty-string|false', 'codepoint'=>'int', 'encoding='=>'string|null'], 'mb_convert_case' => ['string', 'string'=>'string', 'mode'=>'int', 'encoding='=>'string|null'], 'mb_convert_encoding' => ['string|false', 'string'=>'string', 'to_encoding'=>'string', 'from_encoding='=>'array|string|null'], @@ -8149,7 +8149,7 @@ 'newrelic_set_appname' => ['bool', 'name'=>'string', 'license='=>'string', 'xmit='=>'bool'], 'newrelic_set_user_attributes' => ['bool', 'user'=>'string', 'account'=>'string', 'product'=>'string'], 'newrelic_start_transaction' => ['bool', 'appname'=>'string', 'license='=>'string'], -'next' => ['mixed', '&r_array'=>'array|object'], +'next' => ['mixed', '&r_array'=>'array'], 'ngettext' => ['string', 'singular'=>'string', 'plural'=>'string', 'count'=>'int'], 'nl2br' => ['string', 'string'=>'string', 'use_xhtml='=>'bool'], 'nl_langinfo' => ['string|false', 'item'=>'int'], @@ -9429,7 +9429,7 @@ 'preg_replace_callback_array\'1' => ['string[]|null', 'pattern'=>'array', 'subject'=>'string[]', 'limit='=>'int', '&w_count='=>'int', 'flags='=>'int'], 'preg_split' => ['list|false', 'pattern'=>'string', 'subject'=>'string', 'limit'=>'int', 'flags='=>'null'], 'preg_split\'1' => ['list|list>|false', 'pattern'=>'string', 'subject'=>'string', 'limit='=>'int', 'flags='=>'int'], -'prev' => ['mixed', '&r_array'=>'array|object'], +'prev' => ['mixed', '&r_array'=>'array'], 'print' => ['int', 'arg'=>'string'], 'print_r' => ['string', 'value'=>'mixed'], 'print_r\'1' => ['true', 'value'=>'mixed', 'return='=>'bool'], @@ -10699,7 +10699,7 @@ 'register_tick_function' => ['bool', 'callback'=>'callable():void', '...args='=>'mixed'], 'rename' => ['bool', 'from'=>'string', 'to'=>'string', 'context='=>'resource'], 'rename_function' => ['bool', 'original_name'=>'string', 'new_name'=>'string'], -'reset' => ['mixed|false', '&r_array'=>'array|object'], +'reset' => ['mixed|false', '&r_array'=>'array'], 'ResourceBundle::__construct' => ['void', 'locale'=>'?string', 'bundle'=>'?string', 'fallback='=>'bool'], 'ResourceBundle::count' => ['int'], 'ResourceBundle::create' => ['?ResourceBundle', 'locale'=>'?string', 'bundle'=>'?string', 'fallback='=>'bool'], diff --git a/dictionaries/CallMap_81_delta.php b/dictionaries/CallMap_81_delta.php index bfb2da40bb6..eaa855863e6 100644 --- a/dictionaries/CallMap_81_delta.php +++ b/dictionaries/CallMap_81_delta.php @@ -1211,6 +1211,74 @@ 'old' => ['int|false', '&rw_read'=>'?resource[]', '&rw_write'=>'?resource[]', '&rw_except'=>'?resource[]', 'seconds'=>'?int', 'microseconds='=>'int'], 'new' => ['int|false', '&rw_read'=>'?resource[]', '&rw_write'=>'?resource[]', '&rw_except'=>'?resource[]', 'seconds'=>'?int', 'microseconds='=>'?int'], ], + 'mb_check_encoding' => [ + 'old' => ['bool', 'value='=>'array|string|null', 'encoding='=>'string|null'], + 'new' => ['bool', 'value'=>'array|string', 'encoding='=>'string|null'], + ], + 'ctype_alnum' => [ + 'old' => ['bool', 'text'=>'string|int'], + 'new' => ['bool', 'text'=>'string'], + ], + 'ctype_alpha' => [ + 'old' => ['bool', 'text'=>'string|int'], + 'new' => ['bool', 'text'=>'string'], + ], + 'ctype_cntrl' => [ + 'old' => ['bool', 'text'=>'string|int'], + 'new' => ['bool', 'text'=>'string'], + ], + 'ctype_digit' => [ + 'old' => ['bool', 'text'=>'string|int'], + 'new' => ['bool', 'text'=>'string'], + ], + 'ctype_graph' => [ + 'old' => ['bool', 'text'=>'string|int'], + 'new' => ['bool', 'text'=>'string'], + ], + 'ctype_lower' => [ + 'old' => ['bool', 'text'=>'string|int'], + 'new' => ['bool', 'text'=>'string'], + ], + 'ctype_print' => [ + 'old' => ['bool', 'text'=>'string|int'], + 'new' => ['bool', 'text'=>'string'], + ], + 'ctype_punct' => [ + 'old' => ['bool', 'text'=>'string|int'], + 'new' => ['bool', 'text'=>'string'], + ], + 'ctype_space' => [ + 'old' => ['bool', 'text'=>'string|int'], + 'new' => ['bool', 'text'=>'string'], + ], + 'ctype_upper' => [ + 'old' => ['bool', 'text'=>'string|int'], + 'new' => ['bool', 'text'=>'string'], + ], + 'ctype_xdigit' => [ + 'old' => ['bool', 'text'=>'string|int'], + 'new' => ['bool', 'text'=>'string'], + ], + 'key' => [ + 'old' => ['int|string|null', 'array'=>'array|object'], + 'new' => ['int|string|null', 'array'=>'array'], + ], + 'current' => [ + 'old' => ['mixed|false', 'array'=>'array|object'], + 'new' => ['mixed|false', 'array'=>'array'], + ], + 'next' => [ + 'old' => ['mixed', '&r_array'=>'array|object'], + 'new' => ['mixed', '&r_array'=>'array'], + ], + 'prev' => [ + 'old' => ['mixed', '&r_array'=>'array|object'], + 'new' => ['mixed', '&r_array'=>'array'], + ], + 'reset' => [ + 'old' => ['mixed|false', '&r_array'=>'array|object'], + 'new' => ['mixed|false', '&r_array'=>'array'], + ], ], 'removed' => [ diff --git a/dictionaries/CallMap_83_delta.php b/dictionaries/CallMap_83_delta.php index 8a4a76077b8..005017e4dfe 100644 --- a/dictionaries/CallMap_83_delta.php +++ b/dictionaries/CallMap_83_delta.php @@ -117,6 +117,14 @@ 'old' => ['string|false', 'haystack'=>'string', 'needle'=>'string'], 'new' => ['string|false', 'haystack'=>'string', 'needle'=>'string', 'before_needle='=>'bool'], ], + 'get_class' => [ + 'old' => ['class-string', 'object='=>'object'], + 'new' => ['class-string', 'object'=>'object'], + ], + 'get_parent_class' => [ + 'old' => ['class-string|false', 'object_or_class='=>'object|class-string'], + 'new' => ['class-string|false', 'object_or_class'=>'object|class-string'], + ], ], 'removed' => [ diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php index 92746fd697b..dd45575237d 100644 --- a/dictionaries/CallMap_historical.php +++ b/dictionaries/CallMap_historical.php @@ -10671,7 +10671,7 @@ 'getimagesize' => ['array{0:int, 1: int, 2: int, 3: string, mime: string, channels?: 3|4, bits?: int}|false', 'filename'=>'string', '&w_image_info='=>'array'], 'getimagesizefromstring' => ['array{0:int, 1: int, 2: int, 3: string, mime: string, channels?: 3|4, bits?: int}|false', 'string'=>'string', '&w_image_info='=>'array'], 'getlastmod' => ['int|false'], - 'getmxrr' => ['bool', 'hostname'=>'string', '&w_hosts'=>'array', '&w_weights='=>'array'], + 'getmxrr' => ['bool', 'hostname'=>'string', '&w_hosts'=>'array', '&w_weights='=>'array'], 'getmygid' => ['int|false'], 'getmyinode' => ['int|false'], 'getmypid' => ['int|false'], diff --git a/docs/running_psalm/error_levels.md b/docs/running_psalm/error_levels.md index 2392aeb2eb7..7e5fc58dfb4 100644 --- a/docs/running_psalm/error_levels.md +++ b/docs/running_psalm/error_levels.md @@ -234,6 +234,7 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even - [InvalidDocblockParamName](issues/InvalidDocblockParamName.md) - [InvalidFalsableReturnType](issues/InvalidFalsableReturnType.md) - [InvalidStringClass](issues/InvalidStringClass.md) +- [MissingClassConstType](issues/MissingClassConstType.md) - [MissingClosureParamType](issues/MissingClosureParamType.md) - [MissingClosureReturnType](issues/MissingClosureReturnType.md) - [MissingConstructor](issues/MissingConstructor.md) diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md index 45e76af86c6..5a006d1a15f 100644 --- a/docs/running_psalm/issues.md +++ b/docs/running_psalm/issues.md @@ -114,6 +114,7 @@ - [MismatchingDocblockParamType](issues/MismatchingDocblockParamType.md) - [MismatchingDocblockPropertyType](issues/MismatchingDocblockPropertyType.md) - [MismatchingDocblockReturnType](issues/MismatchingDocblockReturnType.md) + - [MissingClassConstType](issues/MissingClassConstType.md) - [MissingClosureParamType](issues/MissingClosureParamType.md) - [MissingClosureReturnType](issues/MissingClosureReturnType.md) - [MissingConstructor](issues/MissingConstructor.md) diff --git a/docs/running_psalm/issues/ConstructorSignatureMismatch.md b/docs/running_psalm/issues/ConstructorSignatureMismatch.md index 767375e14fa..710e313ea01 100644 --- a/docs/running_psalm/issues/ConstructorSignatureMismatch.md +++ b/docs/running_psalm/issues/ConstructorSignatureMismatch.md @@ -9,7 +9,7 @@ Emitted when a constructor parameter differs from a parent constructor parameter * @psalm-consistent-constructor */ class A { - public function __construct(int $i) {} + public function __construct(int $s) {} } class B extends A { public function __construct(string $s) {} diff --git a/docs/running_psalm/issues/MissingClassConstType.md b/docs/running_psalm/issues/MissingClassConstType.md new file mode 100644 index 00000000000..c4fa9049fc6 --- /dev/null +++ b/docs/running_psalm/issues/MissingClassConstType.md @@ -0,0 +1,21 @@ +# MissingClassConstType + +Emitted when a class constant doesn't have a declared type. + +```php + template_extended_params]]> template_types]]> + overridden_method_ids[$method_name])]]> @@ -940,8 +941,6 @@ - - @@ -1190,6 +1189,7 @@ + overridden_method_ids[$method_name])]]> @@ -1481,6 +1481,9 @@ line_number]]> type_end]]> type_start]]> + + + @@ -1514,6 +1517,12 @@ + + 0]]> + + + + @@ -1620,6 +1629,13 @@ cache->getFileMapCache()]]> + + + + + + + @@ -1714,6 +1730,7 @@ + @@ -1865,6 +1882,7 @@ template_extended_params]]> + offset_param_name])]]> @@ -1878,6 +1896,7 @@ template_extended_params[$container_class])]]> template_extended_params[$base_type->as_type->value])]]> template_extended_params[$base_type->value])]]> + lower_bounds[$atomic_type->offset_param_name])]]> @@ -2318,6 +2337,11 @@ + + + + + @@ -2327,4 +2351,9 @@ + + + + + diff --git a/src/Psalm/Codebase.php b/src/Psalm/Codebase.php index bc31ac99c01..7e13e4f1727 100644 --- a/src/Psalm/Codebase.php +++ b/src/Psalm/Codebase.php @@ -809,7 +809,7 @@ public function getMethodReturnsByRef(string|MethodIdentifier $method_id): bool public function getMethodReturnTypeLocation( string|MethodIdentifier $method_id, - CodeLocation &$defined_location = null, + ?CodeLocation &$defined_location = null, ): ?CodeLocation { return $this->methods->getMethodReturnTypeLocation( MethodIdentifier::wrap($method_id), @@ -1343,7 +1343,7 @@ public function getFunctionArgumentAtPosition(string $file_path, Position $posit */ public function getSignatureInformation( string $function_symbol, - string $file_path = null, + ?string $file_path = null, ): ?SignatureInformation { $signature_label = ''; $signature_documentation = null; @@ -1568,7 +1568,7 @@ public function getCompletionItemsForClassishThing( string $type_string, string $gap, bool $snippets_supported = false, - array $allow_visibilities = null, + ?array $allow_visibilities = null, array $ignore_fq_class_names = [], ): array { if ($allow_visibilities === null) { @@ -2052,8 +2052,21 @@ public function removeTemporaryFileChanges(string $file_path): void public function isTypeContainedByType( Union $input_type, Union $container_type, + bool $ignore_null = false, + bool $ignore_false = false, + bool $allow_interface_equality = false, + bool $allow_float_int_equality = true, ): bool { - return UnionTypeComparator::isContainedBy($this, $input_type, $container_type); + return UnionTypeComparator::isContainedBy( + $this, + $input_type, + $container_type, + $ignore_null, + $ignore_false, + null, + $allow_interface_equality, + $allow_float_int_equality, + ); } /** diff --git a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php index 25021770c34..bb2edb6e1c8 100644 --- a/src/Psalm/Internal/Analyzer/ClassAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/ClassAnalyzer.php @@ -834,7 +834,12 @@ public static function addContextProperties( $property_type = $property_storage->type; if (!$property_type->isMixed() - && !$property_storage->is_promoted + && (!$property_storage->is_promoted + || (strtolower($fq_class_name) !== strtolower($property_class_name) + && isset($storage->declaring_method_ids['__construct']) + && strtolower( + $storage->declaring_method_ids['__construct']->fq_class_name, + ) === strtolower($fq_class_name))) && !$property_storage->has_default && !($property_type->isNullable() && $property_type->from_docblock) ) { @@ -845,7 +850,13 @@ public static function addContextProperties( ]); } } else { - if (!$property_storage->has_default && !$property_storage->is_promoted) { + if (!$property_storage->has_default + && (!$property_storage->is_promoted + || (strtolower($fq_class_name) !== strtolower($property_class_name) + && isset($storage->declaring_method_ids['__construct']) + && strtolower( + $storage->declaring_method_ids['__construct']->fq_class_name, + ) === strtolower($fq_class_name)))) { $property_type = new Union([new TMixed()], [ 'initialized' => false, 'from_property' => true, @@ -1061,6 +1072,13 @@ private function checkPropertyInitialization( continue; } + if ($property->is_promoted + && strtolower($property_class_name) !== $fq_class_name_lc + && isset($storage->declaring_method_ids['__construct']) + && strtolower($storage->declaring_method_ids['__construct']->fq_class_name) === $fq_class_name_lc) { + $property_is_initialized = false; + } + if ($property->has_default || $property_is_initialized) { continue; } diff --git a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php index 758b44ef342..d5f53b4b15b 100644 --- a/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/FunctionLikeAnalyzer.php @@ -1593,7 +1593,7 @@ public function examineParamTypes( StatementsAnalyzer $statements_analyzer, Context $context, Codebase $codebase, - PhpParser\Node $stmt = null, + ?PhpParser\Node $stmt = null, ): void { $storage = $this->getFunctionLikeStorage($statements_analyzer); diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php index 913c31d5338..ff919b6c31e 100644 --- a/src/Psalm/Internal/Analyzer/MethodComparator.php +++ b/src/Psalm/Internal/Analyzer/MethodComparator.php @@ -42,7 +42,6 @@ use Psalm\Type\Union; use function array_filter; -use function count; use function in_array; use function str_starts_with; use function strtolower; @@ -506,15 +505,15 @@ private static function compareMethodParams( && $implementer_classlike_storage->user_defined && $implementer_param->location && $guide_method_storage->cased_name - && !str_starts_with($guide_method_storage->cased_name, '__') + && (!str_starts_with($guide_method_storage->cased_name, '__') + || ($guide_classlike_storage->preserve_constructor_signature + && $guide_method_storage->cased_name === '__construct')) && $config->isInProjectDirs( $implementer_param->location->file_path, ) ) { - if (!$guide_classlike_storage->user_defined && $i === 0 && count($guide_method_storage->params) < 2) { - // if it's third party defined and a single arg, renaming is unnecessary - // if we still want to psalter it, move this if and change the else below to elseif - } elseif ($config->allow_named_arg_calls + // even if it's just a single arg, it needs to be renamed in case it's called with a single named arg + if ($config->allow_named_arg_calls || ($guide_classlike_storage->location && !$config->isInProjectDirs($guide_classlike_storage->location->file_path) ) diff --git a/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php index 60e3acaa964..84d2961e979 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Block/LoopAnalyzer.php @@ -46,7 +46,7 @@ public static function analyze( array $pre_conditions, array $post_expressions, LoopScope $loop_scope, - Context &$continue_context = null, + ?Context &$continue_context = null, bool $is_do = false, bool $always_enters_loop = false, ): ?bool { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php index 04e1c9e2e48..10d14b45f18 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ArithmeticOpAnalyzer.php @@ -308,7 +308,7 @@ private static function analyzeOperands( bool &$has_valid_left_operand, bool &$has_valid_right_operand, bool &$has_string_increment, - Union &$result_type = null, + ?Union &$result_type = null, ): ?Union { if (($left_type_part instanceof TLiteralInt || $left_type_part instanceof TLiteralFloat) && ($right_type_part instanceof TLiteralInt || $right_type_part instanceof TLiteralFloat) diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php index f06c4f8a1b6..84f75075ed7 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/BinaryOp/ConcatAnalyzer.php @@ -61,7 +61,7 @@ public static function analyze( PhpParser\Node\Expr $left, PhpParser\Node\Expr $right, Context $context, - Union &$result_type = null, + ?Union &$result_type = null, ): void { $codebase = $statements_analyzer->getCodebase(); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php index 407304045c9..abc4ce59d70 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/ArgumentAnalyzer.php @@ -31,6 +31,7 @@ use Psalm\Internal\Type\TemplateStandinTypeReplacer; use Psalm\Internal\Type\TypeExpander; use Psalm\Issue\ArgumentTypeCoercion; +use Psalm\Issue\DeprecatedConstant; use Psalm\Issue\ImplicitToStringCast; use Psalm\Issue\InvalidArgument; use Psalm\Issue\InvalidLiteralArgument; @@ -40,6 +41,7 @@ use Psalm\Issue\NamedArgumentNotAllowed; use Psalm\Issue\NoValue; use Psalm\Issue\NullArgument; +use Psalm\Issue\ParentNotFound; use Psalm\Issue\PossiblyFalseArgument; use Psalm\Issue\PossiblyInvalidArgument; use Psalm\Issue\PossiblyNullArgument; @@ -60,6 +62,7 @@ use Psalm\Type\Atomic\TMixed; use Psalm\Type\Atomic\TNamedObject; use Psalm\Type\Union; +use UnexpectedValueException; use function count; use function explode; @@ -881,6 +884,20 @@ public static function verifyType( true, $context->insideUse(), ); + + if (self::verifyCallableInContext( + $potential_method_id, + $cased_method_id, + $method_id, + $atomic_type, + $argument_offset, + $arg_location, + $context, + $codebase, + $statements_analyzer, + ) === false) { + continue; + } } $input_type->removeType($key); @@ -943,7 +960,39 @@ public static function verifyType( $statements_analyzer->getFilePath(), ); + if ($potential_method_id === null && $codebase->analysis_php_version_id >= 8_02_00) { + [$lhs,] = $input_type_part->properties; + if ($lhs->isSingleStringLiteral() + && in_array( + strtolower($lhs->getSingleStringLiteral()->value), + ['self', 'parent', 'static'], + true, + )) { + IssueBuffer::maybeAdd( + new DeprecatedConstant( + 'Use of "' . $lhs->getSingleStringLiteral()->value . '" in callables is deprecated', + $arg_location, + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + } + if ($potential_method_id && $potential_method_id !== 'not-callable') { + if (self::verifyCallableInContext( + $potential_method_id, + $cased_method_id, + $method_id, + $input_type_part, + $argument_offset, + $arg_location, + $context, + $codebase, + $statements_analyzer, + ) === false) { + continue; + } + $potential_method_ids[] = $potential_method_id; } } elseif ($input_type_part instanceof TLiteralString @@ -951,10 +1000,41 @@ public static function verifyType( ) { $parts = explode('::', $input_type_part->value); /** @psalm-suppress PossiblyUndefinedIntArrayOffset */ - $potential_method_ids[] = new MethodIdentifier( + $potential_method_id = new MethodIdentifier( $parts[0], strtolower($parts[1]), ); + + if ($codebase->analysis_php_version_id >= 8_02_00 + && in_array( + strtolower($potential_method_id->fq_class_name), + ['self', 'parent', 'static'], + true, + )) { + IssueBuffer::maybeAdd( + new DeprecatedConstant( + 'Use of "' . $potential_method_id->fq_class_name . '" in callables is deprecated', + $arg_location, + ), + $statements_analyzer->getSuppressedIssues(), + ); + } + + if (self::verifyCallableInContext( + $potential_method_id, + $cased_method_id, + $method_id, + $input_type_part, + $argument_offset, + $arg_location, + $context, + $codebase, + $statements_analyzer, + ) === false) { + continue; + } + + $potential_method_ids[] = $potential_method_id; } } @@ -1191,6 +1271,131 @@ public static function verifyType( return null; } + private static function verifyCallableInContext( + MethodIdentifier $potential_method_id, + ?string $cased_method_id, + ?MethodIdentifier $method_id, + Atomic $input_type_part, + int $argument_offset, + CodeLocation $arg_location, + Context $context, + Codebase $codebase, + StatementsAnalyzer $statements_analyzer + ): ?bool { + $method_identifier = $cased_method_id !== null ? ' of ' . $cased_method_id : ''; + + if (!$method_id + || $potential_method_id->fq_class_name !== $context->self + || $method_id->fq_class_name !== $context->self) { + if ($input_type_part instanceof TKeyedArray) { + [$lhs,] = $input_type_part->properties; + } else { + $lhs = Type::getString($potential_method_id->fq_class_name); + } + + try { + $method_storage = $codebase->methods->getStorage($potential_method_id); + + $lhs_atomic = $lhs->getSingleAtomic(); + if ($lhs->isSingle() + && $lhs->hasNamedObjectType() + && ($lhs->isStaticObject() + || ($lhs_atomic instanceof TNamedObject + && !$lhs_atomic->definite_class + && $lhs_atomic->value === $context->self))) { + // callable $this + // some PHP-internal functions (e.g. array_filter) will call the callback within the current context + // unlike user-defined functions which call the callback in their context + // however this doesn't apply to all + // e.g. header_register_callback will not throw an error immediately like user-land functions + // however error log "Could not call the sapi_header_callback" if it's not public + // this is NOT a complete list, but just what was easily available and to be extended + $php_native_non_public_cb = [ + 'array_diff_uassoc', + 'array_diff_ukey', + 'array_filter', + 'array_intersect_uassoc', + 'array_intersect_ukey', + 'array_map', + 'array_reduce', + 'array_udiff', + 'array_udiff_assoc', + 'array_udiff_uassoc', + 'array_uintersect', + 'array_uintersect_assoc', + 'array_uintersect_uassoc', + 'array_walk', + 'array_walk_recursive', + 'preg_replace_callback', + 'preg_replace_callback_array', + 'call_user_func', + 'call_user_func_array', + 'forward_static_call', + 'forward_static_call_array', + 'is_callable', + 'ob_start', + 'register_shutdown_function', + 'register_tick_function', + 'session_set_save_handler', + 'set_error_handler', + 'set_exception_handler', + 'spl_autoload_register', + 'spl_autoload_unregister', + 'uasort', + 'uksort', + 'usort', + ]; + + if ($potential_method_id->fq_class_name !== $context->self + || ($cased_method_id !== null + && !$method_id + && !in_array($cased_method_id, $php_native_non_public_cb, true)) + || ($method_id + && $method_id->fq_class_name !== $context->self + && $method_id->fq_class_name !== 'Closure') + ) { + if ($method_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'Argument ' . ($argument_offset + 1) . $method_identifier + . ' expects a public callable, but a non-public callable provided', + $arg_location, + $cased_method_id, + ), + $statements_analyzer->getSuppressedIssues(), + ); + return false; + } + } + } elseif ($lhs->isSingle()) { + // instance from e.g. new Foo() or static string like Foo::bar + if ((!$method_storage->is_static && !$lhs->hasNamedObjectType()) + || $method_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC) { + IssueBuffer::maybeAdd( + new InvalidArgument( + 'Argument ' . ($argument_offset + 1) . $method_identifier + . ' expects a public static callable, but a ' + . ($method_storage->visibility !== ClassLikeAnalyzer::VISIBILITY_PUBLIC ? + 'non-public ' : '') + . (!$method_storage->is_static ? 'non-static ' : '') + . 'callable provided', + $arg_location, + $cased_method_id, + ), + $statements_analyzer->getSuppressedIssues(), + ); + + return false; + } + } + } catch (UnexpectedValueException $e) { + // do nothing + } + } + + return null; + } + /** * @param PhpParser\Node\Scalar\String_|PhpParser\Node\Expr\Array_|PhpParser\Node\Expr\BinaryOp\Concat $input_expr */ @@ -1289,6 +1494,16 @@ private static function verifyExplicitParam( if ($callable_fq_class_name === 'parent') { $container_class = $statements_analyzer->getParentFQCLN(); + if ($container_class === null) { + IssueBuffer::accepts( + new ParentNotFound( + 'Cannot call method on parent' + . ' as this class does not extend another', + $arg_location, + ), + $statements_analyzer->getSuppressedIssues(), + ); + } } if (!$container_class) { diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgInfo.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgInfo.php index 54921838c0c..8e45e6ad925 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgInfo.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/HighOrderFunctionArgInfo.php @@ -27,7 +27,7 @@ final class HighOrderFunctionArgInfo */ public function __construct( private readonly string $type, - private functionLikeStorage $function_storage, + private FunctionLikeStorage $function_storage, private readonly ?ClassLikeStorage $class_storage = null, ) { } diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php index c0139c041ca..cb146feceaf 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/NewAnalyzer.php @@ -78,7 +78,7 @@ public static function analyze( StatementsAnalyzer $statements_analyzer, PhpParser\Node\Expr\New_ $stmt, Context $context, - TemplateResult $template_result = null, + ?TemplateResult $template_result = null, ): bool { $fq_class_name = null; @@ -312,7 +312,7 @@ private static function analyzeNamedConstructor( string $fq_class_name, bool $from_static, bool $can_extend, - TemplateResult $template_result = null, + ?TemplateResult $template_result = null, ): void { $storage = $codebase->classlike_storage_provider->get($fq_class_name); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php index 5cd62f721c5..007886756e3 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticCallAnalyzer.php @@ -83,6 +83,28 @@ public static function analyze( $class_storage = $codebase->classlike_storage_provider->get($fq_class_name); $fq_class_name = $class_storage->name; + + if ($context->collect_initializations + && isset($stmt->name->name) + && $stmt->name->name === '__construct' + && isset($class_storage->declaring_method_ids['__construct'])) { + $construct_fq_class_name = $class_storage->declaring_method_ids['__construct']->fq_class_name; + $construct_class_storage = $codebase->classlike_storage_provider->get($construct_fq_class_name); + $construct_fq_class_name = $construct_class_storage->name; + + foreach ($construct_class_storage->properties as $property_name => $property_storage) { + if ($property_storage->is_promoted + && isset($context->vars_in_scope['$this->' . $property_name])) { + $context_type = $context->vars_in_scope['$this->' . $property_name]; + $context->vars_in_scope['$this->' . $property_name] = $context_type->setProperties( + [ + 'initialized_class' => $construct_fq_class_name, + 'initialized' => true, + ], + ); + } + } + } } elseif ($context->self) { if ($stmt->class->getFirst() === 'static' && isset($context->vars_in_scope['$this'])) { $fq_class_name = (string) $context->vars_in_scope['$this']; 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 e61af332e2c..156424de827 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/StaticMethod/AtomicStaticCallAnalyzer.php @@ -791,7 +791,7 @@ private static function handleNamedCall( } } - if (!$callstatic_method_exists || $class_storage->hasSealedMethods($config)) { + if ($naive_method_exists || !$callstatic_method_exists || $class_storage->hasSealedMethods($config)) { $does_method_exist = MethodAnalyzer::checkMethodExists( $codebase, $method_id, diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php index c23cf7610bc..6dd527c2827 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Fetch/ArrayFetchAnalyzer.php @@ -172,7 +172,7 @@ public static function analyze( $codebase = $statements_analyzer->getCodebase(); - if ($keyed_array_var_id + if ($keyed_array_var_id !== null && $context->hasVariable($keyed_array_var_id) && !$context->vars_in_scope[$keyed_array_var_id]->possibly_undefined && $stmt_var_type @@ -251,6 +251,10 @@ public static function analyze( } } + if ($context->inside_isset && !$stmt_type->hasMixed()) { + $stmt_type = Type::combineUnionTypes($stmt_type, Type::getNull()); + } + $statements_analyzer->node_data->setType($stmt, $stmt_type); if ($context->inside_isset @@ -305,7 +309,7 @@ public static function analyze( } } - if ($keyed_array_var_id + if ($keyed_array_var_id !== null && $context->hasVariable($keyed_array_var_id) && (!($stmt_type = $statements_analyzer->node_data->getType($stmt)) || $stmt_type->isVanillaMixed()) ) { @@ -476,8 +480,8 @@ public static function getArrayAccessTypeGivenOffset( bool $in_assignment, ?string $extended_var_id, Context $context, - PhpParser\Node\Expr $assign_value = null, - Union $replacement_type = null, + ?PhpParser\Node\Expr $assign_value = null, + ?Union $replacement_type = null, ): Union { $offset_type = $offset_type_original->getBuilder(); diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php index 93b6d68ea48..8005cd349df 100644 --- a/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php +++ b/src/Psalm/Internal/Analyzer/Statements/Expression/SimpleTypeInferer.php @@ -57,7 +57,7 @@ public static function infer( NodeDataProvider $nodes, PhpParser\Node\Expr $stmt, Aliases $aliases, - FileSource $file_source = null, + ?FileSource $file_source = null, ?array $existing_class_constants = null, ?string $fq_classlike_name = null, ): ?Union { @@ -541,7 +541,7 @@ private static function inferArrayType( NodeDataProvider $nodes, PhpParser\Node\Expr\Array_ $stmt, Aliases $aliases, - FileSource $file_source = null, + ?FileSource $file_source = null, ?array $existing_class_constants = null, ?string $fq_classlike_name = null, ): ?Union { @@ -625,7 +625,7 @@ private static function handleArrayItem( ArrayCreationInfo $array_creation_info, PhpParser\Node\ArrayItem $item, Aliases $aliases, - FileSource $file_source = null, + ?FileSource $file_source = null, ?array $existing_class_constants = null, ?string $fq_classlike_name = null, ): bool { diff --git a/src/Psalm/Internal/Codebase/Analyzer.php b/src/Psalm/Internal/Codebase/Analyzer.php index 80cdba04ddc..9f6b4f21691 100644 --- a/src/Psalm/Internal/Codebase/Analyzer.php +++ b/src/Psalm/Internal/Codebase/Analyzer.php @@ -1184,7 +1184,7 @@ public function addNodeType( string $file_path, PhpParser\Node $node, string $node_type, - PhpParser\Node $parent_node = null, + ?PhpParser\Node $parent_node = null, ): void { if ($node_type === '') { throw new UnexpectedValueException('non-empty node_type expected'); diff --git a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php index 35d0c77058d..73584f3c4e8 100644 --- a/src/Psalm/Internal/Codebase/ConstantTypeResolver.php +++ b/src/Psalm/Internal/Codebase/ConstantTypeResolver.php @@ -60,7 +60,7 @@ final class ConstantTypeResolver public static function resolve( ClassLikes $classlikes, UnresolvedConstantComponent $c, - StatementsAnalyzer $statements_analyzer = null, + ?StatementsAnalyzer $statements_analyzer = null, array $visited_constant_ids = [], ): Atomic { $c_id = spl_object_id($c); diff --git a/src/Psalm/Internal/Codebase/Methods.php b/src/Psalm/Internal/Codebase/Methods.php index 82f57aa381c..71739ce5e51 100644 --- a/src/Psalm/Internal/Codebase/Methods.php +++ b/src/Psalm/Internal/Codebase/Methods.php @@ -941,7 +941,7 @@ public function getMethodReturnsByRef(MethodIdentifier $method_id): bool public function getMethodReturnTypeLocation( MethodIdentifier $method_id, - CodeLocation &$defined_location = null, + ?CodeLocation &$defined_location = null, ): ?CodeLocation { $method_id = $this->getDeclaringMethodId($method_id); diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php index d7fc2f2ffcd..7e228846813 100644 --- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php +++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php @@ -51,6 +51,7 @@ use Psalm\Issue\InvalidEnumBackingType; use Psalm\Issue\InvalidEnumCaseValue; use Psalm\Issue\InvalidTypeImport; +use Psalm\Issue\MissingClassConstType; use Psalm\Issue\MissingDocblockType; use Psalm\Issue\MissingPropertyType; use Psalm\Issue\ParseError; @@ -81,6 +82,7 @@ use function ltrim; use function preg_match; use function preg_split; +use function sprintf; use function strtolower; use function trim; use function usort; @@ -418,9 +420,11 @@ public function start(PhpParser\Node\Stmt\ClassLike $node): ?bool try { $type_string = CommentAnalyzer::splitDocLine($type_string)[0]; } catch (DocblockParseException $e) { - throw new DocblockParseException( - $type_string . ' is not a valid type: ' . $e->getMessage(), + $storage->docblock_issues[] = new InvalidDocblock( + $e->getMessage() . ' in docblock for ' . $fq_classlike_name, + $name_location ?? $class_location, ); + continue; } $type_string = CommentAnalyzer::sanitizeDocblockType($type_string); try { @@ -1399,6 +1403,23 @@ private function visitClassConstDeclaration( $description, ); + + if ($this->codebase->analysis_php_version_id >= 8_03_00 + && $stmt->type === null + ) { + IssueBuffer::maybeAdd( + new MissingClassConstType( + sprintf( + 'Class constant "%s::%s" should have a declared type.', + $storage->name, + $const->name->name, + ), + new CodeLocation($this->file_scanner, $const), + ), + $suppressed_issues, + ); + } + if ($exists) { $existing_constants[$const->name->name] = $constant_storage; } diff --git a/src/Psalm/Internal/Provider/FakeFileProvider.php b/src/Psalm/Internal/Provider/FakeFileProvider.php index 134243213b7..24b78fac33b 100644 --- a/src/Psalm/Internal/Provider/FakeFileProvider.php +++ b/src/Psalm/Internal/Provider/FakeFileProvider.php @@ -81,7 +81,7 @@ public function deleteFile(string $file_path): void * @param null|callable(string):bool $filter * @return list */ - public function getFilesInDir(string $dir_path, array $file_extensions, callable $filter = null): array + public function getFilesInDir(string $dir_path, array $file_extensions, ?callable $filter = null): array { $file_paths = parent::getFilesInDir($dir_path, $file_extensions, $filter); diff --git a/src/Psalm/Internal/Provider/FileProvider.php b/src/Psalm/Internal/Provider/FileProvider.php index 2f26b8e32b7..91803a37061 100644 --- a/src/Psalm/Internal/Provider/FileProvider.php +++ b/src/Psalm/Internal/Provider/FileProvider.php @@ -157,7 +157,7 @@ public function isDirectory(string $file_path): bool * @param null|callable(string):bool $filter * @return list */ - public function getFilesInDir(string $dir_path, array $file_extensions, callable $filter = null): array + public function getFilesInDir(string $dir_path, array $file_extensions, ?callable $filter = null): array { $file_paths = []; diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php index b3bc3ad48b4..0ab9e045b88 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterInputReturnTypeProvider.php @@ -14,6 +14,7 @@ use Psalm\Type\Union; use UnexpectedValueException; +use function array_flip; use function array_search; use function in_array; use function is_array; @@ -50,7 +51,16 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev throw new UnexpectedValueException('Expected StatementsAnalyzer not StatementsSource'); } - $call_args = $event->getCallArgs(); + $arg_names = array_flip(['type', 'var_name', 'filter', 'options']); + $call_args = []; + foreach ($event->getCallArgs() as $idx => $arg) { + if (isset($arg->name)) { + $call_args[$arg_names[$arg->name->name]] = $arg; + } else { + $call_args[$idx] = $arg; + } + } + $function_id = $event->getFunctionId(); $code_location = $event->getCodeLocation(); $codebase = $statements_analyzer->getCodebase(); diff --git a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php index 7d56d35cce6..f3800f26713 100644 --- a/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php +++ b/src/Psalm/Internal/Provider/ReturnTypeProvider/FilterVarReturnTypeProvider.php @@ -11,6 +11,7 @@ use Psalm\Type\Union; use UnexpectedValueException; +use function array_flip; use function is_array; use function is_int; @@ -39,7 +40,16 @@ public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $ev throw new UnexpectedValueException(); } - $call_args = $event->getCallArgs(); + $arg_names = array_flip(['value', 'filter', 'options']); + $call_args = []; + foreach ($event->getCallArgs() as $idx => $arg) { + if (isset($arg->name)) { + $call_args[$arg_names[$arg->name->name]] = $arg; + } else { + $call_args[$idx] = $arg; + } + } + $function_id = $event->getFunctionId(); $code_location = $event->getCodeLocation(); $codebase = $statements_analyzer->getCodebase(); diff --git a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php index e024bc95fc0..48369076150 100644 --- a/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/AtomicTypeComparator.php @@ -10,6 +10,7 @@ use Psalm\Type\Atomic\Scalar; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; +use Psalm\Type\Atomic\TCallableInterface; use Psalm\Type\Atomic\TCallableKeyedArray; use Psalm\Type\Atomic\TCallableObject; use Psalm\Type\Atomic\TCallableString; @@ -187,7 +188,8 @@ public static function isContainedBy( } if (($container_type_part instanceof TCallable - && $input_type_part instanceof TCallable) + && $input_type_part instanceof TCallableInterface + ) || ($container_type_part instanceof TClosure && $input_type_part instanceof TClosure) ) { diff --git a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php index 047ddd08bda..e849cf8a410 100644 --- a/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/CallableTypeComparator.php @@ -20,6 +20,7 @@ use Psalm\Type\Atomic; use Psalm\Type\Atomic\TArray; use Psalm\Type\Atomic\TCallable; +use Psalm\Type\Atomic\TCallableInterface; use Psalm\Type\Atomic\TClassString; use Psalm\Type\Atomic\TClosure; use Psalm\Type\Atomic\TKeyedArray; @@ -40,16 +41,28 @@ */ final class CallableTypeComparator { - /** - * @param TCallable|TClosure $input_type_part - * @param TCallable|TClosure $container_type_part - */ public static function isContainedBy( Codebase $codebase, - Atomic $input_type_part, - Atomic $container_type_part, + TClosure|TCallableInterface $input_type_part, + TCallable|TClosure $container_type_part, ?TypeComparisonResult $atomic_comparison_result, ): bool { + if ($container_type_part instanceof TClosure) { + if ($input_type_part instanceof TCallableInterface + && !$input_type_part instanceof TCallable // it has stricter checks below + ) { + if ($atomic_comparison_result) { + $atomic_comparison_result->type_coerced = true; + } + return false; + } + } + if ($input_type_part instanceof TCallableInterface + && !$input_type_part instanceof TCallable // it has stricter checks below + ) { + return true; + } + if ($container_type_part->is_pure && !$input_type_part->is_pure) { if ($atomic_comparison_result) { $atomic_comparison_result->type_coerced = $input_type_part->is_pure === null; diff --git a/src/Psalm/Internal/Type/ParseTree/Value.php b/src/Psalm/Internal/Type/ParseTree/Value.php index 04036e7b9f2..94a434da8ea 100644 --- a/src/Psalm/Internal/Type/ParseTree/Value.php +++ b/src/Psalm/Internal/Type/ParseTree/Value.php @@ -18,7 +18,7 @@ public function __construct( public int $offset_start, public int $offset_end, ?string $text, - ParseTree $parent = null, + ?ParseTree $parent = null, ) { $this->parent = $parent; $this->text = $text === $value ? null : $text; diff --git a/src/Psalm/Internal/Type/TypeCombiner.php b/src/Psalm/Internal/Type/TypeCombiner.php index 2abaedef7b7..b0c3fdbc8b5 100644 --- a/src/Psalm/Internal/Type/TypeCombiner.php +++ b/src/Psalm/Internal/Type/TypeCombiner.php @@ -1001,7 +1001,20 @@ private static function scrapeStringProperties( if (!$type->as_type) { $combination->class_string_types['object'] = new TObject(); } else { - $combination->class_string_types[$type->as] = $type->as_type; + if (isset($combination->class_string_types[$type->as]) + && $combination->class_string_types[$type->as] instanceof TNamedObject + ) { + if ($combination->class_string_types[$type->as]->extra_types === []) { + // do nothing, existing type is wider or the same + } elseif ($type->as_type->extra_types === []) { + $combination->class_string_types[$type->as] = $type->as_type; + } else { + // todo: figure out what to do with class-string|class-string + $combination->class_string_types[$type->as] = $type->as_type; + } + } else { + $combination->class_string_types[$type->as] = $type->as_type; + } } } elseif ($type instanceof TLiteralString) { if ($combination->strings !== null && count($combination->strings) < $literal_limit) { diff --git a/src/Psalm/Issue/MissingClassConstType.php b/src/Psalm/Issue/MissingClassConstType.php new file mode 100644 index 00000000000..38f29dbcec5 --- /dev/null +++ b/src/Psalm/Issue/MissingClassConstType.php @@ -0,0 +1,11 @@ +defining_class); } diff --git a/src/Psalm/Storage/FunctionLikeStorage.php b/src/Psalm/Storage/FunctionLikeStorage.php index da1fe09ecf0..f4f7d37f341 100644 --- a/src/Psalm/Storage/FunctionLikeStorage.php +++ b/src/Psalm/Storage/FunctionLikeStorage.php @@ -257,7 +257,7 @@ public function setParams(array $params): void /** * @internal */ - public function addParam(FunctionLikeParameter $param, bool $lookup_value = null): void + public function addParam(FunctionLikeParameter $param, ?bool $lookup_value = null): void { $this->params[] = $param; $this->param_lookup[$param->name] = $lookup_value ?? true; diff --git a/src/Psalm/Type/Atomic.php b/src/Psalm/Type/Atomic.php index 3f1aa6be384..f8c501395b0 100644 --- a/src/Psalm/Type/Atomic.php +++ b/src/Psalm/Type/Atomic.php @@ -744,7 +744,7 @@ public function replaceTemplateTypesWithStandins( TemplateResult $template_result, Codebase $codebase, ?StatementsAnalyzer $statements_analyzer = null, - Atomic $input_type = null, + ?Atomic $input_type = null, ?int $input_arg_offset = null, ?string $calling_class = null, ?string $calling_function = null, diff --git a/src/Psalm/Type/Atomic/TCallable.php b/src/Psalm/Type/Atomic/TCallable.php index cbf8141d1db..96a7150b927 100644 --- a/src/Psalm/Type/Atomic/TCallable.php +++ b/src/Psalm/Type/Atomic/TCallable.php @@ -17,7 +17,7 @@ * * @psalm-immutable */ -final class TCallable extends Atomic +final class TCallable extends Atomic implements TCallableInterface { use UnserializeMemoryUsageSuppressionTrait; use CallableTrait; diff --git a/src/Psalm/Type/Atomic/TCallableInterface.php b/src/Psalm/Type/Atomic/TCallableInterface.php new file mode 100644 index 00000000000..e25f076d1a7 --- /dev/null +++ b/src/Psalm/Type/Atomic/TCallableInterface.php @@ -0,0 +1,7 @@ + [], 'ignored_issues' => ['UndefinedDocblockClass'], ], + 'canExtendArrayObjectOffsetSet' => [ + 'code' => <<<'PHP' + */ + class C extends ArrayObject { + public function offsetSet(mixed $key, mixed $value): void { + parent::offsetSet($key, $value); + } + } + PHP, + 'assertions' => [], + 'ignored_issues' => [], + 'php_version' => '8.0', + ], ]; } @@ -1562,6 +1581,16 @@ function takesArrayOfFloats(array $arr): void { if ($x === null) {}', 'error_message' => 'PossiblyUndefinedArrayOffset', ], + 'cannotUseNamedArgumentsForArrayAccess' => [ + 'code' => <<<'PHP' + $a */ + function f(ArrayAccess $a): void { + echo $a->offsetGet(offset: 0); + } + PHP, + 'error_message' => 'NamedArgumentNotAllowed', + ], ]; } } diff --git a/tests/ArrayAssignmentTest.php b/tests/ArrayAssignmentTest.php index 343b86506b2..8ae84b946c8 100644 --- a/tests/ArrayAssignmentTest.php +++ b/tests/ArrayAssignmentTest.php @@ -1236,6 +1236,8 @@ function takesList(array $arr) : void { foreach ($arr[0] as $k => $v) {} } }', + 'assertions' => [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'nonEmptyAssignmentToListElement' => [ 'code' => ' [ '$line===' => 'array{0: int, ...}', ], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'arrayUnshiftOnEmptyArrayMeansNonEmptyList' => [ 'code' => 'analyzeFile('somefile.php', new Context()); } + public function testAssertsAllongCallStaticMethodWork(): void + { + $this->addFile( + 'somefile.php', + 'analyzeFile('somefile.php', new Context()); + } + public function testAssertInvalidDocblockMessageDoesNotIncludeTrace(): void { $this->expectException(CodeException::class); diff --git a/tests/CallableTest.php b/tests/CallableTest.php index 18b2f343439..185f23593dd 100644 --- a/tests/CallableTest.php +++ b/tests/CallableTest.php @@ -1788,6 +1788,421 @@ function takesCallable(callable $c) : void {} takesCallable(function() { return; });', ], + 'callableMethodOutOfClassContextStaticPublic' => [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => ' [ + 'code' => 'run_in_c(array($this, "hello")); + } + + public function hello(): void { + echo "hello"; + } + + /** + * @param callable $callable + * @return void + */ + public function run_in_c($callable) { + call_user_func($callable); + } + }', + ], + 'callableInstanceArrayMethodClassContextNonStaticNonPublic' => [ + 'code' => 'run_in_c(array($this, "hello")); + } + + protected function hello(): void { + echo "hello"; + } + + /** + * @param callable $callable + * @return void + */ + private function run_in_c($callable) { + call_user_func($callable); + } + }', + ], + 'callableClassConstantArrayMethodClassContextStaticNonPublic' => [ + 'code' => 'run_in_c(array(Foo::class, "hello")); + } + + protected static function hello(): void { + echo "hello"; + } + + /** + * @param callable $callable + * @return void + */ + private function run_in_c($callable) { + call_user_func($callable); + } + }', + ], + 'callableClassConstantArrayMethodClassContextNonStaticNonPublic' => [ + 'code' => 'run_in_c(array(Foo::class, "hello")); + } + + protected function hello(): void { + echo "hello"; + } + + /** + * @param callable $callable + * @return void + */ + private function run_in_c($callable) { + call_user_func($callable); + } + }', + ], + 'callableClassStringArrayMethodOtherClassContextStaticPublic' => [ + 'code' => 'run_in_c(array(Foo::class, "hello")); + } + + public static function hello(): void { + echo "hello"; + } + } + + class Bar { + /** + * @param callable $callable + * @return void + */ + public function run_in_c($callable) { + call_user_func($callable); + } + } + ', + ], + 'callableInstanceArrayMethodOtherClassContextNonStaticPublic' => [ + 'code' => 'run_in_c(array($this, "hello")); + } + + public function hello(): void { + echo "hello"; + } + } + + class Bar { + /** + * @param callable $callable + * @return void + */ + public function run_in_c($callable) { + call_user_func($callable); + } + } + ', + ], + 'callableClassLiteralStringMethodOtherClassContextStaticPublic' => [ + 'code' => 'run_in_c("Foo::hello"); + } + + public static function hello(): void { + echo "hello"; + } + } + + class Bar { + /** + * @param callable $callable + * @return void + */ + public function run_in_c($callable) { + call_user_func($callable); + } + } + ', + ], + # @todo valid 'notCallableListNoUndefinedClass' => [ 'code' => ' [], 'php_version' => '8.0', ], + 'callableArrayPassedAsCallable' => [ + 'code' => <<<'PHP' + 'InvalidFunctionCall', ], + 'callableMethodOutOfClassContextNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableMethodOutOfClassContextNonStaticNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableClassStringArrayMethodOutOfClassContextNonStatic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableClassStringArrayMethodOutOfClassContextNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableClassStringArrayMethodOutOfClassContextNonStaticNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableClassStringMethodOutOfClassContextNonStatic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableClassStringMethodOutOfClassContextNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableClassStringMethodOutOfClassContextNonStaticNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInClassStringArrayMethodOutOfClassContextNonStatic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInClassStringArrayMethodOutOfClassContextNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInClassStringArrayMethodOutOfClassContextNonStaticNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInClassLiteralStringArrayMethodOutOfClassContextNonStatic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInClassLiteralStringArrayMethodOutOfClassContextNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInClassLiteralStringArrayMethodOutOfClassContextNonStaticNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInClassConstantArrayMethodOutOfClassContextNonStatic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInClassConstantArrayMethodOutOfClassContextNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInClassConstantArrayMethodOutOfClassContextNonStaticNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInClassStringMethodOutOfClassContextNonStatic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInClassStringMethodOutOfClassContextNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInClassStringMethodOutOfClassContextNonStaticNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInstanceArrayMethodClassContextPhpNativeUnsupportedNonStaticNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInstanceArrayMethodOutOfClassContextNonStaticNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableInstanceArrayMethodOutOfClassContextStaticNonPublic' => [ + 'code' => ' 'InvalidArgument', + ], + 'callableClassStringArrayMethodOtherClassContextNonStaticPublic' => [ + 'code' => 'run_in_c(array(Foo::class, "hello")); + } + + public function hello(): void { + echo "hello"; + } + } + + class Bar { + /** + * @param callable $callable + * @return void + */ + public function run_in_c($callable) { + call_user_func($callable); + } + }', + 'error_message' => 'InvalidArgument', + ], + 'callableInstanceArrayMethodOtherClassContextNonStaticNonPublic' => [ + 'code' => 'run_in_c(array($this, "hello")); + } + + protected function hello(): void { + echo "hello"; + } + } + + class Bar { + /** + * @param callable $callable + * @return void + */ + public function run_in_c($callable) { + call_user_func($callable); + } + }', + 'error_message' => 'InvalidArgument', + ], + 'callableClassLiteralStringMethodOtherClassContextStaticNonPublic' => [ + 'code' => 'run_in_c("Foo::hello"); + } + + protected static function hello(): void { + echo "hello"; + } + } + + class Bar { + /** + * @param callable $callable + * @return void + */ + public function run_in_c($callable) { + call_user_func($callable); + } + }', + 'error_message' => 'InvalidArgument', + ], + # @todo invalid 'ImpureFunctionCall' => [ 'code' => ' 'InvalidArgument', ], + 'callableArrayParentConstantDeprecated' => [ + 'code' => 'run(["parent", "hello"]); + } + + /** + * @param callable $callable + * @return void + */ + public function run($callable) { + call_user_func($callable); + } + }', + 'error_message' => 'DeprecatedConstant', + 'ignored_issues' => [], + 'php_version' => '8.2', + ], + 'callableParentConstantDeprecated' => [ + 'code' => 'run("parent::hello"); + } + + /** + * @param callable $callable + * @return void + */ + public function run($callable) { + call_user_func($callable); + } + }', + 'error_message' => 'DeprecatedConstant', + 'ignored_issues' => [], + 'php_version' => '8.2', + ], + 'callableSelfConstantDeprecated' => [ + 'code' => 'run("self::hello"); + } + + public static function hello(): void { + echo "hello"; + } + + /** + * @param callable $callable + * @return void + */ + public function run($callable) { + call_user_func($callable); + } + }', + 'error_message' => 'DeprecatedConstant', + 'ignored_issues' => [], + 'php_version' => '8.2', + ], + 'callableStaticConstantDeprecated' => [ + 'code' => 'run("static::hello"); + } + + public static function hello(): void { + echo "hello"; + } + + /** + * @param callable $callable + * @return void + */ + public function run($callable) { + call_user_func($callable); + } + }', + 'error_message' => 'DeprecatedConstant', + 'ignored_issues' => [], + 'php_version' => '8.2', + ], + 'callableArrayStaticConstantDeprecated' => [ + 'code' => 'run(["static", "hello"]); + } + + public static function hello(): void { + echo "hello"; + } + + /** + * @param callable $callable + * @return void + */ + public function run($callable) { + call_user_func($callable); + } + }', + 'error_message' => 'DeprecatedConstant', + 'ignored_issues' => [], + 'php_version' => '8.2', + ], 'invalidFirstClassCallableCannotBeInferred' => [ 'code' => ' [], 'php_version' => '8.0', ], + 'parentCallableArrayWithoutParent' => [ + 'code' => 'run(["parent", "hello"]); + } + + /** + * @param callable $callable + * @return void + */ + public function run($callable) { + call_user_func($callable); + } + }', + 'error_message' => 'ParentNotFound', + ], + 'parentCallableWithoutParent' => [ + 'code' => 'run("parent::hello"); + } + + /** + * @param callable $callable + * @return void + */ + public function run($callable) { + call_user_func($callable); + } + }', + 'error_message' => 'ParentNotFound', + ], ]; } } diff --git a/tests/ClassTest.php b/tests/ClassTest.php index e51d3bb2bed..ff312a72460 100644 --- a/tests/ClassTest.php +++ b/tests/ClassTest.php @@ -985,6 +985,29 @@ class A {} echo A::HELLO;', 'error_message' => 'UndefinedConstant', ], + 'consistentNamesConstructor' => [ + 'code' => ' 'ParamNameMismatch', + ], 'overridePublicAccessLevelToPrivate' => [ 'code' => 'addFile( $file_path, - ' "Psalm\Internal\Analyzer\ProjectAnalyzer", - ]; - }', + sprintf( + <<<'PHP' + "Psalm\Internal\Analyzer\ProjectAnalyzer", + ]; + } + PHP, + $this->project_analyzer->getCodebase()->analysis_php_version_id >= 8_03_00 ? 'array' : '', + ), ); $this->analyzeFile($file_path, new Context()); @@ -175,14 +181,18 @@ public function testStringAnalyzerPluginWithClassConstantConcat(): void $this->addFile( $file_path, - ' \Psalm\Internal\Analyzer\ProjectAnalyzer::class . "::foo", - ]; - }', + sprintf( + <<<'PHP' + \Psalm\Internal\Analyzer\ProjectAnalyzer::class . "::foo", + ]; + } + PHP, + $this->project_analyzer->getCodebase()->analysis_php_version_id >= 8_03_00 ? 'array' : '', + ), ); $this->analyzeFile($file_path, new Context()); diff --git a/tests/DocumentationTest.php b/tests/DocumentationTest.php index 1518f792b56..7745cfb07f9 100644 --- a/tests/DocumentationTest.php +++ b/tests/DocumentationTest.php @@ -320,6 +320,7 @@ public function providerInvalidCodeParse(): array case 'InvalidOverride': case 'MissingOverrideAttribute': + case 'MissingClassConstType': $php_version = '8.3'; break; } diff --git a/tests/EndToEnd/PsalmEndToEndTest.php b/tests/EndToEnd/PsalmEndToEndTest.php index 1e1f8124ab6..c92bb736f05 100644 --- a/tests/EndToEnd/PsalmEndToEndTest.php +++ b/tests/EndToEnd/PsalmEndToEndTest.php @@ -28,6 +28,7 @@ use function unlink; use const DIRECTORY_SEPARATOR; +use const PHP_BINARY; /** * Tests some of the most important use cases of the psalm and psalter commands, by launching a new @@ -120,7 +121,7 @@ public function testAlter(): void public function testPsalter(): void { $this->runPsalmInit(); - (new Process(['php', $this->psalter, '--alter', '--issues=InvalidReturnType'], self::$tmpDir))->mustRun(); + (new Process([PHP_BINARY, $this->psalter, '--alter', '--issues=InvalidReturnType'], self::$tmpDir))->mustRun(); $this->assertSame(0, $this->runPsalm([], self::$tmpDir)['CODE']); } @@ -250,7 +251,7 @@ public function testLegacyConfigWithoutresolveFromConfigFile(): void file_put_contents(self::$tmpDir . '/src/psalm.xml', $psalmXmlContent); - $process = new Process(['php', $this->psalm, '--config=src/psalm.xml'], self::$tmpDir); + $process = new Process([PHP_BINARY, $this->psalm, '--config=src/psalm.xml'], self::$tmpDir); $process->run(); $this->assertSame(2, $process->getExitCode()); $this->assertStringContainsString('InvalidReturnType', $process->getOutput()); diff --git a/tests/EndToEnd/PsalmRunnerTrait.php b/tests/EndToEnd/PsalmRunnerTrait.php index 33d82e7c7a7..950f02190e6 100644 --- a/tests/EndToEnd/PsalmRunnerTrait.php +++ b/tests/EndToEnd/PsalmRunnerTrait.php @@ -10,6 +10,8 @@ use function array_unshift; use function in_array; +use const PHP_BINARY; + trait PsalmRunnerTrait { private string $psalm = __DIR__ . '/../../psalm'; @@ -39,9 +41,9 @@ private function runPsalm( // we run `php psalm` rather than just `psalm`. if ($relyOnConfigDir) { - $process = new Process(array_merge(['php', $this->psalm, '-c=' . $workingDir . '/psalm.xml'], $args), null); + $process = new Process(array_merge([PHP_BINARY, $this->psalm, '-c=' . $workingDir . '/psalm.xml'], $args), null); } else { - $process = new Process(array_merge(['php', $this->psalm], $args), $workingDir); + $process = new Process(array_merge([PHP_BINARY, $this->psalm], $args), $workingDir); } if (!$shouldFail) { diff --git a/tests/FunctionCallTest.php b/tests/FunctionCallTest.php index c64a959265a..f85381503dc 100644 --- a/tests/FunctionCallTest.php +++ b/tests/FunctionCallTest.php @@ -1077,6 +1077,9 @@ function foo(string $s) : void { ], 'filterInput' => [ 'code' => ' [ 'code' => ' ['8.0'], 'fiber::start', + 'get_class' => ['8.3'], + 'get_parent_class' => ['8.3'], 'imagefilledpolygon', 'imagegd', 'imagegd2', @@ -97,6 +99,7 @@ class InternalCallMapHandlerTest extends TestCase 'mailparse_msg_get_structure', 'mailparse_msg_parse', 'mailparse_stream_encode', + 'mb_check_encoding' => ['8.1', '8.2', '8.3'], 'memcached::cas', // memcached 3.2.0 has incorrect reflection 'memcached::casbykey', // memcached 3.2.0 has incorrect reflection 'oauth::fetch', diff --git a/tests/MethodCallTest.php b/tests/MethodCallTest.php index b8719abb685..7c28032e862 100644 --- a/tests/MethodCallTest.php +++ b/tests/MethodCallTest.php @@ -1146,9 +1146,9 @@ public static function new() : self { class Datetime extends \DateTime { - public static function createFromInterface(\DateTimeInterface $datetime): static + public static function createFromInterface(\DateTimeInterface $object): static { - return parent::createFromInterface($datetime); + return parent::createFromInterface($object); } }', 'assertions' => [], diff --git a/tests/MethodSignatureTest.php b/tests/MethodSignatureTest.php index 2bb229acf8f..28d5e2a8ca8 100644 --- a/tests/MethodSignatureTest.php +++ b/tests/MethodSignatureTest.php @@ -463,13 +463,13 @@ class A implements Serializable { private $id = 1; /** - * @param string $serialized + * @param string $data */ - public function unserialize($serialized) : void + public function unserialize($data) : void { [ $this->id, - ] = (array) \unserialize($serialized); + ] = (array) \unserialize($data); } public function serialize() : string @@ -1074,6 +1074,21 @@ public function fooFoo(int $a, bool $c): void { }', 'error_message' => 'ParamNameMismatch', ], + 'differentArgumentName' => [ + 'code' => ' 'ParamNameMismatch', + ], 'nonNullableSubclassParam' => [ 'code' => ' [ 'code' => ' 'ImplementedParamTypeMismatch', diff --git a/tests/MissingClassConstTypeTest.php b/tests/MissingClassConstTypeTest.php new file mode 100644 index 00000000000..de413050fdb --- /dev/null +++ b/tests/MissingClassConstTypeTest.php @@ -0,0 +1,60 @@ += PHP 8.3' => [ + 'code' => <<<'PHP' + [], + 'ignored_issues' => [], + 'php_version' => '8.3', + ], + 'no type; < PHP 8.3' => [ + 'code' => <<<'PHP' + [], + 'ignored_issues' => [], + 'php_version' => '8.2', + ], + ]; + } + + public function providerInvalidCodeParse(): iterable + { + return [ + 'no type; >= PHP 8.3' => [ + 'code' => <<<'PHP' + MissingClassConstType::getIssueType(), + 'error_levels' => [], + 'php_version' => '8.3', + ], + ]; + } +} diff --git a/tests/PropertyTypeTest.php b/tests/PropertyTypeTest.php index 0a6cb497b83..e447c80d28a 100644 --- a/tests/PropertyTypeTest.php +++ b/tests/PropertyTypeTest.php @@ -589,6 +589,22 @@ class A { 'MixedAssignment', ], ], + 'promotedPropertyNoExtendedConstructor' => [ + 'code' => ' [], + 'ignored_issues' => [], + 'php_version' => '8.0', + ], 'propertyWithoutTypeSuppressingIssueAndAssertingNull' => [ 'code' => ' 'PropertyNotSetInConstructor', ], + 'promotedPropertyNotSetInExtendedConstructor' => [ + 'code' => ' 'PropertyNotSetInConstructor', + 'ignored_issues' => [], + 'php_version' => '8.0', + ], 'nullableTypedPropertyNoConstructor' => [ 'code' => ' 'InvalidReturnStatement', ], + 'noCrashOnBrokenTemplate' => [ + 'code' => <<<'PHP' + |string + */ + class C {} + PHP, + 'error_message' => 'InvalidDocblock', + ], ]; } } diff --git a/tests/TypeCombinationTest.php b/tests/TypeCombinationTest.php index a30e9e55308..d6bed2ea739 100644 --- a/tests/TypeCombinationTest.php +++ b/tests/TypeCombinationTest.php @@ -942,6 +942,13 @@ public function providerTestValidTypeCombination(): array '"0"', ], ], + 'unionOfClassStringAndClassStringWithIntersection' => [ + 'class-string', + [ + 'class-string', + 'class-string', + ], + ], ]; } diff --git a/tests/TypeComparatorTest.php b/tests/TypeComparatorTest.php index 00ad55bea49..45cad1e367b 100644 --- a/tests/TypeComparatorTest.php +++ b/tests/TypeComparatorTest.php @@ -165,6 +165,22 @@ public function getSuccessfulComparisons(): array '(callable(int,string[]): void)|(callable(int): void)', '(callable(int): void)|(callable(int,string[]): void)', ], + 'callableAcceptsCallableArray' => [ + 'callable', + "callable-array{0: class-string, 1: 'from'}", + ], + 'callableAcceptsCallableObject' => [ + 'callable', + "callable-object", + ], + 'callableAcceptsCallableString' => [ + 'callable', + 'callable-string', + ], + 'callableAcceptsCallableKeyedList' => [ + 'callable', + "callable-list{class-string, 'from'}", + ], ]; } diff --git a/tests/TypeParseTest.php b/tests/TypeParseTest.php index 5250079e45a..d73fe0a1609 100644 --- a/tests/TypeParseTest.php +++ b/tests/TypeParseTest.php @@ -1158,6 +1158,14 @@ public function testIntMaskOfWithValidValueOf(): void $this->assertSame('int-mask-of>', $docblock_type->getId()); } + public function testUnionOfClassStringAndClassStringWithIntersection(): void + { + $this->assertSame( + 'class-string', + (string) Type::parseString('class-string|class-string'), + ); + } + public function testReflectionTypeParse(): void { if (!function_exists('Psalm\Tests\someFunction')) { diff --git a/tests/TypeReconciliation/EmptyTest.php b/tests/TypeReconciliation/EmptyTest.php index 9d5658d3236..8804692efab 100644 --- a/tests/TypeReconciliation/EmptyTest.php +++ b/tests/TypeReconciliation/EmptyTest.php @@ -233,6 +233,8 @@ function _processScopes($scopes) : void { function foo(array $o) : void { if (empty($o[0]) && empty($o[1])) {} }', + 'assertions' => [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'multipleEmptiesInConditionWithMixedOffset' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'doubleEmptyCheckTwoArrays' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'doubleEmptyCheckOnTKeyedArrayVariableOffsets' => [ 'code' => ' [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'checkArrayEmptyUnknownRoot' => [ 'code' => ' 'true', ], ], + 'emptyArrayFetch' => [ + 'code' => ' $a */ + if (empty($a["a"])) {}', + ], ]; } @@ -760,6 +773,13 @@ function bar() { }', 'error_message' => 'RedundantConditionGivenDocblockType', ], + 'redundantEmptyArrayFetch' => [ + 'code' => ' $a */; + assert(isset($a["a"])); + if (empty($a["a"])) {}', + 'error_message' => 'DocblockTypeContradiction', + ], ]; } } diff --git a/tests/TypeReconciliation/RedundantConditionTest.php b/tests/TypeReconciliation/RedundantConditionTest.php index 59c66fdfbad..27027bbd7de 100644 --- a/tests/TypeReconciliation/RedundantConditionTest.php +++ b/tests/TypeReconciliation/RedundantConditionTest.php @@ -635,6 +635,8 @@ function foo(array $a) : void { if (empty($a["foo"])) {} } }', + 'assertions' => [], + 'ignored_issues' => ['RiskyTruthyFalsyComparison'], ], 'suppressRedundantConditionAfterAssertNonEmpty' => [ 'code' => '