remember_property_assignments_after_call = false; $this->addFile( 'somefile.php', 'bar */ function assertBarNotNull(Foo $foo): bool { return $foo->bar !== null; } $foo = new Foo(); if (assertBarNotNull($foo)) { $foo->mutationFree(); requiresString($foo->bar); } function requiresString(string $str): void {} ' ); $this->analyzeFile('somefile.php', new Context()); } public function testForgetAssertionAfterNonMutationFreeCall(): void { $this->expectExceptionMessage('PossiblyNullArgument'); $this->expectException(CodeException::class); Config::getInstance()->remember_property_assignments_after_call = false; $this->addFile( 'somefile.php', 'bar */ function assertBarNotNull(Foo $foo): bool { return $foo->bar !== null; } $foo = new Foo(); if (assertBarNotNull($foo)) { $foo->nonMutationFree(); requiresString($foo->bar); } function requiresString(string $_str): void {} ' ); $this->analyzeFile('somefile.php', new Context()); } /** * @return iterable,error_levels?:string[]}> */ public function providerValidCodeParse(): iterable { return [ 'implictAssertInstanceOfB' => [ 'foo(); }', ], 'implicitAssertEqualsNull' => [ ' [ ' [ ' [ ' [ 'bar(); $a->foo(); }', ], 'implicitAssertInstanceOfMultipleInterfaces' => [ 'bar(); $a->foo1(); }', ], 'implicitAssertInstanceOfBInClassMethod' => [ 'assertInstanceOfB($a); $a->foo(); } }', ], 'implicitAssertPropertyNotNull' => [ 'a) { throw new \Exception(); } } public function takesA(A $a): void { $this->assertNotNullProperty(); $a->foo(); } }', ], 'implicitAssertWithoutRedundantCondition' => [ ' [ 'foo(); }', ], 'assertIfTrueAnnotation' => [ ' [ ' [ ' [ 'foo()); assertFalse($a->bar()); }', ], 'assertAllStrings' => [ ' $i * * @param iterable $i */ function assertAllStrings(iterable $i): void { /** @psalm-suppress MixedAssignment */ foreach ($i as $s) { if (!is_string($s)) { throw new \UnexpectedValueException(""); } } } function getArray(): array { return []; } function getIterable(): iterable { return []; } $array = getArray(); assertAllStrings($array); $iterable = getIterable(); assertAllStrings($iterable);', [ '$array' => 'array', '$iterable' => 'iterable', ], ], 'assertStaticMethodIfFalse' => [ ' [ ' [ 'bar();', ], 'assertThisType' => [ 'isFoo(); $t->bar(); }' ], 'assertThisTypeIfTrue' => [ 'isFoo()) { $t->bar(); } }' ], 'assertThisTypeCombined' => [ 'assertFoo(); $t->assertBar(); $t->foo(); $t->bar(); }' ], 'assertThisTypeCombinedInsideMethod' => [ 'assertFoo(); $t->assertBar(); $t->foo(); $t->bar(); } } interface FooType { public function foo(): void; } interface BarType { public function bar(): void; } ' ], 'assertThisTypeSimpleCombined' => [ 'assertBar(); $t->foo(); $t->bar(); }' ], 'assertThisTypeIfTrueCombined' => [ 'assertFoo() && $t->assertBar()) { $t->foo(); $t->bar(); } }' ], 'assertThisTypeSimpleAndIfTrueCombined' => [ 'isFoo()) { $t->foo(); } $t->bar(); }' ], 'assertThisTypeSwitchTrue' => [ 'isFoo(): $t->bar(); } }' ], 'assertNotArray' => [ ' [ 'assertProperty()) { $this->a->foo(); } } /** * @psalm-assert-if-true !null $this->a */ public function assertProperty() : bool { return $this->a !== null; } }' ], 'assertIfFalseOnProperty' => [ 'assertProperty()) { $this->a->foo(); } } /** * @psalm-assert-if-false null $this->a */ public function assertProperty() : bool { return $this->a !== null; } }' ], 'assertIfTrueOnPropertyNegated' => [ 'assertProperty()) { $this->a->foo(); } } /** * @psalm-assert-if-true null $this->a */ public function assertProperty() : bool { return $this->a !== null; } }' ], 'assertIfFalseOnPropertyNegated' => [ 'assertProperty()) { $this->a->foo(); } } /** * @psalm-assert-if-false !null $this->a */ public function assertProperty() : bool { return $this->a !== null; } }' ], 'assertPropertyVisibleOutside' => [ 'x = 0; } } /** * @psalm-assert !null $this->x */ public function assertProperty() : void { if (is_null($this->x)) { throw new RuntimeException(); } } } $a = new A(); $a->maybeAssignX(); $a->assertProperty(); echo (2 * $a->x);', ], 'parseAssertion' => [ ' $data * @param mixed $data */ function isArrayOfStrings($data): void {} function foo(array $arr) : void { isArrayOfStrings($arr); foreach ($arr as $a) { foreach ($a as $b) { echo $b; } } }' ], 'noExceptionOnShortArrayAssertion' => [ ' [ ' $arr * @return array */ function foo(iterable $arr) : array { isArray($arr); return $arr; }' ], 'listAssertion' => [ ' $arr * @return list */ function foo(array $arr) : array { isList($arr); return $arr; }' ], 'scanAssertionTypes' => [ ' [ ' [ ' [ ' [ 'arr = $arr; } } /** @psalm-immutable */ class A { public B $b; public function __construct(B $b) { $this->b = $b; } /** @psalm-assert-if-true !null $this->b->arr */ public function hasArray() : bool { return $this->b->arr !== null; } } function foo(A $a) : void { if ($a->hasArray()) { echo count($a->b->arr); } }' ], 'assertOnNestedMethod' => [ 'arr = $arr; } public function getArray() : ?array { return $this->arr; } } /** @psalm-immutable */ class A { public B $b; public function __construct(B $b) { $this->b = $b; } /** @psalm-assert-if-true !null $this->b->getarray() */ public function hasArray() : bool { return $this->b->getArray() !== null; } } function foo(A $a) : void { if ($a->hasArray()) { echo count($a->b->getArray()); } }' ], 'assertOnThisMethod' => [ 'arr = $arr; } /** @psalm-assert-if-true !null $this->getarray() */ public function hasArray() : bool { return $this->arr !== null; } public function getArray() : ?array { return $this->arr; } } function foo(A $a) : void { if (!$a->hasArray()) { return; } echo count($a->getArray()); }' ], 'preventErrorWhenAssertingOnArrayUnion' => [ ' $data */ function validate(array $data): void {}' ], 'nonEmptyList' => [ ' */ function consume1($value): array { isNonEmptyList($value); return $value; } /** * @psalm-param list $values */ function consume2(array $values): void { isNonEmptyList($values); foreach ($values as $str) {} echo $str; }' ], 'nonEmptyListOfStrings' => [ ' $array * * @param mixed $array */ function isNonEmptyListOfStrings($array): void {} /** * @psalm-param list $values */ function consume2(array $values): void { isNonEmptyListOfStrings($values); foreach ($values as $str) {} echo $str; }' ], 'assertResource' => [ ' [ ', * env?: array, * buildDeps?: list, * configure?: string * }> * }> * } $data * * @param mixed $data */ function assertStructure($data): void {}' ], 'intersectArraysAfterAssertion' => [ ' [ ' $value * * @param mixed $value * * @throws InvalidArgumentException */ function allString($value): void {} function takesAnArray(array $a): void { $keys = array_keys($a); allString($keys); }', ], 'assertListIsListOfStrings' => [ ' $value * * @param mixed $value * * @throws InvalidArgumentException */ function allString($value): void {} function takesAnArray(array $a): void { $keys = array_keys($a); allString($keys); }', ], 'multipleAssertIfTrue' => [ ' [ ' [ ' [ '' ], 'assertIfTrueStaticSelf' => [ '' ], 'assertIfFalseStaticSelf' => [ '' ], 'assertStaticByInheritedMethod' => [ '' ], 'assertInheritedStatic' => [ '' ], 'assertStaticOnUnrelatedClass' => [ '' ], 'implicitComplexAssertionNoCrash' => [ 'status && "complete" === $status) || ("canceled" === $this->status && "pending" === $status) || ("complete" === $this->status && "canceled" === $status) || ("complete" === $this->status && "pending" === $status) ) { throw new \LogicException(); } } }' ], 'assertArrayIteratorIsIterableOfStrings' => [ ' $value * @param mixed $value * * @return void */ function assertAllString($value) : void { throw new \Exception(\var_export($value, true)); } /** * @param ArrayIterator $value * * @return ArrayIterator */ function preserveContainerAllArrayIterator($value) { assertAllString($value); return $value; }' ], 'implicitReflectionParameterAssertion' => [ 'getParameters(); foreach ($parameters as $parameter) { if ($parameter->hasType()) { $parameter->getType()->__toString(); } }', ], 'reflectionNameTypeClassStringIfNotBuiltin' => [ 'getType(); return ($type instanceof \ReflectionNamedType) && !$type->isBuiltin() ? $type->getName() : null; }', [], [], '7.4', ], 'withHasTypeCall' => [ 'getType() */ public function hasType() : bool { return true; } public function getType() : ?ReflectionType { return null; } } function takesParam(Param $p) : void { if ($p->hasType()) { echo $p->getType()->__toString(); } }', ], 'assertTemplatedIterable' => [ ' $foos * @return array */ function foo(array $foos) : array { allIsInstanceOf($foos, Foo::class); return $foos; } /** * @template ExpectedType of object * * @param mixed $value * @param class-string $class * @psalm-assert iterable $value */ function allIsInstanceOf($value, $class): void {}' ], 'implicitReflectionPropertyAssertion' => [ 'getProperties(); foreach ($properties as $property) { if ($property->hasType()) { $property->getType()->allowsNull(); } }', [], [], '7.4' ], 'onPropertyOfImmutableArgument' => [ 'b = $b; } } /** @psalm-assert !null $item->b */ function c(\Aclass $item): void { if (null === $item->b) { throw new \InvalidArgumentException(""); } } /** @var \Aclass $a */ c($a); echo strlen($a->b);', ], 'inTrueOnPropertyOfImmutableArgument' => [ 'b = $b; } } /** @psalm-assert-if-true !null $item->b */ function c(A $item): bool { return null !== $item->b; } function check(int $a): void {} /** @var A $a */ if (c($a)) { check($a->b); }', ], 'inFalseOnPropertyOfAImmutableArgument' => [ 'b = $b; } } /** @psalm-assert-if-false !null $item->b */ function c(A $item): bool { return null === $item->b; } function check(int $a): void {} /** @var A $a */ if (!c($a)) { check($a->b); }', ], 'ifTrueOnNestedPropertyOfArgument' => [ 'c = $c; } } /** @psalm-immutable */ class Aclass { public B $b; public function __construct(B $b) { $this->b = $b; } } /** @psalm-assert-if-true !null $item->b->c */ function c(\Aclass $item): bool { return null !== $item->b->c; } $a = new \Aclass(new \B(null)); if (c($a)) { echo strlen($a->b->c); }', ], 'ifFalseOnNestedPropertyOfArgument' => [ 'c = $c; } } /** @psalm-immutable */ class Aclass { public B $b; public function __construct(B $b) { $this->b = $b; } } /** @psalm-assert-if-false !null $item->b->c */ function c(\Aclass $item): bool { return null !== $item->b->c; } $a = new \Aclass(new \B(null)); if (!c($a)) { echo strlen($a->b->c); }', ], 'assertOnKeyedArrayWithClassStringOffset' => [ ' ""]; /** @var array $b */ $b = []; $this->assertSame($a, $b); } /** * @template T * @param T $expected * @param mixed $actual * @psalm-assert =T $actual */ public function assertSame($expected, $actual): void { return; } }', ], 'assertOnKeyedArrayWithSpecialCharsInNames' => [ ' */ public array $bar; /** * @param array $bar */ public function __construct(array $bar) { $this->bar = $bar; } } $expected = [ "#[]" => 21, "<<>>" => 6, ]; $foo = new Foo($expected); assertSame($expected, $foo->bar); /** * @psalm-template ExpectedType * @psalm-param ExpectedType $expected * @psalm-param mixed $actual * @psalm-assert =ExpectedType $actual */ function assertSame($expected, $actual): void { if ($expected !== $actual) { throw new Exception("Expected doesn\'t match actual"); } }', ], 'dontForgetAssertionAfterIrrelevantNonMutationFreeCall' => [ 'bar */ function assertBarNotNull(Foo $foo): bool { return $foo->bar !== null; } $foo = new Foo(); if (assertBarNotNull($foo)) { $foo->nonMutationFree(); requiresString($foo->bar); } function requiresString(string $_str): void {} ', ], 'SKIPPED-applyAssertionsToReferences' => [ // See #7254 'bar */ function assertBarNotNull(Foo $foo): bool { return $foo->bar !== null; } $foo = new Foo(); $bar = &$foo; if (assertBarNotNull($foo)) { requiresString($bar->bar); } function requiresString(string $_str): void {} ', ], 'SKIPPED-applyAssertionsFromReferences' => [ // See #7254 'bar */ function assertBarNotNull(Foo $foo): bool { return $foo->bar !== null; } $foo = new Foo(); $bar = &$foo; if (assertBarNotNull($bar)) { requiresString($foo->bar); } function requiresString(string $_str): void {} ', ], 'SKIPPED-applyAssertionsToReferencesWithConditionalOperator' => [ // See #7254 'bar */ function assertBarNotNull(Foo $foo): bool { return $foo->bar !== null; } $foo = new Foo(); $bar = &$foo; requiresString(assertBarNotNull($foo) ? $bar->bar : "bar"); function requiresString(string $_str): void {} ', ], 'assertInArrayWithTemplateDontCrash' => [ ' $objects * @return array */ private function uniquateObjects(array $objects) : array { $uniqueObjects = []; foreach ($objects as $object) { if (in_array($object, $uniqueObjects, true)) { continue; } $uniqueObjects[] = $object; } return $uniqueObjects; } } ', ], 'assertionOnMagicProperty' => [ 'b */ function assertString(A $arg): bool {return $arg->b !== null;} if (assertString($a)) { requiresString($a->b); } function requiresString(string $_str): void {} ', ], 'assertWithEmptyStringOnKeyedArray' => [ ' ""]; /** @var array $b */ $b = []; $this->assertSame($a, $b); } /** * @template T * @param T $expected * @param mixed $actual * @psalm-assert =T $actual */ public function assertSame($expected, $actual): void { return; } } ', ], ]; } /** * @return iterable */ public function providerInvalidCodeParse(): iterable { return [ 'assertInstanceOfMultipleInterfaces' => [ 'bar(); $a->foo1(); }', 'error_message' => 'UndefinedMethod', ], 'assertIfTrueNoAnnotation' => [ ' 'PossiblyNullOperand', ], 'assertIfFalseNoAnnotation' => [ ' 'PossiblyNullOperand', ], 'assertIfTrueMethodCall' => [ 'isInt($p)) { strlen($p); } } }', 'error_message' => 'InvalidScalarArgument', ], 'assertIfStaticTrueMethodCall' => [ 'isInt($p)) { strlen($p); } } }', 'error_message' => 'InvalidScalarArgument', ], 'noFatalForUnknownAssertClass' => [ 'sayHello();', 'error_message' => 'UndefinedDocblockClass', ], 'assertValueImpossible' => [ ' 'TypeDoesNotContainType', ], 'sortOfReplacementForAssert' => [ ' 'TypeDoesNotContainType', ], 'assertScalarAndEmpty' => [ ' 'RedundantConditionGivenDocblockType - src' . DIRECTORY_SEPARATOR . 'somefile.php:19:29', ], 'assertOneOfStrings' => [ ' 'DocblockTypeContradiction', ], 'assertThisType' => [ 'bar(); $t->isFoo(); }', 'error_message' => 'UndefinedMethod', ], 'invalidUnionAssertion' => [ ' 'InvalidDocblock', ], 'assertNotEmptyOnBool' => [ ' 'RedundantConditionGivenDocblockType', ], 'withoutHasTypeCall' => [ 'getParameters(); foreach ($parameters as $parameter) { $parameter->getType()->__toString(); }', 'error_message' => 'PossiblyNullReference', ], 'forgetAssertionAfterRelevantNonMutationFreeCall' => [ 'bar = null; } } /** * @psalm-assert-if-true !null $foo->bar */ function assertBarNotNull(Foo $foo): bool { return $foo->bar !== null; } $foo = new Foo(); if (assertBarNotNull($foo)) { $foo->nonMutationFree(); requiresString($foo->bar); } function requiresString(string $_str): void {} ', 'error_message' => 'PossiblyNullArgument', ], 'SKIPPED-forgetAssertionAfterRelevantNonMutationFreeCallOnReference' => [ // See #7254 'bar = null; } } /** * @psalm-assert-if-true !null $foo->bar */ function assertBarNotNull(Foo $foo): bool { return $foo->bar !== null; } $foo = new Foo(); $fooRef = &$foo; if (assertBarNotNull($foo)) { $fooRef->nonMutationFree(); requiresString($foo->bar); } function requiresString(string $_str): void {} ', 'error_message' => 'PossiblyNullArgument', ], 'SKIPPED-forgetAssertionAfterReferenceModification' => [ // See #7254 'bar */ function assertBarNotNull(Foo $foo): bool { return $foo->bar !== null; } $foo = new Foo(); $barRef = &$foo->bar; if (assertBarNotNull($foo)) { $barRef = null; requiresString($foo->bar); } function requiresString(string $_str): void {} ', 'error_message' => 'PossiblyNullArgument', ], 'assertionOnMagicPropertyWithoutMutationFreeGet' => [ 'b */ function assertString(A $arg): bool {return $arg->b !== null;} if (assertString($a)) { requiresString($a->b); } function requiresString(string $_str): void {} ', 'error_message' => 'A::__get is not mutation-free', ], 'randomValueFromMagicGetterIsNotMutationFree' => [ ' $b */ class A { /** @psalm-mutation-free */ public function __get(string $key) { if ($key === "b") { return random_int(1, 10); } return null; } public function __set(string $key, string $value): void { throw new \Exception("Setting not supported!"); } } $a = new A; /** @psalm-assert-if-true =1 $arg->b */ function assertBIsOne(A $arg): bool { return $arg->b === 1; } if (assertBIsOne($a)) { takesOne($a->b); } /** @param 1 $_arg */ function takesOne(int $_arg): void {} ', 'error_message' => 'ImpureFunctionCall - src' . DIRECTORY_SEPARATOR . 'somefile.php:10:40', ], ]; } }