diff --git a/config.xsd b/config.xsd
index 42ad433c2..ad48f587e 100644
--- a/config.xsd
+++ b/config.xsd
@@ -292,6 +292,7 @@
+
diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php
index 39ca59e43..02baede99 100644
--- a/dictionaries/CallMap.php
+++ b/dictionaries/CallMap.php
@@ -7304,7 +7304,7 @@ return [
'mb_regex_set_options' => ['string', 'options='=>'string|null'],
'mb_scrub' => ['string', 'string'=>'string', 'encoding='=>'string|null'],
'mb_send_mail' => ['bool', 'to'=>'string', 'subject'=>'string', 'message'=>'string', 'additional_headers='=>'string|array', 'additional_params='=>'string|null'],
-'mb_split' => ['list', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'],
+'mb_split' => ['list|false', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'],
'mb_str_split' => ['list', 'string'=>'string', 'length='=>'positive-int', 'encoding='=>'string|null'],
'mb_strcut' => ['string', 'string'=>'string', 'start'=>'int', 'length='=>'?int', 'encoding='=>'string|null'],
'mb_strimwidth' => ['string', 'string'=>'string', 'start'=>'int', 'width'=>'int', 'trim_marker='=>'string', 'encoding='=>'string|null'],
@@ -10291,7 +10291,7 @@ return [
'proc_close' => ['int', 'process'=>'resource'],
'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}', 'process'=>'resource'],
'proc_nice' => ['bool', 'priority'=>'int'],
-'proc_open' => ['resource|false', 'cmd'=>'string|array', 'descriptorspec'=>'array', '&w_pipes'=>'resource[]', 'cwd='=>'?string', 'env='=>'?array', 'other_options='=>'array'],
+'proc_open' => ['resource|false', 'command'=>'string|array', 'descriptor_spec'=>'array', '&pipes'=>'resource[]', 'cwd='=>'?string', 'env_vars='=>'?array', 'options='=>'?array'],
'proc_terminate' => ['bool', 'process'=>'resource', 'signal='=>'int'],
'projectionObj::__construct' => ['void', 'projectionString'=>'string'],
'projectionObj::getUnits' => ['int'],
@@ -10567,7 +10567,7 @@ return [
'rd_kafka_offset_tail' => ['int', 'cnt'=>'int'],
'RdKafka::addBrokers' => ['int', 'broker_list'=>'string'],
'RdKafka::flush' => ['int', 'timeout_ms'=>'int'],
-'RdKafka::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'RdKafka\Topic', 'timeout_ms'=>'int'],
+'RdKafka::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'],
'RdKafka::getOutQLen' => ['int'],
'RdKafka::newQueue' => ['RdKafka\Queue'],
'RdKafka::newTopic' => ['RdKafka\Topic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'],
@@ -10582,7 +10582,7 @@ return [
'RdKafka\Conf::setStatsCb' => ['void', 'callback'=>'callable'],
'RdKafka\Consumer::__construct' => ['void', 'conf='=>'?RdKafka\Conf'],
'RdKafka\Consumer::addBrokers' => ['int', 'broker_list'=>'string'],
-'RdKafka\Consumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'RdKafka\Topic', 'timeout_ms'=>'int'],
+'RdKafka\Consumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'],
'RdKafka\Consumer::getOutQLen' => ['int'],
'RdKafka\Consumer::newQueue' => ['RdKafka\Queue'],
'RdKafka\Consumer::newTopic' => ['RdKafka\ConsumerTopic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'],
@@ -10601,7 +10601,7 @@ return [
'RdKafka\KafkaConsumer::commitAsync' => ['void', 'message_or_offsets='=>'RdKafka\Message|RdKafka\TopicPartition[]|null'],
'RdKafka\KafkaConsumer::consume' => ['RdKafka\Message', 'timeout_ms'=>'int'],
'RdKafka\KafkaConsumer::getAssignment' => ['RdKafka\TopicPartition[]'],
-'RdKafka\KafkaConsumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'RdKafka\KafkaConsumerTopic', 'timeout_ms'=>'int'],
+'RdKafka\KafkaConsumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\KafkaConsumerTopic', 'timeout_ms'=>'int'],
'RdKafka\KafkaConsumer::getSubscription' => ['array'],
'RdKafka\KafkaConsumer::subscribe' => ['void', 'topics'=>'array'],
'RdKafka\KafkaConsumer::unsubscribe' => ['void'],
@@ -10629,7 +10629,7 @@ return [
'RdKafka\Metadata\Topic::getTopic' => ['string'],
'RdKafka\Producer::__construct' => ['void', 'conf='=>'?RdKafka\Conf'],
'RdKafka\Producer::addBrokers' => ['int', 'broker_list'=>'string'],
-'RdKafka\Producer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'RdKafka\Topic', 'timeout_ms'=>'int'],
+'RdKafka\Producer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'],
'RdKafka\Producer::getOutQLen' => ['int'],
'RdKafka\Producer::newQueue' => ['RdKafka\Queue'],
'RdKafka\Producer::newTopic' => ['RdKafka\ProducerTopic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'],
@@ -12069,7 +12069,7 @@ return [
'session_reset' => ['bool'],
'session_save_path' => ['string', 'path='=>'string'],
'session_set_cookie_params' => ['bool', 'lifetime'=>'int', 'path='=>'?string', 'domain='=>'?string', 'secure='=>'?bool', 'httponly='=>'?bool'],
-'session_set_cookie_params\'1' => ['bool', 'options'=>'array{lifetime?:int,path?:string,domain?:?string,secure?:bool,httponly?:bool,samesite?:string}'],
+'session_set_cookie_params\'1' => ['bool', 'options'=>'array{lifetime?:?int,path?:?string,domain?:?string,secure?:?bool,httponly?:?bool,samesite?:?string}'],
'session_set_save_handler' => ['bool', 'open'=>'callable(string,string):bool', 'close'=>'callable():bool', 'read'=>'callable(string):string', 'write'=>'callable(string,string):bool', 'destroy'=>'callable(string):bool', 'gc'=>'callable(string):bool', 'create_sid='=>'callable():string', 'validate_sid='=>'callable(string):bool', 'update_timestamp='=>'callable(string):bool'],
'session_set_save_handler\'1' => ['bool', 'open'=>'SessionHandlerInterface', 'close='=>'bool'],
'session_start' => ['bool', 'options='=>'array'],
diff --git a/dictionaries/CallMap_73_delta.php b/dictionaries/CallMap_73_delta.php
index 65d5420b5..7b1d9e508 100644
--- a/dictionaries/CallMap_73_delta.php
+++ b/dictionaries/CallMap_73_delta.php
@@ -7,7 +7,7 @@
* The 'added' section contains function/method names from FunctionSignatureMap (And alternates, if applicable) that do not exist in php 7.2
* The 'removed' section contains the signatures that were removed in php 7.3.
* The 'changed' section contains functions for which the signature has changed for php 7.3.
- * Each function in the 'changed' section has an 'old' and a 'new' section,
+ * Each function in the 'changed' section has an 'old' and a 'new' section,
* representing the function as it was in PHP 7.2 and in PHP 7.3, respectively
*
* @see CallMap.php
@@ -42,7 +42,7 @@ return [
'is_countable' => ['bool', 'value'=>'mixed'],
'net_get_interfaces' => ['array>|false'],
'openssl_pkey_derive' => ['string|false', 'public_key'=>'mixed', 'private_key'=>'mixed', 'key_length='=>'?int'],
- 'session_set_cookie_params\'1' => ['bool', 'options'=>'array{lifetime?:int,path?:string,domain?:?string,secure?:bool,httponly?:bool,samesite?:string}'],
+ 'session_set_cookie_params\'1' => ['bool', 'options'=>'array{lifetime?:?int,path?:?string,domain?:?string,secure?:?bool,httponly?:?bool,samesite?:?string}'],
'setcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array'],
'setrawcookie\'1' => ['bool', 'name'=>'string', 'value='=>'string', 'options='=>'array'],
'socket_wsaprotocol_info_export' => ['string|false', 'socket'=>'resource', 'process_id'=>'int'],
diff --git a/dictionaries/CallMap_74_delta.php b/dictionaries/CallMap_74_delta.php
index c4d7a9e9e..83b9108bd 100644
--- a/dictionaries/CallMap_74_delta.php
+++ b/dictionaries/CallMap_74_delta.php
@@ -33,8 +33,8 @@ return [
'new' => ['bool', 'hash'=>'string', 'algo'=>'int|string|null', 'options='=>'array'],
],
'proc_open' => [
- 'old' => ['resource|false', 'command'=>'string', 'descriptorspec'=>'array', '&w_pipes'=>'resource[]', 'cwd='=>'?string', 'env='=>'?array', 'other_options='=>'array'],
- 'new' => ['resource|false', 'cmd'=>'string|array', 'descriptorspec'=>'array', '&w_pipes'=>'resource[]', 'cwd='=>'?string', 'env='=>'?array', 'other_options='=>'array'],
+ 'old' => ['resource|false', 'command'=>'string', 'descriptor_spec'=>'array', '&pipes'=>'resource[]', 'cwd='=>'?string', 'env_vars='=>'?array', 'options='=>'?array'],
+ 'new' => ['resource|false', 'command'=>'string|array', 'descriptor_spec'=>'array', '&pipes'=>'resource[]', 'cwd='=>'?string', 'env_vars='=>'?array', 'options='=>'?array'],
],
],
'removed' => [
diff --git a/dictionaries/CallMap_historical.php b/dictionaries/CallMap_historical.php
index b8d0b83fb..9b1589575 100644
--- a/dictionaries/CallMap_historical.php
+++ b/dictionaries/CallMap_historical.php
@@ -5198,7 +5198,7 @@ return [
'RarException::setUsingExceptions' => ['RarEntry', 'using_exceptions'=>'bool'],
'RdKafka::addBrokers' => ['int', 'broker_list'=>'string'],
'RdKafka::flush' => ['int', 'timeout_ms'=>'int'],
- 'RdKafka::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'RdKafka\Topic', 'timeout_ms'=>'int'],
+ 'RdKafka::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'],
'RdKafka::getOutQLen' => ['int'],
'RdKafka::newQueue' => ['RdKafka\Queue'],
'RdKafka::newTopic' => ['RdKafka\Topic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'],
@@ -5213,7 +5213,7 @@ return [
'RdKafka\Conf::setStatsCb' => ['void', 'callback'=>'callable'],
'RdKafka\Consumer::__construct' => ['void', 'conf='=>'?RdKafka\Conf'],
'RdKafka\Consumer::addBrokers' => ['int', 'broker_list'=>'string'],
- 'RdKafka\Consumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'RdKafka\Topic', 'timeout_ms'=>'int'],
+ 'RdKafka\Consumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'],
'RdKafka\Consumer::getOutQLen' => ['int'],
'RdKafka\Consumer::newQueue' => ['RdKafka\Queue'],
'RdKafka\Consumer::newTopic' => ['RdKafka\ConsumerTopic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'],
@@ -5232,7 +5232,7 @@ return [
'RdKafka\KafkaConsumer::commitAsync' => ['void', 'message_or_offsets='=>'RdKafka\Message|RdKafka\TopicPartition[]|null'],
'RdKafka\KafkaConsumer::consume' => ['RdKafka\Message', 'timeout_ms'=>'int'],
'RdKafka\KafkaConsumer::getAssignment' => ['RdKafka\TopicPartition[]'],
- 'RdKafka\KafkaConsumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'RdKafka\KafkaConsumerTopic', 'timeout_ms'=>'int'],
+ 'RdKafka\KafkaConsumer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\KafkaConsumerTopic', 'timeout_ms'=>'int'],
'RdKafka\KafkaConsumer::getSubscription' => ['array'],
'RdKafka\KafkaConsumer::subscribe' => ['void', 'topics'=>'array'],
'RdKafka\KafkaConsumer::unsubscribe' => ['void'],
@@ -5260,7 +5260,7 @@ return [
'RdKafka\Metadata\Topic::getTopic' => ['string'],
'RdKafka\Producer::__construct' => ['void', 'conf='=>'?RdKafka\Conf'],
'RdKafka\Producer::addBrokers' => ['int', 'broker_list'=>'string'],
- 'RdKafka\Producer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'RdKafka\Topic', 'timeout_ms'=>'int'],
+ 'RdKafka\Producer::getMetadata' => ['RdKafka\Metadata', 'all_topics'=>'bool', 'only_topic='=>'?RdKafka\Topic', 'timeout_ms'=>'int'],
'RdKafka\Producer::getOutQLen' => ['int'],
'RdKafka\Producer::newQueue' => ['RdKafka\Queue'],
'RdKafka\Producer::newTopic' => ['RdKafka\ProducerTopic', 'topic_name'=>'string', 'topic_conf='=>'?RdKafka\TopicConf'],
@@ -12914,7 +12914,7 @@ return [
'mb_regex_encoding' => ['string|bool', 'encoding='=>'string'],
'mb_regex_set_options' => ['string', 'options='=>'string'],
'mb_send_mail' => ['bool', 'to'=>'string', 'subject'=>'string', 'message'=>'string', 'additional_headers='=>'string|array', 'additional_params='=>'string'],
- 'mb_split' => ['list', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'],
+ 'mb_split' => ['list|false', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'],
'mb_strcut' => ['string', 'string'=>'string', 'start'=>'int', 'length='=>'?int', 'encoding='=>'string'],
'mb_strimwidth' => ['string', 'string'=>'string', 'start'=>'int', 'width'=>'int', 'trim_marker='=>'string', 'encoding='=>'string'],
'mb_stripos' => ['int|false', 'haystack'=>'string', 'needle'=>'string', 'offset='=>'int', 'encoding='=>'string'],
@@ -14473,7 +14473,7 @@ return [
'proc_close' => ['int', 'process'=>'resource'],
'proc_get_status' => ['array{command: string, pid: int, running: bool, signaled: bool, stopped: bool, exitcode: int, termsig: int, stopsig: int}|false', 'process'=>'resource'],
'proc_nice' => ['bool', 'priority'=>'int'],
- 'proc_open' => ['resource|false', 'command'=>'string', 'descriptorspec'=>'array', '&w_pipes'=>'resource[]', 'cwd='=>'?string', 'env='=>'?array', 'other_options='=>'array'],
+ 'proc_open' => ['resource|false', 'command'=>'string', 'descriptor_spec'=>'array', '&pipes'=>'resource[]', 'cwd='=>'?string', 'env_vars='=>'?array', 'options='=>'?array'],
'proc_terminate' => ['bool', 'process'=>'resource', 'signal='=>'int'],
'projectionObj::__construct' => ['void', 'projectionString'=>'string'],
'projectionObj::getUnits' => ['int'],
diff --git a/docs/running_psalm/error_levels.md b/docs/running_psalm/error_levels.md
index 8cd594272..a131a41f0 100644
--- a/docs/running_psalm/error_levels.md
+++ b/docs/running_psalm/error_levels.md
@@ -56,6 +56,7 @@ Level 5 and above allows a more non-verifiable code, and higher levels are even
- [InvalidThrow](issues/InvalidThrow.md)
- [LoopInvalidation](issues/LoopInvalidation.md)
- [MethodSignatureMustOmitReturnType](issues/MethodSignatureMustOmitReturnType.md)
+ - [MethodSignatureMustProvideReturnType](issues/MethodSignatureMustProvideReturnType.md)
- [MissingDependency](issues/MissingDependency.md)
- [MissingFile](issues/MissingFile.md)
- [MissingImmutableAnnotation](issues/MissingImmutableAnnotation.md)
diff --git a/docs/running_psalm/issues.md b/docs/running_psalm/issues.md
index b392b7fad..7c3da4843 100644
--- a/docs/running_psalm/issues.md
+++ b/docs/running_psalm/issues.md
@@ -98,6 +98,7 @@
- [LoopInvalidation](issues/LoopInvalidation.md)
- [MethodSignatureMismatch](issues/MethodSignatureMismatch.md)
- [MethodSignatureMustOmitReturnType](issues/MethodSignatureMustOmitReturnType.md)
+ - [MethodSignatureMustProvideReturnType](issues/MethodSignatureMustProvideReturnType.md)
- [MismatchingDocblockParamType](issues/MismatchingDocblockParamType.md)
- [MismatchingDocblockPropertyType](issues/MismatchingDocblockPropertyType.md)
- [MismatchingDocblockReturnType](issues/MismatchingDocblockReturnType.md)
diff --git a/docs/running_psalm/issues/MethodSignatureMustProvideReturnType.md b/docs/running_psalm/issues/MethodSignatureMustProvideReturnType.md
new file mode 100644
index 000000000..80aa53c8a
--- /dev/null
+++ b/docs/running_psalm/issues/MethodSignatureMustProvideReturnType.md
@@ -0,0 +1,17 @@
+# MethodSignatureMustProvideReturnType
+
+In PHP 8.1+, [most non-final internal methods now require overriding methods to declare a compatible return type, otherwise a deprecated notice is emitted during inheritance validation](https://www.php.net/manual/en/migration81.incompatible.php#migration81.incompatible.core.type-compatibility-internal).
+
+This issue is emitted when a method overriding a native method is defined without a return type.
+
+**Only if** the return type cannot be declared to keep support for PHP 7, a `#[ReturnTypeWillChange]` attribute can be added to silence the PHP deprecation notice and Psalm issue.
+
+```php
+ 'A'];
+ }
+}
+```
diff --git a/src/Psalm/Internal/Analyzer/MethodComparator.php b/src/Psalm/Internal/Analyzer/MethodComparator.php
index 7e16c4269..8ffd003a0 100644
--- a/src/Psalm/Internal/Analyzer/MethodComparator.php
+++ b/src/Psalm/Internal/Analyzer/MethodComparator.php
@@ -20,12 +20,14 @@ use Psalm\Issue\ImplementedParamTypeMismatch;
use Psalm\Issue\ImplementedReturnTypeMismatch;
use Psalm\Issue\LessSpecificImplementedReturnType;
use Psalm\Issue\MethodSignatureMismatch;
+use Psalm\Issue\MethodSignatureMustProvideReturnType;
use Psalm\Issue\MissingImmutableAnnotation;
use Psalm\Issue\MoreSpecificImplementedParamType;
use Psalm\Issue\OverriddenMethodAccess;
use Psalm\Issue\ParamNameMismatch;
use Psalm\Issue\TraitMethodSignatureMismatch;
use Psalm\IssueBuffer;
+use Psalm\Storage\AttributeStorage;
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\FunctionLikeParameter;
use Psalm\Storage\MethodStorage;
@@ -34,6 +36,7 @@ use Psalm\Type\Atomic\TNull;
use Psalm\Type\Atomic\TTemplateParam;
use Psalm\Type\Union;
+use function array_filter;
use function in_array;
use function strpos;
use function strtolower;
@@ -115,6 +118,29 @@ class MethodComparator
);
}
+ if (!$guide_classlike_storage->user_defined
+ && $implementer_classlike_storage->user_defined
+ && $codebase->analysis_php_version_id >= 80100
+ && ($guide_method_storage->return_type
+ || $guide_method_storage->signature_return_type
+ )
+ && !$implementer_method_storage->signature_return_type
+ && !array_filter(
+ $implementer_method_storage->attributes,
+ function (AttributeStorage $s) {
+ return $s->fq_class_name === 'ReturnTypeWillChange';
+ }
+ )
+ ) {
+ IssueBuffer::maybeAdd(
+ new MethodSignatureMustProvideReturnType(
+ 'Method ' . $cased_implementer_method_id . ' must have a return type signature!',
+ $implementer_method_storage->location ?: $code_location
+ ),
+ $suppressed_issues + $implementer_classlike_storage->suppressed_issues
+ );
+ }
+
if ($guide_method_storage->return_type
&& $implementer_method_storage->return_type
&& !$implementer_method_storage->inherited_return_type
@@ -860,7 +886,14 @@ class MethodComparator
$implementer_signature_return_type,
$guide_signature_return_type
)
- : UnionTypeComparator::isContainedByInPhp($implementer_signature_return_type, $guide_signature_return_type);
+ : (!$implementer_signature_return_type
+ && $guide_signature_return_type->isMixed()
+ ? false
+ : UnionTypeComparator::isContainedByInPhp(
+ $implementer_signature_return_type,
+ $guide_signature_return_type
+ )
+ );
if (!$is_contained_by) {
if ($codebase->analysis_php_version_id >= 8_00_00
diff --git a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php
index b84b8e172..d9f802643 100644
--- a/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php
+++ b/src/Psalm/Internal/Analyzer/Statements/Expression/Call/Method/MissingMethodCallHandler.php
@@ -116,7 +116,7 @@ class MissingMethodCallHandler
$found_generic_params = ClassTemplateParamCollector::collect(
$codebase,
- $class_storage,
+ $defining_class_storage,
$class_storage,
$method_name_lc,
$lhs_type_part,
@@ -160,7 +160,7 @@ class MissingMethodCallHandler
$codebase,
$return_type_candidate,
$defining_class_storage->name,
- $fq_class_name,
+ $lhs_type_part instanceof Atomic\TNamedObject ? $lhs_type_part : $fq_class_name,
$defining_class_storage->parent_class
);
@@ -281,7 +281,7 @@ class MissingMethodCallHandler
$found_generic_params = ClassTemplateParamCollector::collect(
$codebase,
- $class_storage,
+ $defining_class_storage,
$class_storage,
$method_name_lc,
$lhs_type_part,
@@ -337,7 +337,7 @@ class MissingMethodCallHandler
$codebase,
$return_type_candidate,
$defining_class_storage->name,
- $fq_class_name,
+ $lhs_type_part instanceof Atomic\TNamedObject ? $lhs_type_part : $fq_class_name,
$defining_class_storage->parent_class,
true,
false,
@@ -424,11 +424,20 @@ class MissingMethodCallHandler
ClassLikeStorage $static_class_storage,
string $method_name_lc
): ?array {
+ if (isset($static_class_storage->declaring_pseudo_method_ids[$method_name_lc])) {
+ $method_id = $static_class_storage->declaring_pseudo_method_ids[$method_name_lc];
+ $class_storage = $codebase->classlikes->getStorageFor($method_id->fq_class_name);
+
+ if ($class_storage && isset($class_storage->pseudo_methods[$method_name_lc])) {
+ return [$class_storage->pseudo_methods[$method_name_lc], $class_storage];
+ }
+ }
+
if ($pseudo_method_storage = $static_class_storage->pseudo_methods[$method_name_lc] ?? null) {
return [$pseudo_method_storage, $static_class_storage];
}
- $ancestors = $static_class_storage->class_implements + $static_class_storage->parent_classes;
+ $ancestors = $static_class_storage->class_implements;
foreach ($ancestors as $fq_class_name => $_) {
$class_storage = $codebase->classlikes->getStorageFor($fq_class_name);
diff --git a/src/Psalm/Internal/Codebase/Populator.php b/src/Psalm/Internal/Codebase/Populator.php
index 15cc6fc83..6937916be 100644
--- a/src/Psalm/Internal/Codebase/Populator.php
+++ b/src/Psalm/Internal/Codebase/Populator.php
@@ -399,6 +399,7 @@ class Populator
$storage->pseudo_property_set_types += $trait_storage->pseudo_property_set_types;
$storage->pseudo_methods += $trait_storage->pseudo_methods;
+ $storage->declaring_pseudo_method_ids += $trait_storage->declaring_pseudo_method_ids;
}
}
@@ -555,6 +556,7 @@ class Populator
$parent_storage->dependent_classlikes[strtolower($storage->name)] = true;
$storage->pseudo_methods += $parent_storage->pseudo_methods;
+ $storage->declaring_pseudo_method_ids += $parent_storage->declaring_pseudo_method_ids;
}
private function populateInterfaceDataFromParentInterfaces(
@@ -641,6 +643,7 @@ class Populator
$this->inheritMethodsFromParent($storage, $parent_interface_storage);
$storage->pseudo_methods += $parent_interface_storage->pseudo_methods;
+ $storage->declaring_pseudo_method_ids += $parent_interface_storage->declaring_pseudo_method_ids;
}
$storage->parent_interfaces = array_merge($parent_interfaces, $storage->parent_interfaces);
diff --git a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
index 4c77a4ae1..977d7888e 100644
--- a/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
+++ b/src/Psalm/Internal/PhpVisitor/Reflector/ClassLikeNodeScanner.php
@@ -600,11 +600,16 @@ class ClassLikeNodeScanner
/** @var MethodStorage */
$pseudo_method_storage = $functionlike_node_scanner->start($method, true);
+ $lc_method_name = strtolower($method->name->name);
if ($pseudo_method_storage->is_static) {
- $storage->pseudo_static_methods[strtolower($method->name->name)] = $pseudo_method_storage;
+ $storage->pseudo_static_methods[$lc_method_name] = $pseudo_method_storage;
} else {
- $storage->pseudo_methods[strtolower($method->name->name)] = $pseudo_method_storage;
+ $storage->pseudo_methods[$lc_method_name] = $pseudo_method_storage;
+ $storage->declaring_pseudo_method_ids[$lc_method_name] = new MethodIdentifier(
+ $fq_classlike_name,
+ $lc_method_name
+ );
}
$storage->sealed_methods = true;
diff --git a/src/Psalm/Issue/MethodSignatureMustProvideReturnType.php b/src/Psalm/Issue/MethodSignatureMustProvideReturnType.php
new file mode 100644
index 000000000..ab05f06c0
--- /dev/null
+++ b/src/Psalm/Issue/MethodSignatureMustProvideReturnType.php
@@ -0,0 +1,9 @@
+
+ */
+ public $declaring_pseudo_method_ids = [];
+
/**
* @var array
*/
diff --git a/tests/Config/PluginListTest.php b/tests/Config/PluginListTest.php
index 1274eff9d..52fce5d3b 100644
--- a/tests/Config/PluginListTest.php
+++ b/tests/Config/PluginListTest.php
@@ -159,7 +159,7 @@ class PluginListTest extends TestCase
{
$plugin_list = new PluginList($this->config_file->reveal(), $this->composer_lock->reveal());
$this->expectException(InvalidArgumentException::class);
- $this->expectExceptionMessageRegExp('/unknown plugin/i');
+ $this->expectExceptionMessageMatches('/unknown plugin/i');
$plugin_list->resolvePluginClass('vendor/package');
}
diff --git a/tests/Config/PluginTest.php b/tests/Config/PluginTest.php
index 017d85af2..0de42408e 100644
--- a/tests/Config/PluginTest.php
+++ b/tests/Config/PluginTest.php
@@ -1018,7 +1018,7 @@ class PluginTest extends TestCase
$this->project_analyzer->trackTaintedInputs();
$this->expectException(CodeException::class);
- $this->expectExceptionMessageRegExp('/TaintedHtml/');
+ $this->expectExceptionMessageMatches('/TaintedHtml/');
$this->analyzeFile($file_path, new Context());
}
diff --git a/tests/CoreStubsTest.php b/tests/CoreStubsTest.php
index 946e1e1d1..3719fdd03 100644
--- a/tests/CoreStubsTest.php
+++ b/tests/CoreStubsTest.php
@@ -18,5 +18,20 @@ class CoreStubsTest extends TestCase
new RecursiveArrayIterator([], RecursiveArrayIterator::CHILD_ARRAYS_ONLY);'
];
+ yield 'proc_open() named arguments' => [
+ ' [],
+ 'error_levels' => [],
+ 'php_version' => '8.0',
+ ];
}
}
diff --git a/tests/DocumentationTest.php b/tests/DocumentationTest.php
index fce66e37a..c24ff0462 100644
--- a/tests/DocumentationTest.php
+++ b/tests/DocumentationTest.php
@@ -218,6 +218,10 @@ class DocumentationTest extends TestCase
$this->markTestSkipped();
}
+ if (strpos($error_message, 'MethodSignatureMustProvideReturnType') !== false) {
+ $php_version = '8.1';
+ }
+
$this->project_analyzer->setPhpVersion($php_version, 'tests');
if ($check_references) {
@@ -237,7 +241,7 @@ class DocumentationTest extends TestCase
}
$this->expectException(CodeException::class);
- $this->expectExceptionMessageRegExp('/\b' . preg_quote($error_message, '/') . '\b/');
+ $this->expectExceptionMessageMatches('/\b' . preg_quote($error_message, '/') . '\b/');
$codebase = $this->project_analyzer->getCodebase();
$codebase->config->visitPreloadedStubFiles($codebase);
diff --git a/tests/FileUpdates/ErrorAfterUpdateTest.php b/tests/FileUpdates/ErrorAfterUpdateTest.php
index 1f2bade42..c9805f589 100644
--- a/tests/FileUpdates/ErrorAfterUpdateTest.php
+++ b/tests/FileUpdates/ErrorAfterUpdateTest.php
@@ -90,7 +90,7 @@ class ErrorAfterUpdateTest extends TestCase
}
$this->expectException(CodeException::class);
- $this->expectExceptionMessageRegExp('/\b' . preg_quote($error_message, '/') . '\b/');
+ $this->expectExceptionMessageMatches('/\b' . preg_quote($error_message, '/') . '\b/');
$codebase->reloadFiles($this->project_analyzer, array_keys($end_files));
diff --git a/tests/IncludeTest.php b/tests/IncludeTest.php
index dee302928..3f13695fd 100644
--- a/tests/IncludeTest.php
+++ b/tests/IncludeTest.php
@@ -88,7 +88,7 @@ class IncludeTest extends TestCase
$config->skip_checks_on_unresolvable_includes = false;
$this->expectException(CodeException::class);
- $this->expectExceptionMessageRegExp('/\b' . preg_quote($error_message, '/') . '\b/');
+ $this->expectExceptionMessageMatches('/\b' . preg_quote($error_message, '/') . '\b/');
$codebase->scanFiles();
diff --git a/tests/MagicMethodAnnotationTest.php b/tests/MagicMethodAnnotationTest.php
index 0a9e78ef4..482658fce 100644
--- a/tests/MagicMethodAnnotationTest.php
+++ b/tests/MagicMethodAnnotationTest.php
@@ -819,6 +819,92 @@ class MagicMethodAnnotationTest extends TestCase
consumeInt(B::bar());'
],
+ 'returnThisShouldKeepGenerics' => [
+ ' $a */
+ $a = new A();
+ $b = $a->foo();
+
+ /** @var I $i */
+ $c = $i->foo();',
+ [
+ '$b' => 'A',
+ '$c' => 'I',
+ ]
+ ],
+ 'genericsOfInheritedMethodsShouldBeResolved' => [
+ '
+ */
+ class A implements I
+ {
+ public function __call(string $name, array $args) {}
+ }
+
+ /**
+ * @template E
+ * @extends I
+ */
+ interface I2 extends I {}
+
+ class B {}
+
+ /**
+ * @template E
+ * @method E get()
+ */
+ class C
+ {
+ public function __call(string $name, array $args) {}
+ }
+
+ /**
+ * @template E
+ * @extends C
+ */
+ class D extends C {}
+
+ /** @var A $a */
+ $a = new A();
+ $b = $a->get();
+
+ /** @var I2 $i */
+ $c = $i->get();
+
+ /** @var D $d */
+ $d = new D();
+ $e = $d->get();',
+ [
+ '$b' => 'B',
+ '$c' => 'B',
+ '$e' => 'B',
+ ]
+ ],
];
}
diff --git a/tests/MethodSignatureTest.php b/tests/MethodSignatureTest.php
index 86276aa9e..81e907ab6 100644
--- a/tests/MethodSignatureTest.php
+++ b/tests/MethodSignatureTest.php
@@ -1568,6 +1568,37 @@ class MethodSignatureTest extends TestCase
',
'error_message' => 'MethodSignatureMismatch',
],
+ 'noMixedTypehintInDescendant' => [
+ ' 'MethodSignatureMismatch',
+ [],
+ false,
+ '8.0'
+ ],
+ 'noTypehintInNativeDescendant' => [
+ ' 'MethodSignatureMustProvideReturnType',
+ [],
+ false,
+ '8.1'
+ ],
];
}
}
diff --git a/tests/PsalmPluginTest.php b/tests/PsalmPluginTest.php
index 4a33e6034..592c2d97e 100644
--- a/tests/PsalmPluginTest.php
+++ b/tests/PsalmPluginTest.php
@@ -150,7 +150,7 @@ class PsalmPluginTest extends TestCase
$help_command = new CommandTester($this->app->find('help'));
$help_command->execute(['command_name' => $command]);
$output = $help_command->getDisplay();
- $this->assertRegExp('/Usage:.*$\s+' . preg_quote($command, '/') . '\b/m', $output);
+ $this->assertMatchesRegularExpression('/Usage:.*$\s+' . preg_quote($command, '/') . '\b/m', $output);
}
/**
diff --git a/tests/TaintTest.php b/tests/TaintTest.php
index 2fc31b0d9..4259da95d 100644
--- a/tests/TaintTest.php
+++ b/tests/TaintTest.php
@@ -50,7 +50,7 @@ class TaintTest extends TestCase
}
$this->expectException(CodeException::class);
- $this->expectExceptionMessageRegExp('/\b' . preg_quote($error_message, '/') . '\b/');
+ $this->expectExceptionMessageMatches('/\b' . preg_quote($error_message, '/') . '\b/');
$file_path = self::$src_dir_path . 'somefile.php';
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 84a50bfff..db21c93dd 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -24,7 +24,6 @@ use function defined;
use function getcwd;
use function ini_set;
use function is_string;
-use function method_exists;
use const ARRAY_FILTER_USE_KEY;
use const DIRECTORY_SEPARATOR;
@@ -153,28 +152,6 @@ class TestCase extends BaseTestCase
return $this->getName($withDataSet);
}
- /**
- * Compatibility alias
- */
- public function expectExceptionMessageRegExp(string $regexp): void
- {
- if (method_exists($this, 'expectExceptionMessageMatches')) {
- $this->expectExceptionMessageMatches($regexp);
- } else {
- /** @psalm-suppress UndefinedMethod */
- parent::expectExceptionMessageRegExp($regexp);
- }
- }
-
- public static function assertRegExp(string $pattern, string $string, string $message = ''): void
- {
- if (method_exists(self::class, 'assertMatchesRegularExpression')) {
- self::assertMatchesRegularExpression($pattern, $string, $message);
- } else {
- parent::assertRegExp($pattern, $string, $message);
- }
- }
-
public static function assertArrayKeysAreStrings(array $array, string $message = ''): void
{
$validKeys = array_filter($array, 'is_string', ARRAY_FILTER_USE_KEY);
diff --git a/tests/Traits/InvalidCodeAnalysisTestTrait.php b/tests/Traits/InvalidCodeAnalysisTestTrait.php
index 9dd9cbae2..ab2aa52b8 100644
--- a/tests/Traits/InvalidCodeAnalysisTestTrait.php
+++ b/tests/Traits/InvalidCodeAnalysisTestTrait.php
@@ -6,7 +6,7 @@ use Psalm\Config;
use Psalm\Context;
use Psalm\Exception\CodeException;
-use function method_exists;
+use function is_int;
use function preg_quote;
use function str_replace;
use function strpos;
@@ -64,11 +64,7 @@ trait InvalidCodeAnalysisTestTrait
$this->expectException(CodeException::class);
- if (method_exists($this, 'expectExceptionMessageMatches')) {
- $this->expectExceptionMessageMatches('/\b' . preg_quote($error_message, '/') . '\b/');
- } else {
- $this->expectExceptionMessageRegExp('/\b' . preg_quote($error_message, '/') . '\b/');
- }
+ $this->expectExceptionMessageMatches('/\b' . preg_quote($error_message, '/') . '\b/');
$codebase = $this->project_analyzer->getCodebase();
$codebase->config->visitPreloadedStubFiles($codebase);
diff --git a/tests/UnusedCodeTest.php b/tests/UnusedCodeTest.php
index bc0d97cad..bcf358ff3 100644
--- a/tests/UnusedCodeTest.php
+++ b/tests/UnusedCodeTest.php
@@ -90,7 +90,7 @@ class UnusedCodeTest extends TestCase
}
$this->expectException(CodeException::class);
- $this->expectExceptionMessageRegExp('/\b' . preg_quote($error_message, '/') . '\b/');
+ $this->expectExceptionMessageMatches('/\b' . preg_quote($error_message, '/') . '\b/');
$file_path = self::$src_dir_path . 'somefile.php';
diff --git a/tests/UnusedVariableTest.php b/tests/UnusedVariableTest.php
index 76e727ed5..a1fa642d8 100644
--- a/tests/UnusedVariableTest.php
+++ b/tests/UnusedVariableTest.php
@@ -83,7 +83,7 @@ class UnusedVariableTest extends TestCase
}
$this->expectException(CodeException::class);
- $this->expectExceptionMessageRegExp('/\b' . preg_quote($error_message, '/') . '\b/');
+ $this->expectExceptionMessageMatches('/\b' . preg_quote($error_message, '/') . '\b/');
$this->project_analyzer->setPhpVersion('7.4', 'tests');