1
0
mirror of https://github.com/danog/psalm.git synced 2024-12-13 09:47:29 +01:00
psalm/tests/TypeAnnotationTest.php

1115 lines
36 KiB
PHP
Raw Normal View History

2018-12-20 07:06:43 +01:00
<?php
2018-12-20 07:06:43 +01:00
namespace Psalm\Tests;
2021-12-04 21:55:53 +01:00
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
2018-12-20 07:06:43 +01:00
class TypeAnnotationTest extends TestCase
{
2021-12-04 21:55:53 +01:00
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
2018-12-20 07:06:43 +01:00
public function providerValidCodeParse(): iterable
2018-12-20 07:06:43 +01:00
{
return [
'typeAliasBeforeClass' => [
'code' => '<?php
namespace Barrr;
2018-12-20 07:06:43 +01:00
/**
* @psalm-type CoolType = A|B|null
*/
class A {}
class B {}
/** @return CoolType */
function foo() {
if (rand(0, 1)) {
return new A();
}
if (rand(0, 1)) {
return new B();
}
return null;
}
/** @param CoolType $a **/
function bar ($a) : void { }
2019-03-23 19:27:54 +01:00
bar(foo());',
2018-12-20 07:06:43 +01:00
],
'typeAliasBeforeFunction' => [
'code' => '<?php
namespace Barrr;
2018-12-20 07:06:43 +01:00
/**
* @psalm-type A_OR_B = A|B
* @psalm-type CoolType = A_OR_B|null
* @return CoolType
*/
function foo() {
if (rand(0, 1)) {
return new A();
}
if (rand(0, 1)) {
return new B();
}
return null;
}
class A {}
class B {}
/** @param CoolType $a **/
function bar ($a) : void { }
2019-03-23 19:27:54 +01:00
bar(foo());',
2018-12-20 07:06:43 +01:00
],
'typeAliasInSeparateBlockBeforeFunction' => [
'code' => '<?php
namespace Barrr;
2018-12-20 07:06:43 +01:00
/**
* @psalm-type CoolType = A|B|null
*/
/**
* @return CoolType
*/
function foo() {
if (rand(0, 1)) {
return new A();
}
if (rand(0, 1)) {
return new B();
}
return null;
}
class A {}
class B {}
/** @param CoolType $a **/
function bar ($a) : void { }
2019-03-23 19:27:54 +01:00
bar(foo());',
2018-12-20 07:06:43 +01:00
],
'almostFreeStandingTypeAlias' => [
'code' => '<?php
2018-12-20 07:06:43 +01:00
/**
* @psalm-type CoolType = A|B|null
*/
// this breaks up the line
class A {}
class B {}
/** @return CoolType */
function foo() {
if (rand(0, 1)) {
return new A();
}
if (rand(0, 1)) {
return new B();
}
return null;
}
/** @param CoolType $a **/
function bar ($a) : void { }
2019-03-23 19:27:54 +01:00
bar(foo());',
2018-12-20 07:06:43 +01:00
],
'typeAliasUsedTwice' => [
'code' => '<?php
namespace Baz;
2018-12-20 07:06:43 +01:00
/** @psalm-type TA = array<int, string> */
class Bar {
public function foo() : void {
$bar =
/** @return TA */
function() {
return ["hello"];
};
/** @var array<int, TA> */
$bat = [$bar(), $bar()];
foreach ($bat as $b) {
echo $b[0];
}
}
}
/**
* @psalm-type _A=array{elt:int}
2018-12-20 07:06:43 +01:00
* @param _A $p
* @return _A
*/
function f($p) {
/** @var _A */
$r = $p;
return $r;
}',
],
'classTypeAliasSimple' => [
'code' => '<?php
namespace Bar;
/** @psalm-type PhoneType = array{phone: string} */
2020-05-15 22:18:52 +02:00
class Phone {
/** @psalm-return PhoneType */
public function toArray(): array {
return ["phone" => "Nokia"];
}
}
/** @psalm-type NameType = array{name: string} */
2020-05-15 22:18:52 +02:00
class Name {
/** @psalm-return NameType */
function toArray(): array {
return ["name" => "Matt"];
}
}
/**
* @psalm-import-type PhoneType from Phone as PhoneType2
* @psalm-import-type NameType from Name as NameType2
*
* @psalm-type UserType = PhoneType2&NameType2
*/
class User {
/** @psalm-return UserType */
function toArray(): array {
return array_merge(
(new Name)->toArray(),
(new Phone)->toArray()
);
}
2022-12-18 17:15:15 +01:00
}',
2020-05-15 22:18:52 +02:00
],
'classTypeAliasImportWithAlias' => [
'code' => '<?php
namespace Bar;
/** @psalm-type PhoneType = array{phone: string} */
class Phone {
/** @psalm-return PhoneType */
public function toArray(): array {
return ["phone" => "Nokia"];
}
}
/**
* @psalm-import-type PhoneType from Phone as TPhone
*/
class User {
/** @psalm-return TPhone */
function toArray(): array {
return array_merge([], (new Phone)->toArray());
}
2022-12-18 17:15:15 +01:00
}',
],
'classTypeAliasDirectUsage' => [
'code' => '<?php
namespace Bar;
/** @psalm-type PhoneType = array{phone: string} */
class Phone {
/** @psalm-return PhoneType */
public function toArray(): array {
return ["phone" => "Nokia"];
}
}
/**
* @psalm-import-type PhoneType from Phone
*/
class User {
/** @psalm-return PhoneType */
function toArray(): array {
return array_merge([], (new Phone)->toArray());
}
2022-12-18 17:15:15 +01:00
}',
],
'classTypeAliasFromExternalNamespace' => [
'code' => '<?php
namespace Foo {
/** @psalm-type PhoneType = array{phone: string} */
class Phone {
/** @psalm-return PhoneType */
public function toArray(): array {
return ["phone" => "Nokia"];
}
}
}
namespace Bar {
/**
* @psalm-import-type PhoneType from \Foo\Phone
*/
class User {
/** @psalm-return PhoneType */
function toArray(): array {
return (new \Foo\Phone)->toArray();
}
}
2022-12-18 17:15:15 +01:00
}',
],
'importTypeForParam' => [
'code' => '<?php
namespace Bar;
/**
* @psalm-type Type = self::NULL|self::BOOL|self::INT|self::STRING
*/
interface I
{
public const NULL = 0;
public const BOOL = 1;
public const INT = 2;
public const STRING = 3;
/**
* @psalm-param Type $type
*/
public function a(int $type): void;
}
/**
* @psalm-import-type Type from I as Type2
*/
abstract class C implements I
{
public function a(int $type): void
{
$this->b($type);
}
/**
* @psalm-param Type2 $type
*/
private function b(int $type): void
{
}
2022-12-18 17:15:15 +01:00
}',
],
'usedInVarForForeach' => [
'code' => '<?php
/** @psalm-type _B=array{p1:string} */
function e(array $a): void
{
/** @var _B $elt */
foreach ($a as $elt) {
echo $elt["p1"];
}
2022-12-18 17:15:15 +01:00
}',
],
'objectWithPropertiesAlias' => [
'code' => '<?php
/**
* @psalm-type FooStruct=string
*/
class A {}
/**
* @psalm-import-type FooStruct from A as F2
*/
class B {
/**
* @param object{foo: F2} $a
* @return object{foo: string}
*/
public function bar($a) {
return $a;
}
2022-12-18 17:15:15 +01:00
}',
],
'sameDocBlockTypeAliasAsTypeParameterForInterface' => [
'code' => '<?php
/** @template T */
interface A {
/** @return T */
public function output();
}
/**
* @psalm-type Foo=string
* @implements A<Foo>
*/
class C implements A {
public function output() {
return "hello";
}
}
$instance = new C();
$output = $instance->output();',
'assertions' => [
'$output' => 'string',
],
],
'sameDocBlockTypeAliasAsTypeParameterForExtendedRegularClass' => [
'code' => '<?php
/** @template T */
class A {
/** @var T */
public $value;
/** @param T $value */
public function __construct($value) {
$this->value = $value;
}
}
/**
* @psalm-type Foo=string
* @extends A<Foo>
*/
class C extends A {}
$instance = new C("hello");
$output = $instance->value;',
'assertions' => [
'$output' => 'string',
],
],
'sameDocBlockTypeAliasAsTypeParameterForExtendedAbstractClass' => [
'code' => '<?php
/** @template T */
abstract class A {
/** @var T */
public $value;
/** @param T $value */
public function __construct($value) {
$this->value = $value;
}
}
/**
* @psalm-type Foo=string
* @extends A<Foo>
*/
class C extends A {}
$instance = new C("hello");
$output = $instance->value;',
'assertions' => [
'$output' => 'string',
],
],
'importedTypeAliasAsTypeParameterForImplementation' => [
'code' => '<?php
namespace Bar;
/** @template T */
interface A {}
/** @psalm-type Foo=string */
class B {}
/**
* @psalm-import-type Foo from B
* @implements A<Foo>
*/
class C implements A {}',
],
'importedTypeAliasAsConstrainedTypeParameterForImplementation' => [
'code' => '<?php
namespace Bar;
/** @template T of string */
interface A {}
/**
* @psalm-type Foo = "foo"
*/
class B {}
/**
* @psalm-import-type Foo from B
* @implements A<Foo>
*/
class C implements A {}
2022-12-18 17:15:15 +01:00
',
],
'importedTypeAliasAsTypeParameterForExtendedClass' => [
'code' => '<?php
namespace Bar;
/** @template T */
class A {}
/** @psalm-type Foo=string */
class B {}
/**
* @psalm-import-type Foo from B
* @extends A<Foo>
*/
class C extends A {}',
],
'importedTypeAliasAsTypeParameterForExtendedAbstractClass' => [
'code' => '<?php
namespace Bar;
/** @template T */
abstract class A {}
/** @psalm-type Foo=string */
class B {}
/**
* @psalm-import-type Foo from B
* @extends A<Foo>
*/
class C extends A {}',
],
'importedTypeAliasRenamedAsTypeParameterForImplementation' => [
'code' => '<?php
namespace Bar;
/** @template T */
interface A {}
/** @psalm-type Foo=string */
class B {}
/**
* @psalm-import-type Foo from B as NewName
* @implements A<NewName>
*/
class C implements A {}',
],
'importedTypeAliasRenamedAsTypeParameterForExtendedClass' => [
'code' => '<?php
namespace Bar;
/** @template T */
class A {}
/** @psalm-type Foo=string */
class B {}
/**
* @psalm-import-type Foo from B as NewName
* @extends A<NewName>
*/
class C extends A {}',
],
'importedTypeAliasRenamedAsTypeParameterForExtendedAbstractClass' => [
'code' => '<?php
namespace Bar;
/** @template T */
abstract class A {}
/** @psalm-type Foo=string */
class B {}
/**
* @psalm-import-type Foo from B as NewName
* @extends A<NewName>
*/
class C extends A {}',
],
'importedTypeInsideLocalTypeAliasUsedAsTypeParameter' => [
'code' => '<?php
/** @template T */
abstract class A {
/** @var T */
public $value;
/** @param T $value */
public function __construct($value) {
$this->value = $value;
}
}
/**
* @psalm-type Foo=string
*/
class B {}
/**
* @psalm-import-type Foo from B
* @psalm-type Baz=Foo
*
* @extends A<Baz>
*/
class C extends A {}
$instance = new C("hello");
$output = $instance->value;',
'assertions' => [
'$output' => 'string',
],
],
'importedTypeWithPhpstanAnnotation' => [
'code' => '<?php
/** @template T */
abstract class A {
/** @var T */
public $value;
/** @param T $value */
public function __construct($value) {
$this->value = $value;
}
}
/**
* @phpstan-type Foo=string
*/
class B {}
/**
* @phpstan-import-type Foo from B
* @phpstan-type Baz=Foo
*
* @extends A<Baz>
*/
class C extends A {}
$instance = new C("hello");
$output = $instance->value;',
'assertions' => [
'$output' => 'string',
],
],
'importedTypeUsedInAssertion' => [
'code' => '<?php
2022-11-14 19:54:55 +01:00
/** @psalm-type Foo = string */
class A {}
/**
* @psalm-immutable
* @psalm-import-type Foo from A as FooAlias
*/
class B {
/**
* @param mixed $input
* @psalm-return FooAlias
*/
public function convertToFoo($input) {
$this->assertFoo($input);
return $input;
}
/**
* @param mixed $value
* @psalm-assert FooAlias $value
*/
private function assertFoo($value): void {
if(!is_string($value)) {
throw new \InvalidArgumentException();
}
}
}
$instance = new B();
$output = $instance->convertToFoo("hallo");
',
'assertions' => [
'$output' => 'string',
2022-12-18 17:15:15 +01:00
],
],
'importedTypeUsedInOtherType' => [
'code' => '<?php
/** @psalm-type OpeningTypes=self::TYPE_A|self::TYPE_B */
class Foo {
public const TYPE_A = 1;
public const TYPE_B = 2;
}
/**
* @psalm-import-type OpeningTypes from Foo
* @psalm-type OpeningTypeAssignment=list<OpeningTypes>
*/
class Main {
/** @return OpeningTypeAssignment */
public function doStuff(): array {
return [];
}
}
$instance = new Main();
$output = $instance->doStuff();
',
'assertions' => [
'$output===' => 'list<1|2>',
2022-12-18 17:15:15 +01:00
],
],
2023-03-12 18:34:41 +01:00
'callableWithReturnTypeTypeAliasWithinBackets' => [
'code' => '<?php
/** @psalm-type TCallback (callable():int) */
class Foo {
/** @psalm-var TCallback */
public static callable $callback;
}
$output = Foo::$callback;
',
'assertions' => [
'$output===' => 'callable():int',
],
],
'callableWithReturnTypeTypeAlias' => [
'code' => '<?php
/** @psalm-type TCallback callable():int */
class Foo {
/** @psalm-var TCallback */
public static callable $callback;
}
$output = Foo::$callback;
',
'assertions' => [
'$output===' => 'callable():int',
],
],
2023-03-13 06:07:19 +01:00
'unionOfStringsContainingBraceChar' => [
2023-03-12 18:34:41 +01:00
'code' => '<?php
/** @psalm-type T \'{\'|\'}\' */
class Foo {
/** @psalm-var T */
public static string $t;
}
$t = Foo::$t;
',
'assertions' => [
'$t===' => '\'{\'|\'}\'',
],
],
2023-03-13 06:07:19 +01:00
'unionOfStringsContainingGTChar' => [
2023-03-12 18:34:41 +01:00
'code' => '<?php
/** @psalm-type T \'<\'|\'>\' */
class Foo {
/** @psalm-var T */
public static string $t;
}
$t = Foo::$t;
',
'assertions' => [
'$t===' => '\'<\'|\'>\'',
],
],
2023-03-13 06:07:19 +01:00
'unionOfStringsContainingBracketChar' => [
2023-03-12 18:34:41 +01:00
'code' => '<?php
/** @psalm-type T \'(\'|\')\' */
class Foo {
/** @psalm-var T */
public static string $t;
}
$t = Foo::$t;
',
'assertions' => [
'$t===' => '\'(\'|\')\'',
],
],
2023-03-13 06:07:19 +01:00
'bareWordsCommentAfterType' => [
2023-03-12 18:34:41 +01:00
'code' => '<?php
/**
* @psalm-type T string
*
2023-03-13 06:07:19 +01:00
* Lorem ipsum
2023-03-12 18:34:41 +01:00
*/
class Foo {
/** @psalm-var T */
public static string $t;
}
$t = Foo::$t;
',
'assertions' => [
'$t===' => 'string',
],
],
'handlesTypeWhichEndsWithRoundBracket' => [
'code' => '<?php
/**
* @psalm-type Foo=(iterable<mixed>)
*/
class A {}
',
],
'commentAfterType' => [
'code' => '<?php
/**
* @psalm-type TTest string
*
* This is a test class.
*/
class Test {}',
],
'multilineTypeWithComment' => [
'code' => '<?php
/**
* @psalm-type PhoneType = array{
* phone: string
* }
*
* Bar
*/
class Foo {
/** @var PhoneType */
public static $phone;
}
$output = Foo::$phone;
',
'assertions' => [
'$output===' => 'array{phone: string}',
],
],
'combineAliasOfArrayAndArrayCorrectly' => [
'code' => '<?php
/**
* @psalm-type C = array{c: string}
*/
class A {}
/**
* @template F
*/
class D {
/**
* @param F $data
*/
public function __construct(public $data) {}
}
/**
* @psalm-import-type C from A
*/
class G {
/**
* @param D<array{b: bool}>|D<C> $_doesNotWork
*/
public function doesNotWork($_doesNotWork): void {
/** @psalm-check-type-exact $_doesNotWork = D<array{b?: bool, c?: string}> */;
}
}',
],
2018-12-20 07:06:43 +01:00
];
}
public function providerInvalidCodeParse(): iterable
2018-12-20 07:06:43 +01:00
{
return [
'invalidTypeAlias' => [
'code' => '<?php
namespace Barrr;
2018-12-20 07:06:43 +01:00
/**
* @psalm-type CoolType = A|B>
*/
class A {}',
'error_message' => 'InvalidDocblock',
],
'typeAliasInTKeyedArray' => [
'code' => '<?php
namespace Barrr;
2018-12-20 07:06:43 +01:00
/**
* @psalm-type aType null|"a"|"b"|"c"|"d"
*/
/** @psalm-return array{0:bool,1:aType} */
2018-12-20 07:06:43 +01:00
function f(): array {
return [(bool)rand(0,1), rand(0,1) ? "z" : null];
}',
'error_message' => 'InvalidReturnStatement',
],
2020-05-15 22:18:52 +02:00
'classTypeAliasInvalidReturn' => [
'code' => '<?php
namespace Barrr;
/** @psalm-type PhoneType = array{phone: string} */
2020-05-15 22:18:52 +02:00
class Phone {
/** @psalm-return PhoneType */
public function toArray(): array {
return ["phone" => "Nokia"];
}
}
/** @psalm-type NameType = array{name: string} */
2020-05-15 22:18:52 +02:00
class Name {
/** @psalm-return NameType */
function toArray(): array {
return ["name" => "Matt"];
}
}
/**
* @psalm-import-type PhoneType from Phone as PhoneType2
* @psalm-import-type NameType from Name as NameType2
*
* @psalm-type UserType = PhoneType2&NameType2
*/
class User {
/** @psalm-return UserType */
function toArray(): array {
return array_merge(
(new Name)->toArray(),
["foo" => "bar"]
);
}
}',
'error_message' => 'InvalidReturnStatement',
],
'classTypeInvalidAliasImport' => [
'code' => '<?php
namespace Barrr;
class Phone {
function toArray(): array {
return ["name" => "Matt"];
}
}
/**
* @psalm-import-type PhoneType from Phone
*/
class User {}',
'error_message' => 'InvalidTypeImport',
],
'classTypeAliasFromInvalidClass' => [
'code' => '<?php
namespace Barrr;
/**
* @psalm-import-type PhoneType from Phone
*/
class User {}',
'error_message' => 'UndefinedDocblockClass',
],
'malformedImportMissingFrom' => [
'code' => '<?php
namespace Barrr;
/** @psalm-import-type Thing */
class C {}
',
'error_message' => 'InvalidTypeImport',
],
'malformedImportMissingSourceClass' => [
'code' => '<?php
namespace Barrr;
/** @psalm-import-type Thing from */
class C {}
',
'error_message' => 'InvalidTypeImport',
],
'malformedImportMisspelledFrom' => [
'code' => '<?php
namespace Barrr;
/** @psalm-import-type Thing morf */
class C {}
',
'error_message' => 'InvalidTypeImport',
],
'malformedImportMissingAlias' => [
'code' => '<?php
namespace Barrr;
/** @psalm-import-type Thing from Somewhere as */
class C {}
',
'error_message' => 'InvalidTypeImport',
],
'noCrashWithPriorReference' => [
'code' => '<?php
namespace Barrr;
/**
* @psalm-type _C=array{c:_CC}
* @psalm-type _CC=float
*/
class A {
/**
* @param _C $arr
*/
public function foo(array $arr) : void {}
}',
'error_message' => 'UndefinedDocblockClass',
],
'mergeImportedTypes' => [
'code' => '<?php
namespace A\B;
/**
* @psalm-type _A=array{
* id:int
* }
*
* @psalm-type _B=array{
* id:int,
* something:int
* }
*/
class Types
{
}
namespace A;
/**
* @psalm-import-type _A from \A\B\Types as _AA
* @psalm-import-type _B from \A\B\Types as _BB
*/
class Id
{
/**
* @psalm-param _AA|_BB $_item
*/
public function ff(array $_item): int
{
return $_item["something"];
}
}',
'error_message' => 'PossiblyUndefinedArrayOffset',
],
'noCrashWithSelfReferencingType' => [
'code' => '<?php
/**
* @psalm-type SomeType = array{
* parent?: SomeType,
* foo?: int,
* }
* @psalm-param SomeType $input
*/
function test(array $input):void {}',
'error_message' => 'InvalidDocblock',
],
'invalidTypeWhenNotImported' => [
'code' => '<?php
/** @psalm-type Foo = string */
class A {}
/** @template T */
interface B {}
/** @implements B<Foo> */
class C implements B {}',
'error_message' => 'UndefinedDocblockClass',
],
'invalidTypeWhenNotImportedInsideAnotherTypeAlias' => [
'code' => '<?php
/** @psalm-type Foo = string */
class A {}
/** @template T */
interface B {}
/**
* @psalm-type Baz=Foo
* @implements B<Baz>
*/
class C implements B {}',
'error_message' => 'UndefinedDocblockClass',
],
'duplicateKeyInArrayShapeOnInterfaceIsReported' => [
'code' => <<<'PHP'
<?php
/**
* @psalm-type Attributes = array{
* name: string,
* email: string,
* email: string,
* }
*/
interface A {
/**
* @return Attributes
*/
public function getAttributes(): array;
}
PHP,
'error_message' => 'InvalidDocblock',
],
'duplicateKeyInArrayShapeOnAClassIsReported' => [
'code' => <<<'PHP'
<?php
/**
* @psalm-type Attributes = array{
* name: string,
* email: string,
* email: string,
* }
*/
class A {
/**
* @return Attributes
*/
public function getAttributes(): array {
return [];
}
}
PHP,
'error_message' => 'InvalidDocblock',
],
'duplicateKeyInArrayShapeOnATraitIsReported' => [
'code' => <<<'PHP'
<?php
/**
* @psalm-type Attributes = array{
* name: string,
* email: string,
* email: string,
* }
*/
trait A {
/**
* @return Attributes
*/
public function getAttributes(): array {
return [];
}
}
PHP,
'error_message' => 'InvalidDocblock',
],
'duplicateKeyInArrayShapeOnAnEnumIsReported' => [
'code' => <<<'PHP'
<?php
/**
* @psalm-type Attributes = array{
* name: string,
* email: string,
* email: string,
* }
*/
enum A {
case FOO;
}
PHP,
'error_message' => 'InvalidDocblock',
'ignored_issues' => [],
'php_version' => '8.1',
],
2018-12-20 07:06:43 +01:00
];
}
}