mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
cc7ff94f7c
Crash seen when running this test in PHP 7.4 because the method does not exist, but the call map includes it in 8.0
1485 lines
49 KiB
PHP
1485 lines
49 KiB
PHP
<?php
|
|
namespace Psalm\Tests;
|
|
|
|
use function class_exists;
|
|
use const DIRECTORY_SEPARATOR;
|
|
|
|
class MethodCallTest extends TestCase
|
|
{
|
|
use Traits\InvalidCodeAnalysisTestTrait;
|
|
use Traits\ValidCodeAnalysisTestTrait;
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function testExtendDocblockParamType()
|
|
{
|
|
if (class_exists('SoapClient') === false) {
|
|
$this->markTestSkipped('Cannot run test, base class "SoapClient" does not exist!');
|
|
|
|
return;
|
|
}
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
new SoapFault("1", "faultstring", "faultactor");'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new \Psalm\Context());
|
|
}
|
|
|
|
public function testMethodCallMemoize(): void
|
|
{
|
|
$this->project_analyzer->getConfig()->memoize_method_calls = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
class A {
|
|
function getFoo() : ?Foo {
|
|
return rand(0, 1) ? new Foo : null;
|
|
}
|
|
}
|
|
class Foo {
|
|
function getBar() : ?Bar {
|
|
return rand(0, 1) ? new Bar : null;
|
|
}
|
|
}
|
|
class Bar {
|
|
public function bat() : void {}
|
|
};
|
|
|
|
$a = new A();
|
|
|
|
if ($a->getFoo()) {
|
|
if ($a->getFoo()->getBar()) {
|
|
$a->getFoo()->getBar()->bat();
|
|
}
|
|
}'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new \Psalm\Context());
|
|
}
|
|
|
|
public function testPropertyMethodCallMemoize(): void
|
|
{
|
|
$this->project_analyzer->getConfig()->memoize_method_calls = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
class Foo
|
|
{
|
|
private ?string $bar;
|
|
|
|
public function __construct(?string $bar) {
|
|
$this->bar = $bar;
|
|
}
|
|
|
|
public function getBar(): ?string {
|
|
return $this->bar;
|
|
}
|
|
}
|
|
|
|
function doSomething(Foo $foo): string {
|
|
if ($foo->getBar() !== null){
|
|
return $foo->getBar();
|
|
}
|
|
|
|
return "hello";
|
|
}'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new \Psalm\Context());
|
|
}
|
|
|
|
public function testPropertyMethodCallMutationFreeMemoize(): void
|
|
{
|
|
$this->project_analyzer->getConfig()->memoize_method_calls = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
class Foo
|
|
{
|
|
private ?string $bar;
|
|
|
|
public function __construct(?string $bar) {
|
|
$this->bar = $bar;
|
|
}
|
|
|
|
/**
|
|
* @psalm-mutation-free
|
|
*/
|
|
public function getBar(): ?string {
|
|
return $this->bar;
|
|
}
|
|
}
|
|
|
|
function doSomething(Foo $foo): string {
|
|
if ($foo->getBar() !== null){
|
|
return $foo->getBar();
|
|
}
|
|
|
|
return "hello";
|
|
}'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new \Psalm\Context());
|
|
}
|
|
|
|
public function testUnchainedMethodCallMemoize(): void
|
|
{
|
|
$this->project_analyzer->getConfig()->memoize_method_calls = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
class SomeClass {
|
|
private ?int $int;
|
|
|
|
public function __construct() {
|
|
$this->int = 1;
|
|
}
|
|
|
|
final public function getInt(): ?int {
|
|
return $this->int;
|
|
}
|
|
}
|
|
|
|
function printInt(int $int): void {
|
|
echo $int;
|
|
}
|
|
|
|
$obj = new SomeClass();
|
|
|
|
if ($obj->getInt()) {
|
|
printInt($obj->getInt());
|
|
}'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new \Psalm\Context());
|
|
}
|
|
|
|
public function testUnchainedMutationFreeMethodCallMemoize(): void
|
|
{
|
|
$this->project_analyzer->getConfig()->memoize_method_calls = true;
|
|
|
|
$this->addFile(
|
|
'somefile.php',
|
|
'<?php
|
|
class SomeClass {
|
|
private ?int $int;
|
|
|
|
public function __construct() {
|
|
$this->int = 1;
|
|
}
|
|
|
|
/**
|
|
* @psalm-mutation-free
|
|
*/
|
|
public function getInt(): ?int {
|
|
return $this->int;
|
|
}
|
|
}
|
|
|
|
function printInt(int $int): void {
|
|
echo $int;
|
|
}
|
|
|
|
$obj = new SomeClass();
|
|
|
|
if ($obj->getInt()) {
|
|
printInt($obj->getInt());
|
|
}'
|
|
);
|
|
|
|
$this->analyzeFile('somefile.php', new \Psalm\Context());
|
|
}
|
|
|
|
/**
|
|
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
|
|
*/
|
|
public function providerValidCodeParse(): iterable
|
|
{
|
|
return [
|
|
'notInCallMapTest' => [
|
|
'<?php
|
|
new DOMImplementation();',
|
|
],
|
|
'parentStaticCall' => [
|
|
'<?php
|
|
class A {
|
|
/** @return void */
|
|
public static function foo(){}
|
|
}
|
|
|
|
class B extends A {
|
|
/** @return void */
|
|
public static function bar(){
|
|
parent::foo();
|
|
}
|
|
}',
|
|
],
|
|
'nonStaticInvocation' => [
|
|
'<?php
|
|
class Foo {
|
|
public static function barBar(): void {}
|
|
}
|
|
|
|
(new Foo())->barBar();',
|
|
],
|
|
'staticInvocation' => [
|
|
'<?php
|
|
class A {
|
|
public static function fooFoo(): void {}
|
|
}
|
|
|
|
class B extends A {
|
|
|
|
}
|
|
|
|
B::fooFoo();',
|
|
],
|
|
'staticCallOnVar' => [
|
|
'<?php
|
|
class A {
|
|
public static function bar(): int {
|
|
return 5;
|
|
}
|
|
}
|
|
$foo = new A;
|
|
$b = $foo::bar();',
|
|
],
|
|
'uppercasedSelf' => [
|
|
'<?php
|
|
class X33{
|
|
public static function main(): void {
|
|
echo SELF::class . "\n"; // Class or interface SELF does not exist
|
|
}
|
|
}
|
|
X33::main();',
|
|
],
|
|
'dateTimeImmutableStatic' => [
|
|
'<?php
|
|
/** @psalm-immutable */
|
|
final class MyDate extends DateTimeImmutable {}
|
|
|
|
$today = new MyDate();
|
|
$yesterday = $today->sub(new DateInterval("P1D"));
|
|
|
|
$b = (new DateTimeImmutable())->modify("+3 hours");',
|
|
'assertions' => [
|
|
'$yesterday' => 'MyDate|false',
|
|
'$b' => 'DateTimeImmutable',
|
|
],
|
|
],
|
|
'magicCall' => [
|
|
'<?php
|
|
class A {
|
|
public function __call(string $method_name, array $args) : string {
|
|
return "hello";
|
|
}
|
|
}
|
|
|
|
$a = new A;
|
|
$s = $a->bar();',
|
|
[
|
|
'$s' => 'string',
|
|
]
|
|
],
|
|
'canBeCalledOnMagic' => [
|
|
'<?php
|
|
class A {
|
|
public function __call(string $method, array $args) {}
|
|
}
|
|
|
|
class B {}
|
|
|
|
$a = rand(0, 1) ? new A : new B;
|
|
|
|
$a->maybeUndefinedMethod();',
|
|
'assertions' => [],
|
|
'error_levels' => ['PossiblyUndefinedMethod'],
|
|
],
|
|
'canBeCalledOnMagicWithMethod' => [
|
|
'<?php
|
|
class A {
|
|
public function __call(string $method, array $args) {}
|
|
}
|
|
|
|
class B {
|
|
public function bar() : void {}
|
|
}
|
|
|
|
$a = rand(0, 1) ? new A : new B;
|
|
|
|
$a->bar();',
|
|
'assertions' => [],
|
|
],
|
|
'invokeCorrectType' => [
|
|
'<?php
|
|
class A {
|
|
public function __invoke(string $p): void {}
|
|
}
|
|
|
|
$q = new A;
|
|
$q("asda");',
|
|
],
|
|
'domDocumentAppendChild' => [
|
|
'<?php
|
|
$doc = new DOMDocument("1.0");
|
|
$node = $doc->createElement("foo");
|
|
if ($node instanceof DOMElement) {
|
|
$newnode = $doc->appendChild($node);
|
|
$newnode->setAttribute("bar", "baz");
|
|
}',
|
|
],
|
|
'nonStaticSelfCall' => [
|
|
'<?php
|
|
class A11 {
|
|
public function call() : self {
|
|
$result = self::method();
|
|
return $result;
|
|
}
|
|
|
|
public function method() : self {
|
|
return $this;
|
|
}
|
|
}
|
|
$x = new A11();
|
|
var_export($x->call());',
|
|
],
|
|
'simpleXml' => [
|
|
'<?php
|
|
$xml = new SimpleXMLElement("<a><b></b></a>");
|
|
$a = $xml->asXML();
|
|
$b = $xml->asXML("foo.xml");',
|
|
'assertions' => [
|
|
'$a' => 'false|string',
|
|
'$b' => 'bool',
|
|
],
|
|
],
|
|
'datetimeformatNotFalse' => [
|
|
'<?php
|
|
$format = random_bytes(10);
|
|
$dt = new DateTime;
|
|
$formatted = $dt->format($format);
|
|
if (false !== $formatted) {}
|
|
function takesString(string $s) : void {}
|
|
takesString($formatted);',
|
|
],
|
|
'domElement' => [
|
|
'<?php
|
|
function foo(DOMElement $e) : ?string {
|
|
$a = $e->getElementsByTagName("bar");
|
|
$b = $a->item(0);
|
|
if (!$b) {
|
|
return null;
|
|
}
|
|
return $b->getAttribute("bat");
|
|
}',
|
|
],
|
|
'domElementIteratorOrEmptyArray' => [
|
|
'<?php
|
|
function foo(string $XML) : void {
|
|
$dom = new DOMDocument();
|
|
$dom->loadXML($XML);
|
|
|
|
$elements = rand(0, 1) ? $dom->getElementsByTagName("bar") : [];
|
|
foreach ($elements as $element) {
|
|
$element->getElementsByTagName("bat");
|
|
}
|
|
}',
|
|
],
|
|
'reflectionParameter' => [
|
|
'<?php
|
|
function getTypeName(ReflectionParameter $parameter): string {
|
|
$type = $parameter->getType();
|
|
|
|
if ($type === null) {
|
|
return "mixed";
|
|
}
|
|
|
|
if ($type instanceof ReflectionUnionType) {
|
|
return "union";
|
|
}
|
|
|
|
if ($type instanceof ReflectionNamedType) {
|
|
return $type->getName();
|
|
}
|
|
|
|
throw new RuntimeException("unexpected type");
|
|
}',
|
|
'assertions' => [],
|
|
'error_levels' => [],
|
|
'php_version' => '8.0'
|
|
],
|
|
'PDOMethod' => [
|
|
'<?php
|
|
function md5_and_reverse(string $string) : string {
|
|
return strrev(md5($string));
|
|
}
|
|
|
|
$db = new PDO("sqlite:sqlitedb");
|
|
$db->sqliteCreateFunction("md5rev", "md5_and_reverse", 1);',
|
|
],
|
|
'dontConvertedMaybeMixedAfterCall' => [
|
|
'<?php
|
|
class B {
|
|
public function foo() : void {}
|
|
}
|
|
/**
|
|
* @param array<B> $b
|
|
*/
|
|
function foo(array $a, array $b) : void {
|
|
$c = array_merge($b, $a);
|
|
|
|
foreach ($c as $d) {
|
|
$d->foo();
|
|
if ($d instanceof B) {}
|
|
}
|
|
}',
|
|
[],
|
|
'error_levels' => ['MixedAssignment', 'MixedMethodCall'],
|
|
],
|
|
'methodResolution' => [
|
|
'<?php
|
|
interface ReturnsString {
|
|
public function getId(): string;
|
|
}
|
|
|
|
/**
|
|
* @param mixed $a
|
|
*/
|
|
function foo(ReturnsString $user, $a): string {
|
|
strlen($user->getId());
|
|
|
|
(is_object($a) && method_exists($a, "getS")) ? (string)$a->getS() : "";
|
|
|
|
return $user->getId();
|
|
}',
|
|
],
|
|
'defineVariableCreatedInArgToMixed' => [
|
|
'<?php
|
|
function bar($a) : void {
|
|
if ($a->foo($b = (int) "5")) {
|
|
echo $b;
|
|
}
|
|
}',
|
|
[],
|
|
'error_levels' => ['MixedMethodCall', 'MissingParamType'],
|
|
],
|
|
'staticCallAfterMethodExists' => [
|
|
'<?php
|
|
class A
|
|
{
|
|
protected static function existing() : string
|
|
{
|
|
return "hello";
|
|
}
|
|
|
|
protected static function foo() : string
|
|
{
|
|
if (!method_exists(static::class, "maybeExists")) {
|
|
return "hello";
|
|
}
|
|
|
|
self::maybeExists();
|
|
|
|
return static::existing();
|
|
}
|
|
}',
|
|
],
|
|
'varSelfCall' => [
|
|
'<?php
|
|
class Foo {
|
|
/** @var self */
|
|
public static $current;
|
|
public function bar() : void {}
|
|
}
|
|
|
|
Foo::$current->bar();',
|
|
],
|
|
'pdoStatementSetFetchMode' => [
|
|
'<?php
|
|
class A {
|
|
/** @var ?string */
|
|
public $a;
|
|
}
|
|
|
|
$db = new PDO("sqlite::memory:");
|
|
$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
|
|
$stmt = $db->prepare("select \"a\" as a");
|
|
$stmt->setFetchMode(PDO::FETCH_CLASS, A::class);
|
|
$stmt->execute();
|
|
/** @psalm-suppress MixedAssignment */
|
|
$a = $stmt->fetch();',
|
|
],
|
|
'datePeriodConstructor' => [
|
|
'<?php
|
|
function foo(DateTime $d1, DateTime $d2) : void {
|
|
new DatePeriod(
|
|
$d1,
|
|
DateInterval::createFromDateString("1 month"),
|
|
$d2
|
|
);
|
|
}',
|
|
],
|
|
'callMethodAfterCheckingExistence' => [
|
|
'<?php
|
|
class A {}
|
|
|
|
function foo(A $a) : void {
|
|
if (method_exists($a, "bar")) {
|
|
/** @psalm-suppress MixedArgument */
|
|
echo $a->bar();
|
|
}
|
|
}'
|
|
],
|
|
'callMethodAfterCheckingExistenceInClosure' => [
|
|
'<?php
|
|
class A {}
|
|
|
|
function foo(A $a) : void {
|
|
if (method_exists($a, "bar")) {
|
|
(function() use ($a) : void {
|
|
/** @psalm-suppress MixedArgument */
|
|
echo $a->bar();
|
|
})();
|
|
|
|
}
|
|
}'
|
|
],
|
|
'callManyMethodsAfterCheckingExistence' => [
|
|
'<?php
|
|
function foo(object $object) : void {
|
|
if (!method_exists($object, "foo")) {
|
|
return;
|
|
}
|
|
if (!method_exists($object, "bar")) {
|
|
return;
|
|
}
|
|
$object->foo();
|
|
$object->bar();
|
|
}'
|
|
],
|
|
'callManyMethodsAfterCheckingExistenceChained' => [
|
|
'<?php
|
|
function foo(object $object) : void {
|
|
if (method_exists($object, "foo") && method_exists($object, "bar")) {
|
|
$object->foo();
|
|
$object->bar();
|
|
}
|
|
}'
|
|
],
|
|
'callManyMethodsOnKnownObjectAfterCheckingExistenceChained' => [
|
|
'<?php
|
|
class A {}
|
|
function foo(A $object) : void {
|
|
if (method_exists($object, "foo") && method_exists($object, "bar")) {
|
|
$object->foo();
|
|
$object->bar();
|
|
}
|
|
}'
|
|
],
|
|
'preserveMethodExistsType' => [
|
|
'<?php
|
|
/**
|
|
* @param class-string $foo
|
|
*/
|
|
function foo(string $foo): string {
|
|
if (!method_exists($foo, "something")) {
|
|
return "";
|
|
}
|
|
|
|
return $foo;
|
|
}'
|
|
],
|
|
'methodDoesNotExistOnClass' => [
|
|
'<?php
|
|
class A {}
|
|
|
|
/**
|
|
* @param class-string<A> $foo
|
|
*/
|
|
function foo(string $foo): string {
|
|
if (!method_exists($foo, "something")) {
|
|
return "";
|
|
}
|
|
|
|
return $foo;
|
|
}'
|
|
],
|
|
'pdoStatementFetchAssoc' => [
|
|
'<?php
|
|
/** @return array<string,null|scalar>|false */
|
|
function fetch_assoc() {
|
|
$p = new PDO("sqlite::memory:");
|
|
$sth = $p->prepare("SELECT 1");
|
|
$sth->execute();
|
|
return $sth->fetch(PDO::FETCH_ASSOC);
|
|
}'
|
|
],
|
|
'pdoStatementFetchBoth' => [
|
|
'<?php
|
|
/** @return array<null|scalar>|false */
|
|
function fetch_both() {
|
|
$p = new PDO("sqlite::memory:");
|
|
$sth = $p->prepare("SELECT 1");
|
|
$sth->execute();
|
|
return $sth->fetch(PDO::FETCH_BOTH);
|
|
}'
|
|
],
|
|
'pdoStatementFetchBound' => [
|
|
'<?php
|
|
/** @return bool */
|
|
function fetch_both() : bool {
|
|
$p = new PDO("sqlite::memory:");
|
|
$sth = $p->prepare("SELECT 1");
|
|
$sth->execute();
|
|
return $sth->fetch(PDO::FETCH_BOUND);
|
|
}'
|
|
],
|
|
'pdoStatementFetchClass' => [
|
|
'<?php
|
|
/** @return object|false */
|
|
function fetch_class() {
|
|
$p = new PDO("sqlite::memory:");
|
|
$sth = $p->prepare("SELECT 1");
|
|
$sth->execute();
|
|
return $sth->fetch(PDO::FETCH_CLASS);
|
|
}'
|
|
],
|
|
'pdoStatementFetchLazy' => [
|
|
'<?php
|
|
/** @return object|false */
|
|
function fetch_lazy() {
|
|
$p = new PDO("sqlite::memory:");
|
|
$sth = $p->prepare("SELECT 1");
|
|
$sth->execute();
|
|
return $sth->fetch(PDO::FETCH_LAZY);
|
|
}'
|
|
],
|
|
'pdoStatementFetchNamed' => [
|
|
'<?php
|
|
/** @return array<string,scalar|list<scalar>>|false */
|
|
function fetch_named() {
|
|
$p = new PDO("sqlite::memory:");
|
|
$sth = $p->prepare("SELECT 1");
|
|
$sth->execute();
|
|
return $sth->fetch(PDO::FETCH_NAMED);
|
|
}'
|
|
],
|
|
'pdoStatementFetchNum' => [
|
|
'<?php
|
|
/** @return list<null|scalar>|false */
|
|
function fetch_named() {
|
|
$p = new PDO("sqlite::memory:");
|
|
$sth = $p->prepare("SELECT 1");
|
|
$sth->execute();
|
|
return $sth->fetch(PDO::FETCH_NUM);
|
|
}'
|
|
],
|
|
'pdoStatementFetchObj' => [
|
|
'<?php
|
|
/** @return stdClass|false */
|
|
function fetch_named() {
|
|
$p = new PDO("sqlite::memory:");
|
|
$sth = $p->prepare("SELECT 1");
|
|
$sth->execute();
|
|
return $sth->fetch(PDO::FETCH_OBJ);
|
|
}'
|
|
],
|
|
'dateTimeSecondArg' => [
|
|
'<?php
|
|
$date = new DateTime(null, new DateTimeZone("Pacific/Nauru"));
|
|
echo $date->format("Y-m-d H:i:sP") . "\n";'
|
|
],
|
|
'noCrashOnGetClassMethodCallWithNull' => [
|
|
'<?php
|
|
class User {
|
|
/**
|
|
* @psalm-suppress NullArgument
|
|
*/
|
|
public function give(): void{
|
|
$model = null;
|
|
$class = \get_class($model);
|
|
$class::foo();
|
|
}
|
|
}',
|
|
],
|
|
'unknownMethodCallWithProperty' => [
|
|
'<?php
|
|
class A {
|
|
private string $b = "c";
|
|
|
|
public function passesByRef(object $a): void {
|
|
/** @psalm-suppress MixedMethodCall */
|
|
$a->passedByRef($this->b);
|
|
}
|
|
}',
|
|
],
|
|
'maybeNotTooManyArgumentsToInstance' => [
|
|
'<?php
|
|
class A {
|
|
public function fooFoo(int $a): void {}
|
|
}
|
|
|
|
class B {
|
|
public function fooFoo(int $a, string $s): void {}
|
|
}
|
|
|
|
(rand(0, 1) ? new A : new B)->fooFoo(5, "dfd");',
|
|
],
|
|
'interfaceMethodCallCheck' => [
|
|
'<?php
|
|
interface A {
|
|
function foo() : void;
|
|
}
|
|
|
|
interface B extends A {
|
|
function foo(string $a = "") : void;
|
|
}
|
|
|
|
class C implements B {
|
|
public function foo(string $a = "") : void {}
|
|
}
|
|
|
|
function takesWithoutArguments(A $a) : void {
|
|
if ($a instanceof B) {
|
|
$a->foo("");
|
|
}
|
|
}
|
|
|
|
takesWithoutArguments(new C);'
|
|
],
|
|
'getterAutomagicAssertion' => [
|
|
'<?php
|
|
class A {
|
|
/** @var string|null */
|
|
public $a;
|
|
|
|
/** @return string|null */
|
|
final public function getA() {
|
|
return $this->a;
|
|
}
|
|
}
|
|
|
|
$a = new A();
|
|
|
|
if ($a->getA()) {
|
|
echo strlen($a->getA());
|
|
}'
|
|
],
|
|
'ignorePossiblyNull' => [
|
|
'<?php
|
|
class Foo {
|
|
protected ?string $type = null;
|
|
|
|
public function prepend(array $arr) : string {
|
|
return $this->getType();
|
|
}
|
|
|
|
/**
|
|
* @psalm-ignore-nullable-return
|
|
*/
|
|
public function getType() : ?string
|
|
{
|
|
return $this->type;
|
|
}
|
|
}'
|
|
],
|
|
'abstractMethodExistsOnChild' => [
|
|
'<?php
|
|
abstract class Foo {}
|
|
|
|
abstract class FooChild extends Foo {}
|
|
|
|
abstract class AbstractTestCase {
|
|
abstract public function createFoo(): Foo;
|
|
}
|
|
|
|
abstract class AbstractChildTestCase extends AbstractTestCase {
|
|
abstract public function createFoo(): FooChild;
|
|
|
|
public function testFoo(): FooChild {
|
|
return $this->createFoo();
|
|
}
|
|
}',
|
|
[],
|
|
[],
|
|
'7.4'
|
|
],
|
|
'pdoQueryTwoArgs' => [
|
|
'<?php
|
|
$pdo = new PDO("test");
|
|
$pdo->query("SELECT * FROM projects", PDO::FETCH_NAMED);'
|
|
],
|
|
'unchainedInferredMutationFreeMethodCallMemoize' => [
|
|
'<?php
|
|
class SomeClass {
|
|
private ?int $int;
|
|
|
|
public function __construct() {
|
|
$this->int = 1;
|
|
}
|
|
|
|
/**
|
|
* @psalm-mutation-free
|
|
*/
|
|
public function getInt(): ?int {
|
|
return $this->int;
|
|
}
|
|
}
|
|
|
|
function printInt(int $int): void {
|
|
echo $int;
|
|
}
|
|
|
|
$obj = new SomeClass();
|
|
|
|
if ($obj->getInt()) {
|
|
printInt($obj->getInt());
|
|
}',
|
|
],
|
|
'unchainedInferredInferredFinalMutationFreeMethodCallMemoize' => [
|
|
'<?php
|
|
class SomeClass {
|
|
private ?int $int;
|
|
|
|
public function __construct() {
|
|
$this->int = 1;
|
|
}
|
|
|
|
final public function getInt(): ?int {
|
|
return $this->int;
|
|
}
|
|
}
|
|
|
|
function printInt(int $int): void {
|
|
echo $int;
|
|
}
|
|
|
|
$obj = new SomeClass();
|
|
|
|
if ($obj->getInt()) {
|
|
printInt($obj->getInt());
|
|
}',
|
|
],
|
|
'privateInferredMutationFreeMethodCallMemoize' => [
|
|
'<?php
|
|
class PropertyClass {
|
|
public function test() : void {
|
|
echo "test";
|
|
}
|
|
}
|
|
class SomeClass {
|
|
private ?PropertyClass $property = null;
|
|
|
|
private function getProperty(): ?PropertyClass {
|
|
return $this->property;
|
|
}
|
|
|
|
public function test(int $int): void {
|
|
if ($this->getProperty() !== null) {
|
|
$this->getProperty()->test();
|
|
}
|
|
}
|
|
}',
|
|
],
|
|
'inferredFinalMethod' => [
|
|
'<?php
|
|
class PropertyClass {
|
|
public function test() : bool {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
class MainClass {
|
|
private ?PropertyClass $property = null;
|
|
|
|
final public function getProperty(): ?PropertyClass {
|
|
return $this->property;
|
|
}
|
|
}
|
|
|
|
$main = new MainClass();
|
|
|
|
if ($main->getProperty() !== null && $main->getProperty()->test()) {}'
|
|
],
|
|
'getterTypeInferring' => [
|
|
'<?php
|
|
class A {
|
|
/** @var int|string|null */
|
|
public $a;
|
|
|
|
/** @return int|string|null */
|
|
final public function getValue() {
|
|
return $this->a;
|
|
}
|
|
}
|
|
|
|
$a = new A();
|
|
|
|
if (is_string($a->getValue())) {
|
|
echo strlen($a->getValue());
|
|
}',
|
|
],
|
|
'newSplObjectStorageDefaultEmpty' => [
|
|
'<?php
|
|
$a = new SplObjectStorage();',
|
|
[
|
|
'$a' => 'SplObjectStorage<empty, empty>',
|
|
]
|
|
],
|
|
'allowIteratorToBeNull' => [
|
|
'<?php
|
|
/**
|
|
* @return Iterator<string>
|
|
*/
|
|
function buildIterator(int $size): Iterator {
|
|
|
|
$values = [];
|
|
for ($i = 0; $i < $size; $i++) {
|
|
$values[] = "Item $i\n";
|
|
}
|
|
|
|
return new ArrayIterator($values);
|
|
}
|
|
|
|
$it = buildIterator(2);
|
|
|
|
if ($it->current() === null) {}'
|
|
],
|
|
'resolveFinalInParentCall' => [
|
|
'<?php
|
|
abstract class A {
|
|
protected static function create() : static {
|
|
return new static();
|
|
}
|
|
|
|
final private function __construct() {}
|
|
}
|
|
|
|
final class B extends A {
|
|
public static function new() : self {
|
|
return parent::create();
|
|
}
|
|
}'
|
|
],
|
|
'noCrashWhenCallingParent' => [
|
|
'<?php
|
|
namespace FooBar;
|
|
|
|
class Datetime extends \DateTime
|
|
{
|
|
public static function createFromInterface(\DatetimeInterface $datetime): \DateTime
|
|
{
|
|
return parent::createFromInterface($datetime);
|
|
}
|
|
}',
|
|
[],
|
|
['MixedReturnStatement', 'MixedInferredReturnType'],
|
|
'8.0'
|
|
]
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return iterable<string,array{string,error_message:string,1?:string[],2?:bool,3?:string}>
|
|
*/
|
|
public function providerInvalidCodeParse(): iterable
|
|
{
|
|
return [
|
|
'staticInvocation' => [
|
|
'<?php
|
|
class Foo {
|
|
public function barBar(): void {}
|
|
}
|
|
|
|
Foo::barBar();',
|
|
'error_message' => 'InvalidStaticInvocation',
|
|
],
|
|
'parentStaticCall' => [
|
|
'<?php
|
|
class A {
|
|
/** @return void */
|
|
public function foo(){}
|
|
}
|
|
|
|
class B extends A {
|
|
/** @return void */
|
|
public static function bar(){
|
|
parent::foo();
|
|
}
|
|
}',
|
|
'error_message' => 'InvalidStaticInvocation',
|
|
],
|
|
'mixedMethodCall' => [
|
|
'<?php
|
|
class Foo {
|
|
public static function barBar(): void {}
|
|
}
|
|
|
|
/** @var mixed */
|
|
$a = (new Foo());
|
|
|
|
$a->barBar();',
|
|
'error_message' => 'MixedMethodCall',
|
|
'error_levels' => [
|
|
'MissingPropertyType',
|
|
'MixedAssignment',
|
|
],
|
|
],
|
|
'invalidMethodCall' => [
|
|
'<?php
|
|
("hello")->someMethod();',
|
|
'error_message' => 'InvalidMethodCall',
|
|
],
|
|
'possiblyInvalidMethodCall' => [
|
|
'<?php
|
|
class A1 {
|
|
public function methodOfA(): void {
|
|
}
|
|
}
|
|
|
|
/** @param A1|string $x */
|
|
function example($x, bool $isObject) : void {
|
|
if ($isObject) {
|
|
$x->methodOfA();
|
|
}
|
|
}',
|
|
'error_message' => 'PossiblyInvalidMethodCall',
|
|
],
|
|
'selfNonStaticInvocation' => [
|
|
'<?php
|
|
class A {
|
|
public function fooFoo(): void {}
|
|
|
|
public static function barBar(): void {
|
|
self::fooFoo();
|
|
}
|
|
}',
|
|
'error_message' => 'NonStaticSelfCall',
|
|
],
|
|
'noParent' => [
|
|
'<?php
|
|
class Foo {
|
|
public function barBar(): void {
|
|
parent::barBar();
|
|
}
|
|
}',
|
|
'error_message' => 'ParentNotFound',
|
|
],
|
|
'coercedClass' => [
|
|
'<?php
|
|
class NullableClass {
|
|
}
|
|
|
|
class NullableBug {
|
|
/**
|
|
* @param class-string|null $className
|
|
* @return object|null
|
|
*/
|
|
public static function mock($className) {
|
|
if (!$className) { return null; }
|
|
return new $className();
|
|
}
|
|
|
|
/**
|
|
* @return ?NullableClass
|
|
*/
|
|
public function returns_nullable_class() {
|
|
/** @psalm-suppress ArgumentTypeCoercion */
|
|
return self::mock("NullableClass");
|
|
}
|
|
}',
|
|
'error_message' => 'LessSpecificReturnStatement',
|
|
'error_levels' => ['MixedInferredReturnType', 'MixedReturnStatement', 'MixedMethodCall'],
|
|
],
|
|
'undefinedVariableStaticCall' => [
|
|
'<?php
|
|
$foo::bar();',
|
|
'error_message' => 'UndefinedGlobalVariable',
|
|
],
|
|
'staticCallOnString' => [
|
|
'<?php
|
|
class A {
|
|
public static function bar(): int {
|
|
return 5;
|
|
}
|
|
}
|
|
$foo = "A";
|
|
/** @psalm-suppress InvalidStringClass */
|
|
$b = $foo::bar();',
|
|
'error_message' => 'MixedAssignment',
|
|
],
|
|
'possiblyNullFunctionCall' => [
|
|
'<?php
|
|
$this->foo();',
|
|
'error_message' => 'InvalidScope',
|
|
],
|
|
'possiblyFalseReference' => [
|
|
'<?php
|
|
class A {
|
|
public function bar(): void {}
|
|
}
|
|
|
|
$a = rand(0, 1) ? new A : false;
|
|
$a->bar();',
|
|
'error_message' => 'PossiblyFalseReference',
|
|
],
|
|
'undefinedParentClass' => [
|
|
'<?php
|
|
/**
|
|
* @psalm-suppress UndefinedClass
|
|
*/
|
|
class B extends A {}
|
|
|
|
$b = new B();',
|
|
'error_message' => 'MissingDependency - src' . DIRECTORY_SEPARATOR . 'somefile.php:7',
|
|
],
|
|
'variableMethodCallOnArray' => [
|
|
'<?php
|
|
$arr = [];
|
|
$b = "foo";
|
|
$arr->$b();',
|
|
'error_message' => 'InvalidMethodCall',
|
|
],
|
|
'intVarStaticCall' => [
|
|
'<?php
|
|
$a = 5;
|
|
$a::bar();',
|
|
'error_message' => 'UndefinedClass',
|
|
],
|
|
'intVarNewCall' => [
|
|
'<?php
|
|
$a = 5;
|
|
new $a();',
|
|
'error_message' => 'UndefinedClass',
|
|
],
|
|
'invokeTypeMismatch' => [
|
|
'<?php
|
|
class A {
|
|
public function __invoke(string $p): void {}
|
|
}
|
|
|
|
$q = new A;
|
|
$q(1);',
|
|
'error_message' => 'InvalidScalarArgument',
|
|
],
|
|
'explicitInvokeTypeMismatch' => [
|
|
'<?php
|
|
class A {
|
|
public function __invoke(string $p): void {}
|
|
}
|
|
(new A)->__invoke(1);',
|
|
'error_message' => 'InvalidScalarArgument',
|
|
],
|
|
'undefinedMethodPassedAsArg' => [
|
|
'<?php
|
|
class A {
|
|
public function __call(string $method, array $args) {}
|
|
}
|
|
|
|
$q = new A;
|
|
$q->foo(bar());',
|
|
'error_message' => 'UndefinedFunction',
|
|
],
|
|
'noIntersectionMethod' => [
|
|
'<?php
|
|
interface A {}
|
|
interface B {}
|
|
|
|
/** @param B&A $p */
|
|
function f($p): void {
|
|
$p->zugzug();
|
|
}',
|
|
'error_message' => 'UndefinedInterfaceMethod - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:29 - Method (B&A)::zugzug does not exist',
|
|
],
|
|
'noInstanceCallAsStatic' => [
|
|
'<?php
|
|
class C {
|
|
public function foo() : void {}
|
|
}
|
|
|
|
(new C)::foo();',
|
|
'error_message' => 'InvalidStaticInvocation',
|
|
],
|
|
'noExceptionOnMissingClass' => [
|
|
'<?php
|
|
/** @psalm-suppress UndefinedClass */
|
|
class A
|
|
{
|
|
/** @var class-string<Foo> */
|
|
protected $bar;
|
|
|
|
public function foo(string $s): void
|
|
{
|
|
$bar = $this->bar;
|
|
$bar::baz();
|
|
}
|
|
}',
|
|
'error_message' => 'MissingConstructor',
|
|
],
|
|
'checkMixedMethodCallStaticMethodCallArg' => [
|
|
'<?php
|
|
class B {}
|
|
/** @param mixed $a */
|
|
function foo($a) : void {
|
|
/** @psalm-suppress MixedMethodCall */
|
|
$a->bar(B::bat());
|
|
}',
|
|
'error_message' => 'UndefinedMethod',
|
|
],
|
|
'complainAboutUndefinedPropertyOnMixedCall' => [
|
|
'<?php
|
|
class C {
|
|
/** @param mixed $a */
|
|
public function foo($a) : void {
|
|
/** @psalm-suppress MixedMethodCall */
|
|
$a->bar($this->d);
|
|
}
|
|
}',
|
|
'error_message' => 'UndefinedThisPropertyFetch',
|
|
],
|
|
'complainAboutUndefinedPropertyOnMixedCallConcatOp' => [
|
|
'<?php
|
|
class A {
|
|
/**
|
|
* @psalm-suppress MixedMethodCall
|
|
*/
|
|
public function foo(object $a) : void {
|
|
$a->bar("bat" . $this->baz);
|
|
}
|
|
}',
|
|
'error_message' => 'UndefinedThisPropertyFetch',
|
|
],
|
|
'alreadyHasmethod' => [
|
|
'<?php
|
|
class A {
|
|
public function foo() : void {}
|
|
}
|
|
|
|
function foo(A $a) : void {
|
|
if (method_exists($a, "foo")) {
|
|
$object->foo();
|
|
}
|
|
}',
|
|
'error_message' => 'RedundantCondition',
|
|
],
|
|
'possiblyNullOrMixedArg' => [
|
|
'<?php
|
|
class A {
|
|
/**
|
|
* @var mixed
|
|
*/
|
|
public $foo;
|
|
}
|
|
|
|
function takesString(string $s) : void {}
|
|
|
|
function takesA(?A $a) : void {
|
|
/**
|
|
* @psalm-suppress PossiblyNullPropertyFetch
|
|
* @psalm-suppress MixedArgument
|
|
*/
|
|
takesString($a->foo);
|
|
}',
|
|
'error_message' => 'PossiblyNullArgument',
|
|
],
|
|
'callOnVoid' => [
|
|
'<?php
|
|
class A {
|
|
public function foo(): void {}
|
|
}
|
|
|
|
$p = new A();
|
|
$p->foo()->bar();',
|
|
'error_message' => 'NullReference'
|
|
],
|
|
'dateTimeNullFirstArg' => [
|
|
'<?php
|
|
$date = new DateTime(null);',
|
|
'error_message' => 'NullArgument'
|
|
],
|
|
'noCrashOnGetClassMethodCall' => [
|
|
'<?php
|
|
class User {
|
|
/**
|
|
* @psalm-suppress MixedArgument
|
|
*/
|
|
public function give(): void{
|
|
/** @var mixed */
|
|
$model = null;
|
|
$class = \get_class($model);
|
|
$class::foo();
|
|
}
|
|
}',
|
|
'error_message' => 'InvalidStringClass',
|
|
],
|
|
'preventAbstractMethodCall' => [
|
|
'<?php
|
|
abstract class Base {
|
|
public static function callAbstract() : void {
|
|
static::bar();
|
|
}
|
|
|
|
abstract static function bar() : void;
|
|
}
|
|
|
|
Base::bar();',
|
|
'error_message' => 'AbstractMethodCall',
|
|
],
|
|
'tooManyArgumentsToStatic' => [
|
|
'<?php
|
|
class A {
|
|
public static function fooFoo(int $a): void {}
|
|
}
|
|
|
|
A::fooFoo(5, "dfd");',
|
|
'error_message' => 'TooManyArguments',
|
|
],
|
|
'tooFewArgumentsToStatic' => [
|
|
'<?php
|
|
class A {
|
|
public static function fooFoo(int $a): void {}
|
|
}
|
|
|
|
A::fooFoo();',
|
|
'error_message' => 'TooFewArguments',
|
|
],
|
|
'tooManyArgumentsToInstance' => [
|
|
'<?php
|
|
class A {
|
|
public function fooFoo(int $a): void {}
|
|
}
|
|
|
|
(new A)->fooFoo(5, "dfd");',
|
|
'error_message' => 'TooManyArguments',
|
|
],
|
|
'tooFewArgumentsToInstance' => [
|
|
'<?php
|
|
class A {
|
|
public function fooFoo(int $a): void {}
|
|
}
|
|
|
|
(new A)->fooFoo();',
|
|
'error_message' => 'TooFewArguments',
|
|
],
|
|
'getterAutomagicOverridden' => [
|
|
'<?php
|
|
class A {
|
|
/** @var string|null */
|
|
public $a;
|
|
|
|
/** @return string|null */
|
|
function getA() {
|
|
return $this->a;
|
|
}
|
|
}
|
|
|
|
class AChild extends A {
|
|
function getA() {
|
|
return rand(0, 1) ? $this->a : null;
|
|
}
|
|
}
|
|
|
|
function foo(A $a) : void {
|
|
if ($a->getA()) {
|
|
echo strlen($a->getA());
|
|
}
|
|
}
|
|
|
|
foo(new AChild());',
|
|
'error_message' => 'PossiblyNullArgument'
|
|
],
|
|
'getterAutomagicOverriddenWithAssertion' => [
|
|
'<?php
|
|
class A {
|
|
/** @var string|null */
|
|
public $a;
|
|
|
|
/** @psalm-assert-if-true string $this->a */
|
|
function hasA() {
|
|
return is_string($this->a);
|
|
}
|
|
|
|
/** @return string|null */
|
|
function getA() {
|
|
return $this->a;
|
|
}
|
|
}
|
|
|
|
class AChild extends A {
|
|
function getA() {
|
|
return rand(0, 1) ? $this->a : null;
|
|
}
|
|
}
|
|
|
|
function foo(A $a) : void {
|
|
if ($a->hasA()) {
|
|
echo strlen($a->getA());
|
|
}
|
|
}
|
|
|
|
foo(new AChild());',
|
|
'error_message' => 'PossiblyNullArgument'
|
|
],
|
|
'checkVariableInUnknownClassConstructor' => [
|
|
'<?php
|
|
/** @psalm-suppress UndefinedClass */
|
|
new Missing($class_arg);',
|
|
'error_message' => 'PossiblyUndefinedVariable',
|
|
],
|
|
'unchainedInferredInferredMutationFreeMethodCallDontMemoize' => [
|
|
'<?php
|
|
class SomeClass {
|
|
private ?int $int;
|
|
|
|
public function __construct() {
|
|
$this->int = 1;
|
|
}
|
|
|
|
public function getInt(): ?int {
|
|
return $this->int;
|
|
}
|
|
}
|
|
|
|
function printInt(int $int): void {
|
|
echo $int;
|
|
}
|
|
|
|
$obj = new SomeClass();
|
|
|
|
if ($obj->getInt()) {
|
|
printInt($obj->getInt());
|
|
}',
|
|
'error_message' => 'PossiblyNullArgument',
|
|
],
|
|
'getterTypeInferringWithChange' => [
|
|
'<?php
|
|
class A {
|
|
/** @var int|string|null */
|
|
public $val;
|
|
|
|
/** @return int|string|null */
|
|
final public function getValue() {
|
|
return $this->val;
|
|
}
|
|
}
|
|
|
|
$a = new A();
|
|
|
|
if (is_string($a->getValue())) {
|
|
$a->val = 5;
|
|
echo strlen($a->getValue());
|
|
}',
|
|
'error_message' => 'InvalidScalarArgument',
|
|
],
|
|
];
|
|
}
|
|
}
|