mirror of
https://github.com/danog/psalm.git
synced 2025-01-21 21:31:13 +01:00
Merge remote-tracking branch 'upstream/4.x' into upstream-master4
This commit is contained in:
commit
af1888b631
@ -292,6 +292,7 @@
|
||||
<xs:element name="LoopInvalidation" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="MethodSignatureMismatch" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="MethodSignatureMustOmitReturnType" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="MethodSignatureMustProvideReturnType" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="MismatchingDocblockParamType" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="MismatchingDocblockPropertyType" type="IssueHandlerType" minOccurs="0" />
|
||||
<xs:element name="MismatchingDocblockReturnType" type="IssueHandlerType" minOccurs="0" />
|
||||
|
@ -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<string>', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'],
|
||||
'mb_split' => ['list<string>|false', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'],
|
||||
'mb_str_split' => ['list<string>', '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'],
|
||||
|
@ -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<string,array<string,mixed>>|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'],
|
||||
|
@ -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' => [
|
||||
|
@ -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<string>', 'pattern'=>'string', 'string'=>'string', 'limit='=>'int'],
|
||||
'mb_split' => ['list<string>|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'],
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
<?php
|
||||
|
||||
class A implements JsonSerializable {
|
||||
public function jsonSerialize() {
|
||||
return ['type' => 'A'];
|
||||
}
|
||||
}
|
||||
```
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
9
src/Psalm/Issue/MethodSignatureMustProvideReturnType.php
Normal file
9
src/Psalm/Issue/MethodSignatureMustProvideReturnType.php
Normal file
@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
namespace Psalm\Issue;
|
||||
|
||||
class MethodSignatureMustProvideReturnType extends CodeIssue
|
||||
{
|
||||
public const ERROR_LEVEL = -1;
|
||||
public const SHORTCODE = 282;
|
||||
}
|
@ -236,6 +236,16 @@ class ClassLikeStorage
|
||||
*/
|
||||
public $pseudo_static_methods = [];
|
||||
|
||||
/**
|
||||
* Maps pseudo method names to the original declaring method identifier
|
||||
* The key is the method name in lowercase, and the value is the original `MethodIdentifier` instance
|
||||
*
|
||||
* This property contains all pseudo methods declared on ancestors.
|
||||
*
|
||||
* @var array<lowercase-string, MethodIdentifier>
|
||||
*/
|
||||
public $declaring_pseudo_method_ids = [];
|
||||
|
||||
/**
|
||||
* @var array<lowercase-string, MethodIdentifier>
|
||||
*/
|
||||
|
@ -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');
|
||||
}
|
||||
|
||||
|
@ -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());
|
||||
}
|
||||
|
@ -18,5 +18,20 @@ class CoreStubsTest extends TestCase
|
||||
|
||||
new RecursiveArrayIterator([], RecursiveArrayIterator::CHILD_ARRAYS_ONLY);'
|
||||
];
|
||||
yield 'proc_open() named arguments' => [
|
||||
'<?php
|
||||
|
||||
proc_open(
|
||||
command: "ls",
|
||||
descriptor_spec: [],
|
||||
pipes: $pipes,
|
||||
cwd: null,
|
||||
env_vars: null,
|
||||
options: null
|
||||
);',
|
||||
'assertions' => [],
|
||||
'error_levels' => [],
|
||||
'php_version' => '8.0',
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
|
||||
|
@ -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();
|
||||
|
||||
|
@ -819,6 +819,92 @@ class MagicMethodAnnotationTest extends TestCase
|
||||
|
||||
consumeInt(B::bar());'
|
||||
],
|
||||
'returnThisShouldKeepGenerics' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template E
|
||||
* @method $this foo()
|
||||
*/
|
||||
class A
|
||||
{
|
||||
public function __call(string $name, array $args) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template E
|
||||
* @method $this foo()
|
||||
*/
|
||||
interface I {}
|
||||
|
||||
class B {}
|
||||
|
||||
/** @var A<B> $a */
|
||||
$a = new A();
|
||||
$b = $a->foo();
|
||||
|
||||
/** @var I<B> $i */
|
||||
$c = $i->foo();',
|
||||
[
|
||||
'$b' => 'A<B>',
|
||||
'$c' => 'I<B>',
|
||||
]
|
||||
],
|
||||
'genericsOfInheritedMethodsShouldBeResolved' => [
|
||||
'<?php
|
||||
/**
|
||||
* @template E
|
||||
* @method E get()
|
||||
*/
|
||||
interface I {}
|
||||
|
||||
/**
|
||||
* @template E
|
||||
* @implements I<E>
|
||||
*/
|
||||
class A implements I
|
||||
{
|
||||
public function __call(string $name, array $args) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template E
|
||||
* @extends I<E>
|
||||
*/
|
||||
interface I2 extends I {}
|
||||
|
||||
class B {}
|
||||
|
||||
/**
|
||||
* @template E
|
||||
* @method E get()
|
||||
*/
|
||||
class C
|
||||
{
|
||||
public function __call(string $name, array $args) {}
|
||||
}
|
||||
|
||||
/**
|
||||
* @template E
|
||||
* @extends C<E>
|
||||
*/
|
||||
class D extends C {}
|
||||
|
||||
/** @var A<B> $a */
|
||||
$a = new A();
|
||||
$b = $a->get();
|
||||
|
||||
/** @var I2<B> $i */
|
||||
$c = $i->get();
|
||||
|
||||
/** @var D<B> $d */
|
||||
$d = new D();
|
||||
$e = $d->get();',
|
||||
[
|
||||
'$b' => 'B',
|
||||
'$c' => 'B',
|
||||
'$e' => 'B',
|
||||
]
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
|
@ -1568,6 +1568,37 @@ class MethodSignatureTest extends TestCase
|
||||
',
|
||||
'error_message' => 'MethodSignatureMismatch',
|
||||
],
|
||||
'noMixedTypehintInDescendant' => [
|
||||
'<?php
|
||||
class a {
|
||||
public function test(): mixed {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
class b extends a {
|
||||
public function test() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
',
|
||||
'error_message' => 'MethodSignatureMismatch',
|
||||
[],
|
||||
false,
|
||||
'8.0'
|
||||
],
|
||||
'noTypehintInNativeDescendant' => [
|
||||
'<?php
|
||||
class a implements JsonSerializable {
|
||||
public function jsonSerialize() {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
',
|
||||
'error_message' => 'MethodSignatureMustProvideReturnType',
|
||||
[],
|
||||
false,
|
||||
'8.1'
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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');
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user