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' => '