1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-14 18:36:58 +01:00
psalm/src/Psalm/Internal/Analyzer/MethodComparator.php

1071 lines
46 KiB
PHP
Raw Normal View History

2020-03-12 04:38:49 +01:00
<?php
namespace Psalm\Internal\Analyzer;
2020-08-14 21:25:21 +02:00
use PhpParser\Node\Stmt\ClassMethod;
2020-03-12 04:38:49 +01:00
use Psalm\CodeLocation;
2021-06-08 04:55:21 +02:00
use Psalm\Codebase;
2020-03-12 06:19:11 +01:00
use Psalm\Internal\MethodIdentifier;
2020-08-14 21:25:21 +02:00
use Psalm\Internal\PhpVisitor\ParamReplacementVisitor;
2020-07-22 01:40:35 +02:00
use Psalm\Internal\Type\Comparator\TypeComparisonResult;
use Psalm\Internal\Type\Comparator\UnionTypeComparator;
2021-06-08 04:55:21 +02:00
use Psalm\Internal\Type\TemplateInferredTypeReplacer;
use Psalm\Issue\ConstructorSignatureMismatch;
2020-03-12 04:38:49 +01:00
use Psalm\Issue\ImplementedParamTypeMismatch;
use Psalm\Issue\ImplementedReturnTypeMismatch;
2021-06-08 04:55:21 +02:00
use Psalm\Issue\LessSpecificImplementedReturnType;
2020-03-12 04:38:49 +01:00
use Psalm\Issue\MethodSignatureMismatch;
2021-06-08 04:55:21 +02:00
use Psalm\Issue\MissingImmutableAnnotation;
2020-03-12 04:38:49 +01:00
use Psalm\Issue\MoreSpecificImplementedParamType;
use Psalm\Issue\OverriddenMethodAccess;
2021-06-08 04:55:21 +02:00
use Psalm\Issue\ParamNameMismatch;
2020-03-12 04:38:49 +01:00
use Psalm\Issue\TraitMethodSignatureMismatch;
use Psalm\IssueBuffer;
use Psalm\Storage\ClassLikeStorage;
2020-03-12 06:19:11 +01:00
use Psalm\Storage\FunctionLikeParameter;
2020-03-12 04:38:49 +01:00
use Psalm\Storage\MethodStorage;
use Psalm\Type;
2021-06-08 04:55:21 +02:00
2020-03-12 04:38:49 +01:00
use function in_array;
2021-09-26 23:41:26 +02:00
use function strpos;
2021-06-08 04:55:21 +02:00
use function strtolower;
2020-03-12 04:38:49 +01:00
class MethodComparator
{
/**
* @param string[] $suppressed_issues
*
* @return false|null
2021-10-11 17:23:21 +02:00
*
* @psalm-suppress PossiblyUnusedReturnValue unused but seems important
2020-03-12 04:38:49 +01:00
*/
public static function compare(
Codebase $codebase,
2020-08-14 21:25:21 +02:00
?ClassMethod $stmt,
2020-03-12 04:38:49 +01:00
ClassLikeStorage $implementer_classlike_storage,
ClassLikeStorage $guide_classlike_storage,
MethodStorage $implementer_method_storage,
MethodStorage $guide_method_storage,
string $implementer_called_class_name,
int $implementer_visibility,
CodeLocation $code_location,
array $suppressed_issues,
2020-08-14 21:25:21 +02:00
bool $prevent_abstract_override = true,
bool $prevent_method_signature_mismatch = true
): ?bool {
$implementer_method_id = new MethodIdentifier(
$implementer_classlike_storage->name,
strtolower($guide_method_storage->cased_name ?: '')
);
2020-03-12 04:38:49 +01:00
$implementer_declaring_method_id = $codebase->methods->getDeclaringMethodId(
$implementer_method_id
2020-03-12 04:38:49 +01:00
);
$cased_implementer_method_id = $implementer_classlike_storage->name . '::'
. $implementer_method_storage->cased_name;
$cased_guide_method_id = $guide_classlike_storage->name . '::' . $guide_method_storage->cased_name;
$codebase->methods->file_reference_provider->addMethodDependencyToClassMember(
strtolower((string)($implementer_declaring_method_id ?? $implementer_method_id)),
strtolower($guide_classlike_storage->name . '::' . $guide_method_storage->cased_name)
);
2020-03-12 06:19:11 +01:00
self::checkForObviousMethodMismatches(
$guide_classlike_storage,
$implementer_classlike_storage,
$guide_method_storage,
$implementer_method_storage,
$guide_method_storage->visibility,
$implementer_visibility,
$cased_guide_method_id,
$cased_implementer_method_id,
$prevent_method_signature_mismatch,
$prevent_abstract_override,
$codebase->php_major_version >= 8,
2020-03-12 06:19:11 +01:00
$code_location,
$suppressed_issues
);
if ($guide_method_storage->signature_return_type && $prevent_method_signature_mismatch) {
self::compareMethodSignatureReturnTypes(
$codebase,
$guide_classlike_storage,
$implementer_classlike_storage,
$guide_method_storage,
$implementer_method_storage,
$guide_method_storage->signature_return_type,
$cased_guide_method_id,
$implementer_called_class_name,
2020-03-12 06:19:11 +01:00
$cased_implementer_method_id,
$code_location,
$suppressed_issues
);
}
if ($guide_method_storage->return_type
&& $implementer_method_storage->return_type
&& !$implementer_method_storage->inherited_return_type
&& ($guide_method_storage->signature_return_type !== $guide_method_storage->return_type
|| $implementer_method_storage->signature_return_type !== $implementer_method_storage->return_type)
&& $implementer_classlike_storage->user_defined
&& (!$guide_classlike_storage->stubbed || $guide_classlike_storage->template_types)
) {
self::compareMethodDocblockReturnTypes(
$codebase,
$guide_classlike_storage,
$implementer_classlike_storage,
$implementer_method_storage,
$guide_method_storage->return_type,
$implementer_method_storage->return_type,
$cased_guide_method_id,
$implementer_called_class_name,
$implementer_declaring_method_id,
$code_location,
$suppressed_issues
);
}
foreach ($guide_method_storage->params as $i => $guide_param) {
if (!isset($implementer_method_storage->params[$i])) {
if (!$prevent_abstract_override && $i >= $guide_method_storage->required_param_count) {
continue;
}
if (IssueBuffer::accepts(
new MethodSignatureMismatch(
'Method ' . $cased_implementer_method_id . ' has fewer parameters than parent method ' .
$cased_guide_method_id,
$code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
2020-03-12 06:19:11 +01:00
)) {
return false;
}
return null;
}
self::compareMethodParams(
$codebase,
2020-08-14 21:25:21 +02:00
$stmt,
2020-03-12 06:19:11 +01:00
$implementer_classlike_storage,
$guide_classlike_storage,
$implementer_called_class_name,
$guide_method_storage,
$implementer_method_storage,
$guide_param,
$implementer_method_storage->params[$i],
$i,
$cased_guide_method_id,
$cased_implementer_method_id,
$prevent_method_signature_mismatch,
$code_location,
$suppressed_issues
);
}
if ($guide_classlike_storage->user_defined
&& ($guide_classlike_storage->is_interface
|| $guide_classlike_storage->preserve_constructor_signature
|| $implementer_method_storage->cased_name !== '__construct')
2020-03-12 06:19:11 +01:00
&& $implementer_method_storage->required_param_count > $guide_method_storage->required_param_count
) {
if ($implementer_method_storage->cased_name !== '__construct') {
if (IssueBuffer::accepts(
new MethodSignatureMismatch(
'Method ' . $cased_implementer_method_id . ' has more required parameters than parent method ' .
$cased_guide_method_id,
$code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
)) {
return false;
}
} else {
if (IssueBuffer::accepts(
new ConstructorSignatureMismatch(
'Method ' . $cased_implementer_method_id . ' has more required parameters than parent method ' .
$cased_guide_method_id,
$code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
)) {
return false;
}
2020-03-12 06:19:11 +01:00
}
2020-03-12 06:19:11 +01:00
return null;
}
return null;
2020-03-12 06:19:11 +01:00
}
/**
* @param string[] $suppressed_issues
*/
private static function checkForObviousMethodMismatches(
ClassLikeStorage $guide_classlike_storage,
ClassLikeStorage $implementer_classlike_storage,
MethodStorage $guide_method_storage,
MethodStorage $implementer_method_storage,
int $guide_visibility,
int $implementer_visibility,
string $cased_guide_method_id,
string $cased_implementer_method_id,
bool $prevent_method_signature_mismatch,
bool $prevent_abstract_override,
bool $trait_mismatches_are_fatal,
2020-03-12 06:19:11 +01:00
CodeLocation $code_location,
array $suppressed_issues
) : void {
if ($implementer_visibility > $guide_visibility) {
if ($trait_mismatches_are_fatal
|| $guide_classlike_storage->is_trait === $implementer_classlike_storage->is_trait
2020-03-12 04:38:49 +01:00
|| !in_array($guide_classlike_storage->name, $implementer_classlike_storage->used_traits)
|| $implementer_method_storage->defining_fqcln !== $implementer_classlike_storage->name
|| (!$implementer_method_storage->abstract
&& !$guide_method_storage->abstract)
) {
IssueBuffer::maybeAdd(
2020-03-12 04:38:49 +01:00
new OverriddenMethodAccess(
'Method ' . $cased_implementer_method_id . ' has different access level than '
. $cased_guide_method_id,
$code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
2020-03-12 06:19:11 +01:00
} elseif (IssueBuffer::accepts(
2020-03-12 04:38:49 +01:00
new TraitMethodSignatureMismatch(
'Method ' . $cased_implementer_method_id . ' has different access level than '
. $cased_guide_method_id,
$code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
2020-03-12 04:38:49 +01:00
)) {
// fall through
}
}
if ($guide_method_storage->final
&& $prevent_method_signature_mismatch
&& $prevent_abstract_override
) {
IssueBuffer::maybeAdd(
2020-03-12 04:38:49 +01:00
new MethodSignatureMismatch(
'Method ' . $cased_guide_method_id . ' is declared final and cannot be overridden',
$code_location
),
$guide_method_storage->final_from_docblock ?
$suppressed_issues + $implementer_classlike_storage->suppressed_issues :
[]
);
2020-03-12 04:38:49 +01:00
}
if ($prevent_abstract_override
&& !$guide_method_storage->abstract
&& $implementer_method_storage->abstract
&& !$guide_classlike_storage->abstract
&& !$guide_classlike_storage->is_interface
) {
IssueBuffer::maybeAdd(
2020-03-12 04:38:49 +01:00
new MethodSignatureMismatch(
'Method ' . $cased_implementer_method_id . ' cannot be abstract when inherited method '
. $cased_guide_method_id . ' is non-abstract',
$code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
2020-03-12 04:38:49 +01:00
}
if ($guide_method_storage->external_mutation_free
&& !$implementer_method_storage->external_mutation_free
&& !$guide_method_storage->mutation_free_inferred
&& $prevent_method_signature_mismatch
) {
IssueBuffer::maybeAdd(
2020-03-12 04:38:49 +01:00
new MissingImmutableAnnotation(
2020-11-28 15:03:03 +01:00
$cased_guide_method_id . ' is marked @psalm-immutable, but '
2020-03-12 04:38:49 +01:00
. $implementer_classlike_storage->name . '::'
. ($guide_method_storage->cased_name ?: '')
2020-11-28 15:03:03 +01:00
. ' is not marked @psalm-immutable',
2020-03-12 04:38:49 +01:00
$code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
2020-03-12 04:38:49 +01:00
}
2020-03-12 06:19:11 +01:00
}
2020-03-12 04:38:49 +01:00
2020-03-12 06:19:11 +01:00
/**
* @param string[] $suppressed_issues
*/
private static function compareMethodParams(
Codebase $codebase,
2020-08-14 21:25:21 +02:00
?ClassMethod $stmt,
2020-03-12 06:19:11 +01:00
ClassLikeStorage $implementer_classlike_storage,
ClassLikeStorage $guide_classlike_storage,
string $implementer_called_class_name,
MethodStorage $guide_method_storage,
MethodStorage $implementer_method_storage,
FunctionLikeParameter $guide_param,
FunctionLikeParameter $implementer_param,
int $i,
string $cased_guide_method_id,
string $cased_implementer_method_id,
bool $prevent_method_signature_mismatch,
CodeLocation $code_location,
array $suppressed_issues
) : void {
if ($prevent_method_signature_mismatch) {
if (!$guide_classlike_storage->user_defined
2020-03-12 04:38:49 +01:00
&& $guide_param->type
) {
2020-03-12 06:19:11 +01:00
$implementer_param_type = $implementer_param->signature_type;
2020-03-12 04:38:49 +01:00
$guide_param_signature_type = $guide_param->type;
$or_null_guide_param_signature_type = $guide_param->signature_type
? clone $guide_param->signature_type
: null;
if ($or_null_guide_param_signature_type) {
$or_null_guide_param_signature_type->addType(new Type\Atomic\TNull);
}
if ($cased_guide_method_id === 'Serializable::unserialize') {
$guide_param_signature_type = null;
$or_null_guide_param_signature_type = null;
}
if (!$guide_param->type->hasMixed()
&& !$guide_param->type->from_docblock
&& ($implementer_param_type || $guide_param_signature_type)
) {
2020-03-12 06:19:11 +01:00
$config = \Psalm\Config::getInstance();
2020-03-12 04:38:49 +01:00
if ($implementer_param_type
&& (!$guide_param_signature_type
|| strtolower($implementer_param_type->getId())
!== strtolower($guide_param_signature_type->getId()))
&& (!$or_null_guide_param_signature_type
|| strtolower($implementer_param_type->getId())
!== strtolower($or_null_guide_param_signature_type->getId()))
) {
if ($implementer_method_storage->cased_name === '__construct') {
IssueBuffer::maybeAdd(
new ConstructorSignatureMismatch(
'Argument ' . ($i + 1) . ' of '
. $cased_implementer_method_id . ' has wrong type \''
. $implementer_param_type . '\', expecting \''
. $guide_param_signature_type . '\' as defined by '
. $cased_guide_method_id,
$implementer_param->location
&& $config->isInProjectDirs(
$implementer_param->location->file_path
)
? $implementer_param->location
: $code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
} else {
IssueBuffer::maybeAdd(
new MethodSignatureMismatch(
'Argument ' . ($i + 1) . ' of '
. $cased_implementer_method_id . ' has wrong type \''
. $implementer_param_type . '\', expecting \''
. $guide_param_signature_type . '\' as defined by '
. $cased_guide_method_id,
$implementer_param->location
&& $config->isInProjectDirs(
$implementer_param->location->file_path
)
? $implementer_param->location
: $code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
2020-03-12 04:38:49 +01:00
}
2020-03-12 06:19:11 +01:00
return;
2020-03-12 04:38:49 +01:00
}
}
}
2020-08-14 22:26:55 +02:00
$config = \Psalm\Config::getInstance();
if ($guide_param->name !== $implementer_param->name
2020-08-14 06:27:33 +02:00
&& $guide_method_storage->allow_named_arg_calls
&& $guide_classlike_storage->user_defined
&& $implementer_classlike_storage->user_defined
2020-08-14 22:26:55 +02:00
&& $implementer_param->location
&& $guide_method_storage->cased_name
2021-09-26 22:51:44 +02:00
&& strpos($guide_method_storage->cased_name, '__') !== 0
2020-08-15 20:21:24 +02:00
&& $config->isInProjectDirs(
$implementer_param->location->file_path
)
) {
2020-08-14 06:27:33 +02:00
if ($config->allow_named_arg_calls
|| ($guide_classlike_storage->location
&& !$config->isInProjectDirs($guide_classlike_storage->location->file_path)
)
) {
2020-08-15 20:21:24 +02:00
if ($codebase->alter_code) {
2020-08-14 21:25:21 +02:00
$project_analyzer = \Psalm\Internal\Analyzer\ProjectAnalyzer::getInstance();
if ($stmt && isset($project_analyzer->getIssuesToFix()['ParamNameMismatch'])) {
$param_replacer = new ParamReplacementVisitor(
$implementer_param->name,
$guide_param->name
);
$traverser = new \PhpParser\NodeTraverser();
$traverser->addVisitor($param_replacer);
$traverser->traverse([$stmt]);
if ($replacements = $param_replacer->getReplacements()) {
\Psalm\Internal\FileManipulation\FileManipulationBuffer::add(
2020-08-14 22:26:55 +02:00
$implementer_param->location->file_path,
2020-08-14 21:25:21 +02:00
$replacements
);
}
}
} else {
IssueBuffer::maybeAdd(
2020-08-14 21:25:21 +02:00
new ParamNameMismatch(
'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' has wrong name $'
. $implementer_param->name . ', expecting $'
. $guide_param->name . ' as defined by '
. $cased_guide_method_id,
2020-08-15 20:21:24 +02:00
$implementer_param->location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
}
}
}
2020-03-12 06:19:11 +01:00
if ($guide_classlike_storage->user_defined
2020-03-12 04:38:49 +01:00
&& $implementer_param->signature_type
) {
2020-03-12 06:19:11 +01:00
self::compareMethodSignatureParams(
2020-03-12 04:38:49 +01:00
$codebase,
2020-03-12 06:19:11 +01:00
$i,
$guide_classlike_storage,
$implementer_classlike_storage,
$guide_method_storage,
$implementer_method_storage,
$guide_param,
2020-03-12 04:38:49 +01:00
$implementer_param->signature_type,
2020-03-12 06:19:11 +01:00
$cased_guide_method_id,
$cased_implementer_method_id,
$code_location,
$suppressed_issues
2020-03-12 04:38:49 +01:00
);
2020-03-12 06:19:11 +01:00
}
}
if ($implementer_param->type
&& $guide_param->type
&& $implementer_param->type->getId() !== $guide_param->type->getId()
) {
self::compareMethodDocblockParams(
$codebase,
$i,
$guide_classlike_storage,
$implementer_classlike_storage,
$implementer_called_class_name,
$guide_method_storage,
$implementer_method_storage,
$cased_guide_method_id,
$cased_implementer_method_id,
$guide_param->type,
$implementer_param->type,
$code_location,
$suppressed_issues
);
}
if ($guide_classlike_storage->user_defined && $implementer_param->by_ref !== $guide_param->by_ref) {
$config = \Psalm\Config::getInstance();
IssueBuffer::maybeAdd(
2020-03-12 06:19:11 +01:00
new MethodSignatureMismatch(
'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' is' .
($implementer_param->by_ref ? '' : ' not') . ' passed by reference, but argument ' .
($i + 1) . ' of ' . $cased_guide_method_id . ' is' . ($guide_param->by_ref ? '' : ' not'),
$implementer_param->location
&& $config->isInProjectDirs(
$implementer_param->location->file_path
)
? $implementer_param->location
: $code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
2020-03-12 06:19:11 +01:00
}
}
2020-03-12 04:38:49 +01:00
2020-03-12 06:19:11 +01:00
/**
* @param string[] $suppressed_issues
*/
private static function compareMethodSignatureParams(
Codebase $codebase,
int $i,
ClassLikeStorage $guide_classlike_storage,
ClassLikeStorage $implementer_classlike_storage,
MethodStorage $guide_method_storage,
MethodStorage $implementer_method_storage,
FunctionLikeParameter $guide_param,
Type\Union $implementer_param_signature_type,
string $cased_guide_method_id,
string $cased_implementer_method_id,
CodeLocation $code_location,
array $suppressed_issues
) : void {
$guide_param_signature_type = $guide_param->signature_type
2020-05-11 04:45:01 +02:00
? \Psalm\Internal\Type\TypeExpander::expandUnion(
2020-03-12 06:19:11 +01:00
$codebase,
$guide_param->signature_type,
$guide_classlike_storage->is_trait && $guide_method_storage->abstract
? $implementer_classlike_storage->name
: $guide_classlike_storage->name,
$guide_classlike_storage->is_trait && $guide_method_storage->abstract
? $implementer_classlike_storage->name
: $guide_classlike_storage->name,
$guide_classlike_storage->is_trait && $guide_method_storage->abstract
? $implementer_classlike_storage->parent_class
: $guide_classlike_storage->parent_class
)
: null;
2020-05-11 04:45:01 +02:00
$implementer_param_signature_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
2020-03-12 06:19:11 +01:00
$codebase,
$implementer_param_signature_type,
$implementer_classlike_storage->name,
$implementer_classlike_storage->name,
$implementer_classlike_storage->parent_class
);
$is_contained_by = (($codebase->php_major_version === 7
&& $codebase->php_minor_version === 4)
|| $codebase->php_major_version >= 8)
2020-03-12 06:19:11 +01:00
&& $guide_param_signature_type
2020-07-22 01:40:35 +02:00
? UnionTypeComparator::isContainedBy(
2020-03-12 06:19:11 +01:00
$codebase,
$guide_param_signature_type,
$implementer_param_signature_type
)
2020-07-22 01:40:35 +02:00
: UnionTypeComparator::isContainedByInPhp(
2020-03-12 06:19:11 +01:00
$guide_param_signature_type,
$implementer_param_signature_type
);
if (!$is_contained_by) {
$config = \Psalm\Config::getInstance();
if ($codebase->php_major_version >= 8
|| $guide_classlike_storage->is_trait === $implementer_classlike_storage->is_trait
2020-03-12 06:19:11 +01:00
|| !in_array($guide_classlike_storage->name, $implementer_classlike_storage->used_traits)
|| $implementer_method_storage->defining_fqcln !== $implementer_classlike_storage->name
|| (!$implementer_method_storage->abstract
&& !$guide_method_storage->abstract)
) {
if ($implementer_method_storage->cased_name === '__construct') {
IssueBuffer::maybeAdd(
new ConstructorSignatureMismatch(
'Argument ' . ($i + 1) . ' of '
. $cased_implementer_method_id
. ' has wrong type \''
. $implementer_param_signature_type . '\', expecting \''
. $guide_param_signature_type . '\' as defined by '
. $cased_guide_method_id,
$implementer_method_storage->params[$i]->location
&& $config->isInProjectDirs(
$implementer_method_storage->params[$i]->location->file_path
)
? $implementer_method_storage->params[$i]->location
: $code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
} else {
IssueBuffer::maybeAdd(
new MethodSignatureMismatch(
'Argument ' . ($i + 1) . ' of '
. $cased_implementer_method_id
. ' has wrong type \''
. $implementer_param_signature_type . '\', expecting \''
. $guide_param_signature_type . '\' as defined by '
. $cased_guide_method_id,
$implementer_method_storage->params[$i]->location
&& $config->isInProjectDirs(
$implementer_method_storage->params[$i]->location->file_path
)
? $implementer_method_storage->params[$i]->location
: $code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
2020-03-12 06:19:11 +01:00
}
} else {
IssueBuffer::maybeAdd(
2020-03-12 06:19:11 +01:00
new TraitMethodSignatureMismatch(
'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' has wrong type \'' .
$implementer_param_signature_type . '\', expecting \'' .
$guide_param_signature_type . '\' as defined by ' .
$cased_guide_method_id,
$implementer_method_storage->params[$i]->location
&& $config->isInProjectDirs(
$implementer_method_storage->params[$i]->location->file_path
2020-03-12 04:38:49 +01:00
)
2020-03-12 06:19:11 +01:00
? $implementer_method_storage->params[$i]->location
: $code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
2020-03-12 04:38:49 +01:00
}
2020-03-12 06:19:11 +01:00
}
}
2020-03-12 04:38:49 +01:00
2020-03-12 06:19:11 +01:00
/**
* @param string[] $suppressed_issues
*/
private static function compareMethodDocblockParams(
Codebase $codebase,
int $i,
ClassLikeStorage $guide_classlike_storage,
ClassLikeStorage $implementer_classlike_storage,
string $implementer_called_class_name,
MethodStorage $guide_method_storage,
MethodStorage $implementer_method_storage,
string $cased_guide_method_id,
string $cased_implementer_method_id,
Type\Union $guide_param_type,
Type\Union $implementer_param_type,
CodeLocation $code_location,
array $suppressed_issues
) : void {
2020-05-11 04:45:01 +02:00
$implementer_method_storage_param_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
2020-03-12 06:19:11 +01:00
$codebase,
$implementer_param_type,
$implementer_classlike_storage->name,
$implementer_called_class_name,
$implementer_classlike_storage->parent_class
);
2020-03-12 04:38:49 +01:00
2020-05-11 04:45:01 +02:00
$guide_method_storage_param_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
2020-03-12 06:19:11 +01:00
$codebase,
$guide_param_type,
$guide_classlike_storage->is_trait && $guide_method_storage->abstract
? $implementer_classlike_storage->name
: $guide_classlike_storage->name,
$guide_classlike_storage->is_trait && $guide_method_storage->abstract
? $implementer_classlike_storage->name
: $guide_classlike_storage->name,
$guide_classlike_storage->is_trait && $guide_method_storage->abstract
? $implementer_classlike_storage->parent_class
: $guide_classlike_storage->parent_class
);
2020-03-12 04:38:49 +01:00
2020-03-12 06:19:11 +01:00
$guide_class_name = $guide_classlike_storage->name;
2020-03-12 04:38:49 +01:00
2020-03-12 06:19:11 +01:00
if ($implementer_classlike_storage->is_trait) {
$implementer_called_class_storage = $codebase->classlike_storage_provider->get(
$implementer_called_class_name
);
if (isset(
$implementer_called_class_storage->template_extended_params[$implementer_classlike_storage->name]
2020-03-12 06:19:11 +01:00
)) {
self::transformTemplates(
$implementer_called_class_storage->template_extended_params,
2020-03-12 06:19:11 +01:00
$implementer_classlike_storage->name,
$implementer_method_storage_param_type,
$codebase
);
2020-03-12 04:38:49 +01:00
2020-03-12 06:19:11 +01:00
self::transformTemplates(
$implementer_called_class_storage->template_extended_params,
2020-03-12 06:19:11 +01:00
$guide_class_name,
$guide_method_storage_param_type,
$codebase
);
}
}
2020-03-12 04:38:49 +01:00
foreach ($implementer_method_storage_param_type->getAtomicTypes() as $k => $t) {
if ($t instanceof Type\Atomic\TTemplateParam
&& \strpos($t->defining_class, 'fn-') === 0
) {
$implementer_method_storage_param_type->removeType($k);
foreach ($t->as->getAtomicTypes() as $as_t) {
$implementer_method_storage_param_type->addType($as_t);
}
}
}
foreach ($guide_method_storage_param_type->getAtomicTypes() as $k => $t) {
if ($t instanceof Type\Atomic\TTemplateParam
&& \strpos($t->defining_class, 'fn-') === 0
) {
$guide_method_storage_param_type->removeType($k);
foreach ($t->as->getAtomicTypes() as $as_t) {
$guide_method_storage_param_type->addType($as_t);
}
}
}
if ($implementer_classlike_storage->template_extended_params) {
self::transformTemplates(
$implementer_classlike_storage->template_extended_params,
$guide_class_name,
$guide_method_storage_param_type,
$codebase
);
}
2020-03-12 06:19:11 +01:00
$union_comparison_results = new TypeComparisonResult();
2020-07-22 01:40:35 +02:00
if (!UnionTypeComparator::isContainedBy(
2020-03-12 06:19:11 +01:00
$codebase,
$guide_method_storage_param_type,
$implementer_method_storage_param_type,
!$guide_classlike_storage->user_defined,
!$guide_classlike_storage->user_defined,
$union_comparison_results
)) {
// is the declared return type more specific than the inferred one?
if ($union_comparison_results->type_coerced) {
if ($guide_classlike_storage->user_defined) {
IssueBuffer::maybeAdd(
2020-03-12 06:19:11 +01:00
new MoreSpecificImplementedParamType(
'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id
. ' has the more specific type \'' .
$implementer_method_storage_param_type->getId() . '\', expecting \'' .
$guide_method_storage_param_type->getId() . '\' as defined by ' .
$cased_guide_method_id,
$implementer_method_storage->params[$i]->location
?: $code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
2020-03-12 04:38:49 +01:00
}
2020-03-12 06:19:11 +01:00
} else {
2020-07-22 01:40:35 +02:00
if (UnionTypeComparator::isContainedBy(
2020-03-12 04:38:49 +01:00
$codebase,
$implementer_method_storage_param_type,
2020-03-12 06:19:11 +01:00
$guide_method_storage_param_type,
2020-03-12 04:38:49 +01:00
!$guide_classlike_storage->user_defined,
2020-03-12 06:19:11 +01:00
!$guide_classlike_storage->user_defined
2020-03-12 04:38:49 +01:00
)) {
2020-03-12 06:19:11 +01:00
if (IssueBuffer::accepts(
new MoreSpecificImplementedParamType(
'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id
. ' has the more specific type \'' .
$implementer_method_storage_param_type->getId() . '\', expecting \'' .
$guide_method_storage_param_type->getId() . '\' as defined by ' .
$cased_guide_method_id,
$implementer_method_storage->params[$i]->location
?: $code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
2020-03-12 06:19:11 +01:00
)) {
// fall through
}
} else {
if (IssueBuffer::accepts(
new ImplementedParamTypeMismatch(
'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id
. ' has wrong type \'' .
$implementer_method_storage_param_type->getId() . '\', expecting \'' .
$guide_method_storage_param_type->getId() . '\' as defined by ' .
$cased_guide_method_id,
$implementer_method_storage->params[$i]->location
?: $code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
2020-03-12 06:19:11 +01:00
)) {
// fall through
2020-03-12 04:38:49 +01:00
}
}
}
2020-03-12 06:19:11 +01:00
}
}
2020-03-12 04:38:49 +01:00
2020-03-12 06:19:11 +01:00
/**
* @param string[] $suppressed_issues
*/
private static function compareMethodSignatureReturnTypes(
Codebase $codebase,
ClassLikeStorage $guide_classlike_storage,
ClassLikeStorage $implementer_classlike_storage,
MethodStorage $guide_method_storage,
MethodStorage $implementer_method_storage,
Type\Union $guide_signature_return_type,
string $cased_guide_method_id,
string $implementer_called_class_name,
2020-03-12 06:19:11 +01:00
string $cased_implementer_method_id,
CodeLocation $code_location,
array $suppressed_issues
) : void {
2020-05-11 04:45:01 +02:00
$guide_signature_return_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
2020-03-12 06:19:11 +01:00
$codebase,
$guide_signature_return_type,
$guide_classlike_storage->is_trait && $guide_method_storage->abstract
? $implementer_classlike_storage->name
: $guide_classlike_storage->name,
2020-03-25 14:18:49 +01:00
($guide_classlike_storage->is_trait && $guide_method_storage->abstract)
|| $guide_classlike_storage->final
2020-03-12 06:19:11 +01:00
? $implementer_classlike_storage->name
: $guide_classlike_storage->name,
$guide_classlike_storage->is_trait && $guide_method_storage->abstract
? $implementer_classlike_storage->parent_class
2020-03-25 14:18:49 +01:00
: $guide_classlike_storage->parent_class,
true,
true,
2020-03-25 14:18:49 +01:00
$implementer_method_storage->final
2020-03-12 06:19:11 +01:00
);
$implementer_signature_return_type = $implementer_method_storage->signature_return_type
2020-05-11 04:45:01 +02:00
? \Psalm\Internal\Type\TypeExpander::expandUnion(
2020-03-12 06:19:11 +01:00
$codebase,
$implementer_method_storage->signature_return_type,
$implementer_classlike_storage->is_trait
? $implementer_called_class_name
: $implementer_classlike_storage->name,
$implementer_classlike_storage->is_trait
? $implementer_called_class_name
: $implementer_classlike_storage->name,
2020-03-12 06:19:11 +01:00
$implementer_classlike_storage->parent_class
) : null;
$is_contained_by = (($codebase->php_major_version === 7
&& $codebase->php_minor_version === 4)
|| $codebase->php_major_version >= 8)
2020-03-12 06:19:11 +01:00
&& $implementer_signature_return_type
2020-07-22 01:40:35 +02:00
? UnionTypeComparator::isContainedBy(
2020-03-12 06:19:11 +01:00
$codebase,
$implementer_signature_return_type,
$guide_signature_return_type
)
2020-07-22 01:40:35 +02:00
: UnionTypeComparator::isContainedByInPhp($implementer_signature_return_type, $guide_signature_return_type);
2020-03-12 06:19:11 +01:00
if (!$is_contained_by) {
if ($codebase->php_major_version >= 8
|| $guide_classlike_storage->is_trait === $implementer_classlike_storage->is_trait
2020-03-12 06:19:11 +01:00
|| !in_array($guide_classlike_storage->name, $implementer_classlike_storage->used_traits)
|| $implementer_method_storage->defining_fqcln !== $implementer_classlike_storage->name
|| (!$implementer_method_storage->abstract
&& !$guide_method_storage->abstract)
) {
IssueBuffer::maybeAdd(
2020-03-12 04:38:49 +01:00
new MethodSignatureMismatch(
2020-03-12 06:19:11 +01:00
'Method ' . $cased_implementer_method_id . ' with return type \''
. $implementer_signature_return_type . '\' is different to return type \''
. $guide_signature_return_type . '\' of inherited method ' . $cased_guide_method_id,
$code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
2020-03-12 06:19:11 +01:00
} else {
IssueBuffer::maybeAdd(
2020-03-12 06:19:11 +01:00
new TraitMethodSignatureMismatch(
'Method ' . $cased_implementer_method_id . ' with return type \''
. $implementer_signature_return_type . '\' is different to return type \''
. $guide_signature_return_type . '\' of inherited method ' . $cased_guide_method_id,
$code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
2020-03-12 04:38:49 +01:00
}
}
2020-03-12 06:19:11 +01:00
}
2020-03-12 04:38:49 +01:00
2020-03-12 06:19:11 +01:00
/**
* @param string[] $suppressed_issues
*/
private static function compareMethodDocblockReturnTypes(
Codebase $codebase,
ClassLikeStorage $guide_classlike_storage,
ClassLikeStorage $implementer_classlike_storage,
MethodStorage $implementer_method_storage,
Type\Union $guide_return_type,
Type\Union $implementer_return_type,
string $cased_guide_method_id,
string $implementer_called_class_name,
?MethodIdentifier $implementer_declaring_method_id,
CodeLocation $code_location,
array $suppressed_issues
) : void {
2020-05-11 04:45:01 +02:00
$implementer_method_storage_return_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
2020-03-12 06:19:11 +01:00
$codebase,
$implementer_return_type,
$implementer_classlike_storage->is_trait
? $implementer_called_class_name
: $implementer_classlike_storage->name,
2020-03-12 06:19:11 +01:00
$implementer_called_class_name,
$implementer_classlike_storage->parent_class
);
2020-05-11 04:45:01 +02:00
$guide_method_storage_return_type = \Psalm\Internal\Type\TypeExpander::expandUnion(
2020-03-12 06:19:11 +01:00
$codebase,
$guide_return_type,
$guide_classlike_storage->is_trait
? $implementer_classlike_storage->name
: $guide_classlike_storage->name,
$guide_classlike_storage->is_trait
2020-03-25 14:18:49 +01:00
|| $implementer_method_storage->final
2020-03-12 06:19:11 +01:00
? $implementer_called_class_name
: $guide_classlike_storage->name,
2020-03-25 14:18:49 +01:00
$guide_classlike_storage->parent_class,
true,
true,
2020-03-25 14:18:49 +01:00
$implementer_method_storage->final
2020-03-12 06:19:11 +01:00
);
$guide_class_name = $guide_classlike_storage->name;
if ($implementer_classlike_storage->template_extended_params) {
2020-03-12 06:19:11 +01:00
self::transformTemplates(
$implementer_classlike_storage->template_extended_params,
2020-03-12 06:19:11 +01:00
$guide_class_name,
$guide_method_storage_return_type,
$codebase
);
2021-11-18 23:13:08 +01:00
if ($implementer_method_storage->defining_fqcln) {
self::transformTemplates(
$implementer_classlike_storage->template_extended_params,
$implementer_method_storage->defining_fqcln,
$implementer_method_storage_return_type,
$codebase
);
}
2020-03-12 06:19:11 +01:00
}
if ($implementer_classlike_storage->is_trait) {
$implementer_called_class_storage = $codebase->classlike_storage_provider->get(
$implementer_called_class_name
);
2021-11-22 23:32:05 +01:00
if ($implementer_called_class_storage->template_extended_params) {
2020-03-12 06:19:11 +01:00
self::transformTemplates(
$implementer_called_class_storage->template_extended_params,
2020-03-12 06:19:11 +01:00
$implementer_classlike_storage->name,
$implementer_method_storage_return_type,
$codebase
);
self::transformTemplates(
$implementer_called_class_storage->template_extended_params,
2020-03-12 06:19:11 +01:00
$guide_class_name,
$guide_method_storage_return_type,
$codebase
);
2020-03-12 04:38:49 +01:00
}
2020-03-12 06:19:11 +01:00
}
2020-03-12 04:38:49 +01:00
2020-03-12 06:19:11 +01:00
// treat void as null when comparing against docblock implementer
if ($implementer_method_storage_return_type->isVoid()) {
$implementer_method_storage_return_type = Type::getNull();
}
if ($guide_method_storage_return_type->isVoid()) {
$guide_method_storage_return_type = Type::getNull();
}
$union_comparison_results = new TypeComparisonResult();
2020-07-22 01:40:35 +02:00
if (!UnionTypeComparator::isContainedBy(
2020-03-12 06:19:11 +01:00
$codebase,
$implementer_method_storage_return_type,
$guide_method_storage_return_type,
false,
false,
$union_comparison_results
)) {
// is the declared return type more specific than the inferred one?
if ($union_comparison_results->type_coerced) {
IssueBuffer::maybeAdd(
2020-03-12 06:19:11 +01:00
new LessSpecificImplementedReturnType(
'The inherited return type \'' . $guide_method_storage_return_type->getId()
. '\' for ' . $cased_guide_method_id . ' is more specific than the implemented '
. 'return type for ' . $implementer_declaring_method_id . ' \''
. $implementer_method_storage_return_type->getId() . '\'',
$implementer_method_storage->return_type_location
?: $code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
2020-03-12 06:19:11 +01:00
} else {
IssueBuffer::maybeAdd(
2020-03-12 06:19:11 +01:00
new ImplementedReturnTypeMismatch(
'The inherited return type \'' . $guide_method_storage_return_type->getId()
. '\' for ' . $cased_guide_method_id . ' is different to the implemented '
. 'return type for ' . $implementer_declaring_method_id . ' \''
. $implementer_method_storage_return_type->getId() . '\'',
$implementer_method_storage->return_type_location
?: $code_location
),
$suppressed_issues + $implementer_classlike_storage->suppressed_issues
);
2020-03-12 06:19:11 +01:00
}
2020-03-12 04:38:49 +01:00
}
}
/**
* @param array<string, array<string, Type\Union>> $template_extended_params
2020-03-12 04:38:49 +01:00
*/
private static function transformTemplates(
array $template_extended_params,
2020-03-12 04:38:49 +01:00
string $base_class_name,
Type\Union $templated_type,
Codebase $codebase
) : void {
if (isset($template_extended_params[$base_class_name])) {
$map = $template_extended_params[$base_class_name];
2020-03-12 04:38:49 +01:00
$template_types = [];
foreach ($map as $key => $mapped_type) {
$new_bases = [];
2020-03-12 04:38:49 +01:00
foreach ($mapped_type->getTemplateTypes() as $mapped_atomic_type) {
if ($mapped_atomic_type->defining_class === $base_class_name) {
continue;
2020-03-12 04:38:49 +01:00
}
$new_bases[] = $mapped_atomic_type->defining_class;
}
2020-03-12 04:38:49 +01:00
if ($new_bases) {
$mapped_type = clone $mapped_type;
2020-03-12 04:38:49 +01:00
foreach ($new_bases as $new_base_class_name) {
self::transformTemplates(
$template_extended_params,
$new_base_class_name,
$mapped_type,
$codebase
);
}
2020-03-12 04:38:49 +01:00
}
$template_types[$key][$base_class_name] = $mapped_type;
2020-03-12 04:38:49 +01:00
}
$template_result = new \Psalm\Internal\Type\TemplateResult([], $template_types);
2020-03-12 04:38:49 +01:00
2020-11-29 22:27:00 +01:00
TemplateInferredTypeReplacer::replace(
$templated_type,
$template_result,
2020-03-12 04:38:49 +01:00
$codebase
);
}
}
}