diff --git a/dictionaries/CallMap.php b/dictionaries/CallMap.php index 95333ce217e..2acbfa4241f 100644 --- a/dictionaries/CallMap.php +++ b/dictionaries/CallMap.php @@ -7487,7 +7487,7 @@ 'MessageFormatter::parseMessage' => ['array|false', 'locale'=>'string', 'pattern'=>'string', 'source'=>'string'], 'MessageFormatter::setPattern' => ['bool', 'pattern'=>'string'], 'metaphone' => ['string|false', 'string'=>'string', 'max_phonemes='=>'int'], -'method_exists' => ['bool', 'object_or_class'=>'object|class-string', 'method'=>'string'], +'method_exists' => ['bool', 'object_or_class'=>'object|class-string|interface-string', 'method'=>'string'], 'mhash' => ['string', 'algo'=>'int', 'data'=>'string', 'key='=>'string'], 'mhash_count' => ['int'], 'mhash_get_block_size' => ['int|false', 'algo'=>'int'], @@ -11317,7 +11317,7 @@ 'ReflectionClass::hasConstant' => ['bool', 'name'=>'string'], 'ReflectionClass::hasMethod' => ['bool', 'name'=>'string'], 'ReflectionClass::hasProperty' => ['bool', 'name'=>'string'], -'ReflectionClass::implementsInterface' => ['bool', 'interface_name'=>'class-string|ReflectionClass'], +'ReflectionClass::implementsInterface' => ['bool', 'interface_name'=>'interface-string|ReflectionClass'], 'ReflectionClass::inNamespace' => ['bool'], 'ReflectionClass::isAbstract' => ['bool'], 'ReflectionClass::isAnonymous' => ['bool'], diff --git a/src/Psalm/Internal/Type/Comparator/ClassStringComparator.php b/src/Psalm/Internal/Type/Comparator/ClassLikeStringComparator.php similarity index 98% rename from src/Psalm/Internal/Type/Comparator/ClassStringComparator.php rename to src/Psalm/Internal/Type/Comparator/ClassLikeStringComparator.php index a53cce6a7a3..da6ca9be5eb 100644 --- a/src/Psalm/Internal/Type/Comparator/ClassStringComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ClassLikeStringComparator.php @@ -14,7 +14,7 @@ /** * @internal */ -class ClassStringComparator +class ClassLikeStringComparator { /** * @param TClassString|TLiteralClassString $input_type_part diff --git a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php index c5737e76f6c..79012adad0f 100644 --- a/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php +++ b/src/Psalm/Internal/Type/Comparator/ScalarTypeComparator.php @@ -441,7 +441,7 @@ public static function isContainedBy( if (($container_type_part instanceof TClassString || $container_type_part instanceof TLiteralClassString) && ($input_type_part instanceof TClassString || $input_type_part instanceof TLiteralClassString) ) { - return ClassStringComparator::isContainedBy( + return ClassLikeStringComparator::isContainedBy( $codebase, $input_type_part, $container_type_part, diff --git a/src/Psalm/Internal/Type/TypeParser.php b/src/Psalm/Internal/Type/TypeParser.php index dc25c783d0f..ce44c04d8e4 100644 --- a/src/Psalm/Internal/Type/TypeParser.php +++ b/src/Psalm/Internal/Type/TypeParser.php @@ -567,7 +567,7 @@ private static function getTypeFromGenericTree( return new TNonEmptyList($generic_params[0]); } - if ($generic_type_value === 'class-string') { + if ($generic_type_value === 'class-string' || $generic_type_value === 'interface-string') { $class_name = (string)$generic_params[0]; if (isset($template_type_map[$class_name])) { diff --git a/src/Psalm/Internal/Type/TypeTokenizer.php b/src/Psalm/Internal/Type/TypeTokenizer.php index 5485e5050e9..bb74404a2d0 100644 --- a/src/Psalm/Internal/Type/TypeTokenizer.php +++ b/src/Psalm/Internal/Type/TypeTokenizer.php @@ -41,13 +41,14 @@ class TypeTokenizer 'mixed' => true, 'numeric-string' => true, 'class-string' => true, + 'interface-string' => true, + 'trait-string' => true, 'callable-string' => true, 'callable-array' => true, 'callable-object' => true, 'stringable-object' => true, 'pure-callable' => true, 'pure-Closure' => true, - 'trait-string' => true, 'mysql-escaped-string' => true, // deprecated 'html-escaped-string' => true, // deprecated 'literal-string' => true, diff --git a/stubs/Reflection.phpstub b/stubs/Reflection.phpstub index 3cfa39d56e1..0004f124acf 100644 --- a/stubs/Reflection.phpstub +++ b/stubs/Reflection.phpstub @@ -13,7 +13,7 @@ class ReflectionClass implements Reflector { public $name; /** - * @param T|class-string|trait-string $argument + * @param T|class-string|interface-string|trait-string $argument */ public function __construct($argument) {} diff --git a/tests/ClassStringTest.php b/tests/ClassLikeStringTest.php similarity index 94% rename from tests/ClassStringTest.php rename to tests/ClassLikeStringTest.php index 85912086217..005fa7f1934 100644 --- a/tests/ClassStringTest.php +++ b/tests/ClassLikeStringTest.php @@ -4,7 +4,7 @@ use Psalm\Config; use Psalm\Context; -class ClassStringTest extends TestCase +class ClassLikeStringTest extends TestCase { use Traits\InvalidCodeAnalysisTestTrait; use Traits\ValidCodeAnalysisTestTrait; @@ -437,7 +437,7 @@ public static function two() : void; } /** - * @param class-string $className + * @param interface-string $className */ function foo($className) : void { $className::one(); @@ -455,9 +455,9 @@ public static function two() : bool; } /** - * @param class-string $className + * @param interface-string $className */ - function foo($className) : void { + function foo(string $className) : void { $className::two(); if (is_subclass_of($className, Foo::class, true)) { @@ -492,9 +492,9 @@ function identity(string $shouldBe) : string { return $shouldBe; } 'filterIsObject' => [ '|DateTimeInterface $maybe + * @param interface-string|DateTimeInterface $maybe * - * @return class-string + * @return interface-string */ function Foo($maybe) : string { if (is_object($maybe)) { @@ -507,9 +507,9 @@ function Foo($maybe) : string { 'filterIsString' => [ '|DateTimeInterface $maybe + * @param interface-string|DateTimeInterface $maybe * - * @return class-string + * @return interface-string */ function Bar($maybe) : string { if (is_string($maybe)) { @@ -778,8 +778,36 @@ function a($obj) { $class = $obj::class; return $class; - } - ', + }', + ], + 'classStringAllowsClasses' => [ + ' $s + */ + function takesException(string $s): void {} + + /** + * @param class-string $s + */ + function takesThrowable(string $s): void {} + + takesOpen(InvalidArgumentException::class); + takesException(InvalidArgumentException::class); + takesThrowable(InvalidArgumentException::class);', + ], + 'reflectionClassCoercion' => [ + ' */ + function takesString(string $s) { + /** @psalm-suppress ArgumentTypeCoercion */ + return new ReflectionClass($s); + }', ], ]; } @@ -919,7 +947,6 @@ function foo(string $s) : string { }', 'error_message' => 'InvalidReturnStatement', ], - ]; } } diff --git a/tests/StubTest.php b/tests/StubTest.php index 63c26f4b273..4b6b43a6fdd 100644 --- a/tests/StubTest.php +++ b/tests/StubTest.php @@ -1282,15 +1282,15 @@ public function getReference($entityName, $id) { 'getReference(I::class, 1); + echo $em->getReference(A::class, 1); }' ); $this->expectException(\Psalm\Exception\CodeException::class); - $this->expectExceptionMessage('I|null'); + $this->expectExceptionMessage('A|null'); $this->analyzeFile($file_path, new Context()); } diff --git a/tests/Template/ConditionalReturnTypeTest.php b/tests/Template/ConditionalReturnTypeTest.php index 7e085c9cdf5..676a48d8a6f 100644 --- a/tests/Template/ConditionalReturnTypeTest.php +++ b/tests/Template/ConditionalReturnTypeTest.php @@ -627,7 +627,7 @@ class A {} /** * @template T - * @param string|class-string $name + * @param literal-string|class-string $name * @return ($name is class-string ? T : mixed) */ function get(string $name) { diff --git a/tests/Template/FunctionClassStringTemplateTest.php b/tests/Template/FunctionClassStringTemplateTest.php index a85b841f04d..e7e52d59cad 100644 --- a/tests/Template/FunctionClassStringTemplateTest.php +++ b/tests/Template/FunctionClassStringTemplateTest.php @@ -61,7 +61,7 @@ class FooChild extends Foo{} ' $class */ function foo(string $class) : void { $a = new $class(); @@ -236,7 +236,7 @@ function moreSpecific(string $d_class) : void { * @template T as object * @template S as object * @param array $a - * @param class-string $type + * @param interface-string $type * @return array */ function filter(array $a, string $type): array { @@ -265,7 +265,7 @@ interface B {} * @template T as object * @template S as object * @param T $item - * @param class-string $type + * @param interface-string $type * @return T&S */ function filter($item, string $type) { diff --git a/tests/Template/FunctionTemplateAssertTest.php b/tests/Template/FunctionTemplateAssertTest.php index 63e1b145dcb..af49e456969 100644 --- a/tests/Template/FunctionTemplateAssertTest.php +++ b/tests/Template/FunctionTemplateAssertTest.php @@ -425,7 +425,7 @@ function getIterable(): iterable { * @psalm-assert-if-true iterable $i * * @param iterable $i - * @param class-string $type + * @param class-string|interface-string $type */ function allInstanceOf(iterable $i, string $type): bool { /** @psalm-suppress MixedAssignment */ @@ -458,16 +458,16 @@ function getData(): iterable { /** * @psalm-template ExpectedType of object * @param mixed $value - * @psalm-param class-string $interface - * @psalm-assert ExpectedType|class-string $value + * @psalm-param interface-string $interface + * @psalm-assert ExpectedType|interface-string $value */ function implementsInterface($value, $interface, string $message = ""): void {} /** * @psalm-template ExpectedType of object * @param mixed $value - * @psalm-param class-string $interface - * @psalm-assert null|ExpectedType|class-string $value + * @psalm-param interface-string $interface + * @psalm-assert null|ExpectedType|interface-string $value */ function nullOrImplementsInterface(?object $value, $interface, string $message = ""): void {} diff --git a/tests/Template/NestedTemplateTest.php b/tests/Template/NestedTemplateTest.php index faff452c861..6af04f91b1b 100644 --- a/tests/Template/NestedTemplateTest.php +++ b/tests/Template/NestedTemplateTest.php @@ -88,9 +88,13 @@ public function unwrap(); } /** - * @extends Wrapper + * @implements Wrapper */ - interface StringWrapper extends Wrapper {} + class StringWrapper implements Wrapper { + public function unwrap() { + return "hello"; + } + } /** * @template TInner