[ 'code' => ' $type * @template T * @psalm-assert T $value */ function assertInstanceOf($value, string $type): void { // some code } // Returns concreate implementation of Foo, which in this case is Bar function getImplementationOfFoo(): Foo { return new Bar(); } $bar = getImplementationOfFoo(); assertInstanceOf($bar, Bar::class); $bar->sayHello();', ], 'assertInstanceofTemplatedClassMethodUnknownClass' => [ 'code' => ' $expected * @param mixed $actual * @psalm-assert T $actual */ public function assertInstanceOf($expected, $actual) : void {} /** * @param class-string $c */ function bar(string $c, object $e) : void { $this->assertInstanceOf($c, $e); echo $e->getCode(); } }', 'assertions' => [], 'ignored_issues' => ['MixedArgument', 'MixedMethodCall'], ], 'assertInstanceofTemplatedClassMethodUnknownStringClass' => [ 'code' => ' $expected * @param mixed $actual * @psalm-assert T $actual */ public function assertInstanceOf($expected, $actual) : void {} function bar(string $c, object $e) : void { $this->assertInstanceOf($c, $e); echo $e->getCode(); } }', 'assertions' => [], 'ignored_issues' => ['MixedArgument', 'MixedMethodCall', 'ArgumentTypeCoercion'], ], 'assertInstanceofTemplatedFunctionUnknownClass' => [ 'code' => ' $expected * @param mixed $actual * @psalm-assert T $actual */ function assertInstanceOf($expected, $actual) : void {} /** * @param class-string $c */ function bar(string $c, object $e) : void { assertInstanceOf($c, $e); echo $e->getCode(); }', 'assertions' => [], 'ignored_issues' => ['MixedArgument', 'MixedMethodCall'], ], 'assertInstanceofTemplatedFunctionUnknownStringClass' => [ 'code' => ' $expected * @param mixed $actual * @psalm-assert T $actual */ function assertInstanceOf($expected, $actual) : void {} function bar(string $c, object $e) : void { assertInstanceOf($c, $e); echo $e->getCode(); }', 'assertions' => [], 'ignored_issues' => ['MixedArgument', 'MixedMethodCall', 'ArgumentTypeCoercion'], ], 'assertTypedArray' => [ 'code' => ' $expected * @param mixed $actual * @psalm-assert T[] $actual */ function assertArrayOf($expected, $actual) : void {} function bar(array $arr) : void { assertArrayOf(A::class, $arr); foreach ($arr as $a) { $a->foo(); } }', ], 'assertTemplatedTypeString' => [ 'code' => ' $type * * @psalm-assert T $value */ function assertInstanceOf($value, string $type): void { // some code } function getFoo() : Foo { return new class implements Foo {}; } $f = getFoo(); /** * @var mixed */ $class = "hello"; /** @psalm-suppress MixedArgument */ assertInstanceOf($f, $class);', 'assertions' => [ '$f' => 'Foo', ], ], 'suppressRedundantCondition' => [ 'code' => ' $expected * @param mixed $actual * @param string $message * * @template T * @psalm-assert T $actual */ function assertInstanceOf($expected, $actual) : void { } /** * @psalm-suppress RedundantCondition */ function takesA(A $a) : void { assertInstanceOf(A::class, $a); }', ], 'allowCanBeSameAfterAssertion' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' $i * * @param iterable $i * @param class-string $type */ function assertAllInstanceOf(iterable $i, string $type): void { /** @psalm-suppress MixedAssignment */ foreach ($i as $elt) { if (!$elt instanceof $type) { throw new \UnexpectedValueException(""); } } } class A {} function getArray(): array { return []; } $array = getArray(); assertAllInstanceOf($array, A::class);', 'assertions' => [ '$array' => 'array', ], ], 'assertAllIterableOfClass' => [ 'code' => ' $i * * @param iterable $i * @param class-string $type */ function assertAllInstanceOf(iterable $i, string $type): void { /** @psalm-suppress MixedAssignment */ foreach ($i as $elt) { if (!$elt instanceof $type) { throw new \UnexpectedValueException(""); } } } class A {} function getIterable(): iterable { return []; } $iterable = getIterable(); assertAllInstanceOf($iterable, A::class);', 'assertions' => [ '$iterable' => 'iterable', ], ], 'complicatedAssertAllInstanceOf' => [ 'code' => ' $i * * @param iterable $i * @param class-string|interface-string $type */ function allInstanceOf(iterable $i, string $type): bool { /** @psalm-suppress MixedAssignment */ foreach ($i as $elt) { if (!$elt instanceof $type) { return false; } } return true; } interface IBlogPost { public function getId(): int; } function getData(): iterable { return []; } $data = getData(); assert(allInstanceOf($data, IBlogPost::class)); foreach ($data as $post) { echo $post->getId(); }', ], 'assertUnionInNamespace' => [ 'code' => ' $interface * @psalm-assert ExpectedType|interface-string $value */ function implementsInterface($value, $interface, string $message = ""): void {} /** * @psalm-template ExpectedType of object * @param mixed $value * @psalm-param interface-string $interface * @psalm-assert null|ExpectedType|interface-string $value */ function nullOrImplementsInterface(?object $value, $interface, string $message = ""): void {} interface A { } /** * @param mixed $value * * @psalm-return A|class-string */ function consume($value) { implementsInterface($value, A::class); return $value; } /** * @param mixed $value * * @psalm-return A|class-string|null */ function consume2($value) { nullOrImplementsInterface($value, A::class); return $value; }', ], 'assertTemplatedTemplateSimple' => [ 'code' => ' $c * * @return T2 */ function example(Clazz $c) { /** @var mixed */ $x = 0; $c->is($x); return $x; }', ], 'assertTemplatedTemplateIfTrue' => [ 'code' => ' $c * * @return T2|false */ function example(Clazz $c) { /** @var mixed */ $x = 0; return $c->is($x) ? $x : false; }', ], 'assertOnClass' => [ 'code' => 'matches($value)); return $value; } }', ], 'noCrashWhenAsserting' => [ 'code' => ' $expectedType * @psalm-assert class-string $actualType */ function assertIsA(string $expectedType, string $actualType): void { \assert(\is_a($actualType, $expectedType, true)); } class Foo { /** * @psalm-template OriginalClass of object * @psalm-param class-string $originalClass * @psalm-return class-string|null */ private function generateProxy(string $originalClass) : ?string { $generatedClassName = self::class . \'\\\\\' . $originalClass; if (class_exists($generatedClassName)) { assertIsA($originalClass, $generatedClassName); return $generatedClassName; } return null; } }', ], 'castClassStringWithIsA' => [ 'code' => ' $templated_class_string * @psalm-return class-string */ function castStringToClassString( string $templated_class_string, string $input_string ): string { \assert(\is_a($input_string, $templated_class_string, true)); return $input_string; }', ], 'classTemplateAssert' => [ 'code' => 'value = $value; } } /** * @template FieldDefinitionType * * @param string|bool|int|null $value * @param FieldDefinition $definition * * @return FieldValue */ function fromScalarAndDefinition($value, FieldDefinition $definition) : FieldValue { $definition->assertAppliesToValue($value); return new FieldValue($value); } /** * @template ExpectedFieldType */ final class FieldDefinition { /** * @param mixed $value * @psalm-assert ExpectedFieldType $value */ public function assertAppliesToValue($value): void { throw new \Exception("bad"); } }', ], 'assertThrowsInstanceOfFunction' => [ 'code' => ' $exceptionType * @psalm-assert T $outerEx */ function assertThrowsInstanceOf(\Throwable $outerEx, string $exceptionType) : void { if (!($outerEx instanceof $exceptionType)) { throw new \Exception("thrown instance of wrong type"); } }', ], 'dontBleedTemplateTypeInArray' => [ 'code' => ' $class * @psalm-assert array> $value * * @param array $value * @param string $class */ function allIsAOf($value, $class): void {} /** * @psalm-template T of object * * @param array $value * @param class-string $class * * @return array> */ function f($value, $class) { allIsAOf($value, $class); return $value; }', ], 'noCrashOnListKeyAssertion' => [ 'code' => ' $list */ function takesList(array $list) : void { foreach ($list as $i => $l) { assertSame($i, $l); } }', ], 'assertSameOnMemoizedMethodCall' => [ 'code' => 'getMessage()); } try { validateUsername("invalid#1"); } catch (Exception $e) { assertSame("b", $e->getMessage()); } } /** * @psalm-template ExpectedType * @psalm-param ExpectedType $expected * @psalm-param mixed $actual * @psalm-assert =ExpectedType $actual */ function assertSame($expected, $actual): void { if ($actual !== $expected) { throw new Exception("Bad"); } } function validateUsername(string $username): void { if (strlen($username) < 5) { throw new Exception("Username must be at least 5 characters long"); } }', ], 'ifTrueListAssertionFromGeneric' => [ 'code' => ' $_list */ function acceptsIntList(array $_list): void {} /** @var Type> $numbersT */ $numbersT = new Type(); /** @var mixed $mixed */ $mixed = null; if ($numbersT->is($mixed)) { acceptsIntList($mixed); }', ], 'assertListFromGeneric' => [ 'code' => ' $_list */ function acceptsIntList(array $_list): void {} /** @var Type> $numbersT */ $numbersT = new Type(); /** @var mixed $mixed */ $mixed = null; $numbersT->assert($mixed); acceptsIntList($mixed);', ], 'assertArrayFromGeneric' => [ 'code' => ' $_list */ function acceptsArray(array $_list): void {} /** @var Type> $numbersT */ $numbersT = new Type(); /** @var mixed $mixed */ $mixed = null; $numbersT->assert($mixed); acceptsArray($mixed);', ], 'assertObjectShape' => [ 'code' => 'status; ', 'assertions' => [ '$status===' => "'fail'|'ok'", ], ], ]; } public function providerInvalidCodeParse(): iterable { return [ 'detectRedundantCondition' => [ 'code' => ' $expected * @param mixed $actual * @param string $message * * @template T * @psalm-assert T $actual */ function assertInstanceOf($expected, $actual) : void { } function takesA(A $a) : void { assertInstanceOf(A::class, $a); }', 'error_message' => 'RedundantCondition', ], 'detectAssertSameTypeDoesNotContainType' => [ 'code' => ' 'TypeDoesNotContainType', ], 'detectAssertAlwaysSame' => [ 'code' => ' 'RedundantCondition', ], 'detectNeverCanBeSameAfterAssertion' => [ 'code' => ' 'TypeDoesNotContainType', ], 'detectNeverCanBeNotSameAfterAssertion' => [ 'code' => ' 'RedundantCondition', ], 'detectNeverCanBeEqualAfterAssertion' => [ 'code' => ' 'TypeDoesNotContainType', ], // Ignoring this to put the behaviour on par with regular equality checks 'SKIPPED-detectIntFloatNeverCanBeEqualAfterAssertion' => [ 'code' => ' 'TypeDoesNotContainType', ], 'detectFloatIntNeverCanBeEqualAfterAssertion' => [ 'code' => ' 'TypeDoesNotContainType', ], 'assertTemplateUnionParadox' => [ 'code' => ' 'TypeDoesNotContainType', ], 'assertNotSameDifferentTypes' => [ 'code' => ' 'RedundantCondition', ], 'assertNotSameClasses' => [ 'code' => ' 'TypeDoesNotContainType', ], 'assertNotSameDifferentTypesExplicitString' => [ 'code' => ' 'RedundantCondition', ], 'dontBleedTemplateTypeInArrayAgain' => [ 'code' => ' $array * @psalm-assert array $array */ function isMap(array $array) : void {} /** * @param array $arr */ function bar(array $arr): void { isMap($arr); /** @psalm-trace $arr */ $arr; }', 'error_message' => 'string, string', ], 'SKIPPED-noCrashWhenOnUnparseableTemplatedAssertion' => [ 'code' => ' $arr */ function keyExists(array $arr, $key) : void { if (!array_key_exists($key, $arr)) { throw new \Exception("bad"); } } function fromArray(array $data) : void { keyExists($data, "id"); if (is_string($data["id"])) {} }', 'error_message' => 'InvalidDocblock', ], 'assertObjectShapeOnFinalClass' => [ 'code' => 'status; ', 'error_message' => 'Type Foo for $foo is never', ], ]; } }