1
0
mirror of https://github.com/danog/psalm.git synced 2025-01-22 13:51:54 +01:00
psalm/tests/MethodSignatureTest.php
Bruce Weirdan d19aad7db1
Display target PHP version
Historically it was often not quite clear to users what PHP version
Psalm assumes, and why. This PR addresses this issue by printing the
version and where we got it from right before scanning the files.
2021-11-27 02:18:09 +02:00

1571 lines
51 KiB
PHP

<?php
namespace Psalm\Tests;
use Psalm\Context;
use function class_exists;
use const DIRECTORY_SEPARATOR;
class MethodSignatureTest extends TestCase
{
use Traits\ValidCodeAnalysisTestTrait;
use Traits\InvalidCodeAnalysisTestTrait;
public function testExtendSoapClientWithDocblockTypes(): void
{
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
}
$this->addFile(
'somefile.php',
'<?php
class A extends SoapClient
{
/**
* @param string $function_name
* @param array<mixed> $arguments
* @param array<mixed> $options default null
* @param array|SoapHeader $input_headers default null
* @param array<mixed> $output_headers default null
* @return mixed
*/
public function __soapCall(
$function_name,
$arguments,
$options = [],
$input_headers = [],
&$output_headers = []
) {
return $_GET["foo"];
}
}'
);
$this->analyzeFile('somefile.php', new Context());
}
public function testExtendSoapClientWithNoDocblockTypes(): void
{
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
}
$this->addFile(
'somefile.php',
'<?php
class C extends SoapClient
{
public function __soapCall(
string $function_name,
$arguments,
$options = [],
$input_headers = [],
&$output_headers = []
) {
return $_GET["foo"];
}
}'
);
$this->analyzeFile('somefile.php', new Context());
}
public function testExtendSoapClientWithParamType(): void
{
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
}
$this->addFile(
'somefile.php',
'<?php
class C extends SoapClient
{
public function __soapCall(
string $function_name,
$arguments,
$options = [],
$input_headers = [],
&$output_headers = []
) {
return $_GET["foo"];
}
}'
);
$this->analyzeFile('somefile.php', new Context());
}
public function testMismatchingCovariantReturnIn73(): void
{
$this->expectExceptionMessage('MethodSignatureMismatch');
$this->expectException(\Psalm\Exception\CodeException::class);
$this->project_analyzer->setPhpVersion('7.3', 'tests');
$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());
}
public function testMismatchingCovariantReturnIn74(): void
{
$this->project_analyzer->setPhpVersion('7.4', 'tests');
$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());
}
public function testMismatchingCovariantReturnIn73WithSelf(): void
{
$this->expectExceptionMessage('MethodSignatureMismatch');
$this->expectException(\Psalm\Exception\CodeException::class);
$this->project_analyzer->setPhpVersion('7.3', 'tests');
$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());
}
public function testMismatchingCovariantReturnIn74WithSelf(): void
{
$this->project_analyzer->setPhpVersion('7.4', 'tests');
$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());
}
public function testMismatchingCovariantParamIn73(): void
{
$this->expectExceptionMessage('MethodSignatureMismatch');
$this->expectException(\Psalm\Exception\CodeException::class);
$this->project_analyzer->setPhpVersion('7.3', 'tests');
$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());
}
public function testMismatchingCovariantParamIn74(): void
{
$this->project_analyzer->setPhpVersion('7.4', 'tests');
$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());
}
public function testExtendDocblockParamTypeWithWrongDocblockParam(): void
{
$this->expectExceptionMessage('ImplementedParamTypeMismatch');
$this->expectException(\Psalm\Exception\CodeException::class);
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
}
$this->addFile(
'somefile.php',
'<?php
class A extends SoapClient
{
/**
* @param string $function_name
* @param string $arguments
* @param array<mixed> $options default null
* @param array<mixed> $input_headers default null
* @param array<mixed> $output_headers default null
* @return mixed
*/
public function __soapCall(
$function_name,
$arguments,
$options = [],
$input_headers = [],
&$output_headers = []
) {
}
}'
);
$this->analyzeFile('somefile.php', new Context());
}
public function testExtendDocblockParamTypeWithWrongParam() : void
{
$this->expectException(\Psalm\Exception\CodeException::class);
$this->expectExceptionMessage('MethodSignatureMismatch');
if (class_exists('SoapClient') === false) {
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
}
$this->addFile(
'somefile.php',
'<?php
class A extends SoapClient
{
public function __soapCall(
$function_name,
string $arguments,
$options = [],
$input_headers = [],
&$output_headers = []
) {
}
}'
);
$this->analyzeFile('somefile.php', new Context());
}
/**
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
*/
public function providerValidCodeParse(): iterable
{
return [
'privateArgs' => [
'<?php
class A {
private function foo(): void {}
}
class B extends A {
private function foo(int $arg): void {}
}',
],
'nullableSubclassParam' => [
'<?php
class A {
public function foo(string $s): ?string {
return rand(0, 1) ? $s : null;
}
}
class B extends A {
public function foo(?string $s): string {
return $s ?: "hello";
}
}
echo (new B)->foo(null);',
],
'nullableSubclassParamWithDefault' => [
'<?php
class A {
public function foo(string $s): string {
return $s;
}
}
class B extends A {
public function foo(string $s = null): string {
return $s ?: "hello";
}
}
echo (new B)->foo();',
],
'allowSubclassesForNonInheritedMethodParams' => [
'<?php
class A {}
class B extends A {
public function bar(): void {}
}
class C extends A {
public function bar(): void {}
}
/** @param B|C $a */
function foo(A $a): void {
$a->bar();
}',
],
'allowNoReturnInSubclassWithNullableReturnType' => [
'<?php
class A {
/** @return ?int */
public function foo() {
if (rand(0, 1)) return 5;
}
}
class B extends A {
public function foo() {}
}',
],
'selfReturnShouldBeParent' => [
'<?php
class A {
/** @return self */
public function foo() {
return new A();
}
}
class B extends A {
public function foo() {
return new A();
}
}',
],
'staticReturnShouldBeStatic' => [
'<?php
class A {
/** @return static */
public static function foo() {
return new static();
}
final public function __construct() {}
}
class B extends A {
public static function foo() {
return new static();
}
}
$b = B::foo();',
'assertions' => [
'$b' => 'B',
],
],
'allowSomeCovariance' => [
'<?php
interface I1 {
public function test(string $s) : ?string;
public function testIterable(array $a) : ?iterable;
}
class A1 implements I1 {
public function test(?string $s) : string {
return "value";
}
public function testIterable(?iterable $a) : array {
return [];
}
}',
],
'allowVoidToNullConversion' => [
'<?php
class A {
/** @return ?string */
public function foo() {
return rand(0, 1) ? "hello" : null;
}
}
class B extends A {
public function foo(): void {
return;
}
}
class C extends A {
/** @return void */
public function foo() {
return;
}
}
class D extends A {
/** @return null */
public function foo() {
return null;
}
}',
],
'allowNoChildClassPropertyWhenMixed' => [
'<?php
class A implements Serializable {
/** @var int */
private $id = 1;
/**
* @param string $serialized
*/
public function unserialize($serialized) : void
{
[
$this->id,
] = (array) \unserialize($serialized);
}
public function serialize() : string
{
return serialize([$this->id]);
}
}',
],
'clashWithCallMapClass' => [
'<?php
class HaruDestination {}
class AClass
{
public function get(): HaruDestination
{
return new HaruDestination;
}
}',
],
'classWithTraitExtendsNonAbstractWithMethod' => [
'<?php
class A {
public function foo() : void {}
}
trait T {
abstract public function foo() : void;
}
class B extends A {
use T;
}',
],
'inheritsSplClasses' => [
'<?php
namespace App;
use SplObserver;
use SplSubject;
class Observer implements \SplObserver
{
public function update(SplSubject $subject)
{
}
}
class Subject implements \SplSubject
{
public function attach(SplObserver $observer)
{
}
public function detach(SplObserver $observer)
{
}
public function notify()
{
}
}',
],
'noMixedIssueWhenInheritParamTypes' => [
'<?php
class A {
/**
* @param string $bar
* @return void
*/
public function foo($bar) {
echo $bar;
}
}
class B extends A {
public function foo($bar) {
echo "hello " . $bar;
}
}',
],
'inheritDocumentedSelf' => [
'<?php
interface I {
/**
* @param self $f
*/
public function foo(self $f) : self;
}
class C implements I {
public function foo(I $f) : I {
return new C();
}
}',
],
'allowInterfaceImplementation' => [
'<?php
abstract class A {
/** @return static */
public function foo() {
return $this;
}
}
interface I {
/** @return I */
public function foo();
}
class C extends A implements I {}',
],
'enforceParameterInheritanceWithInheritDocAndParam' => [
'<?php
class A {}
class B extends A {}
class X {
/**
* @param B $class
*/
public function boo(A $class): void {}
}
class Y extends X {
/**
* @inheritdoc
* @param A $class
*/
public function boo(A $class): void {}
}
class Z extends X {
/**
* @inheritDoc
* @param A $class
*/
public function boo(A $class): void {}
}
(new Y())->boo(new A());
(new Z())->boo(new A());',
],
'allowMixedExtensionOfIteratorAggregate' => [
'<?php
class C implements IteratorAggregate {
public function getIterator(): Iterator {
return new ArrayIterator([]);
}
}',
],
'allowExtraVariadic' => [
'<?php
interface I {
public function f(string $a, int $b): void;
}
class C implements I {
public function f(string $a = "a", int $b = 1, float ...$rest): void {}
}
(new C)->f();
(new C)->f("b");
(new C)->f("b", 3);
(new C)->f("b", 3, 0.5);
(new C)->f("b", 3, 0.5, 0.8);',
],
'allowLessSpecificDocblockTypeOnParent' => [
'<?php
abstract class Foo {
/**
* @return array|string
*/
abstract public function getTargets();
}
class Bar extends Foo {
public function getTargets(): string {
return "baz";
}
}
$a = (new Bar)->getTargets();',
[
'$a' => 'string',
],
],
'parentIsKnown' => [
'<?php
class A {
public function returnSelf() : self {
return $this;
}
}
class B extends A {
public function returnSelf() : parent {
return parent::returnSelf();
}
}',
],
'returnStaticParent' => [
'<?php
class A {
/**
* @return static
*/
public static function foo() {
return new static();
}
final public function __construct() {}
}
class B extends A {
/**
* @return static
*/
public static function foo() {
return parent::foo();
}
}',
],
'selfInTraitAbstractIsFine' => [
'<?php
trait SomeTrait {
abstract public function a(self $b): self;
}
class SomeClass {
use SomeTrait;
public function a(self $b): self {
return $this;
}
}'
],
'allowMatchIn74' => [
'<?php
trait FooTrait {
/**
* @return static
*/
public function bar(): self {
return $this;
}
}
interface FooInterface {
/**
* @return static
*/
public function bar(): self;
}
class FooClass implements FooInterface {
use FooTrait;
}',
[],
[],
'7.4'
],
'allowOverridingThrowable' => [
'<?php
/**
* @psalm-immutable
*/
interface MyException extends \Throwable
{
/**
* Informative comment
*/
public function getMessage(): string;
public function getCode();
public function getFile(): string;
public function getLine(): int;
public function getTrace(): array;
public function getPrevious(): ?\Throwable;
public function getTraceAsString(): string;
}'
],
'allowExecptionToStringWithNoType' => [
'<?php
class E extends Exception {
public function __toString() {
return "hello";
}
}'
],
'allowExecptionToStringIn71' => [
'<?php
class E extends Exception {
public function __toString() : string {
return "hello";
}
}',
[],
[],
'7.1'
],
'consistentConstructor' => [
'<?php
/**
* @psalm-consistent-constructor
*/
class A {
public function getInstance() : self {
return new static();
}
}
class AChild extends A {
public function __construct() {}
}'
],
'allowStaticInheritance' => [
'<?php
class A {
public function method(): static {
return $this;
}
}
class B extends A {
public function method(): static {
return $this;
}
}',
[],
[],
'8.0'
],
'suppressDocblockFinal' => [
'<?php
/**
* @final
*/
class A {
public function foo(): void {}
}
/**
* @psalm-suppress InvalidExtendClass
*/
class B extends A {
/**
* @psalm-suppress MethodSignatureMismatch
*/
public function foo(): void {}
}'
],
'inheritParamTypeWhenSignatureReturnTypeChanged' => [
'<?php
class A {
public function __construct(string $s) {}
}
class AChild extends A {}
interface B {
/** @param string $data */
public function create($data): A;
}
class C implements B {
public function create($data): AChild {
return new AChild($data);
}
}',
[],
[],
'7.4'
],
'extendStaticReturnTypeInFinal' => [
'<?php
final class B extends A
{
public static function doCretate1(): self
{
return self::create1();
}
public static function doCretate2(): self
{
return self::create2();
}
}
abstract class A
{
final private function __construct() {}
final protected static function create1(): static
{
return new static();
}
/** @return static */
final protected static function create2()
{
return new static();
}
}',
[],
[],
'8.0'
],
'notExtendedStaticReturntypeInFinal' => [
'<?php
final class X
{
public static function create(): static
{
return new self();
}
}'
],
'callParentMethodFromTrait' => [
'<?php
class MyParentClass
{
/** @return static */
public function myMethod()
{
return $this;
}
}
trait MyTrait
{
final public function myMethod() : self
{
return parent::myMethod();
}
}
class MyChildClass extends MyParentClass
{
use MyTrait;
}'
],
'MixedParamInImplementation' => [
'<?php
interface I
{
/**
* @param mixed $a
*/
public function a($a): void;
}
final class B implements I
{
public function a(mixed $a): void {}
}'
],
];
}
/**
* @return iterable<string,array{string,error_message:string,1?:string[],2?:bool,3?:string}>
*/
public function providerInvalidCodeParse(): iterable
{
return [
'oneParam' => [
'<?php
interface I {
/**
* @param array $i
*/
public function foo(array $i) : void;
}
class C implements I {
public function foo(array $c) : void {
return;
}
}',
'error_message' => 'Argument 1 of C::foo has wrong name $c, expecting $i as defined by I::foo',
],
'moreArguments' => [
'<?php
class A {
public function fooFoo(int $a, bool $b): void {
}
}
class B extends A {
public function fooFoo(int $a, bool $b, array $c): void {
}
}',
'error_message' => 'Method B::fooFoo has more required parameters than parent method A::fooFoo',
],
'fewerArguments' => [
'<?php
class A {
public function fooFoo(int $a, bool $b): void {
}
}
class B extends A {
public function fooFoo(int $a): void {
}
}',
'error_message' => 'Method B::fooFoo has fewer parameters than parent method A::fooFoo',
],
'differentArgumentTypes' => [
'<?php
class A {
public function fooFoo(int $a, bool $b): void {
}
}
class B extends A {
public function fooFoo(int $a, int $b): void {
}
}',
'error_message' => 'Argument 2 of B::fooFoo has wrong type \'int\', expecting \'bool\' as defined ' .
'by A::fooFoo',
],
'differentArgumentNames' => [
'<?php
class A {
public function fooFoo(int $a, bool $b): void {
}
}
class B extends A {
public function fooFoo(int $a, bool $c): void {
}
}',
'error_message' => 'ParamNameMismatch',
],
'nonNullableSubclassParam' => [
'<?php
class A {
public function foo(?string $s): string {
return $s ?: "hello";
}
}
class B extends A {
public function foo(string $s): string {
return $s;
}
}',
'error_message' => 'Argument 1 of B::foo has wrong type \'string\', expecting \'null|string\' as',
],
'misplacedRequiredParam' => [
'<?php
function foo(string $bar = null, int $bat): void {}
foo();',
'error_message' => 'TooFewArguments',
],
'clasginByRef' => [
'<?php
class A {
public function foo(string $a): void {
echo $a;
}
}
class B extends A {
public function foo(string &$a): void {
echo $a;
}
}',
'error_message' => 'MethodSignatureMismatch',
],
'disallowSubclassesForNonInheritedMethodParams' => [
'<?php
class A {}
class B extends A {
public function bar(): void {}
}
class C extends A {
public function bar(): void {}
}
class D {
public function foo(A $a): void {}
}
class E extends D {
/** @param B|C $a */
public function foo(A $a): void {
$a->bar();
}
}',
'error_message' => 'MoreSpecificImplementedParamType',
],
'preventVoidToNullConversionSignature' => [
'<?php
class A {
public function foo(): ?string {
return rand(0, 1) ? "hello" : null;
}
}
class B extends A {
public function foo(): void {
return;
}
}',
'error_message' => 'MethodSignatureMismatch',
],
'abstractExtendsNonAbstractWithMethod' => [
'<?php
class A {
public function foo() : void {}
}
abstract class B extends A {
abstract public function foo() : void;
}',
'error_message' => 'MethodSignatureMismatch',
],
'traitReturnTypeMismatch' => [
'<?php
class A {
public function foo() : void {}
}
trait T {
abstract public function foo() : string;
}
class B extends A {
use T;
}',
'error_message' => 'MethodSignatureMismatch',
],
'abstractTraitMethodWithDifferentReturnType' => [
'<?php
class A {}
class B {}
trait T {
abstract public function foo() : A;
}
class C {
use T;
public function foo() : B{
return new B();
}
}',
'error_message' => 'TraitMethodSignatureMismatch',
],
'traitMoreParams' => [
'<?php
class A {
public function foo() : void {}
}
trait T {
abstract public function foo(string $s) : string;
}
class B extends A {
use T;
}',
'error_message' => 'MethodSignatureMismatch',
],
'abstractTraitMethodWithDifferentParamType' => [
'<?php
class A {}
class B {}
trait T {
abstract public function foo(A $a) : void;
}
class C {
use T;
public function foo(B $a) : void {}
}',
'error_message' => 'TraitMethodSignatureMismatch',
],
'mustOmitReturnType' => [
'<?php
class A
{
public function __construct(): void
{
}
}',
'error_message' => 'MethodSignatureMustOmitReturnType',
],
'requireParam' => [
'<?php
interface I {
function foo(bool $b = false): void;
}
class C implements I {
public function foo(bool $b): void {}
}',
'error_message' => 'MethodSignatureMismatch - src' . DIRECTORY_SEPARATOR . 'somefile.php:6:27 - Method C::foo has more required',
],
'inheritParamTypes' => [
'<?php
class A {
/**
* @param string $bar
* @return void
*/
public function foo($bar) {
echo $bar;
}
}
class B extends A {
public function foo($bar) {
echo "hello " . $bar;
}
}
(new B)->foo(new stdClass);',
'error_message' => 'InvalidArgument',
],
'interfaceHasFewerConstructorArgs' => [
'<?php
interface Foo {
public function __construct();
}
class Bar implements Foo {
public function __construct(bool $foo) {}
}',
'error_message' => 'ConstructorSignatureMismatch',
],
'enforceParameterInheritanceWithInheritDoc' => [
'<?php
class A {}
class B extends A {}
class X {
/**
* @param B $class
*/
public function boo(A $class): void {}
}
class Y extends X {
/**
* @inheritdoc
*/
public function boo(A $class): void {}
}
(new Y())->boo(new A());',
'error_message' => 'ArgumentTypeCoercion',
],
'enforceParameterInheritanceWithCapitalizedInheritDoc' => [
'<?php
class A {}
class B extends A {}
class X {
/**
* @param B $class
*/
public function boo(A $class): void {}
}
class Y extends X {
/**
* @inheritDoc
*/
public function boo(A $class): void {}
}
(new Y())->boo(new A());',
'error_message' => 'ArgumentTypeCoercion',
],
'warnAboutMismatchingClassParamDoc' => [
'<?php
class A {}
class B {}
class X {
/**
* @param B $class
*/
public function boo(A $class): void {}
}',
'error_message' => 'MismatchingDocblockParamType',
],
'warnAboutMismatchingInterfaceParamDoc' => [
'<?php
class A {}
class B {}
interface X {
/**
* @param B $class
*/
public function boo(A $class): void {}
}',
'error_message' => 'MismatchingDocblockParamType',
],
'interfaceInsertDocblockTypes' => [
'<?php
class Foo {}
class Bar {}
interface I {
/** @return array<int, Foo> */
public function getFoos() : array;
}
class A implements I {
public function getFoos() : array {
return [new Bar()];
}
}',
'error_message' => 'InvalidReturnStatement',
],
'classInsertDocblockTypesFromParent' => [
'<?php
class Foo {}
class Bar {}
class B {
/** @return array<int, Foo> */
public function getFoos() : array {
return [new Foo()];
}
}
class A extends B {
public function getFoos() : array {
return [new Bar()];
}
}',
'error_message' => 'InvalidReturnStatement',
],
'preventInterfaceOverload' => [
'<?php
interface I {
public function f(float ...$rest): void;
}
class C implements I {
/**
* @param array<int,float> $f
* @psalm-suppress ParamNameMismatch
*/
public function f($f): void {}
}',
'error_message' => 'MethodSignatureMismatch',
['MoreSpecificImplementedParamType'],
],
'preventOneOfUnionMoreSpecific' => [
'<?php
class A {
/** @param string|int $s */
public function foo($s) : void {}
}
class B extends A {
/** @param string $s */
public function foo($s) : void {}
}',
'error_message' => 'MoreSpecificImplementedParamType',
],
'preventImplementingSerializableWithWrongDocblockType' => [
'<?php
class Foo implements \Serializable {
/** @param int $serialized */
public function unserialize($serialized) {}
public function serialize() {}
}',
'error_message' => 'ImplementedParamTypeMismatch',
],
'returnsParentWithNoParent' => [
'<?php
class Foo {
public function f(): parent {}
}
',
'error_message' => 'InvalidParent',
],
'returnsParentWithNoParentAndInvalidParentSuppressed' => [
'<?php
class Foo {
public function f(): parent {
}
}
',
'error_message' => 'InvalidReturnType',
['InvalidParent'],
],
// not sure how to handle it
'SKIPPED-returnsParentWithNoParentAndInvalidParentSuppressedMismatchingReturn' => [
'<?php
class Foo {
public function f(): parent {
return false;
}
}
',
'error_message' => 'InvalidReturnType',
['InvalidParent'],
],
'regularMethodMismatchFromParentUse' => [
'<?php
trait T2 {
abstract public function test(int $x) : void;
}
abstract class P2 {
use T2;
}
class C2 extends P2 {
public function test(string $x) : void {}
}',
'error_message' => 'MethodSignatureMismatch',
],
'regularMethodMismatchFromChildUse' => [
'<?php
trait T3 {
abstract public function test(int $x) : void;
}
class P3 {
public function test(string $x) : void {}
}
class C3 extends P3 {
use T3;
}',
'error_message' => 'MethodSignatureMismatch',
],
'traitMethodAccessLevel' => [
'<?php
class A {}
class B extends A {}
trait T1 {
abstract protected static function test(A $x) : void;
}
class C1 {
use T1;
private static function test(B $x) : void {}
}',
'error_message' => 'TraitMethodSignatureMismatch',
],
'abstractClassReturnMismatch' => [
'<?php
interface I {
function foo(): array;
}
abstract class C implements I {
public function foo(): void {}
}',
'error_message' => 'MethodSignatureMismatch',
],
'abstractClassParamMismatch' => [
'<?php
interface I {
function foo(int $s): void;
}
abstract class C implements I {
public function foo(string $s): void {}
}',
'error_message' => 'MethodSignatureMismatch',
],
'preventTraitMatchIn73' => [
'<?php
trait FooTrait {
/**
* @return static
*/
public function bar(): self {
return $this;
}
}
interface FooInterface {
/**
* @return static
*/
public function bar(): self;
}
class FooClass implements FooInterface {
use FooTrait;
}',
'error_message' => 'MethodSignatureMismatch',
[],
false,
'7.3'
],
'inconsistentConstructorExplicitParentConstructorArgCount' => [
'<?php
/**
* @psalm-consistent-constructor
*/
class A {
public function getInstance() : self
{
return new static();
}
public function __construct() {}
}
class BadAChild extends A {
public function __construct(string $s) {}
}',
'error_message' => 'ConstructorSignatureMismatch',
],
'inconsistentConstructorExplicitParentConstructorType' => [
'<?php
/**
* @psalm-consistent-constructor
*/
class A {
public function getInstance() : self
{
return new static(5);
}
public function __construct(int $s) {}
}
class BadAChild extends A {
public function __construct(string $s) {}
}',
'error_message' => 'ConstructorSignatureMismatch',
],
'inconsistentConstructorImplicitParentConstructor' => [
'<?php
/**
* @psalm-consistent-constructor
*/
class A {
public function getInstance() : self {
return new static();
}
}
class BadAChild extends A {
public function __construct(string $s) {}
}',
'error_message' => 'ConstructorSignatureMismatch',
],
'inheritDocblockReturnFromInterface' => [
'<?php
interface A {
/** @return ?string */
function foo();
}
class C implements A {
public function foo() : ?string {}
}',
'error_message' => 'InvalidReturnType',
],
'disableNamedArgumentsInDescendant' => [
'<?php
interface Foo {
public function bar(string ...$_args): void;
}
final class Baz implements Foo {
/** @no-named-arguments */
public function bar(string ...$_args): void {}
}
',
'error_message' => 'MethodSignatureMismatch',
],
];
}
}