addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } public function testMethodCallMemoize(): void { $this->project_analyzer->getConfig()->memoize_method_calls = true; $this->addFile( 'somefile.php', 'getFoo()) { if ($a->getFoo()->getBar()) { $a->getFoo()->getBar()->bat(); } }', ); $this->analyzeFile('somefile.php', new Context()); } public function testPropertyMethodCallMemoize(): void { $this->project_analyzer->getConfig()->memoize_method_calls = true; $this->addFile( 'somefile.php', '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 Context()); } public function testPropertyMethodCallMutationFreeMemoize(): void { $this->project_analyzer->getConfig()->memoize_method_calls = true; $this->addFile( 'somefile.php', '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 Context()); } public function testUnchainedMethodCallMemoize(): void { $this->project_analyzer->getConfig()->memoize_method_calls = true; $this->addFile( 'somefile.php', '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 Context()); } public function testUnchainedMutationFreeMethodCallMemoize(): void { $this->project_analyzer->getConfig()->memoize_method_calls = true; $this->addFile( 'somefile.php', '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 Context()); } public function providerValidCodeParse(): iterable { return [ 'notInCallMapTest' => [ 'code' => ' [ 'code' => ' [ 'code' => 'barBar();', ], 'staticInvocation' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => 'sub(new DateInterval("P1D")); $b = (new DateTimeImmutable())->modify("+3 hours");', 'assertions' => [ '$yesterday' => 'MyDate', '$b' => 'DateTimeImmutable', ], ], 'magicCall' => [ 'code' => 'bar();', 'assertions' => [ '$s' => 'string', ], ], 'canBeCalledOnMagic' => [ 'code' => 'maybeUndefinedMethod();', 'assertions' => [], 'ignored_issues' => ['PossiblyUndefinedMethod'], ], 'canBeCalledOnMagicWithMethod' => [ 'code' => 'bar();', 'assertions' => [], ], 'invokeCorrectType' => [ 'code' => ' [ 'code' => 'createElement("foo"); if ($node instanceof DOMElement) { $newnode = $doc->appendChild($node); $newnode->setAttribute("bar", "baz"); }', ], 'nonStaticSelfCall' => [ 'code' => 'call());', ], 'simpleXml' => [ 'code' => '"); $a = $xml->asXML(); $b = $xml->asXML("foo.xml");', 'assertions' => [ '$a' => 'false|string', '$b' => 'bool', ], ], 'datetimeformatNotFalse' => [ 'code' => 'format($format); if (false !== $formatted) {} function takesString(string $s) : void {} takesString($formatted);', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '7.4', ], 'domElement' => [ 'code' => 'getElementsByTagName("bar"); $b = $a->item(0); if (!$b) { return null; } return $b->getAttribute("bat"); }', ], 'domElementIteratorOrEmptyArray' => [ 'code' => 'loadXML($XML); $elements = rand(0, 1) ? $dom->getElementsByTagName("bar") : []; foreach ($elements as $element) { $element->getElementsByTagName("bat"); } }', ], 'reflectionParameter' => [ 'code' => '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' => [], 'ignored_issues' => [], 'php_version' => '8.0', ], 'PDOMethod' => [ 'code' => 'sqliteCreateFunction("md5rev", "md5_and_reverse", 1);', ], 'dontConvertedMaybeMixedAfterCall' => [ 'code' => ' $b */ function foo(array $a, array $b) : void { $c = array_merge($b, $a); foreach ($c as $d) { $d->foo(); if ($d instanceof B) {} } }', 'assertions' => [], 'ignored_issues' => ['MixedAssignment', 'MixedMethodCall'], ], 'methodResolution' => [ 'code' => 'getId()); (is_object($a) && method_exists($a, "getS")) ? (string)$a->GETS() : ""; return $user->getId(); }', ], 'defineVariableCreatedInArgToMixed' => [ 'code' => 'foo($b = (int) "5")) { echo $b; } }', 'assertions' => [], 'ignored_issues' => ['MixedMethodCall', 'MissingParamType'], ], 'staticCallAfterMethodExists' => [ 'code' => ' [ 'code' => 'bar();', ], 'pdoStatementSetFetchMode' => [ 'code' => 'setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); $db->setAttribute(PDO::ATTR_DEFAULT_FETCH_MODE, PDO::FETCH_ASSOC); $stmt = $db->prepare("select \"a\" as a"); $stmt->setFetchMode(PDO::FETCH_CLASS, A::class); $stmt2 = $db->prepare("select \"a\" as a"); $stmt2->setFetchMode(PDO::FETCH_ASSOC); $stmt3 = $db->prepare("select \"a\" as a"); $stmt3->setFetchMode(PDO::ATTR_DEFAULT_FETCH_MODE); $stmt->execute(); $stmt2->execute(); /** @psalm-suppress MixedAssignment */ $a = $stmt->fetch(); $b = $stmt->fetchAll(); $c = $stmt->fetch(PDO::FETCH_CLASS); $d = $stmt->fetchAll(PDO::FETCH_CLASS); $e = $stmt->fetchAll(PDO::FETCH_CLASS, B::class); $f = $stmt->fetch(PDO::FETCH_ASSOC); $g = $stmt->fetchAll(PDO::FETCH_ASSOC); /** @psalm-suppress MixedAssignment */ $h = $stmt2->fetch(); $i = $stmt2->fetchAll(); $j = $stmt2->fetch(PDO::FETCH_BOTH); $k = $stmt2->fetchAll(PDO::FETCH_BOTH); /** @psalm-suppress MixedAssignment */ $l = $stmt3->fetch();', 'assertions' => [ '$a' => 'mixed', '$b' => 'array|false', '$c' => 'false|object', '$d' => 'list', '$e' => 'list', '$f' => 'array|false', '$g' => 'list>', '$h' => 'mixed', '$i' => 'array|false', '$j' => 'array|false', '$k' => 'list>', '$l' => 'mixed', ], ], 'datePeriodConstructor' => [ 'code' => ' [ 'code' => 'bar(); } }', ], 'callMethodAfterCheckingExistenceInClosure' => [ 'code' => 'bar(); })(); } }', ], 'callManyMethodsAfterCheckingExistence' => [ 'code' => 'foo(); $object->bar(); }', ], 'callManyMethodsAfterCheckingExistenceChained' => [ 'code' => 'foo(); $object->bar(); } }', ], 'callManyMethodsOnKnownObjectAfterCheckingExistenceChained' => [ 'code' => 'foo(); $object->bar(); } }', ], 'preserveMethodExistsType' => [ 'code' => ' [ 'code' => ' $foo */ function foo(string $foo): string { if (!method_exists($foo, "something")) { return ""; } return $foo; }', ], 'pdoStatementFetchColumn' => [ 'code' => 'prepare("SELECT 1"); $sth->execute(); return $sth->fetch(PDO::FETCH_COLUMN); }', ], 'pdoStatementFetchAllColumn' => [ 'code' => ' */ function fetch_column() { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetchAll(PDO::FETCH_COLUMN); }', ], 'pdoStatementFetchKeyPair' => [ 'code' => ' */ function fetch_column() { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetch(PDO::FETCH_KEY_PAIR); }', ], 'pdoStatementFetchAllKeyPair' => [ 'code' => ' */ function fetch_column() { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetchAll(PDO::FETCH_KEY_PAIR); }', ], 'pdoStatementFetchAssoc' => [ 'code' => '|false */ function fetch_assoc() { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetch(PDO::FETCH_ASSOC); }', ], 'pdoStatementFetchAllAssoc' => [ 'code' => '> */ function fetch_assoc() : array { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetchAll(PDO::FETCH_ASSOC); }', ], 'pdoStatementFetchBoth' => [ 'code' => '|false */ function fetch_both() { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetch(PDO::FETCH_BOTH); }', ], 'pdoStatementFetchAllBoth' => [ 'code' => '> */ function fetch_both() : array { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetchAll(PDO::FETCH_BOTH); }', ], 'pdoStatementFetchBound' => [ 'code' => 'prepare("SELECT 1"); $sth->execute(); return $sth->fetch(PDO::FETCH_BOUND); }', ], 'pdoStatementFetchAllBound' => [ 'code' => ' */ function fetch_both() : array { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetchAll(PDO::FETCH_BOUND); }', ], 'pdoStatementFetchClass' => [ 'code' => 'prepare("SELECT 1"); $sth->execute(); return $sth->fetch(PDO::FETCH_CLASS); }', ], 'pdoStatementFetchAllClass' => [ 'code' => ' */ function fetch_class() : array { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetchAll(PDO::FETCH_CLASS); }', ], 'pdoStatementFetchAllNamedClass' => [ 'code' => ' */ function fetch_class() : array { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetchAll(PDO::FETCH_CLASS, Foo::class); }', ], 'pdoStatementFetchLazy' => [ 'code' => 'prepare("SELECT 1"); $sth->execute(); return $sth->fetch(PDO::FETCH_LAZY); }', ], 'pdoStatementFetchNamed' => [ 'code' => '>|false */ function fetch_named() { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetch(PDO::FETCH_NAMED); }', ], 'pdoStatementFetchAllNamed' => [ 'code' => '>> */ function fetch_named() : array { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetchAll(PDO::FETCH_NAMED); }', ], 'pdoStatementFetchNum' => [ 'code' => '|false */ function fetch_named() { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetch(PDO::FETCH_NUM); }', ], 'pdoStatementFetchAllNum' => [ 'code' => '> */ function fetch_named() : array { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetchAll(PDO::FETCH_NUM); }', ], 'pdoStatementFetchObj' => [ 'code' => 'prepare("SELECT 1"); $sth->execute(); return $sth->fetch(PDO::FETCH_OBJ); }', ], 'pdoStatementFetchAllObj' => [ 'code' => ' */ function fetch_named() : array { $p = new PDO("sqlite::memory:"); $sth = $p->prepare("SELECT 1"); $sth->execute(); return $sth->fetchAll(PDO::FETCH_OBJ); }', ], 'dateTimeSecondArg' => [ 'code' => 'format("Y-m-d H:i:sP") . "\n";', ], 'noCrashOnGetClassMethodCallWithNull' => [ 'code' => ' [ 'code' => 'passedByRef($this->b); } }', ], 'maybeNotTooManyArgumentsToInstance' => [ 'code' => 'fooFoo(5, "dfd");', ], 'interfaceMethodCallCheck' => [ 'code' => 'foo(""); } } takesWithoutArguments(new C);', ], 'getterAutomagicAssertion' => [ 'code' => 'a; } } $a = new A(); if ($a->getA()) { echo strlen($a->getA()); }', ], 'ignorePossiblyNull' => [ 'code' => 'getType(); } /** * @psalm-ignore-nullable-return */ public function getType() : ?string { return $this->type; } }', ], 'abstractMethodExistsOnChild' => [ 'code' => 'createFoo(); } }', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '7.4', ], 'pdoQueryTwoArgs' => [ 'code' => 'query("SELECT * FROM projects", PDO::FETCH_NAMED);', ], 'unchainedInferredMutationFreeMethodCallMemoize' => [ 'code' => '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' => [ 'code' => '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' => [ 'code' => 'property; } public function test(int $int): void { if ($this->getProperty() !== null) { $this->getProperty()->test(); } } }', ], 'inferredFinalMethod' => [ 'code' => 'property; } } $main = new MainClass(); if ($main->getProperty() !== null && $main->getProperty()->test()) {}', ], 'getterTypeInferring' => [ 'code' => 'a; } } $a = new A(); if (is_string($a->getValue())) { echo strlen($a->getValue()); }', ], 'newSplObjectStorageDefaultEmpty' => [ 'code' => ' [ '$a' => 'SplObjectStorage', ], ], 'allowIteratorToBeNull' => [ 'code' => ' */ 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' => [ 'code' => ' [ 'code' => ' [], 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'], 'php_version' => '8.0', ], 'nullsafeShortCircuit' => [ 'code' => 'getBar()->doBaz();', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '8.0', ], 'parentMagicMethodCall' => [ 'code' => 'create([]);', 'assertions' => [ '$n' => 'BlahModel', ], ], 'methodLevelGenericsWillBeInherited' => [ 'code' => 'method("a"); /** @psalm-check-type-exact $_v = "a" */', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '8.0', ], 'phpdocObjectTypeAndReferenceInParameter' => [ 'code' => 'bar($x);', ], ]; } public function providerInvalidCodeParse(): iterable { return [ 'staticInvocation' => [ 'code' => ' 'InvalidStaticInvocation', ], 'parentStaticCall' => [ 'code' => ' 'InvalidStaticInvocation', ], 'mixedMethodCall' => [ 'code' => 'barBar();', 'error_message' => 'MixedMethodCall', 'ignored_issues' => [ 'MissingPropertyType', 'MixedAssignment', ], ], 'invalidMethodCall' => [ 'code' => 'someMethod();', 'error_message' => 'InvalidMethodCall', ], 'possiblyInvalidMethodCall' => [ 'code' => 'methodOfA(); } }', 'error_message' => 'PossiblyInvalidMethodCall', ], 'selfNonStaticInvocation' => [ 'code' => ' 'NonStaticSelfCall', ], 'noParent' => [ 'code' => ' 'ParentNotFound', ], 'coercedClass' => [ 'code' => ' 'LessSpecificReturnStatement', 'ignored_issues' => ['MixedInferredReturnType', 'MixedReturnStatement', 'MixedMethodCall'], ], 'undefinedVariableStaticCall' => [ 'code' => ' 'UndefinedGlobalVariable', ], 'staticCallOnString' => [ 'code' => ' 'MixedAssignment', ], 'possiblyNullFunctionCall' => [ 'code' => 'foo();', 'error_message' => 'InvalidScope', ], 'possiblyFalseReference' => [ 'code' => 'bar();', 'error_message' => 'PossiblyFalseReference', ], 'undefinedParentClass' => [ 'code' => ' 'MissingDependency - src' . DIRECTORY_SEPARATOR . 'somefile.php:7', ], 'variableMethodCallOnArray' => [ 'code' => '$b();', 'error_message' => 'InvalidMethodCall', ], 'intVarStaticCall' => [ 'code' => ' 'UndefinedClass', ], 'intVarNewCall' => [ 'code' => ' 'UndefinedClass', ], 'invokeTypeMismatch' => [ 'code' => ' 'InvalidScalarArgument', ], 'explicitInvokeTypeMismatch' => [ 'code' => '__invoke(1);', 'error_message' => 'InvalidScalarArgument', ], 'undefinedMethodPassedAsArg' => [ 'code' => 'foo(bar());', 'error_message' => 'UndefinedFunction', ], 'noIntersectionMethod' => [ 'code' => 'zugzug(); }', 'error_message' => 'UndefinedInterfaceMethod - src' . DIRECTORY_SEPARATOR . 'somefile.php:7:29 - Method (B&A)::zugzug does not exist', ], 'noInstanceCallAsStatic' => [ 'code' => ' 'InvalidStaticInvocation', ], 'noExceptionOnMissingClass' => [ 'code' => ' */ protected $bar; public function foo(string $s): void { $bar = $this->bar; $bar::baz(); } }', 'error_message' => 'MissingConstructor', ], 'checkMixedMethodCallStaticMethodCallArg' => [ 'code' => 'bar(B::bat()); }', 'error_message' => 'UndefinedMethod', ], 'complainAboutUndefinedPropertyOnMixedCall' => [ 'code' => 'bar($this->d); } }', 'error_message' => 'UndefinedThisPropertyFetch', ], 'complainAboutUndefinedPropertyOnMixedCallConcatOp' => [ 'code' => 'bar("bat" . $this->baz); } }', 'error_message' => 'UndefinedThisPropertyFetch', ], 'alreadyHasmethod' => [ 'code' => 'foo(); } }', 'error_message' => 'RedundantCondition', ], 'possiblyNullOrMixedArg' => [ 'code' => 'foo); }', 'error_message' => 'PossiblyNullArgument', ], 'callOnVoid' => [ 'code' => 'foo()->bar();', 'error_message' => 'NullReference', ], 'dateTimeNullFirstArg' => [ 'code' => ' 'NullArgument', ], 'noCrashOnGetClassMethodCall' => [ 'code' => ' 'InvalidStringClass', ], 'preventAbstractMethodCall' => [ 'code' => ' 'AbstractMethodCall', ], 'tooManyArgumentsToStatic' => [ 'code' => ' 'TooManyArguments', ], 'tooFewArgumentsToStatic' => [ 'code' => ' 'TooFewArguments', ], 'tooManyArgumentsToInstance' => [ 'code' => 'fooFoo(5, "dfd");', 'error_message' => 'TooManyArguments', ], 'tooFewArgumentsToInstance' => [ 'code' => 'fooFoo();', 'error_message' => 'TooFewArguments', ], 'getterAutomagicOverridden' => [ 'code' => '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' => [ 'code' => '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' => [ 'code' => ' 'PossiblyUndefinedVariable', ], 'unchainedInferredInferredMutationFreeMethodCallDontMemoize' => [ 'code' => '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' => [ 'code' => 'val; } } $a = new A(); if (is_string($a->getValue())) { $a->val = 5; echo strlen($a->getValue()); }', 'error_message' => 'InvalidScalarArgument', ], 'possiblyNullReferenceInInvokedCall' => [ 'code' => 'getLocation()->getId()); }', 'error_message' => 'PossiblyNullReference', ], 'nullsafeShortCircuitInVariable' => [ 'code' => 'getBar(); $a->doBaz();', 'error_message' => 'PossiblyNullReference', 'ignored_issues' => [], 'php_version' => '8.0', ], 'undefinedMethodOnParentCallWithMethodExistsOnSelf' => [ 'code' => ' 'UndefinedMethod', ], 'incorrectCallableParamDefault' => [ 'code' => ' 'InvalidParamDefault', ], ]; } }