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);
$stmt = $db->prepare("select \"a\" as a");
$stmt->setFetchMode(PDO::FETCH_CLASS, A::class);
$stmt->execute();
/** @psalm-suppress MixedAssignment */
$a = $stmt->fetch();',
],
'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;
}',
],
'pdoStatementFetchAssoc' => [
'code' => '|false */
function fetch_assoc() {
$p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1");
$sth->execute();
return $sth->fetch(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);
}',
],
'pdoStatementFetchBound' => [
'code' => 'prepare("SELECT 1");
$sth->execute();
return $sth->fetch(PDO::FETCH_BOUND);
}',
],
'pdoStatementFetchClass' => [
'code' => 'prepare("SELECT 1");
$sth->execute();
return $sth->fetch(PDO::FETCH_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);
}',
],
'pdoStatementFetchNum' => [
'code' => '|false */
function fetch_named() {
$p = new PDO("sqlite::memory:");
$sth = $p->prepare("SELECT 1");
$sth->execute();
return $sth->fetch(PDO::FETCH_NUM);
}',
],
'pdoStatementFetchObj' => [
'code' => 'prepare("SELECT 1");
$sth->execute();
return $sth->fetch(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',
],
],
];
}
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',
],
];
}
}