1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-15 10:57:08 +01:00
psalm/src/Psalm/Internal/Analyzer/MethodAnalyzer.php

1200 lines
47 KiB
PHP
Raw Normal View History

2016-01-08 00:28:27 +01:00
<?php
2018-11-06 03:57:36 +01:00
namespace Psalm\Internal\Analyzer;
2016-01-08 00:28:27 +01:00
2016-11-02 07:29:00 +01:00
use PhpParser;
2018-11-06 03:57:36 +01:00
use Psalm\Codebase;
use Psalm\CodeLocation;
use Psalm\Context;
2018-11-06 03:57:36 +01:00
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
2016-07-26 00:37:44 +02:00
use Psalm\Issue\DeprecatedMethod;
use Psalm\Issue\ImplementedParamTypeMismatch;
2018-02-22 00:59:31 +01:00
use Psalm\Issue\ImplementedReturnTypeMismatch;
2016-11-02 07:29:00 +01:00
use Psalm\Issue\InaccessibleMethod;
use Psalm\Issue\InternalMethod;
2016-07-26 00:37:44 +02:00
use Psalm\Issue\InvalidStaticInvocation;
2018-02-22 00:59:31 +01:00
use Psalm\Issue\MethodSignatureMismatch;
use Psalm\Issue\MethodSignatureMustOmitReturnType;
2018-02-22 00:59:31 +01:00
use Psalm\Issue\MoreSpecificImplementedParamType;
use Psalm\Issue\LessSpecificImplementedReturnType;
use Psalm\Issue\NonStaticSelfCall;
2018-02-22 00:59:31 +01:00
use Psalm\Issue\OverriddenMethodAccess;
use Psalm\Issue\TraitMethodSignatureMismatch;
2016-11-02 07:29:00 +01:00
use Psalm\Issue\UndefinedMethod;
use Psalm\IssueBuffer;
use Psalm\StatementsSource;
2018-02-22 00:59:31 +01:00
use Psalm\Storage\ClassLikeStorage;
use Psalm\Storage\MethodStorage;
use Psalm\Type;
use function strtolower;
use function explode;
use function is_string;
use function in_array;
use Psalm\Issue\MissingImmutableAnnotation;
2016-01-08 00:28:27 +01:00
/**
* @internal
*/
2018-11-06 03:57:36 +01:00
class MethodAnalyzer extends FunctionLikeAnalyzer
2016-01-08 00:28:27 +01:00
{
public function __construct(
PhpParser\Node\Stmt\ClassMethod $function,
SourceAnalyzer $source,
MethodStorage $storage = null
) {
$codebase = $source->getCodebase();
$real_method_id = $source->getFQCLN() . '::' . strtolower((string) $function->name);
if (!$storage) {
try {
$storage = $codebase->methods->getStorage($real_method_id);
} catch (\UnexpectedValueException $e) {
$class_storage = $codebase->classlike_storage_provider->get((string) $source->getFQCLN());
if (!$class_storage->parent_classes) {
throw $e;
}
$declaring_method_id = $codebase->methods->getDeclaringMethodId($real_method_id);
if (!$declaring_method_id) {
throw $e;
}
// happens for fake constructors
$storage = $codebase->methods->getStorage($declaring_method_id);
}
2016-10-15 06:12:57 +02:00
}
parent::__construct($function, $source, $storage);
}
/**
* Determines whether a given method is static or not
2016-11-02 07:29:00 +01:00
*
* @param string $method_id
* @param bool $self_call
* @param bool $is_context_dynamic
* @param CodeLocation $code_location
* @param array<string> $suppressed_issues
* @param bool $is_dynamic_this_method
2017-05-27 02:16:18 +02:00
*
2016-11-02 07:29:00 +01:00
* @return bool
*/
2017-01-12 03:37:53 +01:00
public static function checkStatic(
$method_id,
$self_call,
$is_context_dynamic,
2018-11-06 03:57:36 +01:00
Codebase $codebase,
CodeLocation $code_location,
array $suppressed_issues,
&$is_dynamic_this_method = false
) {
2018-11-06 03:57:36 +01:00
$codebase_methods = $codebase->methods;
2018-02-04 00:52:35 +01:00
2019-01-22 00:01:15 +01:00
if ($method_id === 'Closure::fromcallable') {
return true;
}
$original_method_id = $method_id;
2018-02-04 00:52:35 +01:00
$method_id = $codebase_methods->getDeclaringMethodId($method_id);
2018-03-17 05:19:55 +01:00
if (!$method_id) {
2019-01-22 00:01:15 +01:00
throw new \LogicException('Declaring method for ' . $original_method_id . ' should not be null');
2018-03-17 05:19:55 +01:00
}
2018-02-04 00:52:35 +01:00
$storage = $codebase_methods->getStorage($method_id);
if (!$storage->is_static) {
if ($self_call) {
if (!$is_context_dynamic) {
if (IssueBuffer::accepts(
new NonStaticSelfCall(
'Method ' . $codebase_methods->getCasedMethodId($method_id) .
' is not static, but is called ' .
'using self::',
$code_location
),
$suppressed_issues
)) {
return false;
}
} else {
$is_dynamic_this_method = true;
}
} else {
if (IssueBuffer::accepts(
new InvalidStaticInvocation(
2018-02-04 00:52:35 +01:00
'Method ' . $codebase_methods->getCasedMethodId($method_id) .
' is not static, but is called ' .
'statically',
$code_location
),
$suppressed_issues
)) {
return false;
}
}
}
2016-11-02 07:29:00 +01:00
return true;
}
2016-04-27 00:42:48 +02:00
/**
* @param string $method_id
* @param CodeLocation $code_location
* @param string[] $suppressed_issues
* @param string|null $calling_function_id
2017-05-27 02:16:18 +02:00
*
* @return bool|null
2016-04-27 00:42:48 +02:00
*/
public static function checkMethodExists(
2018-11-06 03:57:36 +01:00
Codebase $codebase,
$method_id,
CodeLocation $code_location,
array $suppressed_issues,
$calling_function_id = null
) {
2018-11-06 03:57:36 +01:00
if ($codebase->methods->methodExists(
$method_id,
$calling_function_id,
$calling_function_id !== $method_id ? $code_location : null,
null,
$code_location->file_path
)) {
2016-10-23 18:24:53 +02:00
return true;
}
if (IssueBuffer::accepts(
new UndefinedMethod('Method ' . $method_id . ' does not exist', $code_location, $method_id),
2016-11-02 07:29:00 +01:00
$suppressed_issues
2016-10-23 18:24:53 +02:00
)) {
return false;
}
2016-11-02 07:29:00 +01:00
return null;
2016-10-23 18:24:53 +02:00
}
2016-10-15 06:12:57 +02:00
/**
* @param string $method_id
* @param CodeLocation $code_location
* @param string[] $suppressed_issues
2017-05-27 02:16:18 +02:00
*
2016-10-15 06:12:57 +02:00
* @return false|null
*/
public static function checkMethodNotDeprecatedOrInternal(
2018-11-06 03:57:36 +01:00
Codebase $codebase,
Context $context,
$method_id,
CodeLocation $code_location,
array $suppressed_issues
) {
2018-11-06 03:57:36 +01:00
$codebase_methods = $codebase->methods;
2018-02-04 00:52:35 +01:00
$method_id = (string) $codebase_methods->getDeclaringMethodId($method_id);
$storage = $codebase_methods->getStorage($method_id);
if ($storage->deprecated) {
if (IssueBuffer::accepts(
2016-11-02 07:29:00 +01:00
new DeprecatedMethod(
2018-02-04 00:52:35 +01:00
'The method ' . $codebase_methods->getCasedMethodId($method_id) .
' has been marked as deprecated',
$code_location,
$method_id
2016-11-02 07:29:00 +01:00
),
$suppressed_issues
)) {
// continue
}
}
2016-11-02 07:29:00 +01:00
if ($storage->psalm_internal
&& $context->self
&& !$context->collect_initializations
&& !$context->collect_mutations
) {
if (! NamespaceAnalyzer::isWithin($context->self, $storage->psalm_internal)
) {
if (IssueBuffer::accepts(
new InternalMethod(
'The method ' . $codebase_methods->getCasedMethodId($method_id) .
' has been marked as internal to ' . $storage->psalm_internal,
$code_location,
$method_id
),
$suppressed_issues
)) {
// fall through
}
}
}
if ($storage->internal
&& $context->self
&& !$context->collect_initializations
&& !$context->collect_mutations
) {
$declaring_class = explode('::', $method_id)[0];
if (! NamespaceAnalyzer::nameSpaceRootsMatch($context->self, $declaring_class)) {
if (IssueBuffer::accepts(
new InternalMethod(
'The method ' . $codebase_methods->getCasedMethodId($method_id) .
' has been marked as internal',
$code_location,
$method_id
),
$suppressed_issues
)) {
// fall through
}
}
}
2016-11-02 07:29:00 +01:00
return null;
}
/**
* @param string $method_id
* @param Context $context
* @param StatementsSource $source
* @param CodeLocation $code_location
* @param string[] $suppressed_issues
2017-05-27 02:16:18 +02:00
*
* @return false|null
2016-04-27 00:42:48 +02:00
*/
2016-11-02 07:29:00 +01:00
public static function checkMethodVisibility(
$method_id,
Context $context,
2016-11-02 07:29:00 +01:00
StatementsSource $source,
CodeLocation $code_location,
2016-11-02 07:29:00 +01:00
array $suppressed_issues
) {
2018-11-06 03:57:36 +01:00
$codebase = $source->getCodebase();
2018-02-04 00:52:35 +01:00
$codebase_methods = $codebase->methods;
$codebase_classlikes = $codebase->classlikes;
list($fq_classlike_name, $method_name) = explode('::', $method_id);
if ($codebase_methods->visibility_provider->has($fq_classlike_name)) {
$method_visible = $codebase_methods->visibility_provider->isMethodVisible(
$source,
$fq_classlike_name,
$method_name,
$context,
$code_location
);
if ($method_visible === false) {
if (IssueBuffer::accepts(
new InaccessibleMethod(
'Cannot access method ' . $codebase_methods->getCasedMethodId($method_id) .
' from context ' . $context->self,
$code_location
),
$suppressed_issues
)) {
return false;
}
} elseif ($method_visible === true) {
return false;
}
}
2018-02-04 00:52:35 +01:00
$declaring_method_id = $codebase_methods->getDeclaringMethodId($method_id);
2016-04-18 19:31:59 +02:00
2017-07-09 20:50:57 +02:00
if (!$declaring_method_id) {
$method_name = explode('::', $method_id)[1];
if ($method_name === '__construct'
|| $method_id === 'Closure::__invoke'
|| $method_id === 'Closure::fromcallable'
) {
return null;
}
throw new \UnexpectedValueException('$declaring_method_id not expected to be null here');
}
2018-02-04 00:52:35 +01:00
$appearing_method_id = $codebase_methods->getAppearingMethodId($method_id);
2017-07-09 20:50:57 +02:00
$appearing_method_class = null;
$appearing_class_storage = null;
$appearing_method_name = null;
2017-07-09 20:50:57 +02:00
if ($appearing_method_id) {
list($appearing_method_class, $appearing_method_name) = explode('::', $appearing_method_id);
2017-07-09 20:50:57 +02:00
// if the calling class is the same, we know the method exists, so it must be visible
if ($appearing_method_class === $context->self) {
2017-07-09 20:50:57 +02:00
return null;
}
$appearing_class_storage = $codebase->classlike_storage_provider->get($appearing_method_class);
2016-12-12 19:50:46 +01:00
}
2017-07-09 20:50:57 +02:00
list($declaring_method_class) = explode('::', $declaring_method_id);
2018-11-06 03:57:36 +01:00
if ($source->getSource() instanceof TraitAnalyzer && $declaring_method_class === $source->getFQCLN()) {
2016-11-02 07:29:00 +01:00
return null;
2016-08-13 17:10:43 +02:00
}
2018-11-06 03:57:36 +01:00
$storage = $codebase->methods->getStorage($declaring_method_id);
$visibility = $storage->visibility;
if ($appearing_method_name
&& isset($appearing_class_storage->trait_visibility_map[$appearing_method_name])
) {
$visibility = $appearing_class_storage->trait_visibility_map[$appearing_method_name];
}
switch ($visibility) {
2018-11-06 03:57:36 +01:00
case ClassLikeAnalyzer::VISIBILITY_PUBLIC:
2016-11-02 07:29:00 +01:00
return null;
2016-04-18 19:31:59 +02:00
2018-11-06 03:57:36 +01:00
case ClassLikeAnalyzer::VISIBILITY_PRIVATE:
if (!$context->self || $appearing_method_class !== $context->self) {
2016-06-26 21:18:40 +02:00
if (IssueBuffer::accepts(
new InaccessibleMethod(
2018-02-04 00:52:35 +01:00
'Cannot access private method ' . $codebase_methods->getCasedMethodId($method_id) .
' from context ' . $context->self,
$code_location
),
2016-11-02 07:29:00 +01:00
$suppressed_issues
)) {
return false;
}
2016-04-18 19:31:59 +02:00
}
2016-11-02 07:29:00 +01:00
return null;
2016-04-18 19:31:59 +02:00
2018-11-06 03:57:36 +01:00
case ClassLikeAnalyzer::VISIBILITY_PROTECTED:
if (!$context->self) {
2016-06-26 21:18:40 +02:00
if (IssueBuffer::accepts(
2016-11-02 07:29:00 +01:00
new InaccessibleMethod(
'Cannot access protected method ' . $method_id,
$code_location
2016-11-02 07:29:00 +01:00
),
$suppressed_issues
)) {
return false;
}
2016-11-01 05:39:41 +01:00
2016-11-02 07:29:00 +01:00
return null;
2016-04-18 19:31:59 +02:00
}
if ($appearing_method_class
&& $codebase_classlikes->classExtends($appearing_method_class, $context->self)
) {
2016-11-02 07:29:00 +01:00
return null;
2016-04-30 20:14:22 +02:00
}
if ($appearing_method_class
&& !$codebase_classlikes->classExtends($context->self, $appearing_method_class)
) {
2016-06-26 21:18:40 +02:00
if (IssueBuffer::accepts(
new InaccessibleMethod(
2018-02-04 00:52:35 +01:00
'Cannot access protected method ' . $codebase_methods->getCasedMethodId($method_id) .
' from context ' . $context->self,
$code_location
),
2016-11-02 07:29:00 +01:00
$suppressed_issues
)) {
return false;
}
2016-04-18 19:31:59 +02:00
}
}
2016-11-02 07:29:00 +01:00
return null;
}
2016-10-15 06:12:57 +02:00
/**
2018-02-04 00:52:35 +01:00
* @param string $method_id
* @param Context $context
2018-02-04 00:52:35 +01:00
* @param StatementsSource $source
2017-05-27 02:16:18 +02:00
*
2018-02-04 00:52:35 +01:00
* @return bool
2016-12-31 17:49:04 +01:00
*/
2018-02-04 00:52:35 +01:00
public static function isMethodVisible(
$method_id,
Context $context,
2018-02-04 00:52:35 +01:00
StatementsSource $source
) {
2018-11-06 03:57:36 +01:00
$codebase = $source->getCodebase();
2017-01-12 03:37:53 +01:00
list($fq_classlike_name, $method_name) = explode('::', $method_id);
if ($codebase->methods->visibility_provider->has($fq_classlike_name)) {
$method_visible = $codebase->methods->visibility_provider->isMethodVisible(
$source,
$fq_classlike_name,
$method_name,
$context,
null
);
if ($method_visible !== null) {
return $method_visible;
}
}
2018-02-04 00:52:35 +01:00
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
2016-11-07 05:29:54 +01:00
2018-02-04 00:52:35 +01:00
if (!$declaring_method_id) {
// this can happen for methods in the callmap that were not reflected
return true;
}
2018-02-04 00:52:35 +01:00
$appearing_method_id = $codebase->methods->getAppearingMethodId($method_id);
2016-12-31 17:49:04 +01:00
2018-02-04 00:52:35 +01:00
$appearing_method_class = null;
2017-01-09 05:58:06 +01:00
2018-02-04 00:52:35 +01:00
if ($appearing_method_id) {
list($appearing_method_class) = explode('::', $appearing_method_id);
2018-02-04 00:52:35 +01:00
// if the calling class is the same, we know the method exists, so it must be visible
if ($appearing_method_class === $context->self) {
2018-02-04 00:52:35 +01:00
return true;
}
2016-12-31 17:49:04 +01:00
}
2018-02-04 00:52:35 +01:00
list($declaring_method_class) = explode('::', $declaring_method_id);
2018-11-06 03:57:36 +01:00
if ($source->getSource() instanceof TraitAnalyzer && $declaring_method_class === $source->getFQCLN()) {
2018-02-04 00:52:35 +01:00
return true;
}
2018-02-04 00:52:35 +01:00
$storage = $codebase->methods->getStorage($declaring_method_id);
2016-10-15 06:12:57 +02:00
2018-02-04 00:52:35 +01:00
switch ($storage->visibility) {
2018-11-06 03:57:36 +01:00
case ClassLikeAnalyzer::VISIBILITY_PUBLIC:
2018-02-04 00:52:35 +01:00
return true;
2018-11-06 03:57:36 +01:00
case ClassLikeAnalyzer::VISIBILITY_PRIVATE:
return $context->self && $appearing_method_class === $context->self;
2016-12-31 01:36:35 +01:00
2018-11-06 03:57:36 +01:00
case ClassLikeAnalyzer::VISIBILITY_PROTECTED:
if (!$context->self) {
2018-02-04 00:52:35 +01:00
return false;
}
2016-10-15 06:12:57 +02:00
2018-02-04 00:52:35 +01:00
if ($appearing_method_class
&& $codebase->classExtends($appearing_method_class, $context->self)
2018-02-04 00:52:35 +01:00
) {
return true;
}
2018-02-04 00:52:35 +01:00
if ($appearing_method_class
&& !$codebase->classExtends($context->self, $appearing_method_class)
2018-02-04 00:52:35 +01:00
) {
return false;
}
}
2018-02-04 00:52:35 +01:00
return true;
2016-08-15 19:37:21 +02:00
}
2018-02-22 00:59:31 +01:00
/**
* @param ClassLikeStorage $implementer_classlike_storage
* @param ClassLikeStorage $guide_classlike_storage
* @param MethodStorage $implementer_method_storage
* @param MethodStorage $guide_method_storage
* @param CodeLocation $code_location
* @param string[] $suppressed_issues
* @param bool $prevent_abstract_override
* @param bool $prevent_method_signature_mismatch
2018-02-22 00:59:31 +01:00
*
* @return false|null
*/
public static function compareMethods(
2018-11-06 03:57:36 +01:00
Codebase $codebase,
2018-02-22 00:59:31 +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,
2018-02-22 00:59:31 +01:00
CodeLocation $code_location,
array $suppressed_issues,
$prevent_abstract_override = true,
$prevent_method_signature_mismatch = true
2018-02-22 00:59:31 +01:00
) {
$config = $codebase->config;
2018-02-22 00:59:31 +01:00
$implementer_method_id = $implementer_classlike_storage->name . '::'
2019-11-25 21:20:31 +01:00
. strtolower($guide_method_storage->cased_name ?: '');
2018-02-22 00:59:31 +01:00
$implementer_declaring_method_id = $codebase->methods->getDeclaringMethodId($implementer_method_id);
$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;
if ($implementer_visibility > $guide_method_storage->visibility) {
2018-02-22 00:59:31 +01:00
if (IssueBuffer::accepts(
new OverriddenMethodAccess(
'Method ' . $cased_implementer_method_id . ' has different access level than '
. $cased_guide_method_id,
$code_location
)
)) {
return false;
}
return null;
}
if ($prevent_abstract_override
&& !$guide_method_storage->abstract
&& $implementer_method_storage->abstract
2018-03-13 18:50:53 +01:00
&& !$guide_classlike_storage->abstract
&& !$guide_classlike_storage->is_interface
) {
if (IssueBuffer::accepts(
new MethodSignatureMismatch(
'Method ' . $cased_implementer_method_id . ' cannot be abstract when inherited method '
. $cased_guide_method_id . ' is non-abstract',
$code_location
)
)) {
return false;
}
return null;
}
if ($guide_method_storage->signature_return_type && $prevent_method_signature_mismatch) {
2018-11-06 03:57:36 +01:00
$guide_signature_return_type = ExpressionAnalyzer::fleshOutType(
$codebase,
2018-02-22 00:59:31 +01:00
$guide_method_storage->signature_return_type,
$guide_classlike_storage->name,
$guide_classlike_storage->name,
$guide_classlike_storage->parent_class
2018-02-22 00:59:31 +01:00
);
$implementer_signature_return_type = $implementer_method_storage->signature_return_type
2018-11-06 03:57:36 +01:00
? ExpressionAnalyzer::fleshOutType(
$codebase,
2018-02-22 00:59:31 +01:00
$implementer_method_storage->signature_return_type,
$implementer_classlike_storage->name,
$implementer_classlike_storage->name,
$implementer_classlike_storage->parent_class
2018-02-22 00:59:31 +01:00
) : null;
$is_contained_by = $codebase->php_major_version >= 7
&& $codebase->php_minor_version >= 4
&& $implementer_signature_return_type
? TypeAnalyzer::isContainedBy(
$codebase,
$implementer_signature_return_type,
$guide_signature_return_type
)
: TypeAnalyzer::isContainedByInPhp($implementer_signature_return_type, $guide_signature_return_type);
if (!$is_contained_by) {
if ($guide_classlike_storage->is_trait === $implementer_classlike_storage->is_trait
|| (!$implementer_method_storage->abstract
2019-12-20 20:37:36 +01:00
&& !$guide_method_storage->abstract)
) {
if (IssueBuffer::accepts(
new MethodSignatureMismatch(
'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
)) {
return false;
}
} else {
if (IssueBuffer::accepts(
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
)) {
return false;
}
2018-02-22 00:59:31 +01:00
}
return null;
}
}
if ($guide_method_storage->external_mutation_free
&& !$implementer_method_storage->external_mutation_free
&& !$guide_method_storage->mutation_free_inferred
) {
if (IssueBuffer::accepts(
new MissingImmutableAnnotation(
$cased_guide_method_id . ' is marked immutable, but '
. $implementer_method_id . ' is not marked immutable',
$code_location
),
$suppressed_issues
)) {
// fall through
}
}
if ($guide_method_storage->return_type
2018-02-22 00:59:31 +01:00
&& $implementer_method_storage->return_type
&& ($guide_method_storage->signature_return_type !== $guide_method_storage->return_type
|| $implementer_method_storage->signature_return_type !== $implementer_method_storage->return_type)
2018-02-22 00:59:31 +01:00
&& $implementer_classlike_storage->user_defined
&& (!$guide_classlike_storage->stubbed || $guide_classlike_storage->template_types)
2018-02-22 00:59:31 +01:00
) {
2018-11-06 03:57:36 +01:00
$implementer_method_storage_return_type = ExpressionAnalyzer::fleshOutType(
$codebase,
2018-02-22 00:59:31 +01:00
$implementer_method_storage->return_type,
$implementer_classlike_storage->name,
$implementer_called_class_name,
$implementer_classlike_storage->parent_class
2018-02-22 00:59:31 +01:00
);
2018-11-06 03:57:36 +01:00
$guide_method_storage_return_type = ExpressionAnalyzer::fleshOutType(
$codebase,
2018-02-22 00:59:31 +01:00
$guide_method_storage->return_type,
$guide_classlike_storage->is_trait
? $implementer_classlike_storage->name
: $guide_classlike_storage->name,
$guide_classlike_storage->is_trait
? $implementer_called_class_name
: $guide_classlike_storage->name,
$guide_classlike_storage->parent_class
2018-02-22 00:59:31 +01:00
);
$guide_class_name = $guide_classlike_storage->name;
$implementer_class_name = $implementer_classlike_storage->name;
$implementer_called_class_storage = $implementer_classlike_storage;
if ($implementer_called_class_name !== $implementer_class_name) {
$implementer_called_class_storage = $codebase->classlike_storage_provider->get(
$implementer_called_class_name
);
}
if ($implementer_called_class_storage !== $implementer_classlike_storage
2019-12-20 20:37:36 +01:00
&& $implementer_called_class_storage->template_type_extends
) {
2019-12-20 20:37:36 +01:00
self::transformTemplates(
$implementer_called_class_storage->template_type_extends,
$implementer_class_name,
$implementer_method_storage_return_type,
$codebase
);
2019-12-20 20:37:36 +01:00
self::transformTemplates(
$implementer_called_class_storage->template_type_extends,
$guide_class_name,
$guide_method_storage_return_type,
$codebase
);
}
2019-12-20 20:37:36 +01:00
if ($implementer_classlike_storage->template_type_extends) {
self::transformTemplates(
$implementer_classlike_storage->template_type_extends,
$guide_class_name,
$implementer_method_storage_return_type,
$codebase
);
2019-12-20 20:37:36 +01:00
self::transformTemplates(
$implementer_classlike_storage->template_type_extends,
$guide_class_name,
$guide_method_storage_return_type,
$codebase
);
}
$guide_trait_name = null;
if ($guide_classlike_storage === $implementer_classlike_storage) {
$guide_trait_name = $implementer_method_storage->defining_fqcln;
}
if ($guide_trait_name
&& isset($implementer_classlike_storage->template_type_extends[$guide_trait_name])
) {
$map = $implementer_classlike_storage->template_type_extends[$guide_trait_name];
$template_types = [];
foreach ($map as $key => $type) {
2019-11-25 21:20:31 +01:00
if (is_string($key) && $implementer_method_storage->defining_fqcln) {
$template_types[$key][$implementer_method_storage->defining_fqcln] = [
$type,
];
}
}
$template_result = new \Psalm\Internal\Type\TemplateResult($template_types, []);
$implementer_method_storage_return_type->replaceTemplateTypesWithArgTypes(
$template_result->template_types,
$codebase
);
$guide_method_storage_return_type->replaceTemplateTypesWithArgTypes(
$template_result->template_types,
$codebase
);
}
2018-02-22 00:59:31 +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();
}
2019-07-10 07:35:57 +02:00
$union_comparison_results = new TypeComparisonResult();
2018-11-06 03:57:36 +01:00
if (!TypeAnalyzer::isContainedBy(
2018-02-22 00:59:31 +01:00
$codebase,
$implementer_method_storage_return_type,
$guide_method_storage_return_type,
false,
false,
2019-07-10 07:35:57 +02:00
$union_comparison_results
2018-02-22 00:59:31 +01:00
)) {
// is the declared return type more specific than the inferred one?
2019-07-10 07:35:57 +02:00
if ($union_comparison_results->type_coerced) {
2018-02-22 00:59:31 +01:00
if (IssueBuffer::accepts(
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
2019-12-20 20:37:36 +01:00
?: $code_location
2018-02-22 00:59:31 +01:00
),
$suppressed_issues
)) {
return false;
}
} else {
if (IssueBuffer::accepts(
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
2019-12-20 20:37:36 +01:00
?: $code_location
2018-02-22 00:59:31 +01:00
),
$suppressed_issues
)) {
return false;
}
}
}
}
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;
}
2018-02-22 00:59:31 +01:00
if (IssueBuffer::accepts(
new MethodSignatureMismatch(
'Method ' . $cased_implementer_method_id . ' has fewer parameters than parent method ' .
2018-02-22 00:59:31 +01:00
$cased_guide_method_id,
$code_location
)
)) {
return false;
}
return null;
}
$implementer_param = $implementer_method_storage->params[$i];
if ($prevent_method_signature_mismatch
&& !$guide_classlike_storage->user_defined
&& $guide_param->type
) {
$implementer_param_type = $implementer_method_storage->params[$i]->signature_type;
$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)
) {
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 (IssueBuffer::accepts(
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_method_storage->params[$i]->location
&& $config->isInProjectDirs(
$implementer_method_storage->params[$i]->location->file_path
)
? $implementer_method_storage->params[$i]->location
: $code_location
)
)) {
return false;
}
return null;
}
}
}
if ($prevent_method_signature_mismatch
&& $guide_classlike_storage->user_defined
2018-02-22 00:59:31 +01:00
&& $implementer_param->signature_type
) {
$guide_param_signature_type = $guide_param->signature_type
? ExpressionAnalyzer::fleshOutType(
$codebase,
$guide_param->signature_type,
$guide_classlike_storage->name,
$guide_classlike_storage->name,
$guide_classlike_storage->parent_class
2018-02-22 00:59:31 +01:00
)
: null;
$implementer_param_signature_type = ExpressionAnalyzer::fleshOutType(
$codebase,
$implementer_param->signature_type,
$implementer_classlike_storage->name,
$implementer_classlike_storage->name,
2019-05-25 18:11:46 +02:00
$implementer_classlike_storage->parent_class
);
$is_contained_by = $codebase->php_major_version >= 7
&& $codebase->php_minor_version >= 4
&& $guide_param_signature_type
? TypeAnalyzer::isContainedBy(
$codebase,
$guide_param_signature_type,
$implementer_param_signature_type
)
: TypeAnalyzer::isContainedByInPhp(
$guide_param_signature_type,
$implementer_param_signature_type
);
if (!$is_contained_by) {
if ($guide_classlike_storage->is_trait === $implementer_classlike_storage->is_trait
|| (!$implementer_method_storage->abstract
2019-12-20 20:37:36 +01:00
&& !$guide_method_storage->abstract)
) {
if (IssueBuffer::accepts(
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
)
)) {
return false;
}
} else {
if (IssueBuffer::accepts(
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
)
? $implementer_method_storage->params[$i]->location
: $code_location
),
$suppressed_issues
)) {
return false;
}
}
2018-02-22 00:59:31 +01:00
return null;
}
2018-02-22 00:59:31 +01:00
}
if ($implementer_param->type
2018-02-22 00:59:31 +01:00
&& $guide_param->type
&& $implementer_param->type->getId() !== $guide_param->type->getId()
) {
$implementer_method_storage_param_type = ExpressionAnalyzer::fleshOutType(
2018-02-22 00:59:31 +01:00
$codebase,
$implementer_param->type,
$implementer_classlike_storage->name,
$implementer_called_class_name,
2019-05-25 18:18:12 +02:00
$implementer_classlike_storage->parent_class
);
$guide_method_storage_param_type = ExpressionAnalyzer::fleshOutType(
$codebase,
$guide_param->type,
$guide_classlike_storage->name,
$guide_classlike_storage->name,
2019-05-25 18:18:12 +02:00
$guide_classlike_storage->parent_class
);
$guide_class_name = $guide_classlike_storage->name;
$implementer_class_name = $implementer_classlike_storage->name;
$implementer_called_class_storage = $implementer_classlike_storage;
if ($implementer_called_class_name !== $implementer_class_name) {
$implementer_called_class_storage = $codebase->classlike_storage_provider->get(
$implementer_called_class_name
);
}
if ($implementer_called_class_storage !== $implementer_classlike_storage
2019-12-20 20:37:36 +01:00
&& $implementer_called_class_storage->template_type_extends
) {
2019-12-20 20:37:36 +01:00
self::transformTemplates(
$implementer_called_class_storage->template_type_extends,
$implementer_class_name,
$implementer_method_storage_param_type,
$codebase
);
}
2019-12-20 20:37:36 +01:00
if ($implementer_called_class_storage->template_type_extends) {
self::transformTemplates(
$implementer_called_class_storage->template_type_extends,
$guide_class_name,
$guide_method_storage_param_type,
$codebase
);
}
2019-07-10 07:35:57 +02:00
$union_comparison_results = new TypeComparisonResult();
if (!TypeAnalyzer::isContainedBy(
$codebase,
$guide_method_storage_param_type,
$implementer_method_storage_param_type,
!$guide_classlike_storage->user_defined,
!$guide_classlike_storage->user_defined,
2019-07-10 07:35:57 +02:00
$union_comparison_results
2018-02-22 00:59:31 +01:00
)) {
// is the declared return type more specific than the inferred one?
2019-07-10 07:35:57 +02:00
if ($union_comparison_results->type_coerced) {
if ($guide_classlike_storage->user_defined) {
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
2019-12-20 20:37:36 +01:00
?: $code_location
),
$suppressed_issues
)) {
return false;
}
}
} else {
if (TypeAnalyzer::isContainedBy(
$codebase,
$implementer_method_storage_param_type,
$guide_method_storage_param_type,
!$guide_classlike_storage->user_defined,
!$guide_classlike_storage->user_defined
)) {
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
2019-12-20 20:37:36 +01:00
?: $code_location
),
$suppressed_issues
)) {
return false;
}
} 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
2019-12-20 20:37:36 +01:00
?: $code_location
),
$suppressed_issues
)) {
return false;
}
}
2018-02-22 00:59:31 +01:00
}
}
}
if ($guide_classlike_storage->user_defined && $implementer_param->by_ref !== $guide_param->by_ref) {
if (IssueBuffer::accepts(
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_method_storage->params[$i]->location
2019-12-20 20:37:36 +01:00
&& $config->isInProjectDirs(
$implementer_method_storage->params[$i]->location->file_path
)
? $implementer_method_storage->params[$i]->location
: $code_location
2018-02-22 00:59:31 +01:00
)
)) {
return false;
}
return null;
}
}
if ($guide_classlike_storage->user_defined
&& ($guide_classlike_storage->is_interface || $implementer_method_storage->cased_name !== '__construct')
2018-02-22 00:59:31 +01:00
&& $implementer_method_storage->required_param_count > $guide_method_storage->required_param_count
) {
if (IssueBuffer::accepts(
new MethodSignatureMismatch(
'Method ' . $cased_implementer_method_id . ' has more required parameters than parent method ' .
2018-02-22 00:59:31 +01:00
$cased_guide_method_id,
$code_location
)
)) {
return false;
}
return null;
}
}
/**
2019-12-20 20:37:36 +01:00
* @param array<string, array<int|string, Type\Union>> $template_type_extends
*/
private static function transformTemplates(
array $template_type_extends,
string $base_class_name,
Type\Union $templated_type,
Codebase $codebase
) : void {
if (isset($template_type_extends[$base_class_name])) {
$map = $template_type_extends[$base_class_name];
$template_types = [];
foreach ($map as $key => $mapped_type) {
if (is_string($key)) {
$new_bases = [];
foreach ($mapped_type->getTypes() as $mapped_atomic_type) {
if ($mapped_atomic_type instanceof Type\Atomic\TTemplateParam) {
2019-12-20 20:37:36 +01:00
$new_bases[] = $mapped_atomic_type->defining_class;
}
}
if ($new_bases) {
$mapped_type = clone $mapped_type;
foreach ($new_bases as $new_base_class_name) {
self::transformTemplates(
$template_type_extends,
$new_base_class_name,
$mapped_type,
$codebase
);
}
}
$template_types[$key][$base_class_name] = [$mapped_type];
}
}
$template_result = new \Psalm\Internal\Type\TemplateResult($template_types, []);
$templated_type->replaceTemplateTypesWithArgTypes(
$template_result->template_types,
$codebase
);
}
}
/**
* Check that __clone, __construct, and __destruct do not have a return type
* hint in their signature.
*
* @param MethodStorage $method_storage
* @param CodeLocation $code_location
* @return false|null
*/
public static function checkMethodSignatureMustOmitReturnType(
MethodStorage $method_storage,
CodeLocation $code_location
) {
if ($method_storage->signature_return_type === null) {
return null;
}
$cased_method_name = $method_storage->cased_name;
$methodsOfInterest = ['__clone', '__construct', '__destruct'];
if (in_array($cased_method_name, $methodsOfInterest)) {
if (IssueBuffer::accepts(
new MethodSignatureMustOmitReturnType(
'Method ' . $cased_method_name . ' must not declare a return type',
$code_location
)
)) {
return false;
}
}
return null;
}
2016-02-04 15:22:46 +01:00
}