file_provider = new FakeFileProvider(); $config = new TestConfig(); $providers = new Providers( $this->file_provider, new FakeParserCacheProvider(), ); $this->project_analyzer = new ProjectAnalyzer( $config, $providers, ); } public function testThisToStatic(): void { $this->assertSame('static', (string) Type::parseString('$this')); } public function testThisToStaticUnion(): void { $this->assertSame('A|static', (string) Type::parseString('$this|A')); } public function testIntOrString(): void { $this->assertSame('int|string', (string) Type::parseString('int|string')); } public function testBracketedIntOrString(): void { $this->assertSame('int|string', (string) Type::parseString('(int|string)')); } public function testBoolOrIntOrString(): void { $this->assertSame('bool|int|string', (string) Type::parseString('bool|int|string')); } public function testNullable(): void { $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 { $this->assertSame('null|stdClass', (string) Type::parseString('?\\stdClass')); } public function testNullableOrNullable(): void { $this->assertSame('int|null|string', (string) Type::parseString('?string|?int')); } public function testBadNullableCharacterInUnion(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('int|array|?'); } public function testBadNullableCharacterInUnionWithFollowing(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('int|array|?|bool'); } public function testArrayWithClosingBracket(): void { $this->assertSame('array', (string) Type::parseString('array')); } public function testArrayWithoutClosingBracket(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('arrayassertSame('array', (string) Type::parseString('array')); } public function testArrayWithNestedSingleArg(): void { $this->assertSame('array>', (string) Type::parseString('array>')); } public function testArrayWithUnion(): void { $this->assertSame('array', (string) Type::parseString('array')); } public function testNonEmptyArrray(): void { $this->assertSame('non-empty-array', (string) Type::parseString('non-empty-array')); } public function testGeneric(): void { $this->assertSame('B', (string) Type::parseString('B')); } public function testIntersection(): void { $this->assertSame('I1&I2&I3', (string) Type::parseString('I1&I2&I3')); } public function testIntersectionOrNull(): void { $this->assertSame('I1&I2|null', (string) Type::parseString('I1&I2|null')); } public function testNullOrIntersection(): void { $this->assertSame('I1&I2|null', (string) Type::parseString('null|I1&I2')); } public function testInteratorAndTraversable(): void { $this->assertSame('Iterator&Traversable', (string) Type::parseString('Iterator&Traversable')); } public function testStaticAndStatic(): void { $this->assertSame('static', (string) Type::parseString('static&static')); } public function testTraversableAndIteratorOrNull(): void { $this->assertSame( 'Traversable&Iterator|null', (string) Type::parseString('Traversable&Iterator|null'), ); } public function testIteratorAndTraversableOrNull(): void { $this->assertSame( 'Iterator&Traversable|null', (string) Type::parseString('Iterator&Traversable|null'), ); } public function testUnsealedArray(): void { $this->assertSame('array{a: int, ...}', Type::parseString('array{a: int, ...}')->getId()); } public function testUnsealedList(): void { $this->assertSame('list{int, ...}', Type::parseString('list{int, ...}')->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&I', (string) Type::parseString('Countable&iterable&I')); } public function testIntersectionOfIterables(): void { $this->assertSame('iterable&iterable', (string) Type::parseString('iterable&iterable')); } 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, ...}', Type::parseString('array{a: int}&array')->getId()); } public function testIntersectionOfTwoDifferentArraysReversed(): void { $this->assertSame('array{a: int, ...}', Type::parseString('array&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}')); } 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)); } public function testIntersectionOfTKeyedArrayWithConflictingProperties(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('array{a: string}&array{a: int}'); } public function testIntersectionOfTwoRegularArrays(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('string[]&array'); } 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 { $this->expectException(TypeParseTreeException::class); Type::parseString('array{a: int}&array{a: string}|array{b: int}'); } public function testIntersectionOfTKeyedArrayAndObject(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('array{a: int}&T1'); } public function testIterableContainingTKeyedArray(): void { $this->assertSame('iterable', Type::parseString('iterable')->getId()); } public function testPhpDocSimpleArray(): void { $this->assertSame('array', (string) Type::parseString('A[]')); } public function testPhpDocUnionArray(): void { $this->assertSame('array', (string) Type::parseString('(A|B)[]')); } public function testPhpDocMultiDimensionalArray(): void { $this->assertSame('array>', (string) Type::parseString('A[][]')); } public function testPhpDocMultidimensionalUnionArray(): void { $this->assertSame('array>', (string) Type::parseString('(A|B)[][]')); } public function testPhpDocTKeyedArray(): void { $this->assertSame( 'array', (string) Type::parseString('array{b: bool, d: string}[]'), ); } public function testPhpDocUnionOfArrays(): void { $this->assertSame('array', (string) Type::parseString('A[]|B[]')); } public function testPhpDocUnionOfArraysOrObject(): void { $this->assertSame('C|array', (string) Type::parseString('A[]|B[]|C')); } public function testPsalmOnlyAtomic(): void { $this->assertSame('class-string', (string) Type::parseString('class-string')); } public function testParameterizedClassString(): void { $this->assertSame('class-string', (string) Type::parseString('class-string')); } public function testParameterizedClassStringUnion(): void { $this->assertSame('class-string|class-string', (string) Type::parseString('class-string|class-string')); } public function testInvalidType(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('array(A)'); } public function testBracketedUnionAndIntersection(): void { $this->expectException(TypeParseTreeException::class); 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}')); } 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 { $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 { $this->expectException(TypeParseTreeException::class); Type::parseString('array{a: int, b: string'); } public function testTKeyedArrayInType(): void { $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 { $this->assertSame( 'array{a: int|string, b: string}', (string) Type::parseString('array{a: int|string, b: string}'), ); } public function testTKeyedArrayWithGenericArgs(): void { $this->assertSame( 'array{a: array, b: string}', (string) Type::parseString('array{a: array, b: string}'), ); } public function testTKeyedArrayWithIntKeysAndUnionArgs(): void { $this->assertSame( 'list{null|stdClass}', (string)Type::parseString('list{stdClass|null}'), ); } public function testTKeyedArrayWithIntKeysAndGenericArgs(): void { $this->assertSame( 'list{array}', (string)Type::parseString('array{array}'), ); $this->assertSame( 'list{array}', (string)Type::parseString('array{array}'), ); } public function testTKeyedArrayOptional(): void { $this->assertSame( 'array{a: int, b?: int}', (string)Type::parseString('array{a: int, b?: int}'), ); } public function testTKeyedArrayNotSealed(): void { $this->assertSame( 'array{a: int, ...}', (string)Type::parseString('array{a: int, ...}'), ); } public function testTKeyedList(): void { $this->assertSame( 'list{int, int, string}', (string)Type::parseString('list{int, int, string}'), ); } public function testTKeyedListOptional(): void { $this->assertSame( 'list{0: int, 1?: int, 2?: string}', (string)Type::parseString('list{0: int, 1?: int, 2?: string}'), ); } public function testTKeyedArrayList(): void { $this->assertSame( 'list{int, int, string}', (string)Type::parseString('array{int, int, string}'), ); } public function testTKeyedArrayNonList(): void { $this->assertSame( 'array{0: int, 1: int, 2: string}', (string)Type::parseString('array{0: int, 1: int, 2: string}'), ); } public function testTKeyedCallableArrayNonList(): void { $this->assertSame( 'callable-array{class-string, string}', (string)Type::parseString('callable-array{0: class-string, 1: string}'), ); } public function testTKeyedListNonList(): void { $this->expectExceptionMessage('A list shape cannot describe a non-list'); Type::parseString('list{a: 0, b: 1, c: 2}'); } public function testTKeyedListNonListOptional(): void { $this->expectExceptionMessage('A list shape cannot describe a non-list'); Type::parseString('list{a: 0, b?: 1, c?: 2}'); } public function testTKeyedListNonListOptionalWrongOrder1(): void { $this->expectExceptionMessage('A list shape cannot describe a non-list'); Type::parseString('list{0?: 0, 1: 1, 2: 2}'); } public function testTKeyedListNonListOptionalWrongOrder2(): void { $this->expectExceptionMessage('A list shape cannot describe a non-list'); Type::parseString('list{0: 0, 1?: 1, 2: 2}'); } public function testTKeyedListWrongOrder(): void { $this->expectExceptionMessage('A list shape cannot describe a non-list'); Type::parseString('list{1: 1, 0: 0}'); } public function testTKeyedListNonListKeys(): void { $this->expectExceptionMessage('A list shape cannot describe a non-list'); Type::parseString('list{1: 1, 2: 2}'); } public function testTKeyedListNoExplicitAndImplicitKeys(): void { $this->expectExceptionMessage('Cannot mix explicit and implicit keys'); Type::parseString('list{0: 0, 1}'); } public function testTKeyedArrayNoExplicitAndImplicitKeys(): void { $this->expectExceptionMessage('Cannot mix explicit and implicit keys'); Type::parseString('array{0, test: 1}'); } public function testTKeyedArrayNoDuplicateKeys(): void { $this->expectExceptionMessage('Duplicate key a detected'); Type::parseString('array{a: int, a: int}'); } public function testSimpleCallable(): void { $this->assertSame( 'callable(int, string):void', (string)Type::parseString('callable(int, string) : void'), ); } public function testCallableWithoutClosingBracket(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('callable(int, string'); } public function testCallableWithParamNames(): void { $this->assertSame( 'callable(int, string):void', (string)Type::parseString('callable(int $foo, string $bar) : void'), ); } public function testCallableReturningIntersection(): void { $this->assertSame( 'callable(int, string):I1&I2', (string)Type::parseString('callable(int, string) : (I1&I2)'), ); } public function testEmptyCallable(): void { $this->assertSame( 'callable():void', (string)Type::parseString('callable() : void'), ); } public function testCallableWithUnionLastType(): void { $this->assertSame( 'callable(int, int|string):void', (string)Type::parseString('callable(int, int|string) : void'), ); } public function testCallableWithVariadic(): void { $this->assertSame( 'callable(int, string...):void', (string)Type::parseString('callable(int, string...) : void'), ); } public function testCallableThatReturnsACallable(): void { $this->assertSame( 'callable():callable():string', (string)Type::parseString('callable() : callable() : string'), ); } public function testCallableThatReturnsACallableThatReturnsACallable(): void { $this->assertSame( 'callable():callable():callable():string', (string)Type::parseString('callable() : callable() : callable() : string'), ); } public function testCallableOrInt(): void { $this->assertSame( 'callable(string):void|int', (string)Type::parseString('callable(string):void|int'), ); } 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', (string)Type::parseString('callable(int, ...string):void'), ); } public function testConditionalTypeWithSpaces(): void { $this->assertSame( '(T is string ? string : int)', (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)', 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)', (string) Type::parseString('(T is array{a: string} ? string : int)', null, ['T' => ['' => Type::getArray()]]), ); } public function testConditionalTypeWithGenericIs(): void { $this->assertSame( '(T is array ? string : int)', (string) Type::parseString('(T is array ? string : int)', null, ['T' => ['' => Type::getArray()]]), ); } public function testConditionalTypeWithIntersection(): void { $this->assertSame( '(T is A&B ? string : int)', (string) Type::parseString('(T is A&B ? string : int)', null, ['T' => ['' => Type::getArray()]]), ); } public function testConditionalTypeWithoutSpaces(): void { $this->assertSame( '(T is string ? string : int)', (string) Type::parseString('(T is string?string:int)', null, ['T' => ['' => Type::getArray()]]), ); } public function testConditionalTypeWithCallableElseBool(): void { $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)', (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) Type::parseString( '(T is string ? string : array)', null, ['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, ['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, ['T' => ['' => Type::getArray()]], ), ); } public function testCallableWithTrailingColon(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('callable(int):'); } public function testCallableWithAnotherBadVariadic(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('callable(int, string..) : void'); } public function testCallableWithMissingVariadicType(): void { $this->assertSame( 'callable(mixed...):void', (string) Type::parseString('callable(...): void'), ); } public function testCallableWithVariadicAndDefault(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('callable(int, string...=) : void'); } public function testBadVariadic(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('string...'); } public function testBadFullStop(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('string.'); } public function testBadSemicolon(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('string;'); } public function testBadGenericString(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('string'); } public function testBadAmpersand(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('&array'); } public function testBadColon(): void { $this->expectException(TypeParseTreeException::class); Type::parseString(':array'); } public function testBadBrackets(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('max(a)'); } public function testMoreBadBrackets(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('max(a):void'); } public function testGeneratorWithWBadBrackets(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('Generator{string, A}'); } public function testBadEquals(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('=array'); } public function testBadBar(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('|array'); } public function testBadColonDash(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('array|string:-'); } public function testDoubleBar(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('PDO||Closure|numeric'); } public function testCallableWithDefault(): void { $this->assertSame( 'callable(int, string=):void', (string)Type::parseString('callable(int, string=) : void'), ); } public function testNestedCallable(): void { $this->assertSame( 'callable(callable(A):B):C', (string)Type::parseString('callable(callable(A):B):C'), ); } public function testCallableWithoutReturn(): void { $this->assertSame( 'callable(int, string)', (string)Type::parseString('callable(int, string)'), ); } public function testCombineLiteralStringWithClassString(): void { $this->assertSame( "'array'|class-string", Type::parseString('"array"|class-string')->getId(), ); } public function testCombineLiteralClassStringWithClassString(): void { $this->assertSame( 'class-string', Type::parseString('A::class|class-string')->getId(), ); } public function testKeyOfClassConstant(): void { $this->assertSame( 'key-of', (string)Type::parseString('key-of'), ); } public function testKeyOfTemplate(): void { $this->assertSame( 'key-of', Type::parseString('key-of', null, ['T' => ['' => Type::getArray()]])->getId(false), ); } public function testValueOfTemplate(): void { $this->assertSame( 'value-of', (string)Type::parseString('value-of', null, ['T' => ['' => Type::getArray()]]), ); } public function testIndexedAccess(): void { $this->assertSame( 'T[K]', (string) Type::parseString( 'T[K]', null, [ 'T' => ['' => Type::getArray()], 'K' => ['' => new Union([ new TTemplateKeyOf('T', 'fn-foo', Type::getMixed()), ])], ], ), ); } public function testValueOfClassConstant(): void { $this->assertSame( 'value-of', (string)Type::parseString('value-of'), ); } public function testClassStringMap(): void { $this->assertSame( 'class-string-map', Type::parseString('class-string-map')->getId(false), ); } public function testVeryLargeType(): void { $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), ); } public function testEnum(): void { $docblock_type = Type::parseString('( \'foo\\\'with\' | "bar\"bar" | "baz" | "bat\\\\" | \'bang bang\' | 1 | 2 | 3 | 4.5)'); $resolved_type = new Union([ 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 testEmptyString(): void { $docblock_type = Type::parseString('""|"admin"|"fun"'); $resolved_type = new Union([ new TLiteralString(''), new TLiteralString('admin'), new TLiteralString('fun'), ]); $this->assertSame($resolved_type->getId(), $docblock_type->getId()); $docblock_type = Type::parseString('"admin"|""|"fun"'); $resolved_type = new Union([ new TLiteralString('admin'), new TLiteralString(''), new TLiteralString('fun'), ]); $this->assertSame($resolved_type->getId(), $docblock_type->getId()); $docblock_type = Type::parseString('"admin"|"fun"|""'); $resolved_type = new Union([ new TLiteralString('admin'), new TLiteralString('fun'), new TLiteralString(''), ]); $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'); $resolved_type = new Union([ 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'", Type::parseString('"var"')->getId(), ); } public function testEmptyArrayShape(): void { $this->assertSame( 'array', (string)Type::parseString('array{}'), ); } public function testSingleLiteralInt(): void { $this->assertSame( '6', 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)', Type::parseString('6.315')->getId(), ); } public function testEnumWithClassConstants(): void { $docblock_type = Type::parseString('("baz" | One2::TWO_THREE | Foo::BAR_BAR | Bat\Bar::BAZ_BAM)'); $resolved_type = new Union([ 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'); $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 { $this->expectException(TypeParseTreeException::class); Type::parseString('int-mask'); } public function testIntMaskOfWithValidClassConstant(): void { $docblock_type = Type::parseString('int-mask-of'); $this->assertSame('int-mask-of', $docblock_type->getId()); } public function testIntMaskOfWithInvalidClassConstant(): void { $this->expectException(TypeParseTreeException::class); Type::parseString('int-mask-of'); } public function testIntMaskOfWithValidValueOf(): void { $docblock_type = Type::parseString('int-mask-of>'); $this->assertSame('int-mask-of>', $docblock_type->getId()); } public function testReflectionTypeParse(): void { if (!function_exists('Psalm\Tests\someFunction')) { /** @psalm-suppress UnusedParam */ function someFunction(string $param, array $param2, ?int $param3 = null): string { return 'hello'; } } /** @psalm-suppress InvalidArgument Psalm couldn't detect the function exists */ $reflectionFunc = new ReflectionFunction('Psalm\Tests\someFunction'); $reflectionParams = $reflectionFunc->getParameters(); $this->assertSame( 'string', (string) Codebase::getPsalmTypeFromReflection($reflectionParams[0]->getType()), ); $this->assertSame( 'array', (string) Codebase::getPsalmTypeFromReflection($reflectionParams[1]->getType()), ); $this->assertSame( 'int|null', (string) Codebase::getPsalmTypeFromReflection($reflectionParams[2]->getType()), ); $this->assertSame( 'string', (string) Codebase::getPsalmTypeFromReflection($reflectionFunc->getReturnType()), ); } public function testValidCallMapType(): void { $callmap_types = InternalCallMapHandler::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 { Type::parseString($return_type); } catch (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 { Type::parseString($param_type_1); } catch (TypeParseTreeException $e) { self::assertTrue(false, $e . ' | ' . print_r($signature, true)); } } if ($param_type_2 && $param_type_2 !== 'mixed') { try { Type::parseString($param_type_2); } catch (TypeParseTreeException $e) { self::assertTrue(false, $e . ' | ' . print_r($signature, true)); } } if ($param_type_3 && $param_type_3 !== 'mixed') { try { Type::parseString($param_type_3); } catch (TypeParseTreeException $e) { self::assertTrue(false, $e . ' | ' . print_r($signature, true)); } } if ($param_type_4 && $param_type_4 !== 'mixed') { try { Type::parseString($param_type_4); } catch (TypeParseTreeException $e) { self::assertTrue(false, $e . ' | ' . print_r($signature, true)); } } } } }