1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00

Fix #228, fix #336 by improving checking of inherited signatures

This commit is contained in:
Matthew Brown 2017-11-26 16:03:17 -05:00
parent 3b2a1d4a3e
commit afcbc113c9
17 changed files with 445 additions and 210 deletions

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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);
}
}
}
}

View File

@ -190,7 +190,7 @@ abstract class SourceChecker implements StatementsSource
}
/**
* @return string
* @return ?string
*/
public function getNamespace()
{

View File

@ -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;

View File

@ -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,

View File

@ -6,7 +6,7 @@ use Psalm\Checker\FileChecker;
interface StatementsSource
{
/**
* @return string
* @return ?string
*/
public function getNamespace();

View File

@ -18,7 +18,7 @@ class TraitSource implements StatementsSource
}
/**
* @return string|null
* @return ?string
*/
public function getNamespace()
{

View File

@ -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) {

View File

@ -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) {

View File

@ -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;
}

View File

@ -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',
],
],
];
}
}

View File

@ -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',
],
];
}
}

View File

@ -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;
}

View File

@ -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;
}