mirror of
https://github.com/danog/psalm.git
synced 2025-01-22 13:51:54 +01:00
d19aad7db1
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.
1571 lines
51 KiB
PHP
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',
|
|
],
|
|
];
|
|
}
|
|
}
|