Skip to content

Commit

Permalink
Support covariant return types & contravariant param types
Browse files Browse the repository at this point in the history
Fixes #2102 and #2264
  • Loading branch information
muglug committed Nov 27, 2019
1 parent 2c08321 commit d1c4c85
Show file tree
Hide file tree
Showing 2 changed files with 185 additions and 35 deletions.
29 changes: 24 additions & 5 deletions src/Psalm/Internal/Analyzer/MethodAnalyzer.php
Expand Up @@ -580,7 +580,17 @@ public static function compareMethods(
$implementer_classlike_storage->parent_class $implementer_classlike_storage->parent_class
) : null; ) : null;


if (!TypeAnalyzer::isContainedByInPhp($implementer_signature_return_type, $guide_signature_return_type)) { $is_contained_by = $codebase->php_major_version >= 7
&& $codebase->php_minor_version >= 4
&& $implementer_signature_return_type
? TypeAnalyzer::isContainedBy(
$codebase,
$implementer_signature_return_type,
$guide_signature_return_type
)
: TypeAnalyzer::isContainedByInPhp($implementer_signature_return_type, $guide_signature_return_type);

if (!$is_contained_by) {
if ($guide_classlike_storage->is_trait === $implementer_classlike_storage->is_trait) { if ($guide_classlike_storage->is_trait === $implementer_classlike_storage->is_trait) {
if (IssueBuffer::accepts( if (IssueBuffer::accepts(
new MethodSignatureMismatch( new MethodSignatureMismatch(
Expand Down Expand Up @@ -860,10 +870,19 @@ public static function compareMethods(
$implementer_classlike_storage->parent_class $implementer_classlike_storage->parent_class
); );


if (!TypeAnalyzer::isContainedByInPhp( $is_contained_by = $codebase->php_major_version >= 7
$guide_param_signature_type, && $codebase->php_minor_version >= 4
$implementer_param_signature_type && $guide_param_signature_type
)) { ? TypeAnalyzer::isContainedBy(
$codebase,
$guide_param_signature_type,
$implementer_param_signature_type
)
: TypeAnalyzer::isContainedByInPhp(
$guide_param_signature_type,
$implementer_param_signature_type
);
if (!$is_contained_by) {
if ($guide_classlike_storage->is_trait === $implementer_classlike_storage->is_trait) { if ($guide_classlike_storage->is_trait === $implementer_classlike_storage->is_trait) {
if (IssueBuffer::accepts( if (IssueBuffer::accepts(
new MethodSignatureMismatch( new MethodSignatureMismatch(
Expand Down
191 changes: 161 additions & 30 deletions tests/MethodSignatureTest.php
Expand Up @@ -111,6 +111,167 @@ public function __soapCall(
$this->analyzeFile('somefile.php', new Context()); $this->analyzeFile('somefile.php', new Context());
} }


/**
* @return void
*/
public function testMismatchingCovariantReturnIn73()
{
$this->expectExceptionMessage('MethodSignatureMismatch');
$this->expectException(\Psalm\Exception\CodeException::class);

$this->project_analyzer->setPhpVersion('7.3');

$this->addFile(
'somefile.php',
'<?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 {}'
);

$this->analyzeFile('somefile.php', new Context());
}

/**
* @return void
*/
public function testMismatchingCovariantReturnIn74()
{
$this->project_analyzer->setPhpVersion('7.4');

$this->addFile(
'somefile.php',
'<?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 {}'
);

$this->analyzeFile('somefile.php', new Context());
}

/**
* @return void
*/
public function testMismatchingCovariantReturnIn73WithSelf()
{
$this->expectExceptionMessage('MethodSignatureMismatch');
$this->expectException(\Psalm\Exception\CodeException::class);

$this->project_analyzer->setPhpVersion('7.3');

$this->addFile(
'somefile.php',
'<?php
class A {
function foo(): self {
return new A();
}
}
class B extends A {
function foo(): self {
return new B();
}
}'
);

$this->analyzeFile('somefile.php', new Context());
}

/**
* @return void
*/
public function testMismatchingCovariantReturnIn74WithSelf()
{
$this->project_analyzer->setPhpVersion('7.4');

$this->addFile(
'somefile.php',
'<?php
class A {
function foo(): self {
return new A();
}
}
class B extends A {
function foo(): self {
return new B();
}
}'
);

$this->analyzeFile('somefile.php', new Context());
}

/**
* @return void
*/
public function testMismatchingCovariantParamIn73()
{
$this->expectExceptionMessage('MethodSignatureMismatch');
$this->expectException(\Psalm\Exception\CodeException::class);

$this->project_analyzer->setPhpVersion('7.3');

$this->addFile(
'somefile.php',
'<?php
class A {
public function foo(D $d) : void {}
}
class B extends A {
public function foo(C $c): void {}
}
class C {}
class D extends C {}'
);

$this->analyzeFile('somefile.php', new Context());
}

/**
* @return void
*/
public function testMismatchingCovariantParamIn74()
{
$this->project_analyzer->setPhpVersion('7.4');

$this->addFile(
'somefile.php',
'<?php
class A {
public function foo(D $d) : void {}
}
class B extends A {
public function foo(C $c): void {}
}
class C {}
class D extends C {}'
);

$this->analyzeFile('somefile.php', new Context());
}

/** /**
* @return void * @return void
*/ */
Expand Down Expand Up @@ -643,36 +804,6 @@ public function foo(string $s): string {
}', }',
'error_message' => 'Argument 1 of B::foo has wrong type \'string\', expecting \'null|string\' as', 'error_message' => 'Argument 1 of B::foo has wrong type \'string\', expecting \'null|string\' as',
], ],
'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',
],
'misplacedRequiredParam' => [ 'misplacedRequiredParam' => [
'<?php '<?php
function foo(string $bar = null, int $bat): void {} function foo(string $bar = null, int $bat): void {}
Expand Down

0 comments on commit d1c4c85

Please sign in to comment.