1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00
psalm/tests/AssertAnnotationTest.php
Matthew Brown 8d36bdc3ed
Make array shapes strict by default (#8701)
* Make array shapes strict by default

* Fix PSL tests
2022-11-11 20:14:21 -05:00

2536 lines
87 KiB
PHP

<?php
namespace Psalm\Tests;
use Psalm\Config;
use Psalm\Context;
use Psalm\Exception\CodeException;
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
use const DIRECTORY_SEPARATOR;
class AssertAnnotationTest extends TestCase
{
use ValidCodeAnalysisTestTrait;
use InvalidCodeAnalysisTestTrait;
public function testDontForgetAssertionAfterMutationFreeCall(): void
{
Config::getInstance()->remember_property_assignments_after_call = false;
$this->addFile(
'somefile.php',
'<?php
class Foo
{
public ?string $bar = null;
/** @psalm-mutation-free */
public function mutationFree(): void {}
}
/**
* @psalm-assert-if-true !null $foo->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',
'<?php
class Foo
{
public ?string $bar = null;
public function nonMutationFree(): void {}
}
/**
* @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 {}
'
);
$this->analyzeFile('somefile.php', new Context());
}
/**
*
*/
public function providerValidCodeParse(): iterable
{
return [
'implictAssertInstanceOfB' => [
'code' => '<?php
namespace Bar;
class A {}
class B extends A {
public function foo(): void {}
}
function assertInstanceOfB(A $var): void {
if (!$var instanceof B) {
throw new \Exception();
}
}
function takesA(A $a): void {
assertInstanceOfB($a);
$a->foo();
}',
],
'implicitAssertEqualsNull' => [
'code' => '<?php
function takesInt(int $int): void { echo $int; }
function getIntOrNull(): ?int {
return rand(0,1) === 0 ? null : 1;
}
/** @param mixed $value */
function assertNotNull($value): void {
if (null === $value) {
throw new Exception();
}
}
$value = getIntOrNull();
assertNotNull($value);
takesInt($value);',
],
'dropInReplacementForAssert' => [
'code' => '<?php
namespace Bar;
/**
* @param mixed $_b
* @psalm-assert !falsy $_b
*/
function myAssert($_b) : void {
if (!$_b) {
throw new \Exception("bad");
}
}
function bar(?string $s) : string {
myAssert($s !== null);
return $s;
}',
],
'dropInReplacementForAntiAssert' => [
'code' => '<?php
/**
* @param mixed $foo
* @psalm-assert falsy $foo
*/
function abort_if($foo): void
{
if ($foo) {
throw new \RuntimeException();
}
}
/**
* @param string|null $foo
*/
function removeNullable($foo): string
{
abort_if(is_null($foo));
return $foo;
}'
],
'sortOfReplacementForAssert' => [
'code' => '<?php
namespace Bar;
/**
* @param mixed $_b
* @psalm-assert true $_b
*/
function myAssert($_b) : void {
if ($_b !== true) {
throw new \Exception("bad");
}
}
function bar(?string $s) : string {
myAssert($s !== null);
return $s;
}',
],
'implictAssertInstanceOfInterface' => [
'code' => '<?php
namespace Bar;
class A {
public function bar() : void {}
}
interface I {
public function foo(): void;
}
class B extends A implements I {
public function foo(): void {}
}
function assertInstanceOfI(A $var): void {
if (!$var instanceof I) {
throw new \Exception();
}
}
function takesA(A $a): void {
assertInstanceOfI($a);
$a->bar();
$a->foo();
}',
],
'implicitAssertInstanceOfMultipleInterfaces' => [
'code' => '<?php
namespace Bar;
class A {
public function bar() : void {}
}
interface I1 {
public function foo1(): void;
}
interface I2 {
public function foo2(): void;
}
class B extends A implements I1, I2 {
public function foo1(): void {}
public function foo2(): void {}
}
function assertInstanceOfInterfaces(A $var): void {
if (!$var instanceof I1 || !$var instanceof I2) {
throw new \Exception();
}
}
function takesA(A $a): void {
assertInstanceOfInterfaces($a);
$a->bar();
$a->foo1();
}',
],
'implicitAssertInstanceOfBInClassMethod' => [
'code' => '<?php
namespace Bar;
class A {}
class B extends A {
public function foo(): void {}
}
class C {
private function assertInstanceOfB(A $var): void {
if (!$var instanceof B) {
throw new \Exception();
}
}
private function takesA(A $a): void {
$this->assertInstanceOfB($a);
$a->foo();
}
}',
],
'implicitAssertPropertyNotNull' => [
'code' => '<?php
namespace Bar;
class A {
public function foo(): void {}
}
class B {
/** @var A|null */
public $a;
private function assertNotNullProperty(): void {
if (!$this->a) {
throw new \Exception();
}
}
public function takesA(A $a): void {
$this->assertNotNullProperty();
$a->foo();
}
}',
],
'implicitAssertWithoutRedundantCondition' => [
'code' => '<?php
namespace Bar;
/**
* @param mixed $data
* @throws \Exception
*/
function assertIsLongString($data): void {
if (!\is_string($data)) {
throw new \Exception;
}
if (strlen($data) < 100) {
throw new \Exception;
}
}
/**
* @throws \Exception
*/
function f(string $s): void {
assertIsLongString($s);
}',
],
'assertInstanceOfBAnnotation' => [
'code' => '<?php
namespace Bar;
class A {}
class B extends A {
public function foo(): void {}
}
/** @psalm-assert B $var */
function myAssertInstanceOfB(A $var): void {
if (!$var instanceof B) {
throw new \Exception();
}
}
function takesA(A $a): void {
myAssertInstanceOfB($a);
$a->foo();
}',
],
'assertIfTrueAnnotation' => [
'code' => '<?php
namespace Bar;
/** @psalm-assert-if-true string $myVar */
function isValidString(?string $myVar) : bool {
return $myVar !== null && $myVar[0] === "a";
}
$myString = rand(0, 1) ? "abacus" : null;
if (isValidString($myString)) {
echo "Ma chaine " . $myString;
}',
],
'assertIfFalseAnnotation' => [
'code' => '<?php
namespace Bar;
/** @psalm-assert-if-false string $myVar */
function isInvalidString(?string $myVar) : bool {
return $myVar === null || $myVar[0] !== "a";
}
$myString = rand(0, 1) ? "abacus" : null;
if (isInvalidString($myString)) {
// do something
} else {
echo "Ma chaine " . $myString;
}',
],
'assertSessionVar' => [
'code' => '<?php
namespace Bar;
/**
* @psalm-assert-if-true string $a
* @param mixed $a
*/
function my_is_string($a) : bool
{
return is_string($a);
}
if (my_is_string($_SESSION["abc"])) {
$i = substr($_SESSION["abc"], 1, 2);
}',
],
'dontBleedBadAssertVarIntoContext' => [
'code' => '<?php
namespace Bar;
class A {
public function foo() : bool {
return (bool) rand(0, 1);
}
public function bar() : bool {
return (bool) rand(0, 1);
}
}
/**
* Asserts that a condition is false.
*
* @param bool $condition
* @param string $message
*
* @psalm-assert false $actual
*/
function assertFalse($condition, $message = "") : void {}
function takesA(A $a) : void {
assertFalse($a->foo());
assertFalse($a->bar());
}',
],
'assertAllStrings' => [
'code' => '<?php
/**
* @psalm-assert iterable<mixed,string> $i
*
* @param iterable<mixed,mixed> $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<array-key, string>',
'$iterable' => 'iterable<mixed, string>',
],
],
'assertStaticMethodIfFalse' => [
'code' => '<?php
class StringUtility {
/**
* @psalm-assert-if-false !null $yStr
*/
public static function isNull(?string $yStr): bool {
if ($yStr === null) {
return true;
}
return false;
}
}
function test(?string $in) : void {
$str = "test";
if(!StringUtility::isNull($in)) {
$str .= $in;
}
}',
],
'assertStaticMethodIfTrue' => [
'code' => '<?php
class StringUtility {
/**
* @psalm-assert-if-true !null $yStr
*/
public static function isNotNull(?string $yStr): bool {
if ($yStr === null) {
return true;
}
return false;
}
}
function test(?string $in) : void {
$str = "test";
if(StringUtility::isNotNull($in)) {
$str .= $in;
}
}',
],
'assertUnion' => [
'code' => '<?php
class Foo{
public function bar() : void {}
}
/**
* @param mixed $b
* @psalm-assert int|Foo $b
*/
function assertIntOrFoo($b) : void {
if (!is_int($b) && !(is_object($b) && $b instanceof Foo)) {
throw new \Exception("bad");
}
}
/** @psalm-suppress MixedAssignment */
$a = $GLOBALS["a"];
assertIntOrFoo($a);
if (!is_int($a)) $a->bar();',
],
'assertThisType' => [
'code' => '<?php
class Type {
/**
* @psalm-assert FooType $this
*/
public function isFoo() : bool {
if (!$this instanceof FooType) {
throw new \Exception();
}
return true;
}
}
class FooType extends Type {
public function bar(): void {}
}
function takesType(Type $t) : void {
$t->isFoo();
$t->bar();
}'
],
'assertThisTypeIfTrue' => [
'code' => '<?php
class Type {
/**
* @psalm-assert-if-true FooType $this
*/
public function isFoo() : bool {
return $this instanceof FooType;
}
}
class FooType extends Type {
public function bar(): void {}
}
function takesType(Type $t) : void {
if ($t->isFoo()) {
$t->bar();
}
}'
],
'assertThisTypeCombined' => [
'code' => '<?php
class Type {
/**
* @psalm-assert FooType $this
*/
public function assertFoo() : void {
if (!$this instanceof FooType) {
throw new \Exception();
}
}
/**
* @psalm-assert BarType $this
*/
public function assertBar() : void {
if (!$this instanceof BarType) {
throw new \Exception();
}
}
}
interface FooType {
public function foo(): void;
}
interface BarType {
public function bar(): void;
}
function takesType(Type $t) : void {
$t->assertFoo();
$t->assertBar();
$t->foo();
$t->bar();
}'
],
'assertThisTypeCombinedInsideMethod' => [
'code' => '<?php
class Type {
/**
* @psalm-assert FooType $this
*/
public function assertFoo() : void {
if (!$this instanceof FooType) {
throw new \Exception();
}
}
/**
* @psalm-assert BarType $this
*/
public function assertBar() : void {
if (!$this instanceof BarType) {
throw new \Exception();
}
}
function takesType(Type $t) : void {
$t->assertFoo();
$t->assertBar();
$t->foo();
$t->bar();
}
}
interface FooType {
public function foo(): void;
}
interface BarType {
public function bar(): void;
}
'
],
'assertThisTypeSimpleCombined' => [
'code' => '<?php
class Type {
/**
* @psalm-assert FooType $this
*/
public function assertFoo() : void {
if (!$this instanceof FooType) {
throw new \Exception();
}
return;
}
/**
* @psalm-assert BarType $this
*/
public function assertBar() : void {
if (!$this instanceof BarType) {
throw new \Exception();
}
return;
}
}
interface FooType {
public function foo(): void;
}
interface BarType {
public function bar(): void;
}
/** @param Type&FooType $t */
function takesType(Type $t) : void {
$t->assertBar();
$t->foo();
$t->bar();
}'
],
'assertThisTypeIfTrueCombined' => [
'code' => '<?php
class Type {
/**
* @psalm-assert-if-true FooType $this
*/
public function assertFoo() : bool {
return $this instanceof FooType;
}
/**
* @psalm-assert-if-true BarType $this
*/
public function assertBar() : bool {
return $this instanceof BarType;
}
}
interface FooType {
public function foo(): void;
}
interface BarType {
public function bar(): void;
}
function takesType(Type $t) : void {
if ($t->assertFoo() && $t->assertBar()) {
$t->foo();
$t->bar();
}
}'
],
'assertThisTypeSimpleAndIfTrueCombined' => [
'code' => '<?php
class Type {
/**
* @psalm-assert BarType $this
* @psalm-assert-if-true FooType $this
*/
public function isFoo() : bool {
if (!$this instanceof BarType) {
throw new \Exception();
}
return $this instanceof FooType;
}
}
interface FooType {
public function foo(): void;
}
interface BarType {
public function bar(): void;
}
function takesType(Type $t) : void {
if ($t->isFoo()) {
$t->foo();
}
$t->bar();
}'
],
'assertThisTypeSwitchTrue' => [
'code' => '<?php
class Type {
/**
* @psalm-assert-if-true FooType $this
*/
public function isFoo() : bool {
return $this instanceof FooType;
}
}
class FooType extends Type {
public function bar(): void {}
}
function takesType(Type $t) : void {
switch (true) {
case $t->isFoo():
$t->bar();
}
}'
],
'assertNotArray' => [
'code' => '<?php
/**
* @param mixed $value
* @psalm-assert !array $value
*/
function myAssertNotArray($value) : void {}
/**
* @param mixed $value
* @psalm-assert !iterable $value
*/
function myAssertNotIterable($value) : void {}
/**
* @param int|array $v
*/
function takesIntOrArray($v) : int {
myAssertNotArray($v);
return $v;
}
/**
* @param int|iterable $v
*/
function takesIntOrIterable($v) : int {
myAssertNotIterable($v);
return $v;
}'
],
'assertIfTrueOnProperty' => [
'code' => '<?php
class A {
public function foo() : void {}
}
class B {
private ?A $a = null;
public function bar() : void {
if ($this->assertProperty()) {
$this->a->foo();
}
}
/**
* @psalm-assert-if-true !null $this->a
*/
public function assertProperty() : bool {
return $this->a !== null;
}
}'
],
'assertIfFalseOnProperty' => [
'code' => '<?php
class A {
public function foo() : void {}
}
class B {
private ?A $a = null;
public function bar() : void {
if ($this->assertProperty()) {
$this->a->foo();
}
}
/**
* @psalm-assert-if-false null $this->a
*/
public function assertProperty() : bool {
return $this->a !== null;
}
}'
],
'assertIfTrueOnPropertyNegated' => [
'code' => '<?php
class A {
public function foo() : void {}
}
class B {
private ?A $a = null;
public function bar() : void {
if (!$this->assertProperty()) {
$this->a->foo();
}
}
/**
* @psalm-assert-if-true null $this->a
*/
public function assertProperty() : bool {
return $this->a !== null;
}
}'
],
'assertIfFalseOnPropertyNegated' => [
'code' => '<?php
class A {
public function foo() : void {}
}
class B {
private ?A $a = null;
public function bar() : void {
if (!$this->assertProperty()) {
$this->a->foo();
}
}
/**
* @psalm-assert-if-false !null $this->a
*/
public function assertProperty() : bool {
return $this->a !== null;
}
}'
],
'assertPropertyVisibleOutside' => [
'code' => '<?php
class A {
public ?int $x = null;
public function maybeAssignX() : void {
if (rand(0, 0) == 0) {
$this->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' => '<?php
/**
* @psalm-assert array<string, string[]> $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' => '<?php
/**
* @param mixed[] $a
*/
function one(array $a): void {
isInts($a);
}
/**
* @psalm-assert int[] $value
* @param mixed $value
*/
function isInts($value): void {}',
],
'simpleArrayAssertion' => [
'code' => '<?php
/**
* @psalm-assert array $data
* @param mixed $data
*/
function isArray($data): void {}
/**
* @param iterable<string> $arr
* @return array<string>
*/
function foo(iterable $arr) : array {
isArray($arr);
return $arr;
}'
],
'listAssertion' => [
'code' => '<?php
/**
* @psalm-assert list $data
* @param mixed $data
*/
function isList($data): void {}
/**
* @param array<string> $arr
* @return list<string>
*/
function foo(array $arr) : array {
isList($arr);
return $arr;
}'
],
'scanAssertionTypes' => [
'code' => '<?php
/**
* @param mixed $_p
* @psalm-assert-if-true Exception $_p
* @psalm-assert-if-false Error $_p
* @psalm-assert Throwable $_p
*/
function f($_p): bool {
return true;
}
$q = null;
if (rand(0, 1) && f($q)) {}
if (!f($q)) {}'
],
'assertDifferentTypeOfArray' => [
'code' => '<?php
/**
* @psalm-assert array{0: string, 1: string} $value
* @param mixed $value
*/
function isStringTuple($value): void {
if (!is_array($value)
|| !isset($value[0])
|| !isset($value[1])
|| !is_string($value[0])
|| !is_string($value[1])
) {
throw new \Exception("bad");
}
}
$s = "";
$parts = explode(":", $s, 2);
isStringTuple($parts);
echo $parts[0];
echo $parts[1];'
],
'assertStringOrIntOnString' => [
'code' => '<?php
/**
* @param mixed $v
* @psalm-assert string|int $v
*/
function assertStringOrInt($v) : void {}
function gimmeAString(?string $v): string {
/** @psalm-suppress TypeDoesNotContainType */
assertStringOrInt($v);
return $v;
}',
],
'assertIfTrueWithSpace' => [
'code' => '<?php
/**
* @param mixed $data
* @return bool
* @psalm-assert-if-true array{type: string} $data
*/
function isBar($data) {
return isset($data["type"]);
}
/**
* @param mixed $data
* @return string
*/
function doBar($data) {
if (isBar($data)) {
return $data["type"];
}
throw new \Exception();
}'
],
'assertOnNestedProperty' => [
'code' => '<?php
/** @psalm-immutable */
class B {
public ?array $arr = null;
public function __construct(?array $arr) {
$this->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' => '<?php
/** @psalm-immutable */
class B {
private ?array $arr = null;
public function __construct(?array $arr) {
$this->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' => '<?php
/** @psalm-immutable */
class A {
private ?array $arr = null;
public function __construct(?array $arr) {
$this->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' => '<?php
/**
* @psalm-assert array<string,string|object> $data
*/
function validate(array $data): void {}'
],
'nonEmptyList' => [
'code' => '<?php
/**
* @psalm-assert non-empty-list $array
*
* @param mixed $array
*/
function isNonEmptyList($array): void {}
/**
* @psalm-param mixed $value
*
* @psalm-return non-empty-list<mixed>
*/
function consume1($value): array {
isNonEmptyList($value);
return $value;
}
/**
* @psalm-param list<string> $values
*/
function consume2(array $values): void {
isNonEmptyList($values);
foreach ($values as $str) {}
echo $str;
}'
],
'nonEmptyListOfStrings' => [
'code' => '<?php
/**
* @psalm-assert non-empty-list<string> $array
*
* @param mixed $array
*/
function isNonEmptyListOfStrings($array): void {}
/**
* @psalm-param list<string> $values
*/
function consume2(array $values): void {
isNonEmptyListOfStrings($values);
foreach ($values as $str) {}
echo $str;
}'
],
'assertResource' => [
'code' => '<?php
/**
* @param mixed $foo
* @psalm-assert resource $foo
*/
function assertResource($foo) : void {
if (!is_resource($foo)) {
throw new \Exception("bad");
}
}
/**
* @param mixed $value
*
* @return resource
*/
function consume($value)
{
assertResource($value);
return $value;
}'
],
'parseLongAssertion' => [
'code' => '<?php
/**
* @psalm-assert array{
* extensions: array<string, array{
* version?: string,
* type?: "bundled"|"pecl",
* require?: list<string>,
* env?: array<string, array{
* deps?: list<string>,
* buildDeps?: list<string>,
* configure?: string
* }>
* }>
* } $data
*
* @param mixed $data
*/
function assertStructure($data): void {}'
],
'intersectArraysAfterAssertion' => [
'code' => '<?php
/**
* @psalm-assert array{foo: string} $v
*/
function hasFoo(array $v): void {}
/**
* @psalm-assert array{bar: int} $v
*/
function hasBar(array $v): void {}
function process(array $data): void {
hasFoo($data);
hasBar($data);
echo sprintf("%s %d", $data["foo"], $data["bar"]);
}'
],
'assertListIsIterableOfStrings' => [
'code' => '<?php
/**
* @psalm-assert iterable<string> $value
*
* @param mixed $value
*
* @throws InvalidArgumentException
*/
function allString($value): void {}
function takesAnArray(array $a): void {
$keys = array_keys($a);
allString($keys);
}',
],
'assertListIsListOfStrings' => [
'code' => '<?php
/**
* @psalm-assert list<string> $value
*
* @param mixed $value
*
* @throws InvalidArgumentException
*/
function allString($value): void {}
function takesAnArray(array $a): void {
$keys = array_keys($a);
allString($keys);
}',
],
'multipleAssertIfTrue' => [
'code' => '<?php
/**
* @param mixed $a
* @param mixed $b
* @psalm-assert-if-true string $a
* @psalm-assert-if-true string $b
*/
function assertAandBAreStrings($a, $b): bool {
if (!is_string($a)) { return false;}
if (!is_string($b)) { return false;}
return true;
}
/**
* @param mixed $a
* @param mixed $b
*/
function test($a, $b): string {
if (!assertAandBAreStrings($a, $b)) {
throw new \Exception();
}
return substr($a, 0, 1) . substr($b, 0, 1);
}'
],
'convertConstStringType' => [
'code' => '<?php
class A {
const T1 = 1;
const T2 = 2;
/**
* @param self::T* $t
*/
public static function bar(int $t):void {}
/**
* @psalm-assert-if-true self::T* $t
*/
public static function isValid(int $t): bool {
return in_array($t, [self::T1, self::T2], true);
}
}
function takesA(int $a) : void {
if (A::isValid($a)) {
A::bar($a);
}
}'
],
'multipleAssertIfTrueOnSameVariable' => [
'code' => '<?php
class A {}
function foo(string|null|A $a) : A {
if (isComputed($a)) {
return $a;
}
throw new Exception("bad");
}
/**
* @psalm-assert-if-true !null $value
* @psalm-assert-if-true !string $value
*/
function isComputed(mixed $value): bool {
return $value !== null && !is_string($value);
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '8.0'
],
'assertStaticSelf' => [
'code' => '<?php
final class C {
/** @var null|int */
private static $q = null;
/** @psalm-assert int self::$q */
private static function prefillQ(): void {
self::$q = 123;
}
public static function getQ(): int {
self::prefillQ();
return self::$q;
}
}
?>'
],
'assertIfTrueStaticSelf' => [
'code' => '<?php
final class C {
/** @var null|int */
private static $q = null;
/** @psalm-assert-if-true int self::$q */
private static function prefillQ(): bool {
if (rand(0,1)) {
self::$q = 123;
return true;
}
return false;
}
public static function getQ(): int {
if (self::prefillQ()) {
return self::$q;
}
return -1;
}
}
?>'
],
'assertIfFalseStaticSelf' => [
'code' => '<?php
final class C {
/** @var null|int */
private static $q = null;
/** @psalm-assert-if-false int self::$q */
private static function prefillQ(): bool {
if (rand(0,1)) {
self::$q = 123;
return false;
}
return true;
}
public static function getQ(): int {
if (self::prefillQ()) {
return -1;
}
return self::$q;
}
}
?>'
],
'assertStaticByInheritedMethod' => [
'code' => '<?php
class A {
/** @var null|int */
protected static $q = null;
/** @psalm-assert int self::$q */
protected static function prefillQ(): void {
self::$q = 123;
}
}
class B extends A {
public static function getQ(): int {
self::prefillQ();
return self::$q;
}
}
?>'
],
'assertInheritedStatic' => [
'code' => '<?php
class A {
/** @var null|int */
protected static $q = null;
}
class B extends A {
/** @psalm-assert int self::$q */
protected static function prefillQ(): void {
self::$q = 123;
}
public static function getQ(): int {
self::prefillQ();
return self::$q;
}
}
?>'
],
'assertStaticOnUnrelatedClass' => [
'code' => '<?php
class A {
/** @var null|int */
public static $q = null;
}
class B {
/** @psalm-assert int A::$q */
private static function prefillQ(): void {
A::$q = 123;
}
public static function getQ(): int {
self::prefillQ();
return A::$q;
}
}
?>'
],
'implicitComplexAssertionNoCrash' => [
'code' => '<?php
class Foo {
private string $status = "";
public function assertValidStatusTransition(string $status): void
{
if (
("canceled" === $this->status && "complete" === $status)
|| ("canceled" === $this->status && "pending" === $status)
|| ("complete" === $this->status && "canceled" === $status)
|| ("complete" === $this->status && "pending" === $status)
) {
throw new \LogicException();
}
}
}'
],
'assertArrayIteratorIsIterableOfStrings' => [
'code' => '<?php
/**
* @psalm-assert iterable<string> $value
* @param mixed $value
*
* @return void
*/
function assertAllString($value) : void {
throw new \Exception(\var_export($value, true));
}
/**
* @param ArrayIterator<string, mixed> $value
*
* @return ArrayIterator<string, string>
*/
function preserveContainerAllArrayIterator($value) {
assertAllString($value);
return $value;
}'
],
'implicitReflectionParameterAssertion' => [
'code' => '<?php
$method = new ReflectionMethod(stdClass::class);
$parameters = $method->getParameters();
foreach ($parameters as $parameter) {
if ($parameter->hasType()) {
$parameter->getType()->__toString();
}
}',
],
'reflectionNameTypeClassStringIfNotBuiltin' => [
'code' => '<?php
/** @return class-string|null */
function getPropertyType(\ReflectionProperty $reflectionItem): ?string {
$type = $reflectionItem->getType();
return ($type instanceof \ReflectionNamedType) && !$type->isBuiltin() ? $type->getName() : null;
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '7.4',
],
'withHasTypeCall' => [
'code' => '<?php
/**
* @psalm-immutable
*/
class Param {
/**
* @psalm-assert-if-true ReflectionType $this->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' => '<?php
class Foo{}
/**
* @param array<Foo> $foos
* @return array<Foo>
*/
function foo(array $foos) : array {
allIsInstanceOf($foos, Foo::class);
return $foos;
}
/**
* @template ExpectedType of object
*
* @param mixed $value
* @param class-string<ExpectedType> $class
* @psalm-assert iterable<ExpectedType> $value
*/
function allIsInstanceOf($value, $class): void {}'
],
'implicitReflectionPropertyAssertion' => [
'code' => '<?php
$class = new ReflectionClass(stdClass::class);
$properties = $class->getProperties();
foreach ($properties as $property) {
if ($property->hasType()) {
$property->getType()->allowsNull();
}
}',
'assertions' => [],
'ignored_issues' => [],
'php_version' => '7.4'
],
'onPropertyOfImmutableArgument' => [
'code' => '<?php
/** @psalm-immutable */
class Aclass {
public ?string $b;
public function __construct(?string $b) {
$this->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' => '<?php
/** @psalm-immutable */
class A {
public ?int $b;
public function __construct(?int $b) {
$this->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' => '<?php
/** @psalm-immutable */
class A {
public ?int $b;
public function __construct(?int $b) {
$this->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' => '<?php
class B {
public ?string $c;
public function __construct(?string $c) {
$this->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' => '<?php
class B {
public ?string $c;
public function __construct(?string $c) {
$this->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' => '<?php
class A
{
function test(): void
{
$a = [stdClass::class => ""];
/** @var array<class-string, mixed> $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' => '<?php
class Foo {
/** @var array<string, int> */
public array $bar;
/**
* @param array<string, int> $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' => '<?php
class Foo
{
public ?string $bar = null;
public function nonMutationFree(): void {}
}
/**
* @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 {}
',
],
'referencesDontBreakAssertions' => [
'code' => '<?php
/** @var string|null */
$foo = "";
$bar = &$foo;
$baz = &$foo;
if (assertNotNull($foo)) {
requiresString($foo);
}
/**
* @param mixed $foo
* @psalm-assert-if-true !null $foo
*/
function assertNotNull($foo): bool
{
return $foo !== null;
}
function requiresString(string $_str): void {}
',
],
'applyAssertionsToReferences' => [
'code' => '<?php
/** @var string|null */
$foo = "";
$bar = &$foo;
if (assertNotNull($foo)) {
requiresString($bar);
}
/**
* @param mixed $foo
* @psalm-assert-if-true !null $foo
*/
function assertNotNull($foo): bool
{
return $foo !== null;
}
function requiresString(string $_str): void {}
',
],
'applyAssertionsFromReferences' => [
'code' => '<?php
/** @var string|null */
$foo = "";
$bar = &$foo;
if (assertNotNull($bar)) {
requiresString($foo);
}
/**
* @param mixed $foo
* @psalm-assert-if-true !null $foo
*/
function assertNotNull($foo): bool
{
return $foo !== null;
}
function requiresString(string $_str): void {}
',
],
'applyAssertionsOnPropertiesToReferences' => [
'code' => '<?php
class Foo
{
public ?string $bar = null;
}
/**
* @psalm-assert-if-true !null $foo->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' => '<?php
class Foo
{
public ?string $bar = null;
}
/**
* @psalm-assert-if-true !null $foo->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' => '<?php
class Foo
{
public ?string $bar = null;
}
/**
* @psalm-assert-if-true !null $foo->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' => '<?php
class A{
/**
* @template T
* @param array<T> $objects
* @return array<T>
*/
private function uniquateObjects(array $objects) : array
{
$uniqueObjects = [];
foreach ($objects as $object) {
if (in_array($object, $uniqueObjects, true)) {
continue;
}
$uniqueObjects[] = $object;
}
return $uniqueObjects;
}
}
',
],
'assertionOnMagicProperty' => [
'code' => '<?php
/**
* @property ?string $b
*/
class A {
/** @psalm-mutation-free */
public function __get(string $key) {return "";}
public function __set(string $key, string $value): void {}
}
$a = new A;
/** @psalm-assert-if-true string $arg->b */
function assertString(A $arg): bool {return $arg->b !== null;}
if (assertString($a)) {
requiresString($a->b);
}
function requiresString(string $_str): void {}
',
],
'assertWithEmptyStringOnKeyedArray' => [
'code' => '<?php
class A
{
function test(): void
{
$a = ["" => ""];
/** @var array<string, mixed> $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' => '<?php
/** @psalm-assert non-empty-string $input */
function assertLowerCase(string $input): void { throw new \Exception($input . " irrelevant"); }
/**
* @param lowercase-string $input
* @return non-empty-lowercase-string
*/
function makeLowerNonEmpty(string $input): string
{
assertLowerCase($input);
return $input;
}',
],
'assertOneOfValuesWithinArray' => [
'code' => '<?php
/**
* @template T
* @param mixed $input
* @param array<array-key,T> $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);
'
],
];
}
/**
*
*/
public function providerInvalidCodeParse(): iterable
{
return [
'assertInstanceOfMultipleInterfaces' => [
'code' => '<?php
class A {
public function bar() : void {}
}
interface I1 {
public function foo1(): void;
}
interface I2 {
public function foo2(): void;
}
class B extends A implements I1, I2 {
public function foo1(): void {}
public function foo2(): void {}
}
function assertInstanceOfInterfaces(A $var): void {
if (!$var instanceof I1 && !$var instanceof I2) {
throw new \Exception();
}
}
function takesA(A $a): void {
assertInstanceOfInterfaces($a);
$a->bar();
$a->foo1();
}',
'error_message' => 'UndefinedMethod',
],
'assertIfTrueNoAnnotation' => [
'code' => '<?php
function isValidString(?string $myVar) : bool {
return $myVar !== null && $myVar[0] === "a";
}
$myString = rand(0, 1) ? "abacus" : null;
if (isValidString($myString)) {
echo "Ma chaine " . $myString;
}',
'error_message' => 'PossiblyNullOperand',
],
'assertIfFalseNoAnnotation' => [
'code' => '<?php
function isInvalidString(?string $myVar) : bool {
return $myVar === null || $myVar[0] !== "a";
}
$myString = rand(0, 1) ? "abacus" : null;
if (isInvalidString($myString)) {
// do something
} else {
echo "Ma chaine " . $myString;
}',
'error_message' => 'PossiblyNullOperand',
],
'assertIfTrueMethodCall' => [
'code' => '<?php
class C {
/**
* @param mixed $p
* @psalm-assert-if-true int $p
*/
public function isInt($p): bool {
return is_int($p);
}
/**
* @param mixed $p
*/
public function doWork($p): void {
if ($this->isInt($p)) {
strlen($p);
}
}
}',
'error_message' => 'InvalidScalarArgument',
],
'assertIfStaticTrueMethodCall' => [
'code' => '<?php
class C {
/**
* @param mixed $p
* @psalm-assert-if-true int $p
*/
public static function isInt($p): bool {
return is_int($p);
}
/**
* @param mixed $p
*/
public function doWork($p): void {
if ($this->isInt($p)) {
strlen($p);
}
}
}',
'error_message' => 'InvalidScalarArgument',
],
'noFatalForUnknownAssertClass' => [
'code' => '<?php
interface Foo {}
class Bar implements Foo {
public function sayHello(): void {
echo "Hello";
}
}
/**
* @param mixed $value
* @param class-string $type
* @psalm-assert SomeUndefinedClass $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();',
'error_message' => 'UndefinedDocblockClass',
],
'assertValueImpossible' => [
'code' => '<?php
/**
* @psalm-assert "foo"|"bar"|"foo-bar" $s
*/
function assertFooBar(string $s) : void {
}
$a = "";
assertFooBar($a);',
'error_message' => 'TypeDoesNotContainType',
],
'sortOfReplacementForAssert' => [
'code' => '<?php
namespace Bar;
/**
* @param mixed $_b
* @psalm-assert true $_b
*/
function myAssert($_b) : void {
if ($_b !== true) {
throw new \Exception("bad");
}
}
function bar(?string $s) : string {
myAssert($s);
return $s;
}',
'error_message' => 'TypeDoesNotContainType',
],
'assertScalarAndEmpty' => [
'code' => '<?php
/**
* @param mixed $value
* @psalm-assert scalar $value
* @psalm-assert !empty $value
*/
function assertScalarNotEmpty($value) : void {}
/** @param scalar $s */
function takesScalar($s) : void {}
/**
* @param mixed $bar
*/
function foo($bar) : void {
assertScalarNotEmpty($bar);
takesScalar($bar);
if ($bar) {}
}',
'error_message' => 'RedundantConditionGivenDocblockType - src'
. DIRECTORY_SEPARATOR . 'somefile.php:19:29',
],
'assertOneOfStrings' => [
'code' => '<?php
/**
* @psalm-assert "a"|"b" $s
*/
function foo(string $s) : void {}
function takesString(string $s) : void {
foo($s);
if ($s === "c") {}
}',
'error_message' => 'DocblockTypeContradiction',
],
'assertThisType' => [
'code' => '<?php
class Type {
/**
* @psalm-assert FooType $this
*/
public function isFoo() : bool {
if (!$this instanceof FooType) {
throw new \Exception();
}
return true;
}
}
class FooType extends Type {
public function bar(): void {}
}
function takesType(Type $t) : void {
$t->bar();
$t->isFoo();
}',
'error_message' => 'UndefinedMethod',
],
'invalidUnionAssertion' => [
'code' => '<?php
interface I {
/**
* @psalm-assert null|!ExpectedType $value
*/
public static function foo($value);
}',
'error_message' => 'InvalidDocblock',
],
'assertNotEmptyOnBool' => [
'code' => '<?php
/**
* @param mixed $value
* @psalm-assert !empty $value
*/
function assertNotEmpty($value) : void {}
function foo(bool $bar) : void {
assertNotEmpty($bar);
if ($bar) {}
}',
'error_message' => 'RedundantConditionGivenDocblockType',
],
'withoutHasTypeCall' => [
'code' => '<?php
$method = new ReflectionMethod(stdClass::class);
$parameters = $method->getParameters();
foreach ($parameters as $parameter) {
$parameter->getType()->__toString();
}',
'error_message' => 'PossiblyNullReference',
],
'forgetAssertionAfterRelevantNonMutationFreeCall' => [
'code' => '<?php
class Foo
{
public ?string $bar = null;
public function nonMutationFree(): void
{
$this->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' => '<?php
class Foo
{
public ?string $bar = null;
public function nonMutationFree(): void
{
$this->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' => '<?php
class Foo
{
public ?string $bar = null;
}
/**
* @psalm-assert-if-true !null $foo->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' => '<?php
/**
* @property ?string $b
*/
class A {
public function __get(string $key) {return "";}
public function __set(string $key, string $value): void {}
}
$a = new A;
/** @psalm-assert-if-true string $arg->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' => '<?php
/**
* @property int<1, 10> $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',
],
];
}
}