1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00
psalm/tests/TypeParseTest.php

1244 lines
38 KiB
PHP
Raw Normal View History

<?php
2023-10-19 13:12:06 +02:00
declare(strict_types=1);
namespace Psalm\Tests;
2021-12-03 20:11:20 +01:00
use Psalm\Codebase;
2021-12-03 20:29:06 +01:00
use Psalm\Exception\TypeParseTreeException;
2021-12-03 20:11:20 +01:00
use Psalm\Internal\Analyzer\ProjectAnalyzer;
use Psalm\Internal\Codebase\InternalCallMapHandler;
use Psalm\Internal\Provider\FakeFileProvider;
2021-12-03 20:11:20 +01:00
use Psalm\Internal\Provider\Providers;
2021-06-08 04:55:21 +02:00
use Psalm\Internal\RuntimeCaches;
2021-12-03 20:11:20 +01:00
use Psalm\Tests\Internal\Provider\FakeParserCacheProvider;
2021-06-08 04:55:21 +02:00
use Psalm\Type;
2021-12-13 04:45:57 +01:00
use Psalm\Type\Atomic\TClassConstant;
use Psalm\Type\Atomic\TLiteralFloat;
use Psalm\Type\Atomic\TLiteralInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TTemplateKeyOf;
2021-12-13 16:28:14 +01:00
use Psalm\Type\Union;
2021-12-03 21:40:18 +01:00
use ReflectionFunction;
2021-06-08 04:55:21 +02:00
use function function_exists;
use function mb_substr;
2021-06-08 04:55:21 +02:00
use function print_r;
use function stripos;
Test parallelization (#4045) * Run tests in random order Being able to run tests in any order is a pre-requisite for being able to run them in parallel. * Reset type coverage between tests, fix affected tests * Reset parser and lexer between test runs and on php version change Previously lexer was reset, but parser kept the reference to the old one, and reference to the parser was kept by StatementsProvider. This resulted in order-dependent tests - if the parser was first initialized with phpVersion set to 7.4 then arrow functions worked fine, but were failing when the parser was initially constructed with settings for 7.3 This can be demonstrated on current master by upgrading to nikic/php-parser:4.9 and running: ``` vendor/bin/phpunit --no-coverage --filter="inferredArgArrowFunction" tests/ClosureTest.php ``` Now all tests using PHP 7.4 features must set the PHP version accordingly. * Marked more tests using 7.4 syntax * Reset newline-between-annotation flag between tests * Resolve real paths before passing them to checkPaths When checkPaths is called from psalm.php the paths are resolved, so we just mimicking SUT behaviour here. * Restore newline-between-annotations in DocCommentTest * Tweak Appveyor caches * Tweak TravisCI caches * Tweak CircleCI caches * Run tests in parallel Use `vendor/bin/paratest` instead of `vendor/bin/phpunit` * Use default paratest runner on Windows WrapperRunner is not supported on Windows. * TRAVIS_TAG could be empty * Restore appveyor conditional caching
2020-08-23 16:32:07 +02:00
class TypeParseTest extends TestCase
{
public function setUp(): void
{
Test parallelization (#4045) * Run tests in random order Being able to run tests in any order is a pre-requisite for being able to run them in parallel. * Reset type coverage between tests, fix affected tests * Reset parser and lexer between test runs and on php version change Previously lexer was reset, but parser kept the reference to the old one, and reference to the parser was kept by StatementsProvider. This resulted in order-dependent tests - if the parser was first initialized with phpVersion set to 7.4 then arrow functions worked fine, but were failing when the parser was initially constructed with settings for 7.3 This can be demonstrated on current master by upgrading to nikic/php-parser:4.9 and running: ``` vendor/bin/phpunit --no-coverage --filter="inferredArgArrowFunction" tests/ClosureTest.php ``` Now all tests using PHP 7.4 features must set the PHP version accordingly. * Marked more tests using 7.4 syntax * Reset newline-between-annotation flag between tests * Resolve real paths before passing them to checkPaths When checkPaths is called from psalm.php the paths are resolved, so we just mimicking SUT behaviour here. * Restore newline-between-annotations in DocCommentTest * Tweak Appveyor caches * Tweak TravisCI caches * Tweak CircleCI caches * Run tests in parallel Use `vendor/bin/paratest` instead of `vendor/bin/phpunit` * Use default paratest runner on Windows WrapperRunner is not supported on Windows. * TRAVIS_TAG could be empty * Restore appveyor conditional caching
2020-08-23 16:32:07 +02:00
RuntimeCaches::clearAll();
$this->file_provider = new FakeFileProvider();
$config = new TestConfig();
2021-12-03 20:11:20 +01:00
$providers = new Providers(
$this->file_provider,
2022-12-18 17:15:15 +01:00
new FakeParserCacheProvider(),
);
2021-12-03 20:11:20 +01:00
$this->project_analyzer = new ProjectAnalyzer(
$config,
2022-12-18 17:15:15 +01:00
$providers,
);
}
public function testThisToStatic(): void
2018-03-22 22:55:36 +01:00
{
$this->assertSame('static', (string) Type::parseString('$this'));
}
public function testThisToStaticUnion(): void
2018-03-22 22:55:36 +01:00
{
$this->assertSame('A|static', (string) Type::parseString('$this|A'));
2018-03-22 22:55:36 +01:00
}
public function testIntOrString(): void
{
2017-05-27 02:05:57 +02:00
$this->assertSame('int|string', (string) Type::parseString('int|string'));
}
public function testBracketedIntOrString(): void
2018-03-23 03:28:06 +01:00
{
$this->assertSame('int|string', (string) Type::parseString('(int|string)'));
}
public function testBoolOrIntOrString(): void
2018-03-21 01:19:26 +01:00
{
$this->assertSame('bool|int|string', (string) Type::parseString('bool|int|string'));
}
public function testNullable(): void
2017-11-20 06:32:40 +01:00
{
$this->assertSame('null|string', (string) Type::parseString('?string'));
}
public function testNullableUnion(): void
{
$this->assertSame('int|null|string', (string) Type::parseString('?(string|int)'));
}
public function testNullableFullyQualified(): void
2018-05-21 18:55:44 +02:00
{
$this->assertSame('null|stdClass', (string) Type::parseString('?\\stdClass'));
}
public function testNullableOrNullable(): void
2018-06-09 05:54:07 +02:00
{
$this->assertSame('int|null|string', (string) Type::parseString('?string|?int'));
2018-06-09 05:54:07 +02:00
}
public function testBadNullableCharacterInUnion(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('int|array|?');
}
public function testBadNullableCharacterInUnionWithFollowing(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('int|array|?|bool');
}
public function testArrayWithClosingBracket(): void
{
2017-05-27 02:05:57 +02:00
$this->assertSame('array<int, int>', (string) Type::parseString('array<int, int>'));
}
public function testArrayWithoutClosingBracket(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('array<int, int');
2018-03-21 01:19:26 +01:00
}
public function testArrayWithSingleArg(): void
2018-03-21 01:19:26 +01:00
{
$this->assertSame('array<array-key, int>', (string) Type::parseString('array<int>'));
2018-03-21 01:19:26 +01:00
}
public function testArrayWithNestedSingleArg(): void
2018-03-21 01:19:26 +01:00
{
$this->assertSame('array<array-key, array<array-key, int>>', (string) Type::parseString('array<array<int>>'));
2018-03-21 01:19:26 +01:00
}
public function testArrayWithUnion(): void
2018-03-21 01:19:26 +01:00
{
2017-05-27 02:05:57 +02:00
$this->assertSame('array<int|string, string>', (string) Type::parseString('array<int|string, string>'));
}
public function testNonEmptyArrray(): void
{
$this->assertSame('non-empty-array<array-key, int>', (string) Type::parseString('non-empty-array<int>'));
}
public function testGeneric(): void
{
2017-05-27 02:05:57 +02:00
$this->assertSame('B<int>', (string) Type::parseString('B<int>'));
}
public function testIntersection(): void
{
$this->assertSame('I1&I2&I3', (string) Type::parseString('I1&I2&I3'));
}
public function testIntersectionOrNull(): void
2018-03-22 22:55:36 +01:00
{
$this->assertSame('I1&I2|null', (string) Type::parseString('I1&I2|null'));
2018-03-22 22:55:36 +01:00
}
public function testNullOrIntersection(): void
2018-03-22 22:55:36 +01:00
{
$this->assertSame('I1&I2|null', (string) Type::parseString('null|I1&I2'));
2018-03-22 22:55:36 +01:00
}
public function testInteratorAndTraversable(): void
2018-03-22 22:55:36 +01:00
{
$this->assertSame('Iterator<mixed, int>&Traversable', (string) Type::parseString('Iterator<int>&Traversable'));
2018-03-22 22:55:36 +01:00
}
public function testStaticAndStatic(): void
{
$this->assertSame('static', (string) Type::parseString('static&static'));
}
public function testTraversableAndIteratorOrNull(): void
2018-03-22 22:55:36 +01:00
{
$this->assertSame(
'Traversable&Iterator<mixed, int>|null',
2022-12-18 17:15:15 +01:00
(string) Type::parseString('Traversable&Iterator<int>|null'),
2018-03-22 22:55:36 +01:00
);
}
public function testIteratorAndTraversableOrNull(): void
{
$this->assertSame(
'Iterator<mixed, int>&Traversable|null',
2022-12-18 17:15:15 +01:00
(string) Type::parseString('Iterator<mixed, int>&Traversable|null'),
);
}
public function testUnsealedArray(): void
{
$this->assertSame('array{a: int, ...<string, string>}', Type::parseString('array{a: int, ...<string, string>}')->getId());
}
2023-04-21 15:49:37 +02:00
public function testUnsealedList(): void
{
$this->assertSame('list{int, ...<string>}', Type::parseString('list{int, ...<string>}')->getId());
}
public function testUnsealedListComplex(): void
{
$this->assertSame('list{array{a: 123}, ...<123>}', Type::parseString('list{0: array{a: 123}, ...<123>}')->getId());
}
public function testIntersectionAfterGeneric(): void
{
$this->assertSame('Countable&iterable<mixed, int>&I', (string) Type::parseString('Countable&iterable<int>&I'));
}
public function testIntersectionOfIterables(): void
{
$this->assertSame('iterable<mixed, A>&iterable<mixed, B>', (string) Type::parseString('iterable<A>&iterable<B>'));
}
public function testIntersectionOfTKeyedArray(): void
{
$this->assertSame('array{a: int, b: int}', (string) Type::parseString('array{a: int}&array{b: int}'));
}
public function testIntersectionOfTwoDifferentArrays(): void
{
$this->assertSame('array{a: int, ...<string, string>}', Type::parseString('array{a: int}&array<string, string>')->getId());
}
public function testIntersectionOfTwoDifferentArraysReversed(): void
{
$this->assertSame('array{a: int, ...<string, string>}', Type::parseString('array<string, string>&array{a: int}')->getId());
}
public function testIntersectionOfTKeyedArrayWithMergedProperties(): void
{
$this->assertSame('array{a: int}', (string) Type::parseString('array{a: int}&array{a: mixed}'));
}
public function testIntersectionOfTKeyedArrayWithPossiblyUndefinedMergedProperties(): void
{
$this->assertSame('array{a: int}', (string) Type::parseString('array{a: int}&array{a?: int}'));
}
2022-07-27 20:24:24 +02:00
public function testIntersectionOfIntranges(): void
{
$this->assertSame('array{a: int<3, 4>}', (string) Type::parseString('array{a: int<2, 4>}&array{a: int<3, 6>}'));
$this->assertSame('array{a: 4}', Type::parseString('array{a: 4}&array{a: int<3, 6>}')->getId(true));
2022-07-27 20:24:24 +02:00
}
public function testIntersectionOfTKeyedArrayWithConflictingProperties(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('array{a: string}&array{a: int}');
}
public function testIntersectionOfTwoRegularArrays(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('string[]&array<string, string>');
}
public function testUnionOfIntersectionOfTKeyedArray(): void
{
$this->assertSame('array{a: int|string, b?: int}', (string) Type::parseString('array{a: int}|array{a: string}&array{b: int}'));
$this->assertSame('array{a: int|string, b?: int}', (string) Type::parseString('array{b: int}&array{a: string}|array{a: int}'));
}
public function testIntersectionOfUnionOfTKeyedArray(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('array{a: int}&array{a: string}|array{b: int}');
}
public function testIntersectionOfTKeyedArrayAndObject(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('array{a: int}&T1');
}
public function testIterableContainingTKeyedArray(): void
{
$this->assertSame('iterable<string, list{int}>', Type::parseString('iterable<string, array{int}>')->getId());
}
public function testPhpDocSimpleArray(): void
2016-10-30 02:57:03 +02:00
{
$this->assertSame('array<array-key, A>', (string) Type::parseString('A[]'));
2018-03-21 01:19:26 +01:00
}
public function testPhpDocUnionArray(): void
2018-03-21 01:19:26 +01:00
{
$this->assertSame('array<array-key, A|B>', (string) Type::parseString('(A|B)[]'));
2018-03-21 01:19:26 +01:00
}
public function testPhpDocMultiDimensionalArray(): void
2018-03-21 01:19:26 +01:00
{
$this->assertSame('array<array-key, array<array-key, A>>', (string) Type::parseString('A[][]'));
2018-03-21 01:19:26 +01:00
}
public function testPhpDocMultidimensionalUnionArray(): void
2018-03-21 01:19:26 +01:00
{
$this->assertSame('array<array-key, array<array-key, A|B>>', (string) Type::parseString('(A|B)[][]'));
2018-03-21 01:19:26 +01:00
}
public function testPhpDocTKeyedArray(): void
{
$this->assertSame(
'array<array-key, array{b: bool, d: string}>',
2022-12-18 17:15:15 +01:00
(string) Type::parseString('array{b: bool, d: string}[]'),
);
}
public function testPhpDocUnionOfArrays(): void
2018-03-21 01:19:26 +01:00
{
$this->assertSame('array<array-key, A|B>', (string) Type::parseString('A[]|B[]'));
2018-03-21 01:19:26 +01:00
}
public function testPhpDocUnionOfArraysOrObject(): void
2018-03-21 01:19:26 +01:00
{
$this->assertSame('C|array<array-key, A|B>', (string) Type::parseString('A[]|B[]|C'));
2016-10-30 02:57:03 +02:00
}
public function testPsalmOnlyAtomic(): void
{
$this->assertSame('class-string', (string) Type::parseString('class-string'));
}
public function testParameterizedClassString(): void
{
$this->assertSame('class-string<A>', (string) Type::parseString('class-string<A>'));
}
public function testParameterizedClassStringUnion(): void
{
$this->assertSame('class-string<A>|class-string<B>', (string) Type::parseString('class-string<A>|class-string<B>'));
}
public function testInvalidType(): void
2017-10-12 20:02:06 +02:00
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
2017-10-12 20:02:06 +02:00
Type::parseString('array(A)');
}
public function testBracketedUnionAndIntersection(): void
2018-03-23 03:28:06 +01:00
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
2018-03-23 03:28:06 +01:00
Type::parseString('(A|B)&C');
}
public function testBracketInUnion(): void
{
Type::parseString('null|(scalar|array|object)');
}
public function testTKeyedArrayWithSimpleArgs(): void
{
$this->assertSame('array{a: int, b: string}', (string) Type::parseString('array{a: int, b: string}'));
2018-03-21 01:19:26 +01:00
}
public function testTKeyedArrayWithSpace(): void
{
$this->assertSame('array{\'a \': int, \'b \': string}', (string) Type::parseString('array{\'a \': int, \'b \': string}'));
}
public function testTKeyedArrayWithQuotedKeys(): void
{
$this->assertSame('array{\'\\"\': int, \'\\\'\': string}', (string) Type::parseString('array{\'"\': int, \'\\\'\': string}'));
$this->assertSame('array{\'\\"\': int, \'\\\'\': string}', (string) Type::parseString('array{"\\"": int, "\\\'": string}'));
}
public function testTKeyedArrayWithClassConstantKey(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('array{self::FOO: string}');
}
public function testTKeyedArrayWithQuotedClassConstantKey(): void
{
$this->assertSame('array{\'self::FOO\': string}', (string) Type::parseString('array{"self::FOO": string}'));
}
public function testTKeyedArrayWithoutClosingBracket(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('array{a: int, b: string');
}
public function testTKeyedArrayInType(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('array{a:[]}');
}
public function testObjectWithSimpleArgs(): void
{
$this->assertSame('object{a:int, b:string}', (string) Type::parseString('object{a:int, b:string}'));
}
public function testObjectWithDollarArgs(): void
{
$this->assertSame('object{a:int, $b:string}', (string) Type::parseString('object{a:int, $b:string}'));
}
public function testTKeyedArrayWithUnionArgs(): void
2018-03-21 01:19:26 +01:00
{
2017-05-27 02:05:57 +02:00
$this->assertSame(
'array{a: int|string, b: string}',
2022-12-18 17:15:15 +01:00
(string) Type::parseString('array{a: int|string, b: string}'),
2016-11-02 07:29:00 +01:00
);
2018-03-21 01:19:26 +01:00
}
2016-11-02 07:29:00 +01:00
public function testTKeyedArrayWithGenericArgs(): void
2018-03-21 01:19:26 +01:00
{
2017-05-27 02:05:57 +02:00
$this->assertSame(
'array{a: array<int, int|string>, b: string}',
2022-12-18 17:15:15 +01:00
(string) Type::parseString('array{a: array<int, string|int>, b: string}'),
2016-11-02 07:29:00 +01:00
);
2018-03-21 01:19:26 +01:00
}
public function testTKeyedArrayWithIntKeysAndUnionArgs(): void
2018-03-21 01:19:26 +01:00
{
$this->assertSame(
'list{null|stdClass}',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('list{stdClass|null}'),
);
2018-03-21 01:19:26 +01:00
}
public function testTKeyedArrayWithIntKeysAndGenericArgs(): void
2018-03-21 01:19:26 +01:00
{
$this->assertSame(
'list{array<array-key, mixed>}',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('array{array}'),
);
$this->assertSame(
'list{array<int, string>}',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('array{array<int, string>}'),
);
}
public function testTKeyedArrayOptional(): void
{
$this->assertSame(
'array{a: int, b?: int}',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('array{a: int, b?: int}'),
);
}
public function testTKeyedArrayNotSealed(): void
{
$this->assertSame(
'array{a: int, ...<array-key, mixed>}',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('array{a: int, ...}'),
);
}
public function testTKeyedList(): void
{
$this->assertSame(
'list{int, int, string}',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('list{int, int, string}'),
);
}
public function testTKeyedListOptional(): void
{
$this->assertSame(
'list{0: int, 1?: int, 2?: string}',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('list{0: int, 1?: int, 2?: string}'),
);
}
public function testTKeyedArrayList(): void
{
$this->assertSame(
'list{int, int, string}',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('array{int, int, string}'),
);
}
public function testTKeyedArrayNonList(): void
{
$this->assertSame(
'array{0: int, 1: int, 2: string}',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('array{0: int, 1: int, 2: string}'),
);
}
public function testTKeyedCallableArrayNonList(): void
{
$this->assertSame(
'callable-array{class-string, string}',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('callable-array{0: class-string, 1: string}'),
);
}
public function testTKeyedListNonList(): void
{
2023-01-25 10:42:05 +01:00
$this->expectExceptionMessage('A list shape cannot describe a non-list');
Type::parseString('list{a: 0, b: 1, c: 2}');
}
public function testTKeyedListNonListOptional(): void
{
2023-01-25 10:42:05 +01:00
$this->expectExceptionMessage('A list shape cannot describe a non-list');
Type::parseString('list{a: 0, b?: 1, c?: 2}');
}
public function testTKeyedListNonListOptionalWrongOrder1(): void
{
2023-01-25 10:42:05 +01:00
$this->expectExceptionMessage('A list shape cannot describe a non-list');
Type::parseString('list{0?: 0, 1: 1, 2: 2}');
}
public function testTKeyedListNonListOptionalWrongOrder2(): void
{
2023-01-25 10:42:05 +01:00
$this->expectExceptionMessage('A list shape cannot describe a non-list');
Type::parseString('list{0: 0, 1?: 1, 2: 2}');
}
public function testTKeyedListWrongOrder(): void
{
2023-01-25 10:42:05 +01:00
$this->expectExceptionMessage('A list shape cannot describe a non-list');
Type::parseString('list{1: 1, 0: 0}');
}
public function testTKeyedListNonListKeys(): void
{
2023-01-25 10:42:05 +01:00
$this->expectExceptionMessage('A list shape cannot describe a non-list');
Type::parseString('list{1: 1, 2: 2}');
}
public function testTKeyedListNoExplicitAndImplicitKeys(): void
{
2023-01-25 10:42:05 +01:00
$this->expectExceptionMessage('Cannot mix explicit and implicit keys');
Type::parseString('list{0: 0, 1}');
}
public function testTKeyedArrayNoExplicitAndImplicitKeys(): void
{
2023-01-25 10:42:05 +01:00
$this->expectExceptionMessage('Cannot mix explicit and implicit keys');
Type::parseString('array{0, test: 1}');
}
2023-01-25 10:42:05 +01:00
public function testTKeyedArrayNoDuplicateKeys(): void
{
$this->expectExceptionMessage('Duplicate key a detected');
Type::parseString('array{a: int, a: int}');
}
public function testSimpleCallable(): void
{
$this->assertSame(
2018-04-08 18:57:56 +02:00
'callable(int, string):void',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('callable(int, string) : void'),
);
}
public function testCallableWithoutClosingBracket(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('callable(int, string');
}
public function testCallableWithParamNames(): void
{
$this->assertSame(
'callable(int, string):void',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('callable(int $foo, string $bar) : void'),
);
}
public function testCallableReturningIntersection(): void
{
$this->assertSame(
'callable(int, string):I1&I2',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('callable(int, string) : (I1&I2)'),
);
}
public function testEmptyCallable(): void
2018-03-28 16:53:19 +02:00
{
$this->assertSame(
2018-04-08 18:57:56 +02:00
'callable():void',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('callable() : void'),
2018-03-28 16:53:19 +02:00
);
}
public function testCallableWithUnionLastType(): void
{
$this->assertSame(
2018-04-08 18:57:56 +02:00
'callable(int, int|string):void',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('callable(int, int|string) : void'),
);
}
public function testCallableWithVariadic(): void
{
$this->assertSame(
2018-04-08 18:57:56 +02:00
'callable(int, string...):void',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('callable(int, string...) : void'),
);
}
public function testCallableThatReturnsACallable(): void
{
$this->assertSame(
'callable():callable():string',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('callable() : callable() : string'),
);
}
public function testCallableThatReturnsACallableThatReturnsACallable(): void
{
$this->assertSame(
'callable():callable():callable():string',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('callable() : callable() : callable() : string'),
);
}
public function testCallableOrInt(): void
2018-04-19 01:00:08 +02:00
{
$this->assertSame(
'callable(string):void|int',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('callable(string):void|int'),
2018-04-19 01:00:08 +02:00
);
}
public function testCallableWithGoodVariadic(): void
{
Type::parseString('callable(int, string...) : void');
Type::parseString('callable(int,string...) : void');
}
public function testCallableWithSpreadBefore(): void
{
$this->assertSame(
'callable(int, string...):void',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('callable(int, ...string):void'),
);
}
public function testConditionalTypeWithSpaces(): void
{
$this->assertSame(
'(T is string ? string : int)',
2022-12-18 17:15:15 +01:00
(string) Type::parseString('(T is string ? string : int)', null, ['T' => ['' => Type::getArray()]]),
);
}
public function testConditionalTypeWithUnion(): void
{
$this->assertSame(
'(T is string|true ? int|string : int)',
2022-12-18 17:15:15 +01:00
Type::parseString('(T is "hello"|true ? string|int : int)', null, ['T' => ['' => Type::getArray()]])->getId(false),
);
}
public function testConditionalTypeWithTKeyedArray(): void
{
$this->assertSame(
'(T is array{a: string} ? string : int)',
2022-12-18 17:15:15 +01:00
(string) Type::parseString('(T is array{a: string} ? string : int)', null, ['T' => ['' => Type::getArray()]]),
);
}
public function testConditionalTypeWithGenericIs(): void
{
$this->assertSame(
'(T is array<array-key, string> ? string : int)',
2022-12-18 17:15:15 +01:00
(string) Type::parseString('(T is array<string> ? string : int)', null, ['T' => ['' => Type::getArray()]]),
);
}
public function testConditionalTypeWithIntersection(): void
{
$this->assertSame(
'(T is A&B ? string : int)',
2022-12-18 17:15:15 +01:00
(string) Type::parseString('(T is A&B ? string : int)', null, ['T' => ['' => Type::getArray()]]),
);
}
public function testConditionalTypeWithoutSpaces(): void
{
$this->assertSame(
'(T is string ? string : int)',
2022-12-18 17:15:15 +01:00
(string) Type::parseString('(T is string?string:int)', null, ['T' => ['' => Type::getArray()]]),
);
}
public function testConditionalTypeWithCallableElseBool(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('(T is string ? callable() : bool)', null, ['T' => ['' => Type::getArray()]]);
}
public function testConditionalTypeWithCallableReturningBoolElseBool(): void
{
$this->assertSame(
'(T is string ? callable():bool : bool)',
2022-12-18 17:15:15 +01:00
(string) Type::parseString('(T is string ? (callable() : bool) : bool)', null, ['T' => ['' => Type::getArray()]]),
);
}
public function testConditionalTypeWithGenerics(): void
{
$this->assertSame(
'(T is string ? string : array<string, string>)',
(string) Type::parseString(
'(T is string ? string : array<string, string>)',
null,
2022-12-18 17:15:15 +01:00
['T' => ['' => Type::getArray()]],
),
);
}
public function testConditionalTypeWithCallableBracketed(): void
{
$this->assertSame(
'(T is string ? callable(string, string):string : callable(mixed...):mixed)',
(string) Type::parseString(
'(T is string ? (callable(string, string):string) : (callable(mixed...):mixed))',
null,
2022-12-18 17:15:15 +01:00
['T' => ['' => Type::getArray()]],
),
);
}
public function testConditionalTypeWithCallableNotBracketed(): void
{
$this->assertSame(
'(T is string ? callable(string, string):string : callable(mixed...):mixed)',
(string) Type::parseString(
'(T is string ? callable(string, string):string : callable(mixed...):mixed)',
null,
2022-12-18 17:15:15 +01:00
['T' => ['' => Type::getArray()]],
),
);
}
public function testCallableWithTrailingColon(): void
2018-04-16 00:16:31 +02:00
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
2018-04-16 00:16:31 +02:00
Type::parseString('callable(int):');
}
public function testCallableWithAnotherBadVariadic(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('callable(int, string..) : void');
}
public function testCallableWithMissingVariadicType(): void
{
$this->assertSame(
'callable(mixed...):void',
2022-12-18 17:15:15 +01:00
(string) Type::parseString('callable(...): void'),
);
}
public function testCallableWithVariadicAndDefault(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('callable(int, string...=) : void');
}
public function testBadVariadic(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('string...');
}
public function testBadFullStop(): void
2018-03-27 20:43:39 +02:00
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
2018-03-27 20:43:39 +02:00
Type::parseString('string.');
}
public function testBadSemicolon(): void
2018-04-05 20:11:57 +02:00
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
2018-04-05 20:11:57 +02:00
Type::parseString('string;');
}
public function testBadGenericString(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('string<T>');
}
public function testBadAmpersand(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('&array');
}
public function testBadColon(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString(':array');
}
public function testBadBrackets(): void
2018-09-06 04:36:32 +02:00
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
2018-09-06 04:36:32 +02:00
Type::parseString('max(a)');
}
public function testMoreBadBrackets(): void
2018-09-06 04:40:52 +02:00
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
2018-09-06 04:40:52 +02:00
Type::parseString('max(a):void');
}
public function testGeneratorWithWBadBrackets(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('Generator{string, A}');
}
public function testBadEquals(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('=array');
}
public function testBadBar(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('|array');
}
public function testBadColonDash(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('array|string:-');
}
public function testDoubleBar(): void
2018-03-29 08:20:19 +02:00
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
2018-03-29 08:20:19 +02:00
Type::parseString('PDO||Closure|numeric');
}
public function testCallableWithDefault(): void
{
$this->assertSame(
2018-04-08 18:57:56 +02:00
'callable(int, string=):void',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('callable(int, string=) : void'),
);
}
public function testNestedCallable(): void
{
$this->assertSame(
'callable(callable(A):B):C',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('callable(callable(A):B):C'),
);
}
public function testCallableWithoutReturn(): void
{
$this->assertSame(
'callable(int, string)',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('callable(int, string)'),
);
}
2018-04-19 01:00:08 +02:00
public function testCombineLiteralStringWithClassString(): void
{
$this->assertSame(
"'array'|class-string",
2022-12-18 17:15:15 +01:00
Type::parseString('"array"|class-string')->getId(),
);
}
public function testCombineLiteralClassStringWithClassString(): void
{
$this->assertSame(
2019-12-02 05:09:34 +01:00
'class-string',
2022-12-18 17:15:15 +01:00
Type::parseString('A::class|class-string')->getId(),
);
}
public function testKeyOfClassConstant(): void
{
$this->assertSame(
'key-of<Foo\Baz::BAR>',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('key-of<Foo\Baz::BAR>'),
);
}
public function testKeyOfTemplate(): void
{
$this->assertSame(
'key-of<T>',
2022-12-18 17:15:15 +01:00
Type::parseString('key-of<T>', null, ['T' => ['' => Type::getArray()]])->getId(false),
);
}
public function testValueOfTemplate(): void
{
$this->assertSame(
'value-of<T>',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('value-of<T>', null, ['T' => ['' => Type::getArray()]]),
);
}
public function testIndexedAccess(): void
{
$this->assertSame(
'T[K]',
(string) Type::parseString(
'T[K]',
null,
[
'T' => ['' => Type::getArray()],
2021-12-13 16:28:14 +01:00
'K' => ['' => new Union([
2022-12-18 17:15:15 +01:00
new TTemplateKeyOf('T', 'fn-foo', Type::getMixed()),
])],
2022-12-18 17:15:15 +01:00
],
),
);
}
public function testValueOfClassConstant(): void
{
$this->assertSame(
'value-of<Foo\Baz::BAR>',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('value-of<Foo\Baz::BAR>'),
);
}
public function testClassStringMap(): void
{
$this->assertSame(
'class-string-map<T as Foo, T>',
2022-12-18 17:15:15 +01:00
Type::parseString('class-string-map<T as Foo, T>')->getId(false),
);
}
public function testVeryLargeType(): void
{
$very_large_type = 'array{a: Closure():(array<array-key, mixed>|null), b?: Closure():array<array-key, mixed>, c?: Closure():array<array-key, mixed>, d?: Closure():array<array-key, mixed>, e?: Closure():(array{f: null|string, g: null|string, h: null|string, i: string, j: mixed, k: mixed, l: mixed, m: mixed, n: bool, o?: array{0: string}}|null), p?: Closure():(array{f: null|string, g: null|string, h: null|string, i: string, j: mixed, k: mixed, l: mixed, m: mixed, n: bool, o?: array{0: string}}|null), q: string, r?: Closure():(array<array-key, mixed>|null), s: array<array-key, mixed>}|null';
$this->assertSame(
$very_large_type,
2022-12-18 17:15:15 +01:00
(string) Type::parseString($very_large_type),
);
}
public function testEnum(): void
2018-05-20 23:19:53 +02:00
{
$docblock_type = Type::parseString('( \'foo\\\'with\' | "bar\"bar" | "baz" | "bat\\\\" | \'bang bang\' | 1 | 2 | 3 | 4.5)');
2018-05-20 23:19:53 +02:00
2021-12-13 16:28:14 +01:00
$resolved_type = new Union([
2021-12-13 04:45:57 +01:00
new TLiteralString('foo\'with'),
new TLiteralString('bar"bar'),
new TLiteralString('baz'),
new TLiteralString('bat\\'),
new TLiteralString('bang bang'),
new TLiteralInt(1),
new TLiteralInt(2),
new TLiteralInt(3),
new TLiteralFloat(4.5),
2018-05-20 23:19:53 +02:00
]);
$this->assertSame($resolved_type->getId(), $docblock_type->getId());
}
public function testEmptyString(): void
2019-12-11 16:52:46 +01:00
{
$docblock_type = Type::parseString('""|"admin"|"fun"');
2021-12-13 16:28:14 +01:00
$resolved_type = new Union([
2021-12-13 04:45:57 +01:00
new TLiteralString(''),
new TLiteralString('admin'),
new TLiteralString('fun'),
2019-12-11 16:52:46 +01:00
]);
$this->assertSame($resolved_type->getId(), $docblock_type->getId());
$docblock_type = Type::parseString('"admin"|""|"fun"');
2021-12-13 16:28:14 +01:00
$resolved_type = new Union([
2021-12-13 04:45:57 +01:00
new TLiteralString('admin'),
new TLiteralString(''),
new TLiteralString('fun'),
2019-12-11 16:52:46 +01:00
]);
$this->assertSame($resolved_type->getId(), $docblock_type->getId());
$docblock_type = Type::parseString('"admin"|"fun"|""');
2021-12-13 16:28:14 +01:00
$resolved_type = new Union([
2021-12-13 04:45:57 +01:00
new TLiteralString('admin'),
new TLiteralString('fun'),
new TLiteralString(''),
2019-12-11 16:52:46 +01:00
]);
$this->assertSame($resolved_type->getId(), $docblock_type->getId());
}
public function testEnumWithoutSpaces(): void
{
$docblock_type = Type::parseString('\'foo\\\'with\'|"bar\"bar"|"baz"|"bat\\\\"|\'bang bang\'|1|2|3|4.5');
2021-12-13 16:28:14 +01:00
$resolved_type = new Union([
2021-12-13 04:45:57 +01:00
new TLiteralString('foo\'with'),
new TLiteralString('bar"bar'),
new TLiteralString('baz'),
new TLiteralString('bat\\'),
new TLiteralString('bang bang'),
new TLiteralInt(1),
new TLiteralInt(2),
new TLiteralInt(3),
new TLiteralFloat(4.5),
]);
$this->assertSame($resolved_type->getId(), $docblock_type->getId());
}
public function testLongUtf8LiteralString(): void
{
$string = "АаБбВвГгДдЕеЁёЖжЗзИиЙйКкЛлМмНнОоПпРрСсТтУуФфХхЦцЧчШшЩщЪъЫыЬьЭэЮюЯя";
$string .= $string;
$expected = mb_substr($string, 0, 80);
$this->assertSame("'$expected...'", Type::parseString("'$string'")->getId());
$this->assertSame("'$expected...'", Type::parseString("\"$string\"")->getId());
}
public function testSingleLiteralString(): void
{
$this->assertSame(
"'var'",
2022-12-18 17:15:15 +01:00
Type::parseString('"var"')->getId(),
);
}
2022-01-04 10:48:29 +01:00
public function testEmptyArrayShape(): void
{
$this->assertSame(
'array<never, never>',
2022-12-18 17:15:15 +01:00
(string)Type::parseString('array{}'),
2022-01-04 10:48:29 +01:00
);
}
public function testSingleLiteralInt(): void
{
$this->assertSame(
'6',
2022-12-18 17:15:15 +01:00
Type::parseString('6')->getId(),
);
}
public function testSingleLiteralIntWithSeparators(): void
{
$this->assertSame('10', Type::parseString('1_0')->getId());
}
public function testIntRangeWithSeparators(): void
{
$this->assertSame('int<10, 20>', Type::parseString('int<1_0, 2_0>')->getId());
}
public function testLiteralIntUnionWithSeparators(): void
{
$this->assertSame('10|20', Type::parseString('1_0|2_0')->getId());
}
public function testIntMaskWithIntsWithSeparators(): void
{
$this->assertSame('0|10|20|30', Type::parseString('int-mask<1_0, 2_0>')->getId());
}
public function testSingleLiteralFloat(): void
{
$this->assertSame(
'float(6.315)',
2022-12-18 17:15:15 +01:00
Type::parseString('6.315')->getId(),
);
}
public function testEnumWithClassConstants(): void
{
$docblock_type = Type::parseString('("baz" | One2::TWO_THREE | Foo::BAR_BAR | Bat\Bar::BAZ_BAM)');
2021-12-13 16:28:14 +01:00
$resolved_type = new Union([
2021-12-13 04:45:57 +01:00
new TLiteralString('baz'),
new TClassConstant('One2', 'TWO_THREE'),
new TClassConstant('Foo', 'BAR_BAR'),
new TClassConstant('Bat\\Bar', 'BAZ_BAM'),
]);
$this->assertSame($resolved_type->getId(), $docblock_type->getId());
}
public function testIntMaskWithInts(): void
{
$docblock_type = Type::parseString('int-mask<0, 1, 2, 4>');
$this->assertSame('0|1|2|3|4|5|6|7', $docblock_type->getId());
$docblock_type = Type::parseString('int-mask<1, 2, 4>');
$this->assertSame('0|1|2|3|4|5|6|7', $docblock_type->getId());
$docblock_type = Type::parseString('int-mask<1, 4>');
$this->assertSame('0|1|4|5', $docblock_type->getId());
$docblock_type = Type::parseString('int-mask<PREG_PATTERN_ORDER, PREG_OFFSET_CAPTURE, PREG_UNMATCHED_AS_NULL>');
$this->assertSame('0|1|256|257|512|513|768|769', $docblock_type->getId());
}
public function testIntMaskWithClassConstant(): void
{
$docblock_type = Type::parseString('int-mask<0, A::FOO, A::BAR>');
$this->assertSame('int-mask<0, A::FOO, A::BAR>', $docblock_type->getId());
}
public function testIntMaskWithInvalidClassConstant(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('int-mask<A::*>');
}
public function testIntMaskOfWithValidClassConstant(): void
{
$docblock_type = Type::parseString('int-mask-of<A::*>');
$this->assertSame('int-mask-of<class-constant(A::*)>', $docblock_type->getId());
}
public function testIntMaskOfWithInvalidClassConstant(): void
{
2021-12-03 20:29:06 +01:00
$this->expectException(TypeParseTreeException::class);
Type::parseString('int-mask-of<A::FOO>');
}
public function testIntMaskOfWithValidValueOf(): void
{
$docblock_type = Type::parseString('int-mask-of<value-of<A::FOO>>');
$this->assertSame('int-mask-of<value-of<A::FOO>>', $docblock_type->getId());
}
public function testReflectionTypeParse(): void
{
2019-05-17 00:36:36 +02:00
if (!function_exists('Psalm\Tests\someFunction')) {
/** @psalm-suppress UnusedParam */
function someFunction(string $param, array $param2, ?int $param3 = null): string
2019-05-17 00:36:36 +02:00
{
return 'hello';
}
}
2021-11-23 20:04:52 +01:00
/** @psalm-suppress InvalidArgument Psalm couldn't detect the function exists */
2021-12-03 21:40:18 +01:00
$reflectionFunc = new ReflectionFunction('Psalm\Tests\someFunction');
$reflectionParams = $reflectionFunc->getParameters();
$this->assertSame(
'string',
2022-12-18 17:15:15 +01:00
(string) Codebase::getPsalmTypeFromReflection($reflectionParams[0]->getType()),
);
$this->assertSame(
'array<array-key, mixed>',
2022-12-18 17:15:15 +01:00
(string) Codebase::getPsalmTypeFromReflection($reflectionParams[1]->getType()),
);
$this->assertSame(
2019-01-27 23:27:12 +01:00
'int|null',
2022-12-18 17:15:15 +01:00
(string) Codebase::getPsalmTypeFromReflection($reflectionParams[2]->getType()),
);
$this->assertSame(
'string',
2022-12-18 17:15:15 +01:00
(string) Codebase::getPsalmTypeFromReflection($reflectionFunc->getReturnType()),
);
}
public function testValidCallMapType(): void
{
2021-12-03 20:11:20 +01:00
$callmap_types = InternalCallMapHandler::getCallMap();
2019-01-09 03:44:50 +01:00
foreach ($callmap_types as $signature) {
$return_type = $signature[0] ?? null;
$param_type_1 = $signature[1] ?? null;
$param_type_2 = $signature[2] ?? null;
$param_type_3 = $signature[3] ?? null;
$param_type_4 = $signature[4] ?? null;
2018-04-19 01:00:08 +02:00
if ($return_type && $return_type !== 'void') {
if (stripos($return_type, 'oci-') !== false) {
2019-04-09 22:52:32 +02:00
continue;
}
try {
2021-12-03 20:11:20 +01:00
Type::parseString($return_type);
2021-12-03 20:29:06 +01:00
} catch (TypeParseTreeException $e) {
self::assertTrue(false, $e . ' | ' . print_r($signature, true));
}
2018-04-19 01:00:08 +02:00
}
if ($param_type_1 && $param_type_1 !== 'mixed') {
if (stripos($param_type_1, 'oci-') !== false) {
2019-04-09 22:52:32 +02:00
continue;
}
2018-04-19 01:00:08 +02:00
try {
2021-12-03 20:11:20 +01:00
Type::parseString($param_type_1);
2021-12-03 20:29:06 +01:00
} catch (TypeParseTreeException $e) {
self::assertTrue(false, $e . ' | ' . print_r($signature, true));
}
}
2018-04-19 01:00:08 +02:00
if ($param_type_2 && $param_type_2 !== 'mixed') {
try {
2021-12-03 20:11:20 +01:00
Type::parseString($param_type_2);
2021-12-03 20:29:06 +01:00
} catch (TypeParseTreeException $e) {
self::assertTrue(false, $e . ' | ' . print_r($signature, true));
}
}
2018-04-19 01:00:08 +02:00
if ($param_type_3 && $param_type_3 !== 'mixed') {
try {
2021-12-03 20:11:20 +01:00
Type::parseString($param_type_3);
2021-12-03 20:29:06 +01:00
} catch (TypeParseTreeException $e) {
self::assertTrue(false, $e . ' | ' . print_r($signature, true));
}
}
2018-04-19 01:00:08 +02:00
if ($param_type_4 && $param_type_4 !== 'mixed') {
try {
2021-12-03 20:11:20 +01:00
Type::parseString($param_type_4);
2021-12-03 20:29:06 +01:00
} catch (TypeParseTreeException $e) {
self::assertTrue(false, $e . ' | ' . print_r($signature, true));
}
}
}
2018-04-19 01:00:08 +02:00
}
}