file_provider = new FakeFileProvider(); $config = new TestConfig(); $providers = new Providers( $this->file_provider, new FakeParserCacheProvider(), ); $this->project_analyzer = new ProjectAnalyzer( $config, $providers, ); } /** * @dataProvider getAllBasicTypes */ public function testTypeAcceptsItself(string $type_string): void { $type_1 = Type::parseString($type_string); $type_2 = Type::parseString($type_string); $this->assertTrue( UnionTypeComparator::isContainedBy( $this->project_analyzer->getCodebase(), $type_1, $type_2, ), ); } /** * @return array */ public function getAllBasicTypes(): array { // these types are not valid without generics attached $basic_generic_types = [ 'key-of' => true, 'arraylike-object' => true, 'value-of' => true, 'class-string-map' => true, 'int-mask-of' => true, 'int-mask' => true, 'pure-Closure' => true, ]; foreach (TPropertiesOf::tokenNames() as $token_name) { $basic_generic_types[$token_name] = true; } $basic_types = array_diff_key( TypeTokenizer::PSALM_RESERVED_WORDS, $basic_generic_types, [ 'open-resource' => true, // unverifiable 'non-empty-countable' => true, // bit weird, maybe a bug? ], [ 'array' => true, // Requires a shape 'list' => true, // Requires a shape ], ); $basic_types['array{test: 123}'] = true; $basic_types['list{123}'] = true; return array_map( static fn($type) => [$type], array_keys($basic_types), ); } /** * @dataProvider getSuccessfulComparisons */ public function testTypeAcceptsType(string $parent_type_string, string $child_type_string): void { $parent_type = Type::parseString($parent_type_string); $child_type = Type::parseString($child_type_string); $this->assertTrue( UnionTypeComparator::isContainedBy( $this->project_analyzer->getCodebase(), $child_type, $parent_type, ), 'Type ' . $parent_type_string . ' should contain ' . $child_type_string, ); } /** * @dataProvider getUnsuccessfulComparisons */ public function testTypeDoesNotAcceptType(string $parent_type_string, string $child_type_string): void { $parent_type = Type::parseString($parent_type_string); $child_type = Type::parseString($child_type_string); $this->assertFalse( UnionTypeComparator::isContainedBy( $this->project_analyzer->getCodebase(), $child_type, $parent_type, ), 'Type ' . $parent_type_string . ' should not contain ' . $child_type_string, ); } /** * @return array */ public function getSuccessfulComparisons(): array { return [ 'iterableAcceptsArray' => [ 'iterable', 'array', ], 'listAcceptsEmptyArray' => [ 'list', 'array', ], 'arrayAcceptsEmptyArray' => [ 'array', 'array', ], 'arrayOptionalKeyed1AcceptsEmptyArray' => [ 'array{foo?: string}', 'array', ], 'arrayOptionalKeyed2AcceptsEmptyArray' => [ 'array{foo?: string}&array', 'array', ], 'Lowercase-stringAndCallable-string' => [ 'lowercase-string', 'callable-string', ], 'callableUnionAcceptsCallableUnion' => [ '(callable(int,string[]): void)|(callable(int): void)', '(callable(int): void)|(callable(int,string[]): void)', ], 'callableAcceptsCallableArray' => [ 'callable', "callable-array{0: class-string, 1: 'from'}", ], 'callableAcceptsCallableObject' => [ 'callable', "callable-object", ], 'callableAcceptsCallableString' => [ 'callable', 'callable-string', ], 'callableAcceptsCallableKeyedList' => [ 'callable', "callable-list{class-string, 'from'}", ], ]; } /** @return iterable */ public function getUnsuccessfulComparisons(): iterable { yield 'genericListDoesNotAcceptListTupleWithMismatchedTypes' => [ 'list', 'list{int, string}', ]; yield 'genericListDoesNotAcceptArrayTupleWithMismatchedTypes' => [ 'list', 'array{int, string}', ]; yield 'nonEmptyMixedDoesNotAcceptMixed' => [ 'non-empty-mixed', 'mixed', ]; } }