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,ignored_issues?:list}> */ public function providerValidCodeParse(): iterable { return [ 'implictAssertInstanceOfB' => [ 'code' => 'foo(); }', ], 'implicitAssertEqualsNull' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => 'bar(); $a->foo(); }', ], 'implicitAssertInstanceOfMultipleInterfaces' => [ 'code' => 'bar(); $a->foo1(); }', ], 'implicitAssertInstanceOfBInClassMethod' => [ 'code' => 'assertInstanceOfB($a); $a->foo(); } }', ], 'implicitAssertPropertyNotNull' => [ 'code' => 'a) { throw new \Exception(); } } public function takesA(A $a): void { $this->assertNotNullProperty(); $a->foo(); } }', ], 'implicitAssertWithoutRedundantCondition' => [ 'code' => ' [ 'code' => 'foo(); }', ], 'assertIfTrueAnnotation' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => 'foo()); assertFalse($a->bar()); }', ], 'assertAllStrings' => [ 'code' => ' $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);', 'assertions' => [ '$array' => 'array', '$iterable' => 'iterable', ], ], 'assertStaticMethodIfFalse' => [ 'code' => ' [ 'code' => ' [ 'code' => 'bar();', ], 'assertThisType' => [ 'code' => 'isFoo(); $t->bar(); }' ], 'assertThisTypeIfTrue' => [ 'code' => 'isFoo()) { $t->bar(); } }' ], 'assertThisTypeCombined' => [ 'code' => 'assertFoo(); $t->assertBar(); $t->foo(); $t->bar(); }' ], 'assertThisTypeCombinedInsideMethod' => [ 'code' => 'assertFoo(); $t->assertBar(); $t->foo(); $t->bar(); } } interface FooType { public function foo(): void; } interface BarType { public function bar(): void; } ' ], 'assertThisTypeSimpleCombined' => [ 'code' => 'assertBar(); $t->foo(); $t->bar(); }' ], 'assertThisTypeIfTrueCombined' => [ 'code' => 'assertFoo() && $t->assertBar()) { $t->foo(); $t->bar(); } }' ], 'assertThisTypeSimpleAndIfTrueCombined' => [ 'code' => 'isFoo()) { $t->foo(); } $t->bar(); }' ], 'assertThisTypeSwitchTrue' => [ 'code' => 'isFoo(): $t->bar(); } }' ], 'assertNotArray' => [ 'code' => ' [ 'code' => 'assertProperty()) { $this->a->foo(); } } /** * @psalm-assert-if-true !null $this->a */ public function assertProperty() : bool { return $this->a !== null; } }' ], 'assertIfFalseOnProperty' => [ 'code' => 'assertProperty()) { $this->a->foo(); } } /** * @psalm-assert-if-false null $this->a */ public function assertProperty() : bool { return $this->a !== null; } }' ], 'assertIfTrueOnPropertyNegated' => [ 'code' => 'assertProperty()) { $this->a->foo(); } } /** * @psalm-assert-if-true null $this->a */ public function assertProperty() : bool { return $this->a !== null; } }' ], 'assertIfFalseOnPropertyNegated' => [ 'code' => 'assertProperty()) { $this->a->foo(); } } /** * @psalm-assert-if-false !null $this->a */ public function assertProperty() : bool { return $this->a !== null; } }' ], 'assertPropertyVisibleOutside' => [ 'code' => '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' => [ 'code' => ' $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' => [ 'code' => ' [ 'code' => ' $arr * @return array */ function foo(iterable $arr) : array { isArray($arr); return $arr; }' ], 'listAssertion' => [ 'code' => ' $arr * @return list */ function foo(array $arr) : array { isList($arr); return $arr; }' ], 'scanAssertionTypes' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => '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' => [ 'code' => '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' => [ 'code' => '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' => [ 'code' => ' $data */ function validate(array $data): void {}' ], 'nonEmptyList' => [ 'code' => ' */ 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' => [ 'code' => ' $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' => [ 'code' => ' [ 'code' => ', * env?: array, * buildDeps?: list, * configure?: string * }> * }> * } $data * * @param mixed $data */ function assertStructure($data): void {}' ], 'intersectArraysAfterAssertion' => [ 'code' => ' [ 'code' => ' $value * * @param mixed $value * * @throws InvalidArgumentException */ function allString($value): void {} function takesAnArray(array $a): void { $keys = array_keys($a); allString($keys); }', ], 'assertListIsListOfStrings' => [ 'code' => ' $value * * @param mixed $value * * @throws InvalidArgumentException */ function allString($value): void {} function takesAnArray(array $a): void { $keys = array_keys($a); allString($keys); }', ], 'multipleAssertIfTrue' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [], 'ignored_issues' => [], 'php_version' => '8.0' ], 'assertStaticSelf' => [ 'code' => '' ], 'assertIfTrueStaticSelf' => [ 'code' => '' ], 'assertIfFalseStaticSelf' => [ 'code' => '' ], 'assertStaticByInheritedMethod' => [ 'code' => '' ], 'assertInheritedStatic' => [ 'code' => '' ], 'assertStaticOnUnrelatedClass' => [ 'code' => '' ], 'implicitComplexAssertionNoCrash' => [ 'code' => 'status && "complete" === $status) || ("canceled" === $this->status && "pending" === $status) || ("complete" === $this->status && "canceled" === $status) || ("complete" === $this->status && "pending" === $status) ) { throw new \LogicException(); } } }' ], 'assertArrayIteratorIsIterableOfStrings' => [ 'code' => ' $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' => [ 'code' => 'getParameters(); foreach ($parameters as $parameter) { if ($parameter->hasType()) { $parameter->getType()->__toString(); } }', ], 'reflectionNameTypeClassStringIfNotBuiltin' => [ 'code' => 'getType(); return ($type instanceof \ReflectionNamedType) && !$type->isBuiltin() ? $type->getName() : null; }', 'assertions' => [], 'ignored_issues' => [], 'php_version' => '7.4', ], 'withHasTypeCall' => [ 'code' => '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' => [ 'code' => ' $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' => [ 'code' => 'getProperties(); foreach ($properties as $property) { if ($property->hasType()) { $property->getType()->allowsNull(); } }', 'assertions' => [], 'ignored_issues' => [], '7.4' ], 'onPropertyOfImmutableArgument' => [ 'code' => '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' => [ 'code' => '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' => [ 'code' => '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' => [ 'code' => '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' => [ 'code' => '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' => [ 'code' => ' ""]; /** @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' => [ 'code' => ' */ 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' => [ 'code' => '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 {} ', ], 'referencesDontBreakAssertions' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => '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 {} ', ], 'applyAssertionsOnPropertiesFromReferences' => [ 'code' => '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 {} ', ], 'applyAssertionsOnPropertiesToReferencesWithConditionalOperator' => [ 'code' => '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' => [ 'code' => ' $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' => [ 'code' => 'b */ function assertString(A $arg): bool {return $arg->b !== null;} if (assertString($a)) { requiresString($a->b); } function requiresString(string $_str): void {} ', ], 'assertWithEmptyStringOnKeyedArray' => [ 'code' => ' ""]; /** @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; } } ', ], 'assertNonEmptyStringWithLowercaseString' => [ 'code' => ' [ 'code' => ' $values * @psalm-assert T $input */ function assertOneOf($input, array $values): void {} /** @param "a" $value */ function consumeSpecificStringValue(string $value): void {} /** @param literal-string $value */ function consumeLiteralStringValue(string $value): void {} function consumeAnyIntegerValue(int $value): void {} function consumeAnyFloatValue(float $value): void {} /** @var string $string */ $string; /** @var string $anotherString */ $anotherString; /** @var null|string $nullableString */ $nullableString; /** @var mixed $maybeInt */ $maybeInt; /** @var mixed $maybeFloat */ $maybeFloat; assertOneOf($string, ["a"]); consumeSpecificStringValue($string); assertOneOf($anotherString, ["a", "b", "c"]); consumeLiteralStringValue($anotherString); assertOneOf($nullableString, ["a", "b", "c"]); assertOneOf($nullableString, ["a", "c"]); assertOneOf($maybeInt, [1, 2, 3]); consumeAnyIntegerValue($maybeInt); assertOneOf($maybeFloat, [1.5, 2.5, 3.5]); consumeAnyFloatValue($maybeFloat); /** @var "a"|"b"|"c" $abc */ $abc; /** @param "a"|"b" $aOrB */ function consumeAOrB(string $aOrB): void {} assertOneOf($abc, ["a", "b"]); consumeAOrB($abc); ' ], ]; } /** * @return iterable,php_version?:string}> */ public function providerInvalidCodeParse(): iterable { return [ 'assertInstanceOfMultipleInterfaces' => [ 'code' => 'bar(); $a->foo1(); }', 'error_message' => 'UndefinedMethod', ], 'assertIfTrueNoAnnotation' => [ 'code' => ' 'PossiblyNullOperand', ], 'assertIfFalseNoAnnotation' => [ 'code' => ' 'PossiblyNullOperand', ], 'assertIfTrueMethodCall' => [ 'code' => 'isInt($p)) { strlen($p); } } }', 'error_message' => 'InvalidScalarArgument', ], 'assertIfStaticTrueMethodCall' => [ 'code' => 'isInt($p)) { strlen($p); } } }', 'error_message' => 'InvalidScalarArgument', ], 'noFatalForUnknownAssertClass' => [ 'code' => 'sayHello();', 'error_message' => 'UndefinedDocblockClass', ], 'assertValueImpossible' => [ 'code' => ' 'TypeDoesNotContainType', ], 'sortOfReplacementForAssert' => [ 'code' => ' 'TypeDoesNotContainType', ], 'assertScalarAndEmpty' => [ 'code' => ' 'RedundantConditionGivenDocblockType - src' . DIRECTORY_SEPARATOR . 'somefile.php:19:29', ], 'assertOneOfStrings' => [ 'code' => ' 'DocblockTypeContradiction', ], 'assertThisType' => [ 'code' => 'bar(); $t->isFoo(); }', 'error_message' => 'UndefinedMethod', ], 'invalidUnionAssertion' => [ 'code' => ' 'InvalidDocblock', ], 'assertNotEmptyOnBool' => [ 'code' => ' 'RedundantConditionGivenDocblockType', ], 'withoutHasTypeCall' => [ 'code' => 'getParameters(); foreach ($parameters as $parameter) { $parameter->getType()->__toString(); }', 'error_message' => 'PossiblyNullReference', ], 'forgetAssertionAfterRelevantNonMutationFreeCall' => [ 'code' => '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', ], 'forgetAssertionAfterRelevantNonMutationFreeCallOnReference' => [ 'code' => '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', ], 'forgetAssertionAfterReferenceModification' => [ 'code' => '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' => 'NullArgument', ], 'assertionOnMagicPropertyWithoutMutationFreeGet' => [ 'code' => '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' => [ 'code' => ' $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', ], ]; } }