2016-01-08 00:28:27 +01:00
|
|
|
<?php
|
2016-08-13 20:20:46 +02:00
|
|
|
namespace Psalm\Checker;
|
2016-01-08 00:28:27 +01:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
use PhpParser;
|
2018-02-22 00:59:31 +01:00
|
|
|
use Psalm\Checker\Statements\ExpressionChecker;
|
2016-12-04 01:11:30 +01:00
|
|
|
use Psalm\CodeLocation;
|
2016-07-26 00:37:44 +02:00
|
|
|
use Psalm\Issue\DeprecatedMethod;
|
2018-02-22 00:59:31 +01:00
|
|
|
use Psalm\Issue\ImplementedReturnTypeMismatch;
|
2016-11-02 07:29:00 +01:00
|
|
|
use Psalm\Issue\InaccessibleMethod;
|
2016-07-26 00:37:44 +02:00
|
|
|
use Psalm\Issue\InvalidStaticInvocation;
|
2018-02-22 00:59:31 +01:00
|
|
|
use Psalm\Issue\MethodSignatureMismatch;
|
2018-04-21 19:36:18 +02:00
|
|
|
use Psalm\Issue\MethodSignatureMustOmitReturnType;
|
2018-02-22 00:59:31 +01:00
|
|
|
use Psalm\Issue\MoreSpecificImplementedParamType;
|
2018-04-17 20:06:27 +02:00
|
|
|
use Psalm\Issue\LessSpecificImplementedReturnType;
|
2016-12-31 15:20:10 +01:00
|
|
|
use Psalm\Issue\NonStaticSelfCall;
|
2018-02-22 00:59:31 +01:00
|
|
|
use Psalm\Issue\OverriddenMethodAccess;
|
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\ClassLikeStorage;
|
|
|
|
use Psalm\Storage\MethodStorage;
|
|
|
|
use Psalm\Type;
|
2016-01-08 00:28:27 +01:00
|
|
|
|
2016-08-15 21:14:28 +02:00
|
|
|
class MethodChecker extends FunctionLikeChecker
|
2016-01-08 00:28:27 +01:00
|
|
|
{
|
2016-10-30 17:46:18 +01:00
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\FunctionLike $function
|
|
|
|
* @param StatementsSource $source
|
2016-12-17 06:48:31 +01:00
|
|
|
* @psalm-suppress MixedAssignment
|
2016-10-30 17:46:18 +01:00
|
|
|
*/
|
2017-02-01 05:24:33 +01:00
|
|
|
public function __construct($function, StatementsSource $source)
|
2016-04-16 22:28:25 +02:00
|
|
|
{
|
2016-10-15 06:12:57 +02:00
|
|
|
if (!$function instanceof PhpParser\Node\Stmt\ClassMethod) {
|
|
|
|
throw new \InvalidArgumentException('Must be called with a ClassMethod');
|
|
|
|
}
|
|
|
|
|
2016-04-16 22:28:25 +02:00
|
|
|
parent::__construct($function, $source);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Determines whether a given method is static or not
|
2016-11-02 07:29:00 +01:00
|
|
|
*
|
2016-12-24 19:23:22 +01:00
|
|
|
* @param string $method_id
|
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 CodeLocation $code_location
|
|
|
|
* @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-11-02 07:29:00 +01:00
|
|
|
* @return bool
|
2016-04-16 22:28:25 +02:00
|
|
|
*/
|
2017-01-12 03:37:53 +01:00
|
|
|
public static function checkStatic(
|
2016-12-31 15:20:10 +01:00
|
|
|
$method_id,
|
|
|
|
$self_call,
|
2018-06-04 01:11:07 +02:00
|
|
|
$is_context_dynamic,
|
2017-07-29 21:05:06 +02:00
|
|
|
ProjectChecker $project_checker,
|
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
|
2016-12-31 15:20:10 +01:00
|
|
|
) {
|
2018-02-04 00:52:35 +01:00
|
|
|
$codebase_methods = $project_checker->codebase->methods;
|
|
|
|
|
|
|
|
$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) {
|
|
|
|
throw new \LogicException('Method id should not be null');
|
|
|
|
}
|
|
|
|
|
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
|
|
|
/**
|
2016-12-04 01:11:30 +01:00
|
|
|
* @param string $method_id
|
|
|
|
* @param CodeLocation $code_location
|
|
|
|
* @param array $suppressed_issues
|
2018-06-19 22:14:51 +02:00
|
|
|
* @param string|null $source_method_id
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-07-24 22:21:42 +02:00
|
|
|
* @return bool|null
|
2016-04-27 00:42:48 +02:00
|
|
|
*/
|
2017-01-30 05:44:05 +01:00
|
|
|
public static function checkMethodExists(
|
2017-07-29 21:05:06 +02:00
|
|
|
ProjectChecker $project_checker,
|
2017-01-30 05:44:05 +01:00
|
|
|
$method_id,
|
|
|
|
CodeLocation $code_location,
|
2018-06-19 22:14:51 +02:00
|
|
|
array $suppressed_issues,
|
|
|
|
$source_method_id = null
|
2017-01-30 05:44:05 +01:00
|
|
|
) {
|
2018-06-19 22:14:51 +02:00
|
|
|
if ($project_checker->codebase->methodExists(
|
|
|
|
$method_id,
|
|
|
|
$source_method_id !== $method_id ? $code_location : null
|
|
|
|
)) {
|
2016-10-23 18:24:53 +02:00
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (IssueBuffer::accepts(
|
2018-03-21 03:36:03 +01:00
|
|
|
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
|
|
|
/**
|
2016-12-04 01:11:30 +01:00
|
|
|
* @param string $method_id
|
|
|
|
* @param CodeLocation $code_location
|
|
|
|
* @param array $suppressed_issues
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-10-15 06:12:57 +02:00
|
|
|
* @return false|null
|
|
|
|
*/
|
2017-07-29 21:05:06 +02:00
|
|
|
public static function checkMethodNotDeprecated(
|
|
|
|
ProjectChecker $project_checker,
|
|
|
|
$method_id,
|
|
|
|
CodeLocation $code_location,
|
|
|
|
array $suppressed_issues
|
|
|
|
) {
|
2018-02-04 00:52:35 +01:00
|
|
|
$codebase_methods = $project_checker->codebase->methods;
|
|
|
|
|
|
|
|
$method_id = (string) $codebase_methods->getDeclaringMethodId($method_id);
|
|
|
|
$storage = $codebase_methods->getStorage($method_id);
|
2016-12-30 21:53:35 +01:00
|
|
|
|
|
|
|
if ($storage->deprecated) {
|
2016-07-22 19:29:46 +02:00
|
|
|
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) .
|
2017-07-29 21:05:06 +02:00
|
|
|
' has been marked as deprecated',
|
2018-03-21 03:36:03 +01:00
|
|
|
$code_location,
|
|
|
|
$method_id
|
2016-11-02 07:29:00 +01:00
|
|
|
),
|
|
|
|
$suppressed_issues
|
2016-07-22 19:29:46 +02:00
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
|
|
|
|
return null;
|
2016-07-22 19:29:46 +02:00
|
|
|
}
|
|
|
|
|
2017-11-11 00:08:17 +01:00
|
|
|
/**
|
|
|
|
* @param string $method_id
|
|
|
|
* @param string|null $calling_context
|
|
|
|
* @param StatementsSource $source
|
|
|
|
* @param CodeLocation $code_location
|
|
|
|
* @param array $suppressed_issues
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-06-14 01:57:32 +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,
|
|
|
|
$calling_context,
|
|
|
|
StatementsSource $source,
|
2016-12-04 01:11:30 +01:00
|
|
|
CodeLocation $code_location,
|
2016-11-02 07:29:00 +01:00
|
|
|
array $suppressed_issues
|
|
|
|
) {
|
2017-07-29 21:05:06 +02:00
|
|
|
$project_checker = $source->getFileChecker()->project_checker;
|
2018-02-01 06:50:01 +01:00
|
|
|
$codebase = $project_checker->codebase;
|
2018-02-04 00:52:35 +01:00
|
|
|
$codebase_methods = $codebase->methods;
|
|
|
|
$codebase_classlikes = $codebase->classlikes;
|
2017-07-29 21:05:06 +02:00
|
|
|
|
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) {
|
2017-11-09 05:14:27 +01:00
|
|
|
$method_name = explode('::', $method_id)[1];
|
2017-07-09 20:36:06 +02:00
|
|
|
|
2018-05-12 19:02:53 +02:00
|
|
|
if ($method_name === '__construct' || $method_id === 'Closure::__invoke') {
|
2017-07-09 20:36:06 +02:00
|
|
|
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);
|
2016-12-12 20:29:58 +01:00
|
|
|
|
2017-07-09 20:50:57 +02:00
|
|
|
$appearing_method_class = null;
|
|
|
|
|
|
|
|
if ($appearing_method_id) {
|
|
|
|
list($appearing_method_class) = explode('::', $appearing_method_id);
|
|
|
|
|
|
|
|
// if the calling class is the same, we know the method exists, so it must be visible
|
|
|
|
if ($appearing_method_class === $calling_context) {
|
|
|
|
return null;
|
|
|
|
}
|
2016-12-12 19:50:46 +01:00
|
|
|
}
|
|
|
|
|
2017-07-09 20:50:57 +02:00
|
|
|
list($declaring_method_class) = explode('::', $declaring_method_id);
|
|
|
|
|
2016-12-12 20:29:58 +01:00
|
|
|
if ($source->getSource() instanceof TraitChecker && $declaring_method_class === $source->getFQCLN()) {
|
2016-11-02 07:29:00 +01:00
|
|
|
return null;
|
2016-08-13 17:10:43 +02:00
|
|
|
}
|
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
$storage = $project_checker->codebase->methods->getStorage($declaring_method_id);
|
2016-12-30 21:53:35 +01:00
|
|
|
|
|
|
|
switch ($storage->visibility) {
|
2017-01-02 01:09:17 +01:00
|
|
|
case ClassLikeChecker::VISIBILITY_PUBLIC:
|
2016-11-02 07:29:00 +01:00
|
|
|
return null;
|
2016-04-18 19:31:59 +02:00
|
|
|
|
2017-01-02 01:09:17 +01:00
|
|
|
case ClassLikeChecker::VISIBILITY_PRIVATE:
|
|
|
|
if (!$calling_context || $appearing_method_class !== $calling_context) {
|
2016-06-26 21:18:40 +02:00
|
|
|
if (IssueBuffer::accepts(
|
2016-07-22 19:29:46 +02:00
|
|
|
new InaccessibleMethod(
|
2018-02-04 00:52:35 +01:00
|
|
|
'Cannot access private method ' . $codebase_methods->getCasedMethodId($method_id) .
|
2016-11-02 07:29:00 +01:00
|
|
|
' from context ' . $calling_context,
|
2016-12-04 01:11:30 +01:00
|
|
|
$code_location
|
2016-07-22 19:29:46 +02:00
|
|
|
),
|
2016-11-02 07:29:00 +01:00
|
|
|
$suppressed_issues
|
2016-06-06 02:25:16 +02:00
|
|
|
)) {
|
|
|
|
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
|
|
|
|
2017-01-02 01:09:17 +01:00
|
|
|
case ClassLikeChecker::VISIBILITY_PROTECTED:
|
2016-04-18 19:31:59 +02:00
|
|
|
if (!$calling_context) {
|
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,
|
2016-12-04 01:11:30 +01:00
|
|
|
$code_location
|
2016-11-02 07:29:00 +01:00
|
|
|
),
|
|
|
|
$suppressed_issues
|
2016-06-06 02:25:16 +02:00
|
|
|
)) {
|
|
|
|
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
|
|
|
}
|
|
|
|
|
2017-07-29 21:05:06 +02:00
|
|
|
if ($appearing_method_class
|
2018-02-04 00:52:35 +01:00
|
|
|
&& $codebase_classlikes->classExtends($appearing_method_class, $calling_context)
|
2017-07-29 21:05:06 +02:00
|
|
|
) {
|
2016-11-02 07:29:00 +01:00
|
|
|
return null;
|
2016-04-30 20:14:22 +02:00
|
|
|
}
|
|
|
|
|
2017-07-29 21:05:06 +02:00
|
|
|
if ($appearing_method_class
|
2018-02-04 00:52:35 +01:00
|
|
|
&& !$codebase_classlikes->classExtends($calling_context, $appearing_method_class)
|
2017-07-29 21:05:06 +02:00
|
|
|
) {
|
2016-06-26 21:18:40 +02:00
|
|
|
if (IssueBuffer::accepts(
|
2016-07-22 19:29:46 +02:00
|
|
|
new InaccessibleMethod(
|
2018-02-04 00:52:35 +01:00
|
|
|
'Cannot access protected method ' . $codebase_methods->getCasedMethodId($method_id) .
|
2016-11-02 07:29:00 +01:00
|
|
|
' from context ' . $calling_context,
|
2016-12-04 01:11:30 +01:00
|
|
|
$code_location
|
2016-07-22 19:29:46 +02:00
|
|
|
),
|
2016-11-02 07:29:00 +01:00
|
|
|
$suppressed_issues
|
2016-06-06 02:25:16 +02:00
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-04-18 19:31:59 +02:00
|
|
|
}
|
2016-04-16 22:28:25 +02:00
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
|
|
|
|
return null;
|
2016-04-16 22:28:25 +02:00
|
|
|
}
|
2016-04-17 18:27:47 +02:00
|
|
|
|
2016-10-15 06:12:57 +02:00
|
|
|
/**
|
2018-02-04 00:52:35 +01:00
|
|
|
* @param string $method_id
|
|
|
|
* @param string|null $calling_context
|
|
|
|
* @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(
|
2018-01-21 19:38:51 +01:00
|
|
|
$method_id,
|
2018-02-04 00:52:35 +01:00
|
|
|
$calling_context,
|
|
|
|
StatementsSource $source
|
2018-01-21 19:38:51 +01:00
|
|
|
) {
|
2018-02-04 00:52:35 +01:00
|
|
|
$project_checker = $source->getFileChecker()->project_checker;
|
|
|
|
$codebase = $project_checker->codebase;
|
2017-01-12 03:37:53 +01:00
|
|
|
|
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) {
|
|
|
|
$method_name = explode('::', $method_id)[1];
|
2017-04-08 01:16:25 +02:00
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
if ($method_name === '__construct') {
|
|
|
|
return true;
|
|
|
|
}
|
2017-04-08 01:16:25 +02:00
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
throw new \UnexpectedValueException('$declaring_method_id not expected to be null here');
|
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) {
|
|
|
|
list($appearing_method_class) = explode('::', $appearing_method_id);
|
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
|
|
|
|
if ($appearing_method_class === $calling_context) {
|
|
|
|
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);
|
2016-12-30 21:53:35 +01:00
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
if ($source->getSource() instanceof TraitChecker && $declaring_method_class === $source->getFQCLN()) {
|
|
|
|
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) {
|
|
|
|
case ClassLikeChecker::VISIBILITY_PUBLIC:
|
|
|
|
return true;
|
2016-12-30 21:53:35 +01:00
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
case ClassLikeChecker::VISIBILITY_PRIVATE:
|
|
|
|
if (!$calling_context || $appearing_method_class !== $calling_context) {
|
|
|
|
return false;
|
|
|
|
}
|
2016-12-31 01:36:35 +01:00
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
return true;
|
2016-12-31 01:36:35 +01:00
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
case ClassLikeChecker::VISIBILITY_PROTECTED:
|
|
|
|
if (!$calling_context) {
|
|
|
|
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, $calling_context)
|
|
|
|
) {
|
|
|
|
return true;
|
|
|
|
}
|
2016-12-27 02:06:05 +01:00
|
|
|
|
2018-02-04 00:52:35 +01:00
|
|
|
if ($appearing_method_class
|
|
|
|
&& !$codebase->classExtends($calling_context, $appearing_method_class)
|
|
|
|
) {
|
|
|
|
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
|
|
|
|
|
|
|
/**
|
|
|
|
* @param ProjectChecker $project_checker
|
|
|
|
* @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 array $suppressed_issues
|
2018-03-13 23:11:57 +01:00
|
|
|
* @param bool $prevent_abstract_override
|
2018-02-22 00:59:31 +01:00
|
|
|
*
|
|
|
|
* @return false|null
|
|
|
|
*/
|
|
|
|
public static function compareMethods(
|
|
|
|
ProjectChecker $project_checker,
|
|
|
|
ClassLikeStorage $implementer_classlike_storage,
|
|
|
|
ClassLikeStorage $guide_classlike_storage,
|
|
|
|
MethodStorage $implementer_method_storage,
|
|
|
|
MethodStorage $guide_method_storage,
|
|
|
|
CodeLocation $code_location,
|
2018-03-13 23:11:57 +01:00
|
|
|
array $suppressed_issues,
|
|
|
|
$prevent_abstract_override = true
|
2018-02-22 00:59:31 +01:00
|
|
|
) {
|
|
|
|
$codebase = $project_checker->codebase;
|
|
|
|
|
|
|
|
$implementer_method_id = $implementer_classlike_storage->name . '::'
|
|
|
|
. strtolower($guide_method_storage->cased_name);
|
|
|
|
|
|
|
|
$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_method_storage->visibility > $guide_method_storage->visibility) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-03-13 23:11:57 +01:00
|
|
|
if ($prevent_abstract_override
|
|
|
|
&& !$guide_method_storage->abstract
|
2018-03-13 17:52:00 +01:00
|
|
|
&& $implementer_method_storage->abstract
|
2018-03-13 18:50:53 +01:00
|
|
|
&& !$guide_classlike_storage->abstract
|
|
|
|
&& !$guide_classlike_storage->is_interface
|
2018-03-13 17:52:00 +01:00
|
|
|
) {
|
|
|
|
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;
|
|
|
|
}
|
|
|
|
|
2018-02-22 00:59:31 +01:00
|
|
|
if ($guide_method_storage->signature_return_type) {
|
|
|
|
$guide_signature_return_type = ExpressionChecker::fleshOutType(
|
|
|
|
$project_checker,
|
|
|
|
$guide_method_storage->signature_return_type,
|
|
|
|
$guide_classlike_storage->name,
|
|
|
|
$guide_classlike_storage->name
|
|
|
|
);
|
|
|
|
|
|
|
|
$implementer_signature_return_type = $implementer_method_storage->signature_return_type
|
|
|
|
? ExpressionChecker::fleshOutType(
|
|
|
|
$project_checker,
|
|
|
|
$implementer_method_storage->signature_return_type,
|
|
|
|
$implementer_classlike_storage->name,
|
|
|
|
$implementer_classlike_storage->name
|
|
|
|
) : null;
|
|
|
|
|
|
|
|
if (!TypeChecker::isContainedByInPhp($implementer_signature_return_type, $guide_signature_return_type)) {
|
|
|
|
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
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
} elseif ($guide_method_storage->return_type
|
|
|
|
&& $implementer_method_storage->return_type
|
|
|
|
&& $implementer_classlike_storage->user_defined
|
|
|
|
&& !$guide_classlike_storage->stubbed
|
|
|
|
) {
|
|
|
|
$implementer_method_storage_return_type = ExpressionChecker::fleshOutType(
|
|
|
|
$project_checker,
|
|
|
|
$implementer_method_storage->return_type,
|
|
|
|
$implementer_classlike_storage->name,
|
|
|
|
$implementer_classlike_storage->name
|
|
|
|
);
|
|
|
|
|
|
|
|
$guide_method_storage_return_type = ExpressionChecker::fleshOutType(
|
|
|
|
$project_checker,
|
|
|
|
$guide_method_storage->return_type,
|
|
|
|
$guide_classlike_storage->name,
|
|
|
|
$guide_classlike_storage->name
|
|
|
|
);
|
|
|
|
|
|
|
|
// 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();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!TypeChecker::isContainedBy(
|
|
|
|
$codebase,
|
|
|
|
$implementer_method_storage_return_type,
|
|
|
|
$guide_method_storage_return_type,
|
|
|
|
false,
|
|
|
|
false,
|
|
|
|
$has_scalar_match,
|
|
|
|
$type_coerced,
|
|
|
|
$type_coerced_from_mixed
|
|
|
|
)) {
|
|
|
|
// is the declared return type more specific than the inferred one?
|
|
|
|
if ($type_coerced) {
|
|
|
|
if (IssueBuffer::accepts(
|
2018-04-17 20:06:27 +02:00
|
|
|
new LessSpecificImplementedReturnType(
|
2018-02-22 00:59:31 +01:00
|
|
|
'The return type \'' . $guide_method_storage->return_type
|
|
|
|
. '\' for ' . $cased_guide_method_id . ' is more specific than the implemented '
|
|
|
|
. 'return type for ' . $implementer_declaring_method_id . ' \''
|
|
|
|
. $implementer_method_storage->return_type . '\'',
|
2018-04-17 18:16:25 +02:00
|
|
|
$implementer_method_storage->return_type_location ?: $code_location
|
2018-02-22 00:59:31 +01:00
|
|
|
),
|
|
|
|
$suppressed_issues
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new ImplementedReturnTypeMismatch(
|
|
|
|
'The return type \'' . $guide_method_storage->return_type
|
|
|
|
. '\' for ' . $cased_guide_method_id . ' is different to the implemented '
|
|
|
|
. 'return type for ' . $implementer_declaring_method_id . ' \''
|
|
|
|
. $implementer_method_storage->return_type . '\'',
|
2018-04-17 18:16:25 +02:00
|
|
|
$implementer_method_storage->return_type_location ?: $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])) {
|
2018-03-14 15:51:13 +01:00
|
|
|
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 arguments than parent method ' .
|
|
|
|
$cased_guide_method_id,
|
|
|
|
$code_location
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$implementer_param = $implementer_method_storage->params[$i];
|
|
|
|
|
|
|
|
if ($guide_classlike_storage->user_defined
|
|
|
|
&& $implementer_param->signature_type
|
|
|
|
&& !TypeChecker::isContainedByInPhp($guide_param->signature_type, $implementer_param->signature_type)
|
|
|
|
) {
|
|
|
|
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
|
|
|
|
?: $code_location
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($guide_classlike_storage->user_defined
|
|
|
|
&& $implementer_param->type
|
|
|
|
&& $guide_param->type
|
|
|
|
&& $implementer_param->type->getId() !== $guide_param->type->getId()
|
|
|
|
) {
|
|
|
|
if (!TypeChecker::isContainedBy(
|
|
|
|
$codebase,
|
|
|
|
$guide_param->type,
|
|
|
|
$implementer_param->type,
|
|
|
|
false,
|
|
|
|
false
|
|
|
|
)) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new MoreSpecificImplementedParamType(
|
|
|
|
'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' has wrong type \'' .
|
|
|
|
$implementer_param->type . '\', expecting \'' .
|
|
|
|
$guide_param->type . '\' as defined by ' .
|
|
|
|
$cased_guide_method_id,
|
|
|
|
$implementer_method_storage->params[$i]->location
|
|
|
|
?: $code_location
|
|
|
|
),
|
|
|
|
$suppressed_issues
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
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
|
|
|
|
?: $code_location
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
$implemeneter_param_type = $implementer_method_storage->params[$i]->type;
|
|
|
|
|
|
|
|
$or_null_guide_type = $guide_param->signature_type
|
|
|
|
? clone $guide_param->signature_type
|
|
|
|
: null;
|
|
|
|
|
|
|
|
if ($or_null_guide_type) {
|
|
|
|
$or_null_guide_type->addType(new Type\Atomic\TNull);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$guide_classlike_storage->user_defined
|
|
|
|
&& $guide_param->type
|
|
|
|
&& !$guide_param->type->isMixed()
|
|
|
|
&& !$guide_param->type->from_docblock
|
|
|
|
&& (
|
|
|
|
!$implemeneter_param_type
|
|
|
|
|| (
|
|
|
|
$implemeneter_param_type->getId() !== $guide_param->type->getId()
|
|
|
|
&& (
|
|
|
|
!$or_null_guide_type
|
|
|
|
|| $implemeneter_param_type->getId() !== $or_null_guide_type->getId()
|
|
|
|
)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new MethodSignatureMismatch(
|
|
|
|
'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' has wrong type \'' .
|
|
|
|
$implementer_method_storage->params[$i]->type . '\', expecting \'' .
|
|
|
|
$guide_param->type . '\' as defined by ' .
|
|
|
|
$cased_guide_method_id,
|
|
|
|
$implementer_method_storage->params[$i]->location
|
|
|
|
?: $code_location
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
if ($guide_classlike_storage->user_defined
|
|
|
|
&& $implementer_method_storage->cased_name !== '__construct'
|
|
|
|
&& $implementer_method_storage->required_param_count > $guide_method_storage->required_param_count
|
|
|
|
) {
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
new MethodSignatureMismatch(
|
|
|
|
'Method ' . $cased_implementer_method_id . ' has more arguments than parent method ' .
|
|
|
|
$cased_guide_method_id,
|
|
|
|
$code_location
|
|
|
|
)
|
|
|
|
)) {
|
|
|
|
return false;
|
|
|
|
}
|
|
|
|
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2018-04-21 19:36:18 +02: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
|
|
|
* @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;
|
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
|
|
|
}
|
2016-02-04 15:22:46 +01:00
|
|
|
}
|