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;
|
2016-12-04 01:11:30 +01:00
|
|
|
use Psalm\CodeLocation;
|
2018-12-02 00:37:49 +01:00
|
|
|
use Psalm\Context;
|
2016-07-26 00:37:44 +02:00
|
|
|
use Psalm\Issue\InvalidStaticInvocation;
|
2018-04-21 19:36:18 +02:00
|
|
|
use Psalm\Issue\MethodSignatureMustOmitReturnType;
|
2016-12-31 15:20:10 +01:00
|
|
|
use Psalm\Issue\NonStaticSelfCall;
|
2016-11-02 07:29:00 +01:00
|
|
|
use Psalm\Issue\UndefinedMethod;
|
|
|
|
use Psalm\IssueBuffer;
|
2016-08-13 20:20:46 +02:00
|
|
|
use Psalm\StatementsSource;
|
2018-02-22 00:59:31 +01:00
|
|
|
use Psalm\Storage\MethodStorage;
|
|
|
|
use Psalm\Type;
|
2019-06-26 22:52:29 +02:00
|
|
|
use function strtolower;
|
|
|
|
use function in_array;
|
2016-01-08 00:28:27 +01:00
|
|
|
|
2018-12-02 00:37:49 +01:00
|
|
|
/**
|
|
|
|
* @internal
|
|
|
|
*/
|
2018-11-06 03:57:36 +01:00
|
|
|
class MethodAnalyzer extends FunctionLikeAnalyzer
|
2016-01-08 00:28:27 +01:00
|
|
|
{
|
2020-02-15 02:54:26 +01:00
|
|
|
/**
|
|
|
|
* @var PhpParser\Node\Stmt\ClassMethod
|
|
|
|
*/
|
|
|
|
protected $function;
|
|
|
|
|
2018-12-18 05:29:27 +01:00
|
|
|
public function __construct(
|
|
|
|
PhpParser\Node\Stmt\ClassMethod $function,
|
|
|
|
SourceAnalyzer $source,
|
|
|
|
MethodStorage $storage = null
|
|
|
|
) {
|
|
|
|
$codebase = $source->getCodebase();
|
|
|
|
|
2020-02-15 02:54:26 +01:00
|
|
|
$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);
|
2018-12-18 05:29:27 +01:00
|
|
|
|
|
|
|
if (!$storage) {
|
|
|
|
try {
|
2020-02-15 02:54:26 +01:00
|
|
|
$storage = $codebase->methods->getStorage($method_id);
|
2018-12-18 05:29:27 +01:00
|
|
|
} catch (\UnexpectedValueException $e) {
|
2020-02-15 02:54:26 +01:00
|
|
|
$class_storage = $codebase->classlike_storage_provider->get($source_fqcln_lc);
|
2018-12-18 05:29:27 +01:00
|
|
|
|
|
|
|
if (!$class_storage->parent_classes) {
|
|
|
|
throw $e;
|
|
|
|
}
|
|
|
|
|
2020-02-15 02:54:26 +01:00
|
|
|
$declaring_method_id = $codebase->methods->getDeclaringMethodId($method_id);
|
2018-12-18 05:29:27 +01:00
|
|
|
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2018-12-18 05:29:27 +01:00
|
|
|
parent::__construct($function, $source, $storage);
|
2016-04-16 22:28:25 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether a given method is static or not
|
2016-11-02 07:29:00 +01:00
|
|
|
*
|
2016-12-31 15:20:10 +01:00
|
|
|
* @param bool $self_call
|
2018-06-04 01:11:07 +02:00
|
|
|
* @param bool $is_context_dynamic
|
2016-12-24 19:23:22 +01:00
|
|
|
* @param array<string> $suppressed_issues
|
2018-06-04 01:11:07 +02:00
|
|
|
* @param bool $is_dynamic_this_method
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-04-16 22:28:25 +02:00
|
|
|
*/
|
2017-01-12 03:37:53 +01:00
|
|
|
public static function checkStatic(
|
2020-02-15 02:54:26 +01:00
|
|
|
\Psalm\Internal\MethodIdentifier $method_id,
|
2016-12-31 15:20:10 +01:00
|
|
|
$self_call,
|
2018-06-04 01:11:07 +02:00
|
|
|
$is_context_dynamic,
|
2018-11-06 03:57:36 +01:00
|
|
|
Codebase $codebase,
|
2016-12-31 15:20:10 +01:00
|
|
|
CodeLocation $code_location,
|
2018-06-04 01:11:07 +02:00
|
|
|
array $suppressed_issues,
|
|
|
|
&$is_dynamic_this_method = false
|
2020-09-04 22:26:33 +02:00
|
|
|
): 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'
|
2020-02-15 02:54:26 +01:00
|
|
|
&& $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);
|
2016-04-16 22:28:25 +02:00
|
|
|
|
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);
|
2016-12-30 21:53:35 +01:00
|
|
|
|
|
|
|
if (!$storage->is_static) {
|
2016-12-31 15:20:10 +01:00
|
|
|
if ($self_call) {
|
2018-06-04 01:11:07 +02:00
|
|
|
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;
|
2016-12-31 15:20:10 +01:00
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new InvalidStaticInvocation(
|
2018-02-04 00:52:35 +01:00
|
|
|
'Method ' . $codebase_methods->getCasedMethodId($method_id) .
|
2017-07-29 21:05:06 +02:00
|
|
|
' is not static, but is called ' .
|
2017-04-28 06:31:55 +02:00
|
|
|
'statically',
|
2016-12-31 15:20:10 +01:00
|
|
|
$code_location
|
|
|
|
),
|
|
|
|
$suppressed_issues
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-07-22 19:29:46 +02:00
|
|
|
}
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
|
|
|
|
return true;
|
2016-04-16 22:28:25 +02:00
|
|
|
}
|
|
|
|
|
2016-04-27 00:42:48 +02:00
|
|
|
/**
|
2019-06-16 15:12:32 +02:00
|
|
|
* @param string[] $suppressed_issues
|
2020-03-26 17:35:27 +01:00
|
|
|
* @param lowercase-string|null $calling_method_id
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-04-27 00:42:48 +02:00
|
|
|
*/
|
2017-01-30 05:44:05 +01:00
|
|
|
public static function checkMethodExists(
|
2018-11-06 03:57:36 +01:00
|
|
|
Codebase $codebase,
|
2020-02-15 02:54:26 +01:00
|
|
|
\Psalm\Internal\MethodIdentifier $method_id,
|
2017-01-30 05:44:05 +01:00
|
|
|
CodeLocation $code_location,
|
2018-06-19 22:14:51 +02:00
|
|
|
array $suppressed_issues,
|
2020-03-26 17:35:27 +01:00
|
|
|
?string $calling_method_id = null
|
2020-09-04 22:26:33 +02:00
|
|
|
): ?bool {
|
2018-11-06 03:57:36 +01:00
|
|
|
if ($codebase->methods->methodExists(
|
2018-06-19 22:14:51 +02:00
|
|
|
$method_id,
|
2020-03-26 17:35:27 +01:00
|
|
|
$calling_method_id,
|
|
|
|
!$calling_method_id
|
|
|
|
|| $calling_method_id !== strtolower((string) $method_id)
|
2020-02-18 18:53:54 +01:00
|
|
|
? $code_location
|
|
|
|
: null,
|
2019-04-16 22:07:48 +02:00
|
|
|
null,
|
|
|
|
$code_location->file_path
|
2018-06-19 22:14:51 +02:00
|
|
|
)) {
|
2016-10-23 18:24:53 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (IssueBuffer::accepts(
|
2020-02-15 02:54:26 +01:00
|
|
|
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
|
|
|
}
|
2020-09-04 22:26:33 +02:00
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
public static function isMethodVisible(
|
2020-02-15 02:54:26 +01:00
|
|
|
\Psalm\Internal\MethodIdentifier $method_id,
|
2019-03-01 14:57:10 +01:00
|
|
|
Context $context,
|
2018-02-04 00:52:35 +01:00
|
|
|
StatementsSource $source
|
2020-09-04 22:26:33 +02:00
|
|
|
): bool {
|
2018-11-06 03:57:36 +01:00
|
|
|
$codebase = $source->getCodebase();
|
2017-01-12 03:37:53 +01:00
|
|
|
|
2020-02-15 02:54:26 +01:00
|
|
|
$fq_classlike_name = $method_id->fq_class_name;
|
|
|
|
$method_name = $method_id->method_name;
|
2019-03-01 14:57:10 +01:00
|
|
|
|
|
|
|
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) {
|
2018-11-11 02:56:31 +01:00
|
|
|
// this can happen for methods in the callmap that were not reflected
|
|
|
|
return true;
|
2016-12-30 21:53:35 +01:00
|
|
|
}
|
2016-05-16 05:06:03 +02:00
|
|
|
|
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) {
|
2020-02-15 02:54:26 +01:00
|
|
|
$appearing_method_class = $appearing_method_id->fq_class_name;
|
2017-01-06 07:07:11 +01:00
|
|
|
|
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
|
2019-03-01 14:57:10 +01:00
|
|
|
if ($appearing_method_class === $context->self) {
|
2018-02-04 00:52:35 +01:00
|
|
|
return true;
|
|
|
|
}
|
2016-12-31 17:49:04 +01:00
|
|
|
}
|
|
|
|
|
2020-02-15 02:54:26 +01:00
|
|
|
$declaring_method_class = $declaring_method_id->fq_class_name;
|
2016-12-30 21:53:35 +01:00
|
|
|
|
2020-02-15 02:54:26 +01:00
|
|
|
if ($source->getSource() instanceof TraitAnalyzer
|
|
|
|
&& strtolower($declaring_method_class) === strtolower((string) $source->getFQCLN())
|
|
|
|
) {
|
2018-02-04 00:52:35 +01:00
|
|
|
return true;
|
|
|
|
}
|
2017-07-29 21:05:06 +02:00
|
|
|
|
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;
|
2016-12-30 21:53:35 +01:00
|
|
|
|
2018-11-06 03:57:36 +01:00
|
|
|
case ClassLikeAnalyzer::VISIBILITY_PRIVATE:
|
2019-03-01 14:57:10 +01:00
|
|
|
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:
|
2019-03-01 14:57:10 +01:00
|
|
|
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
|
2019-03-01 14:57:10 +01:00
|
|
|
&& $codebase->classExtends($appearing_method_class, $context->self)
|
2018-02-04 00:52:35 +01:00
|
|
|
) {
|
|
|
|
return true;
|
|
|
|
}
|
2016-12-27 02:06:05 +01:00
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
if ($appearing_method_class
|
2019-03-01 14:57:10 +01:00
|
|
|
&& !$codebase->classExtends($context->self, $appearing_method_class)
|
2018-02-04 00:52:35 +01:00
|
|
|
) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-12-27 02:06:05 +01:00
|
|
|
}
|
|
|
|
|
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
|
|
|
/**
|
2018-04-21 22:01:10 +02:00
|
|
|
* Check that __clone, __construct, and __destruct do not have a return type
|
|
|
|
* hint in their signature.
|
|
|
|
*
|
2018-04-21 19:36:18 +02:00
|
|
|
* @return false|null
|
|
|
|
*/
|
|
|
|
public static function checkMethodSignatureMustOmitReturnType(
|
|
|
|
MethodStorage $method_storage,
|
|
|
|
CodeLocation $code_location
|
2020-09-04 22:26:33 +02:00
|
|
|
): ?bool {
|
2018-04-21 19:36:18 +02:00
|
|
|
if ($method_storage->signature_return_type === null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$cased_method_name = $method_storage->cased_name;
|
2018-04-21 22:01:10 +02:00
|
|
|
$methodsOfInterest = ['__clone', '__construct', '__destruct'];
|
|
|
|
if (in_array($cased_method_name, $methodsOfInterest)) {
|
2018-04-21 19:36:18 +02:00
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new MethodSignatureMustOmitReturnType(
|
|
|
|
'Method ' . $cased_method_name . ' must not declare a return type',
|
|
|
|
$code_location
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2018-04-21 22:01:10 +02:00
|
|
|
|
|
|
|
return null;
|
2018-04-21 19:36:18 +02:00
|
|
|
}
|
2020-02-15 02:54:26 +01:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @param string|null $context_self
|
|
|
|
*
|
|
|
|
*/
|
2020-09-04 22:26:33 +02:00
|
|
|
public function getMethodId($context_self = null): \Psalm\Internal\MethodIdentifier
|
2020-02-15 02:54:26 +01:00
|
|
|
{
|
|
|
|
$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
|
|
|
}
|