1
0
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:
orklah 2022-01-22 17:48:42 +01:00
commit af1888b631
28 changed files with 261 additions and 63 deletions

View File

@ -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" />

View File

@ -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'],

View File

@ -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'],

View File

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

View File

@ -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'],

View File

@ -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)

View File

@ -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)

View File

@ -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'];
}
}
```

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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;

View File

@ -0,0 +1,9 @@
<?php
namespace Psalm\Issue;
class MethodSignatureMustProvideReturnType extends CodeIssue
{
public const ERROR_LEVEL = -1;
public const SHORTCODE = 282;
}

View File

@ -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>
*/

View File

@ -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');
}

View File

@ -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());
}

View File

@ -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',
];
}
}

View File

@ -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);

View File

@ -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));

View File

@ -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();

View File

@ -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',
]
],
];
}

View File

@ -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'
],
];
}
}

View File

@ -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);
}
/**

View File

@ -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';

View File

@ -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);

View File

@ -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);

View File

@ -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';

View File

@ -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');