mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
parent
3b2a1d4a3e
commit
afcbc113c9
@ -11,6 +11,7 @@ use Psalm\Issue\DuplicateClass;
|
||||
use Psalm\Issue\InaccessibleMethod;
|
||||
use Psalm\Issue\InaccessibleProperty;
|
||||
use Psalm\Issue\InvalidClass;
|
||||
use Psalm\Issue\InvalidReturnType;
|
||||
use Psalm\Issue\MissingConstructor;
|
||||
use Psalm\Issue\MissingPropertyType;
|
||||
use Psalm\Issue\PropertyNotSetInConstructor;
|
||||
@ -264,58 +265,33 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
|
||||
$storage->public_class_constants += $interface_storage->public_class_constants;
|
||||
|
||||
foreach ($interface_storage->methods as $method_name => $method) {
|
||||
if ($method->visibility === self::VISIBILITY_PUBLIC) {
|
||||
$implemented_method_id = $this->fq_class_name . '::' . $method_name;
|
||||
$mentioned_method_id = $interface_name . '::' . $method_name;
|
||||
$declaring_method_id = MethodChecker::getDeclaringMethodId(
|
||||
$code_location = new CodeLocation(
|
||||
$this,
|
||||
$this->class,
|
||||
$class_context ? $class_context->include_location : null,
|
||||
true
|
||||
);
|
||||
|
||||
foreach ($interface_storage->methods as $method_name => $interface_method_storage) {
|
||||
if ($interface_method_storage->visibility === self::VISIBILITY_PUBLIC) {
|
||||
$implementer_declaring_method_id = MethodChecker::getDeclaringMethodId(
|
||||
$project_checker,
|
||||
$implemented_method_id
|
||||
$this->fq_class_name . '::' . $method_name
|
||||
);
|
||||
|
||||
$method_storage = $declaring_method_id
|
||||
? MethodChecker::getStorage($project_checker, $declaring_method_id)
|
||||
$implementer_method_storage = $implementer_declaring_method_id
|
||||
? MethodChecker::getStorage($project_checker, $implementer_declaring_method_id)
|
||||
: null;
|
||||
|
||||
if (!$method_storage) {
|
||||
$cased_method_id = MethodChecker::getCasedMethodId(
|
||||
$project_checker,
|
||||
$mentioned_method_id
|
||||
);
|
||||
$cased_interface_method_id = $interface_storage->name . '::' .
|
||||
$interface_method_storage->cased_name;
|
||||
|
||||
if (!$implementer_method_storage) {
|
||||
if (IssueBuffer::accepts(
|
||||
new UnimplementedInterfaceMethod(
|
||||
'Method ' . $cased_method_id . ' is not defined on class ' .
|
||||
$this->fq_class_name,
|
||||
new CodeLocation(
|
||||
$this,
|
||||
$this->class,
|
||||
$class_context ? $class_context->include_location : null,
|
||||
true
|
||||
)
|
||||
),
|
||||
$this->source->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
} elseif ($method_storage->visibility !== self::VISIBILITY_PUBLIC) {
|
||||
$cased_method_id = MethodChecker::getCasedMethodId(
|
||||
$project_checker,
|
||||
$mentioned_method_id
|
||||
);
|
||||
|
||||
if (IssueBuffer::accepts(
|
||||
new InaccessibleMethod(
|
||||
'Interface-defined method ' . $cased_method_id .
|
||||
' must be public in ' . $this->fq_class_name,
|
||||
new CodeLocation(
|
||||
$this,
|
||||
$this->class,
|
||||
$class_context ? $class_context->include_location : null,
|
||||
true
|
||||
)
|
||||
'Method ' . $method_name . ' is not defined on class ' .
|
||||
$storage->name,
|
||||
$code_location
|
||||
),
|
||||
$this->source->getSuppressedIssues()
|
||||
)) {
|
||||
@ -324,6 +300,31 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($implementer_method_storage->visibility !== self::VISIBILITY_PUBLIC) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InaccessibleMethod(
|
||||
'Interface-defined method ' . $implementer_method_storage->cased_name
|
||||
. ' must be public in ' . $storage->name,
|
||||
$code_location
|
||||
),
|
||||
$this->source->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
FunctionLikeChecker::compareMethods(
|
||||
$project_checker,
|
||||
$storage,
|
||||
$interface_storage,
|
||||
$implementer_method_storage,
|
||||
$interface_method_storage,
|
||||
$code_location,
|
||||
$this->source->getSuppressedIssues()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1070,7 +1071,7 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return ?string
|
||||
*/
|
||||
public function getNamespace()
|
||||
{
|
||||
@ -1738,6 +1739,9 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
|
||||
return isset(self::getPropertyMap()[strtolower($class_name)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FileChecker
|
||||
*/
|
||||
public function getFileChecker()
|
||||
{
|
||||
return $this->file_checker;
|
||||
|
@ -406,7 +406,7 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
}
|
||||
|
||||
/**
|
||||
* @return null
|
||||
* @return ?string
|
||||
*/
|
||||
public function getNamespace()
|
||||
{
|
||||
@ -486,6 +486,9 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
return $this->actual_file_path ?: $this->file_path;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, string>
|
||||
*/
|
||||
public function getSuppressedIssues()
|
||||
{
|
||||
return $this->suppressed_issues;
|
||||
@ -511,21 +514,33 @@ class FileChecker extends SourceChecker implements StatementsSource
|
||||
$this->suppressed_issues = array_diff($this->suppressed_issues, $new_issues);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string
|
||||
*/
|
||||
public function getFQCLN()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return ?string
|
||||
*/
|
||||
public function getClassName()
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return bool
|
||||
*/
|
||||
public function isStatic()
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FileChecker
|
||||
*/
|
||||
public function getFileChecker()
|
||||
{
|
||||
return $this;
|
||||
|
@ -29,6 +29,7 @@ use Psalm\Issue\UntypedParam;
|
||||
use Psalm\Issue\UnusedVariable;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\StatementsSource;
|
||||
use Psalm\Storage\ClassLikeStorage;
|
||||
use Psalm\Storage\FunctionLikeStorage;
|
||||
use Psalm\Storage\MethodStorage;
|
||||
use Psalm\Type;
|
||||
@ -167,7 +168,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
|
||||
$cased_method_id = $fq_class_name . '::' . $storage->cased_name;
|
||||
|
||||
$implemented_method_ids = MethodChecker::getOverriddenMethodIds($project_checker, $method_id);
|
||||
$overridden_method_ids = MethodChecker::getOverriddenMethodIds($project_checker, $method_id);
|
||||
|
||||
if ($this->function->name === '__construct') {
|
||||
$context->inside_constructor = true;
|
||||
@ -175,166 +176,35 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
|
||||
$implemented_docblock_param_types = [];
|
||||
|
||||
if ($implemented_method_ids) {
|
||||
if ($overridden_method_ids && $this->function->name !== '__construct') {
|
||||
$have_emitted = false;
|
||||
|
||||
foreach ($implemented_method_ids as $implemented_method_id) {
|
||||
if ($this->function->name === '__construct') {
|
||||
continue;
|
||||
}
|
||||
foreach ($overridden_method_ids as $overridden_method_id) {
|
||||
$parent_method_storage = MethodChecker::getStorage($project_checker, $overridden_method_id);
|
||||
|
||||
list($implemented_fq_class_name) = explode('::', $implemented_method_id);
|
||||
list($overridden_fq_class_name) = explode('::', $overridden_method_id);
|
||||
|
||||
$class_storage = $classlike_storage_provider->get($implemented_fq_class_name);
|
||||
$parent_storage = $classlike_storage_provider->get($overridden_fq_class_name);
|
||||
|
||||
$implemented_storage = MethodChecker::getStorage($project_checker, $implemented_method_id);
|
||||
self::compareMethods(
|
||||
$project_checker,
|
||||
$class_storage,
|
||||
$parent_storage,
|
||||
$storage,
|
||||
$parent_method_storage,
|
||||
new CodeLocation(
|
||||
$this,
|
||||
$this->function,
|
||||
null,
|
||||
true
|
||||
),
|
||||
$storage->suppressed_issues
|
||||
);
|
||||
|
||||
if ($implemented_storage->visibility < $storage->visibility) {
|
||||
$parent_method_id = MethodChecker::getCasedMethodId(
|
||||
$project_checker,
|
||||
$implemented_method_id
|
||||
);
|
||||
|
||||
if (IssueBuffer::accepts(
|
||||
new OverriddenMethodAccess(
|
||||
'Method ' . $cased_method_id . ' has different access level than ' . $parent_method_id,
|
||||
new CodeLocation($this, $this->function, $context->include_location, true)
|
||||
)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
$implemented_params = $implemented_storage->params;
|
||||
|
||||
foreach ($implemented_params as $i => $implemented_param) {
|
||||
if (!isset($storage->params[$i])) {
|
||||
$parent_method_id = MethodChecker::getCasedMethodId(
|
||||
$project_checker,
|
||||
$implemented_method_id
|
||||
);
|
||||
|
||||
if (IssueBuffer::accepts(
|
||||
new MethodSignatureMismatch(
|
||||
'Method ' . $cased_method_id . ' has fewer arguments than parent method ' .
|
||||
$parent_method_id,
|
||||
new CodeLocation($this, $this->function, $context->include_location, true)
|
||||
)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$have_emitted = true;
|
||||
break 2;
|
||||
}
|
||||
|
||||
$or_null_implemented_type = $implemented_param->signature_type
|
||||
? clone $implemented_param->signature_type
|
||||
: null;
|
||||
|
||||
if ($or_null_implemented_type) {
|
||||
$or_null_implemented_type->types['null'] = new Type\Atomic\TNull;
|
||||
}
|
||||
|
||||
if ($class_storage->user_defined
|
||||
&& (string)$storage->params[$i]->signature_type
|
||||
!== (string)$implemented_param->signature_type
|
||||
&& (string)$storage->params[$i]->signature_type
|
||||
!== (string)$or_null_implemented_type
|
||||
) {
|
||||
$cased_method_id = MethodChecker::getCasedMethodId(
|
||||
$project_checker,
|
||||
(string)$this->getMethodId()
|
||||
);
|
||||
$parent_method_id = MethodChecker::getCasedMethodId(
|
||||
$project_checker,
|
||||
$implemented_method_id
|
||||
);
|
||||
|
||||
if (IssueBuffer::accepts(
|
||||
new MethodSignatureMismatch(
|
||||
'Argument ' . ($i + 1) . ' of ' . $cased_method_id . ' has wrong type \'' .
|
||||
$storage->params[$i]->signature_type . '\', expecting \'' .
|
||||
$implemented_param->signature_type . '\' as defined by ' .
|
||||
$parent_method_id,
|
||||
$storage->params[$i]->location
|
||||
?: new CodeLocation(
|
||||
$this,
|
||||
$this->function,
|
||||
$context->include_location,
|
||||
true
|
||||
)
|
||||
)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$have_emitted = true;
|
||||
break 2;
|
||||
}
|
||||
|
||||
if ($implemented_param->type
|
||||
&& (!$implemented_param->signature_type || !$class_storage->user_defined)
|
||||
) {
|
||||
foreach ($parent_method_storage->params as $i => $guide_param) {
|
||||
if ($guide_param->type && (!$guide_param->signature_type || !$class_storage->user_defined)) {
|
||||
$implemented_docblock_param_types[$i] = true;
|
||||
}
|
||||
|
||||
if (!$class_storage->user_defined &&
|
||||
$implemented_param->type &&
|
||||
!$implemented_param->type->isMixed() &&
|
||||
(string)$storage->params[$i]->type !== (string)$implemented_param->type
|
||||
) {
|
||||
$cased_method_id = MethodChecker::getCasedMethodId(
|
||||
$project_checker,
|
||||
(string)$this->getMethodId()
|
||||
);
|
||||
$parent_method_id = MethodChecker::getCasedMethodId(
|
||||
$project_checker,
|
||||
$implemented_method_id
|
||||
);
|
||||
|
||||
if (IssueBuffer::accepts(
|
||||
new MethodSignatureMismatch(
|
||||
'Argument ' . ($i + 1) . ' of ' . $cased_method_id . ' has wrong type \'' .
|
||||
$storage->params[$i]->type . '\', expecting \'' .
|
||||
$implemented_param->type . '\' as defined by ' .
|
||||
$parent_method_id,
|
||||
$storage->params[$i]->location
|
||||
?: new CodeLocation(
|
||||
$this,
|
||||
$this->function,
|
||||
$context->include_location,
|
||||
true
|
||||
)
|
||||
)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$have_emitted = true;
|
||||
break 2;
|
||||
}
|
||||
}
|
||||
|
||||
if ($storage->cased_name !== '__construct' &&
|
||||
$storage->required_param_count > $implemented_storage->required_param_count
|
||||
) {
|
||||
$parent_method_id = MethodChecker::getCasedMethodId($project_checker, $implemented_method_id);
|
||||
|
||||
if (IssueBuffer::accepts(
|
||||
new MethodSignatureMismatch(
|
||||
'Method ' . $cased_method_id . ' has more arguments than parent method ' .
|
||||
$parent_method_id,
|
||||
new CodeLocation($this, $this->function, $context->include_location, true)
|
||||
)
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$have_emitted = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -420,6 +290,11 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
continue;
|
||||
}
|
||||
|
||||
/**
|
||||
* @psalm-suppress MixedArrayAccess
|
||||
*
|
||||
* @var PhpParser\Node\Param
|
||||
*/
|
||||
$parser_param = $this->function->getParams()[$offset];
|
||||
|
||||
if ($signature_type) {
|
||||
@ -665,6 +540,210 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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
|
||||
*
|
||||
* @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,
|
||||
array $suppressed_issues
|
||||
) {
|
||||
$implementer_method_id = $implementer_classlike_storage->name . '::'
|
||||
. strtolower($guide_method_storage->cased_name);
|
||||
$guide_method_id = $guide_classlike_storage->name . '::' . strtolower($guide_method_storage->cased_name);
|
||||
$implementer_declaring_method_id = MethodChecker::getDeclaringMethodId(
|
||||
$project_checker,
|
||||
$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;
|
||||
}
|
||||
|
||||
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
|
||||
);
|
||||
|
||||
$implementer_signature_return_type = $implementer_method_storage->signature_return_type
|
||||
? ExpressionChecker::fleshOutType(
|
||||
$project_checker,
|
||||
$implementer_method_storage->signature_return_type,
|
||||
$implementer_classlike_storage->name
|
||||
) : null;
|
||||
|
||||
if ((string)$implementer_signature_return_type !== (string)$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
|
||||
),
|
||||
$suppressed_issues
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
} elseif ($guide_method_storage->return_type && $implementer_method_storage->return_type) {
|
||||
if (!TypeChecker::isContainedBy(
|
||||
$project_checker,
|
||||
$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(
|
||||
new MoreSpecificReturnType(
|
||||
'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 . '\'',
|
||||
$implementer_method_storage->location ?: $code_location
|
||||
),
|
||||
$suppressed_issues
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidReturnType(
|
||||
'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 . '\'',
|
||||
$implementer_method_storage->location ?: $code_location
|
||||
),
|
||||
$suppressed_issues
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
list($implemented_fq_class_name) = explode('::', $implementer_method_id);
|
||||
|
||||
foreach ($guide_method_storage->params as $i => $guide_param) {
|
||||
if (!isset($implementer_method_storage->params[$i])) {
|
||||
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;
|
||||
}
|
||||
|
||||
$or_null_guide_type = $guide_param->signature_type
|
||||
? clone $guide_param->signature_type
|
||||
: null;
|
||||
|
||||
if ($or_null_guide_type) {
|
||||
$or_null_guide_type->types['null'] = new Type\Atomic\TNull;
|
||||
}
|
||||
|
||||
if ($guide_classlike_storage->user_defined
|
||||
&& (string)$implementer_method_storage->params[$i]->signature_type
|
||||
!== (string)$guide_param->signature_type
|
||||
&& (string)$implementer_method_storage->params[$i]->signature_type
|
||||
!== (string)$or_null_guide_type
|
||||
) {
|
||||
if (IssueBuffer::accepts(
|
||||
new MethodSignatureMismatch(
|
||||
'Argument ' . ($i + 1) . ' of ' . $cased_implementer_method_id . ' has wrong type \'' .
|
||||
$implementer_method_storage->params[$i]->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 &&
|
||||
$guide_param->type &&
|
||||
!$guide_param->type->isMixed() &&
|
||||
(string)$implementer_method_storage->params[$i]->type !== (string)$guide_param->type
|
||||
) {
|
||||
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 ($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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds return types for the given function
|
||||
*
|
||||
@ -718,8 +797,9 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
}
|
||||
|
||||
if ($this->function instanceof Function_) {
|
||||
return ($this->source->getNamespace() ? strtolower($this->source->getNamespace()) . '\\' : '') .
|
||||
strtolower($this->function->name);
|
||||
$namespace = $this->source->getNamespace();
|
||||
|
||||
return ($namespace ? strtolower($namespace) . '\\' : '') . strtolower($this->function->name);
|
||||
}
|
||||
|
||||
return $this->getFilePath() . ':' . $this->function->getLine() . '::-closure';
|
||||
@ -856,8 +936,12 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
}
|
||||
|
||||
$inferred_yield_types = [];
|
||||
|
||||
/** @var PhpParser\Node\Stmt[] */
|
||||
$function_stmts = $this->function->getStmts();
|
||||
|
||||
$inferred_return_types = EffectsAnalyser::getReturnTypes(
|
||||
$this->function->getStmts(),
|
||||
$function_stmts,
|
||||
$inferred_yield_types,
|
||||
$ignore_nullable_issues,
|
||||
true
|
||||
@ -954,7 +1038,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
return null;
|
||||
}
|
||||
|
||||
if (ScopeChecker::onlyThrows($this->function->getStmts())) {
|
||||
if (ScopeChecker::onlyThrows($function_stmts)) {
|
||||
// if there's a single throw statement, it's presumably an exception saying this method is not to be
|
||||
// used
|
||||
return null;
|
||||
@ -1379,6 +1463,9 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
|
||||
self::$no_effects_hashes = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FileChecker
|
||||
*/
|
||||
public function getFileChecker()
|
||||
{
|
||||
return $this->file_checker;
|
||||
|
@ -180,6 +180,9 @@ class NamespaceChecker extends SourceChecker implements StatementsSource
|
||||
throw new \InvalidArgumentException('Given $visibility not supported');
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FileChecker
|
||||
*/
|
||||
public function getFileChecker()
|
||||
{
|
||||
return $this->source;
|
||||
|
@ -636,7 +636,9 @@ class ProjectChecker
|
||||
$mentioned_method_id = $implemented_interface . '::' . $method_name;
|
||||
$implemented_method_id = $storage->name . '::' . $method_name;
|
||||
|
||||
MethodChecker::setOverriddenMethodId($this, $implemented_method_id, $mentioned_method_id);
|
||||
if ($storage->abstract) {
|
||||
MethodChecker::setOverriddenMethodId($this, $implemented_method_id, $mentioned_method_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -190,7 +190,7 @@ abstract class SourceChecker implements StatementsSource
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
* @return ?string
|
||||
*/
|
||||
public function getNamespace()
|
||||
{
|
||||
|
@ -1178,6 +1178,9 @@ class StatementsChecker extends SourceChecker implements StatementsSource
|
||||
return isset($this->all_vars[$var_name]) ? $this->all_vars[$var_name] : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return FileChecker
|
||||
*/
|
||||
public function getFileChecker()
|
||||
{
|
||||
return $this->file_checker;
|
||||
|
@ -15,7 +15,7 @@ class ErrorLevelFileFilter extends FileFilter
|
||||
* @param bool $inclusive
|
||||
* @param string $base_dir
|
||||
*
|
||||
* @return self
|
||||
* @return static
|
||||
*/
|
||||
public static function loadFromXMLElement(
|
||||
SimpleXMLElement $e,
|
||||
|
@ -6,7 +6,7 @@ use Psalm\Checker\FileChecker;
|
||||
interface StatementsSource
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
* @return ?string
|
||||
*/
|
||||
public function getNamespace();
|
||||
|
||||
|
@ -18,7 +18,7 @@ class TraitSource implements StatementsSource
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string|null
|
||||
* @return ?string
|
||||
*/
|
||||
public function getNamespace()
|
||||
{
|
||||
|
@ -24,6 +24,11 @@ class AssignmentMapVisitor extends PhpParser\NodeVisitorAbstract implements PhpP
|
||||
$this->this_class_name = $this_class_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node $node
|
||||
*
|
||||
* @return ?int
|
||||
*/
|
||||
public function enterNode(PhpParser\Node $node)
|
||||
{
|
||||
if ($node instanceof PhpParser\Node\Expr\Assign) {
|
||||
|
@ -103,6 +103,11 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
|
||||
$this->plugins = $this->config->getPlugins();
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node $node
|
||||
*
|
||||
* @return ?int
|
||||
*/
|
||||
public function enterNode(PhpParser\Node $node)
|
||||
{
|
||||
if ($node instanceof PhpParser\Node\Stmt\Namespace_) {
|
||||
@ -424,6 +429,11 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node $node
|
||||
*
|
||||
* @return array<mixed, PhpParser\Node>|null|false|int|PhpParser\Node
|
||||
*/
|
||||
public function leaveNode(PhpParser\Node $node)
|
||||
{
|
||||
if ($node instanceof PhpParser\Node\Stmt\Namespace_) {
|
||||
@ -492,6 +502,8 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
|
||||
} elseif ($node instanceof PhpParser\Node\FunctionLike) {
|
||||
array_pop($this->functionlike_storages);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -688,7 +700,14 @@ class DependencyFinderVisitor extends PhpParser\NodeVisitorAbstract implements P
|
||||
$storage->assertions = $var_assertions;
|
||||
}
|
||||
|
||||
if ($parser_return_type = $stmt->getReturnType()) {
|
||||
/**
|
||||
* @psalm-suppress MixedAssignment
|
||||
*
|
||||
* @var null|string|PhpParser\Node\Name|PhpParser\Node\NullableType
|
||||
*/
|
||||
$parser_return_type = $stmt->getReturnType();
|
||||
|
||||
if ($parser_return_type) {
|
||||
$suffix = '';
|
||||
|
||||
if ($parser_return_type instanceof PhpParser\Node\NullableType) {
|
||||
|
@ -669,7 +669,10 @@ class ArrayAssignmentTest extends TestCase
|
||||
/** @param string|int $offset */
|
||||
public function offsetUnset($offset) : void {}
|
||||
|
||||
/** @return mixed */
|
||||
/**
|
||||
* @param string $offset
|
||||
* @return mixed
|
||||
*/
|
||||
public function offsetGet($offset) {
|
||||
return 1;
|
||||
}
|
||||
|
@ -40,16 +40,25 @@ class InterfaceTest extends TestCase
|
||||
|
||||
class D implements C
|
||||
{
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function fooFoo()
|
||||
{
|
||||
return "hello";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function barBar()
|
||||
{
|
||||
return "goodbye";
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function baz()
|
||||
{
|
||||
return "hello again";
|
||||
@ -347,6 +356,39 @@ class InterfaceTest extends TestCase
|
||||
}',
|
||||
'error_message' => 'MethodSignatureMismatch',
|
||||
],
|
||||
'mismatchingReturnTypes' => [
|
||||
'<?php
|
||||
interface I1 {
|
||||
public function foo() : string;
|
||||
}
|
||||
interface I2 {
|
||||
public function foo() : int;
|
||||
}
|
||||
class A implements I1, I2 {
|
||||
public function foo() : string {
|
||||
return "hello";
|
||||
}
|
||||
}',
|
||||
'error_message' => 'MethodSignatureMismatch',
|
||||
],
|
||||
'mismatchingDocblockReturnTypes' => [
|
||||
'<?php
|
||||
interface I1 {
|
||||
/** @return string */
|
||||
public function foo();
|
||||
}
|
||||
interface I2 {
|
||||
/** @return int */
|
||||
public function foo();
|
||||
}
|
||||
class A implements I1, I2 {
|
||||
/** @return string */
|
||||
public function foo() {
|
||||
return "hello";
|
||||
}
|
||||
}',
|
||||
'error_message' => 'InvalidReturnType',
|
||||
],
|
||||
'abstractInterfaceImplementsButCallUndefinedMethod' => [
|
||||
'<?php
|
||||
interface I {
|
||||
@ -381,6 +423,25 @@ class InterfaceTest extends TestCase
|
||||
}',
|
||||
'error_message' => 'MoreSpecificReturnType',
|
||||
],
|
||||
'interfaceReturnType' => [
|
||||
'<?php
|
||||
interface A {
|
||||
/** @return string|null */
|
||||
public function blah();
|
||||
}
|
||||
|
||||
class B implements A {
|
||||
public function blah() {
|
||||
return rand(0, 10) === 4 ? "blah" : null;
|
||||
}
|
||||
}
|
||||
|
||||
$blah = (new B())->blah();',
|
||||
'error_message' => 'MixedAssignment',
|
||||
'error_levels' => [
|
||||
'MissingReturnType',
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -210,6 +210,36 @@ class MethodSignatureTest extends TestCase
|
||||
}',
|
||||
'error_message' => 'Argument 1 of B::foo has wrong type \'string\', expecting \'string|null\'',
|
||||
],
|
||||
'mismatchingCovariantReturn' => [
|
||||
'<?php
|
||||
class A {
|
||||
function foo(): C {
|
||||
return new C();
|
||||
}
|
||||
}
|
||||
class B extends A {
|
||||
function foo(): D {
|
||||
return new D();
|
||||
}
|
||||
}
|
||||
class C {}
|
||||
class D extends C {}',
|
||||
'error_message' => 'MethodSignatureMismatch',
|
||||
],
|
||||
'mismatchingCovariantReturnWithSelf' => [
|
||||
'<?php
|
||||
class A {
|
||||
function foo(): self {
|
||||
return new A();
|
||||
}
|
||||
}
|
||||
class B extends A {
|
||||
function foo(): self {
|
||||
return new B();
|
||||
}
|
||||
}',
|
||||
'error_message' => 'MethodSignatureMismatch',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -116,6 +116,7 @@ class Php70Test extends TestCase
|
||||
|
||||
$app = new Application;
|
||||
$app->setLogger(new class implements Logger {
|
||||
/** @return void */
|
||||
public function log(string $msg) {
|
||||
echo $msg;
|
||||
}
|
||||
|
@ -284,6 +284,7 @@ class ReturnTypeTest extends TestCase
|
||||
}
|
||||
|
||||
class B implements A {
|
||||
/** @return string|null */
|
||||
public function blah() {
|
||||
return rand(0, 10) === 4 ? "blah" : null;
|
||||
}
|
||||
@ -305,6 +306,7 @@ class ReturnTypeTest extends TestCase
|
||||
}
|
||||
|
||||
class C extends B {
|
||||
/** @return string|null */
|
||||
public function blah() {
|
||||
return rand(0, 10) === 4 ? "blahblah" : null;
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user