file_provider = new \Psalm\Tests\Internal\Provider\FakeFileProvider(); $config = new TestConfig(); $providers = new \Psalm\Internal\Provider\Providers( $this->file_provider, new \Psalm\Tests\Internal\Provider\FakeParserCacheProvider() ); $this->project_analyzer = new \Psalm\Internal\Analyzer\ProjectAnalyzer( $config, $providers ); } /** * @return void */ public function testThisToStatic() { $this->assertSame('static', (string) Type::parseString('$this')); } /** * @return void */ public function testThisToStaticUnion() { $this->assertSame('A|static', (string) Type::parseString('$this|A')); } /** * @return void */ public function testIntOrString() { $this->assertSame('int|string', (string) Type::parseString('int|string')); } /** * @return void */ public function testBracketedIntOrString() { $this->assertSame('int|string', (string) Type::parseString('(int|string)')); } /** * @return void */ public function testBoolOrIntOrString() { $this->assertSame('bool|int|string', (string) Type::parseString('bool|int|string')); } /** * @return void */ public function testNullable() { $this->assertSame('null|string', (string) Type::parseString('?string')); } /** * @return void */ public function testNullableUnion() { $this->assertSame('int|null|string', (string) Type::parseString('?(string|int)')); } /** * @return void */ public function testNullableFullyQualified() { $this->assertSame('null|stdClass', (string) Type::parseString('?\\stdClass')); } /** * @return void */ public function testNullableOrNullable() { $this->assertSame('int|null|string', (string) Type::parseString('?string|?int')); } /** * @return void */ public function testArrayWithClosingBracket() { $this->assertSame('array', (string) Type::parseString('array')); } /** * @return void */ public function testArrayWithoutClosingBracket() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('arrayassertSame('array', (string) Type::parseString('array')); } /** * @return void */ public function testArrayWithNestedSingleArg() { $this->assertSame('array>', (string) Type::parseString('array>')); } /** * @return void */ public function testArrayWithUnion() { $this->assertSame('array', (string) Type::parseString('array')); } /** * @return void */ public function testNonEmptyArrray() { $this->assertSame('non-empty-array', (string) Type::parseString('non-empty-array')); } /** * @return void */ public function testGeneric() { $this->assertSame('B', (string) Type::parseString('B')); } /** * @return void */ public function testIntersection() { $this->assertSame('I1&I2&I3', (string) Type::parseString('I1&I2&I3')); } /** * @return void */ public function testIntersectionOrNull() { $this->assertSame('I1&I2|null', (string) Type::parseString('I1&I2|null')); } /** * @return void */ public function testNullOrIntersection() { $this->assertSame('I1&I2|null', (string) Type::parseString('null|I1&I2')); } /** * @return void */ public function testInteratorAndTraversable() { $this->assertSame('Iterator&Traversable', (string) Type::parseString('Iterator&Traversable')); } /** * @return void */ public function testTraversableAndIteratorOrNull() { $this->assertSame( 'Traversable&Iterator|null', (string) Type::parseString('Traversable&Iterator|null') ); } /** * @return void */ public function testIntersectionAfterGeneric() { $this->assertSame('Countable&iterable&I', (string) Type::parseString('Countable&iterable&I')); } /** * @return void */ public function testIntersectionOfIterables() { $this->assertSame('iterable&iterable', (string) Type::parseString('iterable&iterable')); } public function testIterableContainingObjectLike() : void { $this->assertSame('iterable', Type::parseString('iterable')->getId()); } /** * @return void */ public function testPhpDocSimpleArray() { $this->assertSame('array', (string) Type::parseString('A[]')); } /** * @return void */ public function testPhpDocUnionArray() { $this->assertSame('array', (string) Type::parseString('(A|B)[]')); } /** * @return void */ public function testPhpDocMultiDimensionalArray() { $this->assertSame('array>', (string) Type::parseString('A[][]')); } /** * @return void */ public function testPhpDocMultidimensionalUnionArray() { $this->assertSame('array>', (string) Type::parseString('(A|B)[][]')); } /** * @return void */ public function testPhpDocObjectLikeArray() { $this->assertSame( 'array', (string) Type::parseString('array{b: bool, d: string}[]') ); } /** * @return void */ public function testPhpDocUnionOfArrays() { $this->assertSame('array', (string) Type::parseString('A[]|B[]')); } /** * @return void */ public function testPhpDocUnionOfArraysOrObject() { $this->assertSame('C|array', (string) Type::parseString('A[]|B[]|C')); } /** * @return void */ public function testPsalmOnlyAtomic() { $this->assertSame('class-string', (string) Type::parseString('class-string')); } /** * @return void */ public function testParamaterizedClassString() { $this->assertSame('class-string', (string) Type::parseString('class-string')); } /** * @return void */ public function testParamaterizedClassStringUnion() { $this->assertSame('class-string|class-string', (string) Type::parseString('class-string|class-string')); } /** * @return void */ public function testInvalidType() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('array(A)'); } /** * @return void */ public function testBracketedUnionAndIntersection() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('(A|B)&C'); } /** * @return void */ public function testBracketInUnion() { Type::parseString('null|(scalar|array|object)'); } /** * @return void */ public function testObjectLikeWithSimpleArgs() { $this->assertSame('array{a: int, b: string}', (string) Type:: parseString('array{a: int, b: string}')); } /** * @return void */ public function testObjectLikeWithoutClosingBracket() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('array{a: int, b: string'); } /** * @return void */ public function testObjectWithSimpleArgs() { $this->assertSame('object{a:int, b:string}', (string) Type::parseString('object{a:int, b:string}')); } /** * @return void */ public function testObjectWithDollarArgs() { $this->assertSame('object{a:int, $b:string}', (string) Type::parseString('object{a:int, $b:string}')); } /** * @return void */ public function testObjectLikeWithUnionArgs() { $this->assertSame( 'array{a: int|string, b: string}', (string) Type::parseString('array{a: int|string, b: string}') ); } /** * @return void */ public function testObjectLikeWithGenericArgs() { $this->assertSame( 'array{a: array, b: string}', (string) Type::parseString('array{a: array, b: string}') ); } /** * @return void */ public function testObjectLikeWithIntKeysAndUnionArgs() { $this->assertSame( 'array{0: null|stdClass}', (string)Type::parseString('array{stdClass|null}') ); } /** * @return void */ public function testObjectLikeWithIntKeysAndGenericArgs() { $this->assertSame( 'array{0: array}', (string)Type::parseString('array{array}') ); $this->assertSame( 'array{0: array}', (string)Type::parseString('array{array}') ); } /** * @return void */ public function testObjectLikeOptional() { $this->assertSame( 'array{a: int, b?: int}', (string)Type::parseString('array{a: int, b?: int}') ); } /** * @return void */ public function testSimpleCallable() { $this->assertSame( 'callable(int, string):void', (string)Type::parseString('callable(int, string) : void') ); } /** * @return void */ public function testCallableWithoutClosingBracket() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('callable(int, string'); } /** * @return void */ public function testCallableWithParamNames() { $this->assertSame( 'callable(int, string):void', (string)Type::parseString('callable(int $foo, string $bar) : void') ); } /** * @return void */ public function testCallableReturningIntersection() { $this->assertSame( 'callable(int, string):I1&I2', (string)Type::parseString('callable(int, string) : (I1&I2)') ); } /** * @return void */ public function testEmptyCallable() { $this->assertSame( 'callable():void', (string)Type::parseString('callable() : void') ); } /** * @return void */ public function testCallableWithUnionLastType() { $this->assertSame( 'callable(int, int|string):void', (string)Type::parseString('callable(int, int|string) : void') ); } /** * @return void */ public function testCallableWithVariadic() { $this->assertSame( 'callable(int, string...):void', (string)Type::parseString('callable(int, string...) : void') ); } /** * @return void */ public function testCallableThatReturnsACallable() { $this->assertSame( 'callable():callable():string', (string)Type::parseString('callable() : callable() : string') ); } /** * @return void */ public function testCallableThatReturnsACallableThatReturnsACallable() { $this->assertSame( 'callable():callable():callable():string', (string)Type::parseString('callable() : callable() : callable() : string') ); } /** * @return void */ public function testCallableOrInt() { $this->assertSame( 'callable(string):void|int', (string)Type::parseString('callable(string):void|int') ); } /** * @return void */ public function testCallableWithGoodVariadic() { Type::parseString('callable(int, string...) : void'); Type::parseString('callable(int,string...) : void'); } /** * @return void */ public function testCallableWithSpreadBefore() { $this->assertSame( 'callable(int, string...):void', (string)Type::parseString('callable(int, ...string):void') ); } /** * @return void */ public function testCallableWithTrailingColon() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('callable(int):'); } /** * @return void */ public function testCallableWithAnotherBadVariadic() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('callable(int, string..) : void'); } /** * @return void */ public function testCallableWithVariadicAndDefault() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('callable(int, string...=) : void'); } /** * @return void */ public function testBadVariadic() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('string...'); } /** * @return void */ public function testBadFullStop() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('string.'); } /** * @return void */ public function testBadSemicolon() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('string;'); } /** * @return void */ public function testBadGenericString() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('string'); } /** * @return void */ public function testBadAmpersand() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('&array'); } /** * @return void */ public function testBadColon() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString(':array'); } /** * @return void */ public function testBadBrackets() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('max(a)'); } /** * @return void */ public function testMoreBadBrackets() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('max(a):void'); } /** * @return void */ public function testGeneratorWithWBadBrackets() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('Generator{string, A}'); } /** * @return void */ public function testBadEquals() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('=array'); } /** * @return void */ public function testBadBar() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('|array'); } /** * @return void */ public function testBadColonDash() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('array|string:-'); } /** * @return void */ public function testDoubleBar() { $this->expectException(\Psalm\Exception\TypeParseTreeException::class); Type::parseString('PDO||Closure|numeric'); } /** * @return void */ public function testCallableWithDefault() { $this->assertSame( 'callable(int, string=):void', (string)Type::parseString('callable(int, string=) : void') ); } /** * @return void */ public function testNestedCallable() { $this->assertSame( 'callable(callable(A):B):C', (string)Type::parseString('callable(callable(A):B):C') ); } /** * @return void */ public function testCallableWithoutReturn() { $this->assertSame( 'callable(int, string)', (string)Type::parseString('callable(int, string)') ); } /** * @return void */ public function testCombineLiteralStringWithClassString() { $this->assertSame( 'class-string|string(array)', Type::parseString('"array"|class-string')->getId() ); } /** * @return void */ public function testCombineLiteralClassStringWithClassString() { $this->assertSame( 'class-string', Type::parseString('A::class|class-string')->getId() ); } /** * @return void */ public function testKeyOfClassConstant() { $this->assertSame( 'key-of', (string)Type::parseString('key-of') ); } /** * @return void */ public function testKeyOfTemplate() { $this->assertSame( 'key-of', (string)Type::parseString('key-of', null, ['T' => ['' => [Type::getArray()]]]) ); } /** * @return void */ public function testIndexedAccess() { $this->assertSame( 'T[K]', (string) Type::parseString( 'T[K]', null, [ 'T' => ['' => [Type::getArray()]], 'K' => ['' => [new Type\Union([ new Type\Atomic\TTemplateKeyOf('T', 'fn-foo', Type::getMixed()) ])]], ] ) ); } /** * @return void */ public function testValueOfClassConstant() { $this->assertSame( 'value-of', (string)Type::parseString('value-of') ); } public function testClassStringMap() : void { $this->assertSame( 'class-string-map', (string)Type::parseString('class-string-map') ); } /** * @return void */ public function testVeryLargeType() { $very_large_type = 'array{a: Closure():(array|null), b?: Closure():array, c?: Closure():array, d?: Closure():array, 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|null), s: array}|null'; $this->assertSame( $very_large_type, (string) Type::parseString($very_large_type) ); } /** * @return void */ public function testEnum() { $docblock_type = Type::parseString('( \'foo\\\'with\' | "bar\"bar" | "baz" | "bat\\\\" | \'bang bang\' | 1 | 2 | 3 | 4.5)'); $resolved_type = new Type\Union([ new Type\Atomic\TLiteralString('foo\'with'), new Type\Atomic\TLiteralString('bar"bar'), new Type\Atomic\TLiteralString('baz'), new Type\Atomic\TLiteralString('bat\\'), new Type\Atomic\TLiteralString('bang bang'), new Type\Atomic\TLiteralInt(1), new Type\Atomic\TLiteralInt(2), new Type\Atomic\TLiteralInt(3), new Type\Atomic\TLiteralFloat(4.5), ]); $this->assertSame($resolved_type->getId(), $docblock_type->getId()); } public function testEmptyString() : void { $docblock_type = Type::parseString('""|"admin"|"fun"'); $resolved_type = new Type\Union([ new Type\Atomic\TLiteralString(''), new Type\Atomic\TLiteralString('admin'), new Type\Atomic\TLiteralString('fun'), ]); $this->assertSame($resolved_type->getId(), $docblock_type->getId()); $docblock_type = Type::parseString('"admin"|""|"fun"'); $resolved_type = new Type\Union([ new Type\Atomic\TLiteralString('admin'), new Type\Atomic\TLiteralString(''), new Type\Atomic\TLiteralString('fun'), ]); $this->assertSame($resolved_type->getId(), $docblock_type->getId()); $docblock_type = Type::parseString('"admin"|"fun"|""'); $resolved_type = new Type\Union([ new Type\Atomic\TLiteralString('admin'), new Type\Atomic\TLiteralString('fun'), new Type\Atomic\TLiteralString(''), ]); $this->assertSame($resolved_type->getId(), $docblock_type->getId()); } /** * @return void */ public function testEnumWithoutSpaces() { $docblock_type = Type::parseString('\'foo\\\'with\'|"bar\"bar"|"baz"|"bat\\\\"|\'bang bang\'|1|2|3|4.5'); $resolved_type = new Type\Union([ new Type\Atomic\TLiteralString('foo\'with'), new Type\Atomic\TLiteralString('bar"bar'), new Type\Atomic\TLiteralString('baz'), new Type\Atomic\TLiteralString('bat\\'), new Type\Atomic\TLiteralString('bang bang'), new Type\Atomic\TLiteralInt(1), new Type\Atomic\TLiteralInt(2), new Type\Atomic\TLiteralInt(3), new Type\Atomic\TLiteralFloat(4.5), ]); $this->assertSame($resolved_type->getId(), $docblock_type->getId()); } /** * @return void */ public function testSingleLiteralString() { $this->assertSame( 'string', (string)Type::parseString('"var"') ); } /** * @return void */ public function testSingleLiteralInt() { $this->assertSame( 'int', (string)Type::parseString('6') ); } /** * @return void */ public function testSingleLiteralFloat() { $this->assertSame( 'float', (string)Type::parseString('6.315') ); } /** * @return void */ public function testEnumWithClassConstants() { $docblock_type = Type::parseString('("baz" | One2::TWO_THREE | Foo::BAR_BAR | Bat\Bar::BAZ_BAM)'); $resolved_type = new Type\Union([ new Type\Atomic\TLiteralString('baz'), new Type\Atomic\TScalarClassConstant('One2', 'TWO_THREE'), new Type\Atomic\TScalarClassConstant('Foo', 'BAR_BAR'), new Type\Atomic\TScalarClassConstant('Bat\\Bar', 'BAZ_BAM'), ]); $this->assertSame($resolved_type->getId(), $docblock_type->getId()); } /** * @return void */ public function testReflectionTypeParse() { if (!function_exists('Psalm\Tests\someFunction')) { /** @psalm-suppress UnusedParam */ function someFunction(string $param, array $param2, int $param3 = null) : string { return 'hello'; } } $reflectionFunc = new \ReflectionFunction('Psalm\Tests\someFunction'); $reflectionParams = $reflectionFunc->getParameters(); $this->assertSame( 'string', (string) \Psalm\Codebase::getPsalmTypeFromReflection($reflectionParams[0]->getType()) ); $this->assertSame( 'array', (string) \Psalm\Codebase::getPsalmTypeFromReflection($reflectionParams[1]->getType()) ); $this->assertSame( 'int|null', (string) \Psalm\Codebase::getPsalmTypeFromReflection($reflectionParams[2]->getType()) ); $this->assertSame( 'string', (string) \Psalm\Codebase::getPsalmTypeFromReflection($reflectionFunc->getReturnType()) ); } /** * @return void */ public function testValidCallMapType() { $callmap_types = \Psalm\Internal\Codebase\CallMap::getCallMap(); 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; if ($return_type && $return_type !== 'void') { if (stripos($return_type, 'oci-') !== false) { continue; } try { \Psalm\Type::parseString($return_type); } catch (\Psalm\Exception\TypeParseTreeException $e) { self::assertTrue(false, $e . ' | ' . print_r($signature, true)); } } if ($param_type_1 && $param_type_1 !== 'mixed') { if (stripos($param_type_1, 'oci-') !== false) { continue; } try { \Psalm\Type::parseString($param_type_1); } catch (\Psalm\Exception\TypeParseTreeException $e) { self::assertTrue(false, $e . ' | ' . print_r($signature, true)); } } if ($param_type_2 && $param_type_2 !== 'mixed') { try { \Psalm\Type::parseString($param_type_2); } catch (\Psalm\Exception\TypeParseTreeException $e) { self::assertTrue(false, $e . ' | ' . print_r($signature, true)); } } if ($param_type_3 && $param_type_3 !== 'mixed') { try { \Psalm\Type::parseString($param_type_3); } catch (\Psalm\Exception\TypeParseTreeException $e) { self::assertTrue(false, $e . ' | ' . print_r($signature, true)); } } if ($param_type_4 && $param_type_4 !== 'mixed') { try { \Psalm\Type::parseString($param_type_4); } catch (\Psalm\Exception\TypeParseTreeException $e) { self::assertTrue(false, $e . ' | ' . print_r($signature, true)); } } } } }