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

287 lines
8.7 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;
2016-07-26 00:37:44 +02:00
use Psalm\Issue\InvalidStaticInvocation;
use Psalm\Issue\MethodSignatureMustOmitReturnType;
use Psalm\Issue\NonStaticSelfCall;
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\MethodStorage;
2020-09-22 07:10:46 +02:00
use function strtolower;
use function in_array;
2016-01-08 00:28:27 +01:00
/**
* @internal
* @extends FunctionLikeAnalyzer<PhpParser\Node\Stmt\ClassMethod>
*/
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();
$method_name_lc = strtolower((string) $function->name);
$source_fqcln = (string) $source->getFQCLN();
$source_fqcln_lc = strtolower($source_fqcln);
$method_id = new \Psalm\Internal\MethodIdentifier($source_fqcln, $method_name_lc);
if (!$storage) {
try {
$storage = $codebase->methods->getStorage($method_id);
} catch (\UnexpectedValueException $e) {
$class_storage = $codebase->classlike_storage_provider->get($source_fqcln_lc);
if (!$class_storage->parent_classes) {
throw $e;
}
$declaring_method_id = $codebase->methods->getDeclaringMethodId($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
* @param array<string> $suppressed_issues
*/
2017-01-12 03:37:53 +01:00
public static function checkStatic(
\Psalm\Internal\MethodIdentifier $method_id,
bool $self_call,
bool $is_context_dynamic,
2018-11-06 03:57:36 +01:00
Codebase $codebase,
CodeLocation $code_location,
array $suppressed_issues,
?bool &$is_dynamic_this_method = false
): bool {
2018-11-06 03:57:36 +01:00
$codebase_methods = $codebase->methods;
2018-02-04 00:52:35 +01:00
2020-08-30 04:02:58 +02:00
if ($method_id->fq_class_name === 'Closure'
&& $method_id->method_name === 'fromcallable'
) {
2019-01-22 00:01:15 +01:00
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[] $suppressed_issues
* @param lowercase-string|null $calling_method_id
2017-05-27 02:16:18 +02:00
*
2016-04-27 00:42:48 +02:00
*/
public static function checkMethodExists(
2018-11-06 03:57:36 +01:00
Codebase $codebase,
\Psalm\Internal\MethodIdentifier $method_id,
CodeLocation $code_location,
array $suppressed_issues,
?string $calling_method_id = null
): ?bool {
2018-11-06 03:57:36 +01:00
if ($codebase->methods->methodExists(
$method_id,
$calling_method_id,
!$calling_method_id
|| $calling_method_id !== strtolower((string) $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, (string) $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
}
2018-02-04 00:52:35 +01:00
public static function isMethodVisible(
\Psalm\Internal\MethodIdentifier $method_id,
Context $context,
2018-02-04 00:52:35 +01:00
StatementsSource $source
): bool {
2018-11-06 03:57:36 +01:00
$codebase = $source->getCodebase();
2017-01-12 03:37:53 +01:00
$fq_classlike_name = $method_id->fq_class_name;
$method_name = $method_id->method_name;
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) {
$appearing_method_class = $appearing_method_id->fq_class_name;
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
}
$declaring_method_class = $declaring_method_id->fq_class_name;
if ($source->getSource() instanceof TraitAnalyzer
&& strtolower($declaring_method_class) === strtolower((string) $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
2019-12-20 20:37:36 +01:00
/**
* Check that __clone, __construct, and __destruct do not have a return type
* hint in their signature.
*
* @return false|null
*/
public static function checkMethodSignatureMustOmitReturnType(
MethodStorage $method_storage,
CodeLocation $code_location
): ?bool {
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;
}
public function getMethodId(?string $context_self = null): \Psalm\Internal\MethodIdentifier
{
$function_name = (string)$this->function->name;
return new \Psalm\Internal\MethodIdentifier(
$context_self ?: (string) $this->source->getFQCLN(),
strtolower($function_name)
);
}
2016-02-04 15:22:46 +01:00
}