1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-06 05:29:00 +01:00
psalm/tests/Template/FunctionTemplateAssertTest.php
Daniil Gentili 1986c8b4a8
Add support for strict arrays, fix type alias intersection, fix array_is_list assertion on non-lists (#8395)
* Immutable CodeLocation

* Remove excess clones

* Remove external clones

* Remove leftover clones

* Fix final clone issue

* Immutable storages

* Refactoring

* Fixes

* Fixes

* Fix

* Fix

* Fixes

* Simplify

* Fixes

* Fix

* Fixes

* Update

* Fix

* Cache global types

* Fix

* Update

* Update

* Fixes

* Fixes

* Refactor

* Fixes

* Fix

* Fix

* More caching

* Fix

* Fix

* Update

* Update

* Fix

* Fixes

* Update

* Refactor

* Update

* Fixes

* Break one more test

* Fix

* FIx

* Fix

* Fix

* Fix

* Fix

* Improve performance and readability

* Equivalent logic

* Fixes

* Revert

* Revert "Revert"

This reverts commit f9175100c8452c80559234200663fd4c4f4dd889.

* Fix

* Fix reference bug

* Make default TypeVisitor immutable

* Bugfix

* Remove clones

* Partial refactoring

* Refactoring

* Fixes

* Fix

* Fixes

* Fixes

* cs-fix

* Fix final bugs

* Add test

* Misc fixes

* Update

* Fixes

* Experiment with removing different property

* revert "Experiment with removing different property"

This reverts commit ac1156e077fc4ea633530d51096d27b6e88bfdf9.

* Uniform naming

* Uniform naming

* Hack hotfix

* Clean up $_FILES ref #8621

* Undo hack, try fixing properly

* Helper method

* Remove redundant call

* Partially fix bugs

* Cleanup

* Change defaults

* Fix bug

* Fix (?, hope this doesn't break anything else)

* cs-fix

* Review fixes

* Bugfix

* Bugfix

* Improve logic

* Add support for list{} and callable-list{} types, properly implement array_is_list assertions (fixes #8389)

* Default to sealed arrays

* Fix array_merge bug

* Fixes

* Fix

* Sealed type checks

* Properly infer properties-of and get_object_vars on final classes

* Fix array_map zipping

* Fix tests

* Fixes

* Fixes

* Fix more stuff

* Recursively resolve type aliases

* Fix typo

* Fixes

* Fix array_is_list assertion on keyed array

* Add BC docs

* Fixes

* fix

* Update

* Update

* Update

* Update

* Seal arrays with count assertions

* Fix #8528

* Fix

* Update

* Improve sealed array foreach logic

* get_object_vars on template properties

* Fix sealed array assertion reconciler logic

* Improved reconciler

* Add tests

* Single source of truth for test types

* Fix tests

* Fixup tests

* Fixup tests

* Fixup tests

* Update

* Fix tests

* Fix tests

* Final fixes

* Fixes

* Use list syntax only when needed

* Fix tests

* Cs-fix

* Update docs

* Update docs

* Update docs

* Update docs

* Update docs

* Document missing types

* Update docs

* Improve class-string-map docs

* Update

* Update

* I love working on psalm :)

* Keep arrays unsealed by default

* Fixup tests

* Fix syntax mistake

* cs-fix

* Fix typo

* Re-import missing types

* Keep strict types only in return types

* argc/argv fixes

* argc/argv fixes

* Fix test

* Comment-out valinor code, pinging @romm pls merge https://github.com/CuyZ/Valinor/pull/246 so we can add valinor to the psalm docs :)
2022-11-05 22:34:42 +01:00

1208 lines
43 KiB
PHP

<?php
namespace Psalm\Tests\Template;
use Psalm\Tests\TestCase;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
class FunctionTemplateAssertTest extends TestCase
{
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
/**
*
*/
public function providerValidCodeParse(): iterable
{
return [
'assertTemplatedType' => [
'code' => '<?php
namespace Bar;
interface Foo {}
class Bar implements Foo {
public function sayHello(): void {
echo "Hello";
}
}
/**
* @param mixed $value
* @param class-string<T> $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' => '<?php
namespace Bar;
class C {
/**
* @template T as object
* @param class-string<T> $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' => '<?php
namespace Bar;
class C {
/**
* @template T
* @param class-string<T> $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' => '<?php
namespace Bar;
/**
* @template T as object
* @param class-string<T> $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' => '<?php
namespace Bar;
/**
* @template T
* @param class-string<T> $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' => '<?php
namespace Bar;
class A {
public function foo() : void {}
}
/**
* @template T
* @param class-string<T> $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' => '<?php
interface Foo {}
/**
* @template T as object
*
* @param mixed $value
* @param class-string<T> $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' => '<?php
namespace Bar;
class A {}
/**
* @param class-string<T> $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' => '<?php
namespace Bar;
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert =T $actual
*/
function assertSame($expected, $actual) : void {}
class Hello {}
class Goodbye {}
$a = rand(0, 1) ? new Goodbye() : new Hello();
$b = rand(0, 1) ? new Hello() : new Goodbye();
assertSame($a, $b);
$c = new Hello();
$d = rand(0, 1) ? new Hello() : new Goodbye();
assertSame($c, $d);
$c = new Hello();
$d = rand(0, 1) ? new Hello() : new Goodbye();
assertSame($d, $c);
$c = 4;
$d = rand(0, 1) ? 4 : 5;
assertSame($d, $c);
$d = rand(0, 1) ? 4 : null;
assertSame(null, $d);
function assertStringsAreSame(string $a, string $b) : void {
assertSame($a, $b);
}
/** @param mixed $a */
function assertMaybeStringsAreSame($a, string $b) : void {
assertSame($a, $b);
}
/** @param mixed $b */
function alsoAssertMaybeStringsAreSame(string $a, $b) : void {
assertSame($a, $b);
}',
],
'allowCanBeSameAfterStaticMethodAssertion' => [
'code' => '<?php
namespace Bar;
class Assertion {
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert =T $actual
*/
public static function assertSame($expected, $actual) : void {}
}
class Hello {}
class Goodbye {}
$a = rand(0, 1) ? new Goodbye() : new Hello();
$b = rand(0, 1) ? new Hello() : new Goodbye();
Assertion::assertSame($a, $b);',
],
'allowCanBeNotSameAfterAssertionReverseUnion' => [
'code' => '<?php
namespace Bar;
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert !=T $actual
*/
function assertNotSame($expected, $actual) : void {}
class Hello {}
class Goodbye {}
$goodbye_or_hello = rand(0, 1) ? new Goodbye() : new Hello();
$hello_or_goodbye = rand(0, 1) ? new Hello() : new Goodbye();
assertNotSame($goodbye_or_hello, $hello_or_goodbye);',
],
'allowCanBeNotSameAfterAssertionAcmpAorB' => [
'code' => '<?php
namespace Bar;
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert !=T $actual
*/
function assertNotSame($expected, $actual) : void {}
class Hello {}
class Goodbye {}
$hello = new Hello();
$hello_or_goodbye = rand(0, 1) ? new Hello() : new Goodbye();
assertNotSame($hello, $hello_or_goodbye);',
],
'allowCanBeNotSameAfterAssertionAorBcmpA' => [
'code' => '<?php
namespace Bar;
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert !=T $actual
*/
function assertNotSame($expected, $actual) : void {}
class Hello {}
class Goodbye {}
$hello = new Hello();
$hello_or_goodbye = rand(0, 1) ? new Hello() : new Goodbye();
assertNotSame($hello_or_goodbye, $hello);',
],
'allowCanBeNotSameAfterAssertionScalar' => [
'code' => '<?php
namespace Bar;
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert !=T $actual
*/
function assertNotSame($expected, $actual) : void {}
$c = 4;
$d = rand(0, 1) ? 4 : 5;
assertNotSame($d, $c);
function foo(string $a, string $b) : void {
assertNotSame($a, $b);
}',
],
'allowCanBeEqualAfterAssertion' => [
'code' => '<?php
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert ~T $actual
*/
function assertEqual($expected, $actual) : void {}
class Hello {}
class Goodbye {}
$a = rand(0, 1) ? new Goodbye() : new Hello();
$b = rand(0, 1) ? new Hello() : new Goodbye();
assertEqual($a, $b);
$c = new Hello();
$d = rand(0, 1) ? new Hello() : new Goodbye();
assertEqual($c, $d);
$c = new Hello();
$d = rand(0, 1) ? new Hello() : new Goodbye();
assertEqual($d, $c);
$c = 4;
$d = rand(0, 1) ? 3.0 : 4.0;
assertEqual($d, $c);
$c = 4.0;
$d = rand(0, 1) ? 3 : 4;
assertEqual($d, $c);
function foo(string $a, string $b) : void {
assertEqual($a, $b);
}',
],
'assertAllArrayOfClass' => [
'code' => '<?php
/**
* @template T
*
* @psalm-assert iterable<mixed,T> $i
*
* @param iterable<mixed,mixed> $i
* @param class-string<T> $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<array-key, A>',
],
],
'assertAllIterableOfClass' => [
'code' => '<?php
/**
* @template T
*
* @psalm-assert iterable<mixed,T> $i
*
* @param iterable<mixed,mixed> $i
* @param class-string<T> $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<mixed, A>',
],
],
'complicatedAssertAllInstanceOf' => [
'code' => '<?php
/**
* @template T
*
* @psalm-assert-if-true iterable<mixed,T> $i
*
* @param iterable<mixed,mixed> $i
* @param class-string<T>|interface-string<T> $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' => '<?php
namespace Foo\Bar\Baz;
/**
* @psalm-template ExpectedType of object
* @param mixed $value
* @psalm-param interface-string<ExpectedType> $interface
* @psalm-assert ExpectedType|interface-string<ExpectedType> $value
*/
function implementsInterface($value, $interface, string $message = ""): void {}
/**
* @psalm-template ExpectedType of object
* @param mixed $value
* @psalm-param interface-string<ExpectedType> $interface
* @psalm-assert null|ExpectedType|interface-string<ExpectedType> $value
*/
function nullOrImplementsInterface(?object $value, $interface, string $message = ""): void {}
interface A
{
}
/**
* @param mixed $value
*
* @psalm-return A|class-string<A>
*/
function consume($value) {
implementsInterface($value, A::class);
return $value;
}
/**
* @param mixed $value
*
* @psalm-return A|class-string<A>|null
*/
function consume2($value)
{
nullOrImplementsInterface($value, A::class);
return $value;
}'
],
'assertTemplatedTemplateSimple' => [
'code' => '<?php
/**
* @template T1
*/
class Clazz
{
/**
* @param mixed $x
*
* @psalm-assert T1 $x
*/
public function is($x) : void {}
}
/**
* @template T2
*
* @param Clazz<T2> $c
*
* @return T2
*/
function example(Clazz $c) {
/** @var mixed */
$x = 0;
$c->is($x);
return $x;
}'
],
'assertTemplatedTemplateIfTrue' => [
'code' => '<?php
/**
* @template T1
*/
class Clazz
{
/**
* @param mixed $x
*
* @return bool
*
* @psalm-assert-if-true T1 $x
*/
public function is($x) : bool {
return true;
}
}
/**
* @template T2
*
* @param Clazz<T2> $c
*
* @return T2|false
*/
function example(Clazz $c) {
/** @var mixed */
$x = 0;
return $c->is($x) ? $x : false;
}'
],
'assertOnClass' => [
'code' => '<?php
/**
* @template T
*/
abstract class Type
{
/**
* @param mixed $value
* @return bool
* @psalm-assert-if-true T $value
*/
abstract public function matches($value): bool;
/**
* @param mixed $value
* @return mixed
* @psalm-return T
* @psalm-assert T $value
*/
public function assert($value)
{
assert($this->matches($value));
return $value;
}
}'
],
'noCrashWhenAsserting' => [
'code' => '<?php
/**
* @psalm-template ExpectedClassType of object
* @psalm-param class-string<ExpectedClassType> $expectedType
* @psalm-assert class-string<ExpectedClassType> $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> $originalClass
* @psalm-return class-string<OriginalClass>|null
*/
private function generateProxy(string $originalClass) : ?string {
$generatedClassName = self::class . \'\\\\\' . $originalClass;
if (class_exists($generatedClassName)) {
assertIsA($originalClass, $generatedClassName);
return $generatedClassName;
}
return null;
}
}',
],
'castClassStringWithIsA' => [
'code' => '<?php
/**
* @psalm-template RequestedClass of object
* @psalm-param class-string<RequestedClass> $templated_class_string
* @psalm-return class-string<RequestedClass>
*/
function castStringToClassString(
string $templated_class_string,
string $input_string
): string {
\assert(\is_a($input_string, $templated_class_string, true));
return $input_string;
}'
],
'classTemplateAssert' => [
'code' => '<?php
/**
* @template ActualFieldType
*/
final class FieldValue
{
/** @var ActualFieldType */
public $value;
/** @param ActualFieldType $value */
public function __construct($value) {
$this->value = $value;
}
}
/**
* @template FieldDefinitionType
*
* @param string|bool|int|null $value
* @param FieldDefinition<FieldDefinitionType> $definition
*
* @return FieldValue<FieldDefinitionType>
*/
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' => '<?php
namespace Foo;
/**
* @template T of \Throwable
* @psalm-param class-string<T> $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' => '<?php
/**
* @psalm-template ExpectedType of object
* @psalm-param class-string<ExpectedType> $class
* @psalm-assert array<class-string<ExpectedType>> $value
*
* @param array<string> $value
* @param string $class
*/
function allIsAOf($value, $class): void {}
/**
* @psalm-template T of object
*
* @param array<string> $value
* @param class-string<T> $class
*
* @return array<class-string<T>>
*/
function f($value, $class) {
allIsAOf($value, $class);
return $value;
}'
],
'noCrashOnListKeyAssertion' => [
'code' => '<?php
/**
* @template T
* @param T $t
* @param mixed $other
* @psalm-assert =T $other
*/
function assertSame($t, $other) : void {}
/** @param list<int> $list */
function takesList(array $list) : void {
foreach ($list as $i => $l) {
assertSame($i, $l);
}
}'
],
'assertSameOnMemoizedMethodCall' => [
'code' => '<?php
function testValidUsername(): void {
try {
validateUsername("123");
throw new Exception("Failed to throw exception for short username");
} catch (Exception $e) {
assertSame("a", $e->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' => '<?php
/**
* @template T
*/
final class Type
{
/**
* @param mixed $toCheck
* @psalm-assert-if-true T $toCheck
*/
function is($toCheck): bool
{
throw new RuntimeException("???");
}
}
/**
* @param list<int> $_list
*/
function acceptsIntList(array $_list): void {}
/** @var Type<list<int>> $numbersT */
$numbersT = new Type();
/** @var mixed $mixed */
$mixed = null;
if ($numbersT->is($mixed)) {
acceptsIntList($mixed);
}'
],
'assertListFromGeneric' => [
'code' => '<?php
/**
* @template T
*/
final class Type
{
/**
* @param mixed $toCheck
* @psalm-assert T $toCheck
*/
function assert($toCheck): void
{
}
}
/**
* @param list<int> $_list
*/
function acceptsIntList(array $_list): void {}
/** @var Type<list<int>> $numbersT */
$numbersT = new Type();
/** @var mixed $mixed */
$mixed = null;
$numbersT->assert($mixed);
acceptsIntList($mixed);'
],
'assertArrayFromGeneric' => [
'code' => '<?php
/**
* @template T
*/
final class Type
{
/**
* @param mixed $toCheck
* @psalm-assert T $toCheck
*/
function assert($toCheck): void
{
}
}
/**
* @param array<string, int> $_list
*/
function acceptsArray(array $_list): void {}
/** @var Type<array<string, int>> $numbersT */
$numbersT = new Type();
/** @var mixed $mixed */
$mixed = null;
$numbersT->assert($mixed);
acceptsArray($mixed);'
],
];
}
/**
*
*/
public function providerInvalidCodeParse(): iterable
{
return [
'detectRedundantCondition' => [
'code' => '<?php
class A {}
/**
* @param class-string<T> $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' => '<?php
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert =T $actual
*/
function assertSame($expected, $actual) : void {}
class Hello {}
$a = 5;
$b = new Hello();
assertSame($a, $b);',
'error_message' => 'TypeDoesNotContainType',
],
'detectAssertAlwaysSame' => [
'code' => '<?php
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert =T $actual
*/
function assertSame($expected, $actual) : void {}
$a = 5;
$b = 5;
assertSame($a, $b);',
'error_message' => 'RedundantCondition',
],
'detectNeverCanBeSameAfterAssertion' => [
'code' => '<?php
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert =T $actual
*/
function assertSame($expected, $actual) : void {}
class Hello {}
class Helloa {}
class Goodbye {}
$c = new Helloa();
$d = rand(0, 1) ? new Hello() : new Goodbye();
assertSame($c, $d);',
'error_message' => 'TypeDoesNotContainType',
],
'detectNeverCanBeNotSameAfterAssertion' => [
'code' => '<?php
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert !=T $actual
*/
function assertNotSame($expected, $actual) : void {}
class Hello {}
class Helloa {}
class Goodbye {}
$c = new Helloa();
$d = rand(0, 1) ? new Hello() : new Goodbye();
assertNotSame($c, $d);',
'error_message' => 'RedundantCondition',
],
'detectNeverCanBeEqualAfterAssertion' => [
'code' => '<?php
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert ~T $actual
*/
function assertEqual($expected, $actual) : void {}
class Hello {}
class Helloa {}
class Goodbye {}
$c = new Helloa();
$d = rand(0, 1) ? new Hello() : new Goodbye();
assertEqual($c, $d);',
'error_message' => 'TypeDoesNotContainType',
],
// Ignoring this to put the behaviour on par with regular equality checks
'SKIPPED-detectIntFloatNeverCanBeEqualAfterAssertion' => [
'code' => '<?php
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert ~T $actual
*/
function assertEqual($expected, $actual) : void {}
$c = 4;
$d = rand(0, 1) ? 5.0 : 6.0;
assertEqual($c, $d);',
'error_message' => 'TypeDoesNotContainType',
],
'detectFloatIntNeverCanBeEqualAfterAssertion' => [
'code' => '<?php
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert ~T $actual
*/
function assertEqual($expected, $actual) : void {}
$c = 4.0;
$d = rand(0, 1) ? 5 : 6;
assertEqual($c, $d);',
'error_message' => 'TypeDoesNotContainType',
],
'assertTemplateUnionParadox' => [
'code' => '<?php
/**
* Asserts that two variables are not the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert T $actual
*/
function assertSame($expected, $actual) : void {}
$expected = rand(0, 1) ? 4 : 5;
$actual = 6;
assertSame($expected, $actual);',
'error_message' => 'TypeDoesNotContainType',
],
'assertNotSameDifferentTypes' => [
'code' => '<?php
/**
* @template T
* @param T $expected
* @param mixed $actual
* @param string $message
* @psalm-assert !=T $actual
* @return void
*/
function assertNotSame($expected, $actual, $message = "") {}
function bar(string $i, array $j) : void {
assertNotSame($i, $j);
}',
'error_message' => 'RedundantCondition',
],
'assertNotSameClasses' => [
'code' => '<?php
/**
* Asserts that two variables are the same.
*
* @template T
* @param T $expected
* @param mixed $actual
* @psalm-assert =T $actual
*/
function assertSame($expected, $actual) : void {}
class a {}
class b {}
final class c {}
$expected = rand(0, 1) ? new a : new b;
$actual = new c;
assertSame($expected, $actual);',
'error_message' => 'TypeDoesNotContainType',
],
'assertNotSameDifferentTypesExplicitString' => [
'code' => '<?php
/**
* @template T
* @param T $expected
* @param mixed $actual
* @param string $message
* @psalm-assert !=T $actual
* @return void
*/
function assertNotSame($expected, $actual, $message = "") {}
class Hello {}
function bar(array $j) : void {
assertNotSame(new Hello(), $j);
}',
'error_message' => 'RedundantCondition',
],
'dontBleedTemplateTypeInArrayAgain' => [
'code' => '<?php
/**
* @psalm-template T
* @psalm-param array<T> $array
* @psalm-assert array<string, T> $array
*/
function isMap(array $array) : void {}
/**
* @param array<string> $arr
*/
function bar(array $arr): void {
isMap($arr);
/** @psalm-trace $arr */
$arr;
}',
'error_message' => 'string, string',
],
'SKIPPED-noCrashWhenOnUnparseableTemplatedAssertion' => [
'code' => '<?php
/**
* @template TCandidateKey as array-key
* @param array $arr
* @param TCandidateKey $key
* @psalm-assert has-array-key<TCandidateKey> $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',
],
];
}
}