2018-08-21 20:47:28 +02:00
|
|
|
<?php
|
|
|
|
namespace Psalm\Tests;
|
|
|
|
|
|
|
|
use Psalm\Config;
|
|
|
|
use Psalm\Context;
|
2021-12-03 20:29:06 +01:00
|
|
|
use Psalm\Exception\CodeException;
|
2021-12-04 21:55:53 +01:00
|
|
|
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
|
|
|
|
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
|
2018-08-21 20:47:28 +02:00
|
|
|
|
2021-07-17 00:38:39 +02:00
|
|
|
class ClassLikeStringTest extends TestCase
|
2018-08-21 20:47:28 +02:00
|
|
|
{
|
2021-12-04 21:55:53 +01:00
|
|
|
use InvalidCodeAnalysisTestTrait;
|
|
|
|
use ValidCodeAnalysisTestTrait;
|
2018-08-21 20:47:28 +02:00
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function testDontAllowStringStandInForNewClass(): void
|
2018-08-21 20:47:28 +02:00
|
|
|
{
|
2019-05-17 00:36:36 +02:00
|
|
|
$this->expectExceptionMessage('InvalidStringClass');
|
2021-12-03 20:29:06 +01:00
|
|
|
$this->expectException(CodeException::class);
|
2018-08-21 20:47:28 +02:00
|
|
|
Config::getInstance()->allow_string_standin_for_class = false;
|
|
|
|
|
|
|
|
$this->addFile(
|
|
|
|
'somefile.php',
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
$a = "A";
|
|
|
|
|
|
|
|
new $a();'
|
|
|
|
);
|
|
|
|
|
|
|
|
$this->analyzeFile('somefile.php', new Context());
|
|
|
|
}
|
|
|
|
|
2020-09-12 17:24:05 +02:00
|
|
|
public function testDontAllowStringStandInForStaticMethodCall(): void
|
2018-08-21 20:47:28 +02:00
|
|
|
{
|
2019-05-17 00:36:36 +02:00
|
|
|
$this->expectExceptionMessage('InvalidStringClass');
|
2021-12-03 20:29:06 +01:00
|
|
|
$this->expectException(CodeException::class);
|
2018-08-21 20:47:28 +02:00
|
|
|
Config::getInstance()->allow_string_standin_for_class = false;
|
|
|
|
|
|
|
|
$this->addFile(
|
|
|
|
'somefile.php',
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
public static function foo() : void {}
|
|
|
|
}
|
|
|
|
|
|
|
|
$a = "A";
|
|
|
|
|
|
|
|
$a::foo();'
|
|
|
|
);
|
|
|
|
|
|
|
|
$this->analyzeFile('somefile.php', new Context());
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2019-03-01 21:55:20 +01:00
|
|
|
* @return iterable<string,array{string,assertions?:array<string,string>,error_levels?:string[]}>
|
2018-08-21 20:47:28 +02:00
|
|
|
*/
|
2020-09-12 17:24:05 +02:00
|
|
|
public function providerValidCodeParse(): iterable
|
2018-08-21 20:47:28 +02:00
|
|
|
{
|
|
|
|
return [
|
|
|
|
'arrayOfClassConstants' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
|
|
|
* @param array<class-string> $arr
|
|
|
|
*/
|
|
|
|
function takesClassConstants(array $arr) : void {}
|
|
|
|
|
|
|
|
class A {}
|
|
|
|
class B {}
|
|
|
|
|
|
|
|
takesClassConstants([A::class, B::class]);',
|
|
|
|
],
|
|
|
|
'arrayOfStringClasses' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
|
|
|
* @param array<class-string> $arr
|
|
|
|
*/
|
|
|
|
function takesClassConstants(array $arr) : void {}
|
|
|
|
|
|
|
|
class A {}
|
|
|
|
class B {}
|
|
|
|
|
|
|
|
takesClassConstants(["A", "B"]);',
|
|
|
|
'annotations' => [],
|
2019-04-26 00:02:19 +02:00
|
|
|
'error_levels' => ['ArgumentTypeCoercion'],
|
2018-08-21 20:47:28 +02:00
|
|
|
],
|
|
|
|
'singleClassConstantAsConstant' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
|
|
|
* @param class-string $s
|
|
|
|
*/
|
|
|
|
function takesClassConstants(string $s) : void {}
|
|
|
|
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
takesClassConstants(A::class);',
|
|
|
|
],
|
|
|
|
'singleClassConstantWithString' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
|
|
|
* @param class-string $s
|
|
|
|
*/
|
|
|
|
function takesClassConstants(string $s) : void {}
|
|
|
|
|
|
|
|
class A {}
|
|
|
|
|
2020-08-30 17:44:14 +02:00
|
|
|
/** @psalm-suppress ArgumentTypeCoercion */
|
2018-08-21 20:47:28 +02:00
|
|
|
takesClassConstants("A");',
|
|
|
|
'annotations' => [],
|
|
|
|
],
|
|
|
|
'returnClassConstant' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return class-string
|
|
|
|
*/
|
|
|
|
function takesClassConstants() : string {
|
|
|
|
return A::class;
|
|
|
|
}',
|
|
|
|
],
|
|
|
|
'returnClassConstantAllowCoercion' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return class-string
|
|
|
|
*/
|
|
|
|
function takesClassConstants() : string {
|
|
|
|
return "A";
|
|
|
|
}',
|
|
|
|
'annotations' => [],
|
|
|
|
'error_levels' => ['LessSpecificReturnStatement', 'MoreSpecificReturnType'],
|
|
|
|
],
|
|
|
|
'returnClassConstantArray' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
class B {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array<class-string>
|
|
|
|
*/
|
|
|
|
function takesClassConstants() : array {
|
|
|
|
return [A::class, B::class];
|
|
|
|
}',
|
|
|
|
],
|
|
|
|
'returnClassConstantArrayAllowCoercion' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
class B {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array<class-string>
|
|
|
|
*/
|
|
|
|
function takesClassConstants() : array {
|
|
|
|
return ["A", "B"];
|
|
|
|
}',
|
|
|
|
'annotations' => [],
|
|
|
|
'error_levels' => ['LessSpecificReturnStatement', 'MoreSpecificReturnType'],
|
|
|
|
],
|
|
|
|
'ifClassStringEquals' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
class B {}
|
|
|
|
|
|
|
|
/** @param class-string $class */
|
|
|
|
function foo(string $class) : void {
|
|
|
|
if ($class === A::class) {}
|
|
|
|
if ($class === A::class || $class === B::class) {}
|
|
|
|
}',
|
|
|
|
],
|
|
|
|
'classStringCombination' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
/** @return class-string */
|
|
|
|
function foo() : string {
|
|
|
|
return A::class;
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @param class-string $a */
|
|
|
|
function bar(string $a) : void {}
|
|
|
|
|
|
|
|
bar(rand(0, 1) ? foo() : A::class);
|
|
|
|
bar(rand(0, 1) ? A::class : foo());',
|
|
|
|
],
|
2018-08-21 23:59:06 +02:00
|
|
|
'assertionToClassString' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
function foo(string $s) : void {
|
|
|
|
if ($s === A::class) {
|
|
|
|
bar($s);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/** @param class-string $s */
|
|
|
|
function bar(string $s) : void {
|
|
|
|
new $s();
|
|
|
|
}',
|
2019-01-02 12:58:49 +01:00
|
|
|
'assertions' => [],
|
|
|
|
'error_levels' => ['MixedMethodCall'],
|
2018-08-21 23:59:06 +02:00
|
|
|
],
|
|
|
|
'constantArrayOffset' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
const FOO = [
|
|
|
|
B::class => "bar",
|
|
|
|
];
|
|
|
|
}
|
|
|
|
class B {}
|
|
|
|
|
|
|
|
/** @param class-string $s */
|
|
|
|
function bar(string $s) : void {}
|
|
|
|
|
|
|
|
foreach (A::FOO as $class => $_) {
|
|
|
|
bar($class);
|
|
|
|
}',
|
|
|
|
],
|
2018-08-23 17:43:53 +02:00
|
|
|
'arrayEquivalence' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
class B {}
|
|
|
|
|
|
|
|
$foo = [
|
|
|
|
A::class,
|
|
|
|
B::class
|
|
|
|
];
|
|
|
|
|
|
|
|
foreach ($foo as $class) {
|
|
|
|
if ($class === A::class) {}
|
|
|
|
}',
|
|
|
|
],
|
2018-09-11 03:55:22 +02:00
|
|
|
'switchMixedVar' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
class B {}
|
|
|
|
class C {}
|
|
|
|
|
|
|
|
/** @param mixed $a */
|
|
|
|
function foo($a) : void {
|
|
|
|
switch ($a) {
|
|
|
|
case A::class:
|
|
|
|
return;
|
2018-11-02 22:03:49 +01:00
|
|
|
|
2018-09-11 03:55:22 +02:00
|
|
|
case B::class:
|
|
|
|
case C::class:
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
}',
|
|
|
|
],
|
2018-11-02 22:03:49 +01:00
|
|
|
'reconcileToFalsy' => [
|
|
|
|
'<?php
|
2018-11-02 22:38:20 +01:00
|
|
|
/** @psalm-param ?class-string $s */
|
|
|
|
function bar(?string $s) : void {}
|
|
|
|
|
2018-11-02 22:03:49 +01:00
|
|
|
class A {}
|
|
|
|
|
|
|
|
/** @psalm-return ?class-string */
|
2018-11-02 22:38:20 +01:00
|
|
|
function bat() {
|
|
|
|
if (rand(0, 1)) return null;
|
|
|
|
return A::class;
|
2018-11-02 22:03:49 +01:00
|
|
|
}
|
|
|
|
|
2018-11-02 22:38:20 +01:00
|
|
|
$a = bat();
|
|
|
|
$a ? 1 : 0;
|
|
|
|
bar($a);',
|
2018-11-02 22:03:49 +01:00
|
|
|
],
|
2019-01-02 03:00:34 +01:00
|
|
|
'allowTraitClassComparison' => [
|
|
|
|
'<?php
|
|
|
|
trait T {
|
|
|
|
public function foo() : void {
|
|
|
|
if (self::class === A::class) {}
|
|
|
|
if (self::class !== A::class) {}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
class A {
|
|
|
|
use T;
|
|
|
|
}
|
|
|
|
|
|
|
|
class B {
|
|
|
|
use T;
|
2019-03-23 19:27:54 +01:00
|
|
|
}',
|
2019-01-02 03:00:34 +01:00
|
|
|
],
|
2019-01-02 17:18:22 +01:00
|
|
|
'refineStringToClassString' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
2019-01-02 17:35:49 +01:00
|
|
|
function foo(string $s) : ?A {
|
2019-01-02 17:18:22 +01:00
|
|
|
if ($s !== A::class) {
|
2019-01-02 17:35:49 +01:00
|
|
|
return null;
|
2019-01-02 17:18:22 +01:00
|
|
|
}
|
2019-01-02 17:35:49 +01:00
|
|
|
return new $s();
|
2019-01-02 17:18:22 +01:00
|
|
|
}',
|
|
|
|
],
|
2019-01-02 20:24:31 +01:00
|
|
|
'takesChildOfClass' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
class AChild extends A {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param class-string<A> $s
|
|
|
|
*/
|
|
|
|
function foo(string $s) : void {}
|
|
|
|
|
|
|
|
foo(AChild::class);',
|
|
|
|
],
|
2019-01-05 21:12:42 +01:00
|
|
|
'returnClassConstantClassStringParameterized' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return class-string<A> $s
|
|
|
|
*/
|
|
|
|
function foo(A $a) : string {
|
|
|
|
return $a::class;
|
|
|
|
}',
|
|
|
|
],
|
2019-01-05 21:42:56 +01:00
|
|
|
'returnGetCalledClassClassStringParameterized' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
/**
|
|
|
|
* @return class-string<A> $s
|
|
|
|
*/
|
|
|
|
function foo() : string {
|
|
|
|
return get_called_class();
|
|
|
|
}
|
|
|
|
}',
|
|
|
|
],
|
2019-01-05 21:12:42 +01:00
|
|
|
'returnGetClassClassStringParameterized' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return class-string<A> $s
|
|
|
|
*/
|
|
|
|
function foo(A $a) : string {
|
|
|
|
return get_class($a);
|
|
|
|
}',
|
|
|
|
],
|
2019-01-05 21:49:50 +01:00
|
|
|
'returnGetParentClassClassStringParameterizedNoArg' => [
|
2019-01-05 21:42:56 +01:00
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
class B extends A {
|
|
|
|
/**
|
|
|
|
* @return class-string<A> $s
|
|
|
|
*/
|
|
|
|
function foo() : string {
|
|
|
|
return get_parent_class();
|
|
|
|
}
|
|
|
|
}',
|
|
|
|
],
|
2019-01-05 21:12:42 +01:00
|
|
|
'createClassOfTypeFromString' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return class-string<A> $s
|
|
|
|
*/
|
|
|
|
function foo(string $s) : string {
|
|
|
|
if (!class_exists($s)) {
|
|
|
|
throw new \UnexpectedValueException("bad");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_a($s, A::class, true)) {
|
|
|
|
throw new \UnexpectedValueException("bad");
|
|
|
|
}
|
|
|
|
|
|
|
|
return $s;
|
|
|
|
}',
|
|
|
|
],
|
2019-01-05 22:58:34 +01:00
|
|
|
'createClassOfTypeFromStringUsingIsSubclassOf' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return class-string<A> $s
|
|
|
|
*/
|
|
|
|
function foo(string $s) : string {
|
|
|
|
if (!class_exists($s)) {
|
|
|
|
throw new \UnexpectedValueException("bad");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_subclass_of($s, A::class)) {
|
|
|
|
throw new \UnexpectedValueException("bad");
|
|
|
|
}
|
|
|
|
|
|
|
|
return $s;
|
|
|
|
}',
|
|
|
|
],
|
|
|
|
'checkSubclassOfAbstract' => [
|
|
|
|
'<?php
|
|
|
|
interface Foo {
|
|
|
|
public static function Bar() : bool;
|
|
|
|
};
|
|
|
|
|
|
|
|
class FooClass implements Foo {
|
|
|
|
public static function Bar() : bool {
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
function foo(string $className) : bool {
|
|
|
|
if (is_subclass_of($className, Foo::class, true)) {
|
|
|
|
return $className::Bar();
|
|
|
|
}
|
|
|
|
|
|
|
|
return false;
|
|
|
|
}',
|
|
|
|
],
|
2019-01-20 05:25:23 +01:00
|
|
|
'explicitIntersectionClassString' => [
|
2019-01-20 04:45:58 +01:00
|
|
|
'<?php
|
|
|
|
interface Foo {
|
2019-01-20 05:25:23 +01:00
|
|
|
public static function one() : void;
|
2019-01-20 04:45:58 +01:00
|
|
|
};
|
|
|
|
|
|
|
|
interface Bar {
|
2019-01-20 05:25:23 +01:00
|
|
|
public static function two() : void;
|
2019-01-20 04:45:58 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-07-17 00:38:39 +02:00
|
|
|
* @param interface-string<Foo&Bar> $className
|
2019-01-20 04:45:58 +01:00
|
|
|
*/
|
2019-01-20 05:25:23 +01:00
|
|
|
function foo($className) : void {
|
|
|
|
$className::one();
|
|
|
|
$className::two();
|
2019-03-23 19:27:54 +01:00
|
|
|
}',
|
2019-01-20 04:45:58 +01:00
|
|
|
],
|
2019-01-20 16:39:08 +01:00
|
|
|
'implicitIntersectionClassString' => [
|
|
|
|
'<?php
|
|
|
|
interface Foo {
|
|
|
|
public static function one() : bool;
|
|
|
|
};
|
|
|
|
|
|
|
|
interface Bar {
|
|
|
|
public static function two() : bool;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-07-17 00:38:39 +02:00
|
|
|
* @param interface-string<Bar> $className
|
2019-01-20 16:39:08 +01:00
|
|
|
*/
|
2021-07-17 00:38:39 +02:00
|
|
|
function foo(string $className) : void {
|
2019-01-20 16:39:08 +01:00
|
|
|
$className::two();
|
|
|
|
|
|
|
|
if (is_subclass_of($className, Foo::class, true)) {
|
|
|
|
$className::one();
|
|
|
|
$className::two();
|
|
|
|
}
|
2019-03-23 19:27:54 +01:00
|
|
|
}',
|
2019-01-20 16:39:08 +01:00
|
|
|
],
|
2019-01-27 20:10:33 +01:00
|
|
|
'instanceofClassString' => [
|
|
|
|
'<?php
|
|
|
|
function f(Exception $e): ?InvalidArgumentException {
|
|
|
|
$type = InvalidArgumentException::class;
|
|
|
|
if ($e instanceof $type) {
|
|
|
|
return $e;
|
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
2019-03-23 19:27:54 +01:00
|
|
|
}',
|
2019-01-27 20:10:33 +01:00
|
|
|
],
|
2021-09-17 19:52:11 +02:00
|
|
|
'instanceofClassStringNotLiteral' => [
|
|
|
|
'<?php
|
|
|
|
final class Z {
|
|
|
|
/**
|
|
|
|
* @psalm-var class-string<stdClass> $class
|
|
|
|
*/
|
|
|
|
private string $class = stdClass::class;
|
|
|
|
|
|
|
|
public function go(object $object): ?stdClass {
|
|
|
|
$a = $this->class;
|
|
|
|
if ($object instanceof $a) {
|
|
|
|
return $object;
|
|
|
|
}
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}'
|
|
|
|
],
|
2019-02-01 03:06:21 +01:00
|
|
|
'returnTemplatedClassString' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
|
|
|
* @template T
|
|
|
|
*
|
|
|
|
* @param class-string<T> $shouldBe
|
|
|
|
* @return class-string<T>
|
|
|
|
*/
|
|
|
|
function identity(string $shouldBe) : string { return $shouldBe; }
|
|
|
|
|
|
|
|
identity(DateTimeImmutable::class)::createFromMutable(new DateTime());',
|
|
|
|
],
|
2019-02-03 23:32:44 +01:00
|
|
|
'filterIsObject' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
2021-07-17 00:38:39 +02:00
|
|
|
* @param interface-string<DateTimeInterface>|DateTimeInterface $maybe
|
2019-02-03 23:32:44 +01:00
|
|
|
*
|
2021-07-17 00:38:39 +02:00
|
|
|
* @return interface-string<DateTimeInterface>
|
2019-02-03 23:32:44 +01:00
|
|
|
*/
|
|
|
|
function Foo($maybe) : string {
|
|
|
|
if (is_object($maybe)) {
|
|
|
|
return get_class($maybe);
|
|
|
|
}
|
|
|
|
|
|
|
|
return $maybe;
|
|
|
|
}',
|
|
|
|
],
|
|
|
|
'filterIsString' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
2021-07-17 00:38:39 +02:00
|
|
|
* @param interface-string<DateTimeInterface>|DateTimeInterface $maybe
|
2019-02-03 23:32:44 +01:00
|
|
|
*
|
2021-07-17 00:38:39 +02:00
|
|
|
* @return interface-string<DateTimeInterface>
|
2019-02-03 23:32:44 +01:00
|
|
|
*/
|
|
|
|
function Bar($maybe) : string {
|
|
|
|
if (is_string($maybe)) {
|
|
|
|
return $maybe;
|
|
|
|
}
|
|
|
|
|
|
|
|
return get_class($maybe);
|
|
|
|
}',
|
|
|
|
],
|
2019-03-08 23:35:09 +01:00
|
|
|
'mergeLiteralClassStringsWithGeneric' => [
|
|
|
|
'<?php
|
|
|
|
class Base {}
|
|
|
|
class A extends Base {}
|
|
|
|
class B extends Base {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array<A::class|B::class> $literal_classes
|
|
|
|
* @param array<class-string<Base>> $generic_classes
|
|
|
|
* @return array<class-string<Base>>
|
|
|
|
*/
|
|
|
|
function foo(array $literal_classes, array $generic_classes) {
|
|
|
|
return array_merge($literal_classes, $generic_classes);
|
2019-03-23 19:27:54 +01:00
|
|
|
}',
|
2019-03-08 23:35:09 +01:00
|
|
|
],
|
|
|
|
'mergeGenericClassStringsWithLiteral' => [
|
|
|
|
'<?php
|
|
|
|
class Base {}
|
|
|
|
class A extends Base {}
|
|
|
|
class B extends Base {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param array<A::class|B::class> $literal_classes
|
|
|
|
* @param array<class-string<Base>> $generic_classes
|
|
|
|
* @return array<class-string<Base>>
|
|
|
|
*/
|
|
|
|
function bar(array $literal_classes, array $generic_classes) {
|
|
|
|
return array_merge($generic_classes, $literal_classes);
|
2019-03-23 19:27:54 +01:00
|
|
|
}',
|
2019-03-08 23:35:09 +01:00
|
|
|
],
|
2019-05-03 15:09:51 +02:00
|
|
|
'noCrashWithIsSubclassOfNonExistentVariable' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
function foo() : void {
|
|
|
|
/**
|
|
|
|
* @psalm-suppress UndefinedVariable
|
|
|
|
* @psalm-suppress MixedArgument
|
|
|
|
*/
|
|
|
|
if (!is_subclass_of($s, A::class)) {}
|
|
|
|
}',
|
|
|
|
],
|
2019-06-17 17:31:43 +02:00
|
|
|
'allowClassExistsCheckOnClassString' => [
|
|
|
|
'<?php
|
|
|
|
class C
|
|
|
|
{
|
|
|
|
public function __construct() {
|
|
|
|
if (class_exists(\Doesnt\Really::class)) {
|
|
|
|
\Doesnt\Really::something();
|
|
|
|
}
|
|
|
|
}
|
2019-07-05 22:24:00 +02:00
|
|
|
}',
|
2019-06-17 17:31:43 +02:00
|
|
|
],
|
|
|
|
'allowClassExistsCheckOnString' => [
|
|
|
|
'<?php
|
|
|
|
class C
|
|
|
|
{
|
|
|
|
public function __construct() {
|
|
|
|
if (class_exists("Doesnt\\Really")) {
|
|
|
|
\Doesnt\Really::something();
|
|
|
|
}
|
|
|
|
}
|
2019-07-05 22:24:00 +02:00
|
|
|
}',
|
2019-06-17 17:31:43 +02:00
|
|
|
],
|
2019-06-26 21:11:16 +02:00
|
|
|
'allowComparisonToStaticClassString' => [
|
|
|
|
'<?php
|
|
|
|
class A {
|
|
|
|
const CLASSES = ["foobar" => B::class];
|
|
|
|
|
|
|
|
function foo(): bool {
|
|
|
|
return self::CLASSES["foobar"] === static::class;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-07-05 22:24:00 +02:00
|
|
|
class B extends A {}',
|
2019-06-26 21:11:16 +02:00
|
|
|
],
|
2019-06-28 16:17:59 +02:00
|
|
|
'noCrashWhenClassExists' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
if (class_exists(A::class)) {
|
|
|
|
new \RuntimeException();
|
2019-07-05 22:24:00 +02:00
|
|
|
}',
|
2019-06-28 16:17:59 +02:00
|
|
|
],
|
2019-06-28 16:48:30 +02:00
|
|
|
|
|
|
|
'noCrashWhenClassExistsNegated' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
if (!class_exists(A::class)) {
|
|
|
|
new \RuntimeException();
|
2019-07-05 22:24:00 +02:00
|
|
|
}',
|
2019-06-28 16:48:30 +02:00
|
|
|
],
|
2019-12-07 20:03:20 +01:00
|
|
|
'createNewObjectFromGetClass' => [
|
|
|
|
'<?php
|
2020-08-06 16:18:55 +02:00
|
|
|
/**
|
|
|
|
* @psalm-consistent-constructor
|
|
|
|
*/
|
2019-12-07 20:03:20 +01:00
|
|
|
class Example {
|
|
|
|
static function staticMethod(): string {
|
|
|
|
return "";
|
|
|
|
}
|
|
|
|
|
|
|
|
public function instanceMethod(): string {
|
|
|
|
$className = get_class();
|
|
|
|
|
|
|
|
return $className::staticMethod();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param class-string<Example> $className
|
|
|
|
*/
|
|
|
|
function example(string $className, Example $object): string {
|
|
|
|
$objectClassName = get_class($object);
|
|
|
|
|
|
|
|
takesExampleClassString($className);
|
|
|
|
takesExampleClassString($objectClassName);
|
|
|
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
return (new $className)->instanceMethod();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
return (new $objectClassName)->instanceMethod();
|
|
|
|
}
|
|
|
|
|
|
|
|
if (rand(0, 1)) {
|
|
|
|
return $className::staticMethod();
|
|
|
|
}
|
|
|
|
|
|
|
|
return $objectClassName::staticMethod();
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/** @param class-string<Example> $className */
|
|
|
|
function takesExampleClassString(string $className): void {}'
|
|
|
|
],
|
2019-12-31 14:04:51 +01:00
|
|
|
'noCrashOnPolyfill' => [
|
|
|
|
'<?php
|
|
|
|
if (class_exists(My_Parent::class) && !class_exists(My_Extend::class)) {
|
|
|
|
/**
|
|
|
|
* Extended class
|
|
|
|
*/
|
|
|
|
class My_Extend extends My_Parent {
|
|
|
|
/**
|
|
|
|
* Construct
|
|
|
|
*
|
|
|
|
* @return void
|
|
|
|
*/
|
|
|
|
public function __construct() {
|
|
|
|
echo "foo";
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}',
|
|
|
|
],
|
2020-03-09 06:29:16 +01:00
|
|
|
'selfResolvedOnStaticProperty' => [
|
|
|
|
'<?php
|
|
|
|
namespace Bar;
|
|
|
|
|
|
|
|
class Foo {
|
|
|
|
/** @var class-string<self> */
|
|
|
|
private static $c;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return class-string<self>
|
|
|
|
*/
|
|
|
|
public static function r() : string
|
|
|
|
{
|
|
|
|
return self::$c;
|
|
|
|
}
|
|
|
|
}'
|
|
|
|
],
|
2020-03-09 23:59:56 +01:00
|
|
|
'traitClassStringClone' => [
|
|
|
|
'<?php
|
|
|
|
trait Factory
|
|
|
|
{
|
|
|
|
/** @return class-string<static> */
|
|
|
|
public static function getFactoryClass()
|
|
|
|
{
|
|
|
|
return static::class;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-06 16:18:55 +02:00
|
|
|
/**
|
|
|
|
* @psalm-consistent-constructor
|
|
|
|
*/
|
2020-03-09 23:59:56 +01:00
|
|
|
class A
|
|
|
|
{
|
|
|
|
use Factory;
|
|
|
|
|
|
|
|
public static function factory(): self
|
|
|
|
{
|
|
|
|
$class = static::getFactoryClass();
|
|
|
|
return new $class;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-08-06 16:18:55 +02:00
|
|
|
/**
|
|
|
|
* @psalm-consistent-constructor
|
|
|
|
*/
|
2020-03-09 23:59:56 +01:00
|
|
|
class B
|
|
|
|
{
|
|
|
|
use Factory;
|
|
|
|
|
|
|
|
public static function factory(): self
|
|
|
|
{
|
|
|
|
$class = static::getFactoryClass();
|
|
|
|
return new $class;
|
|
|
|
}
|
|
|
|
}'
|
|
|
|
],
|
2020-03-24 23:30:08 +01:00
|
|
|
'staticClassReturn' => [
|
|
|
|
'<?php
|
2020-08-06 16:18:55 +02:00
|
|
|
/**
|
|
|
|
* @psalm-consistent-constructor
|
|
|
|
*/
|
2020-03-24 23:30:08 +01:00
|
|
|
class A {
|
|
|
|
/** @return static */
|
|
|
|
public static function getInstance() {
|
|
|
|
$class = static::class;
|
|
|
|
return new $class();
|
|
|
|
}
|
|
|
|
}'
|
|
|
|
],
|
2020-03-27 14:51:53 +01:00
|
|
|
'getCalledClassIsStaticClass' => [
|
|
|
|
'<?php
|
2020-08-06 16:18:55 +02:00
|
|
|
/**
|
|
|
|
* @psalm-consistent-constructor
|
|
|
|
*/
|
2020-03-27 14:51:53 +01:00
|
|
|
class A {
|
|
|
|
/** @return static */
|
|
|
|
public function getStatic() {
|
|
|
|
$c = get_called_class();
|
|
|
|
return new $c();
|
|
|
|
}
|
|
|
|
}'
|
|
|
|
],
|
2020-06-18 17:56:08 +02:00
|
|
|
'accessConstantOnClassStringVariable' => [
|
|
|
|
'<?php
|
|
|
|
class Beep {
|
|
|
|
/** @var string */
|
|
|
|
public static $boop = "boop";
|
|
|
|
}
|
|
|
|
$beep = rand(0, 1) ? new Beep() : Beep::class;
|
|
|
|
echo $beep::$boop;
|
|
|
|
',
|
|
|
|
],
|
2021-03-27 02:21:38 +01:00
|
|
|
'ClassConstFetchWithTemplate' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
|
|
|
* @template T of object
|
|
|
|
* @psalm-param T $obj
|
|
|
|
* @return class-string<T>
|
|
|
|
*/
|
|
|
|
function a($obj) {
|
|
|
|
$class = $obj::class;
|
|
|
|
|
|
|
|
return $class;
|
2021-07-17 00:38:39 +02:00
|
|
|
}',
|
|
|
|
],
|
|
|
|
'classStringAllowsClasses' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
|
|
|
* @param class-string $s
|
|
|
|
*/
|
|
|
|
function takesOpen(string $s): void {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param class-string<Exception> $s
|
|
|
|
*/
|
|
|
|
function takesException(string $s): void {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param class-string<Exception> $s
|
|
|
|
*/
|
|
|
|
function takesThrowable(string $s): void {}
|
|
|
|
|
|
|
|
takesOpen(InvalidArgumentException::class);
|
|
|
|
takesException(InvalidArgumentException::class);
|
|
|
|
takesThrowable(InvalidArgumentException::class);',
|
|
|
|
],
|
|
|
|
'reflectionClassCoercion' => [
|
|
|
|
'<?php
|
|
|
|
/** @return ReflectionClass<object> */
|
|
|
|
function takesString(string $s) {
|
|
|
|
/** @psalm-suppress ArgumentTypeCoercion */
|
|
|
|
return new ReflectionClass($s);
|
|
|
|
}',
|
2021-03-27 02:21:38 +01:00
|
|
|
],
|
2018-08-21 20:47:28 +02:00
|
|
|
];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
2021-03-20 02:44:44 +01:00
|
|
|
* @return iterable<string,array{string,error_message:string,1?:string[],2?:bool,3?:string}>
|
2018-08-21 20:47:28 +02:00
|
|
|
*/
|
2020-09-12 17:24:05 +02:00
|
|
|
public function providerInvalidCodeParse(): iterable
|
2018-08-21 20:47:28 +02:00
|
|
|
{
|
|
|
|
return [
|
|
|
|
'arrayOfStringClasses' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
|
|
|
* @param array<class-string> $arr
|
|
|
|
*/
|
|
|
|
function takesClassConstants(array $arr) : void {}
|
|
|
|
|
|
|
|
class A {}
|
|
|
|
class B {}
|
|
|
|
|
|
|
|
takesClassConstants(["A", "B"]);',
|
2019-04-26 00:02:19 +02:00
|
|
|
'error_message' => 'ArgumentTypeCoercion',
|
2018-08-21 20:47:28 +02:00
|
|
|
],
|
|
|
|
'arrayOfNonExistentStringClasses' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
|
|
|
* @param array<class-string> $arr
|
|
|
|
*/
|
|
|
|
function takesClassConstants(array $arr) : void {}
|
2020-08-30 17:44:14 +02:00
|
|
|
/** @psalm-suppress ArgumentTypeCoercion */
|
2018-08-21 20:47:28 +02:00
|
|
|
takesClassConstants(["A", "B"]);',
|
|
|
|
'error_message' => 'UndefinedClass',
|
|
|
|
],
|
|
|
|
'singleClassConstantWithInvalidDocblock' => [
|
|
|
|
'<?php
|
|
|
|
/**
|
|
|
|
* @param clas-string $s
|
|
|
|
*/
|
|
|
|
function takesClassConstants(string $s) : void {}',
|
|
|
|
'error_message' => 'InvalidDocblock',
|
|
|
|
],
|
|
|
|
'returnClassConstantDisallowCoercion' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return class-string
|
|
|
|
*/
|
|
|
|
function takesClassConstants() : string {
|
|
|
|
return "A";
|
|
|
|
}',
|
|
|
|
'error_message' => 'LessSpecificReturnStatement',
|
|
|
|
],
|
|
|
|
'returnClassConstantArrayDisallowCoercion' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array<class-string>
|
|
|
|
*/
|
|
|
|
function takesClassConstants() : array {
|
|
|
|
return ["A", "B"];
|
|
|
|
}',
|
|
|
|
'error_message' => 'LessSpecificReturnStatement',
|
|
|
|
],
|
|
|
|
'returnClassConstantArrayAllowCoercionWithUndefinedClass' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return array<class-string>
|
|
|
|
*/
|
|
|
|
function takesClassConstants() : array {
|
|
|
|
return ["A", "B"];
|
|
|
|
}',
|
|
|
|
'error_message' => 'UndefinedClass',
|
|
|
|
'error_levels' => ['LessSpecificReturnStatement', 'MoreSpecificReturnType'],
|
|
|
|
],
|
2019-01-02 12:58:49 +01:00
|
|
|
'badClassStringConstructor' => [
|
|
|
|
'<?php
|
|
|
|
class Foo
|
|
|
|
{
|
|
|
|
public function __construct(int $_)
|
|
|
|
{
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return Foo
|
|
|
|
*/
|
|
|
|
function makeFoo()
|
|
|
|
{
|
|
|
|
$fooClass = Foo::class;
|
|
|
|
return new $fooClass;
|
|
|
|
}',
|
|
|
|
'error_message' => 'TooFewArguments',
|
|
|
|
],
|
|
|
|
'unknownConstructorCall' => [
|
|
|
|
'<?php
|
|
|
|
/** @param class-string $s */
|
|
|
|
function bar(string $s) : void {
|
|
|
|
new $s();
|
|
|
|
}',
|
|
|
|
'error_message' => 'MixedMethodCall',
|
|
|
|
],
|
2019-01-02 20:24:31 +01:00
|
|
|
'doesNotTakeChildOfClass' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
class AChild extends A {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param A::class $s
|
|
|
|
*/
|
|
|
|
function foo(string $s) : void {}
|
|
|
|
|
|
|
|
foo(AChild::class);',
|
|
|
|
'error_message' => 'InvalidArgument',
|
|
|
|
],
|
2019-01-05 21:12:42 +01:00
|
|
|
'createClassOfWrongTypeFromString' => [
|
|
|
|
'<?php
|
|
|
|
class A {}
|
|
|
|
class B {}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @return class-string<A> $s
|
|
|
|
*/
|
|
|
|
function foo(string $s) : string {
|
|
|
|
if (!class_exists($s)) {
|
|
|
|
throw new \UnexpectedValueException("bad");
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!is_a($s, B::class, true)) {
|
|
|
|
throw new \UnexpectedValueException("bad");
|
|
|
|
}
|
|
|
|
|
|
|
|
return $s;
|
|
|
|
}',
|
|
|
|
'error_message' => 'InvalidReturnStatement',
|
|
|
|
],
|
2018-08-21 20:47:28 +02:00
|
|
|
];
|
|
|
|
}
|
|
|
|
}
|