expectExceptionMessage('InvalidStringClass'); $this->expectException(CodeException::class); Config::getInstance()->allow_string_standin_for_class = false; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } public function testDontAllowStringStandInForStaticMethodCall(): void { $this->expectExceptionMessage('InvalidStringClass'); $this->expectException(CodeException::class); Config::getInstance()->allow_string_standin_for_class = false; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } public function providerValidCodeParse(): iterable { return [ 'arrayOfClassConstants' => [ 'code' => ' $arr */ function takesClassConstants(array $arr) : void {} class A {} class B {} takesClassConstants([A::class, B::class]);', ], 'arrayOfStringClasses' => [ 'code' => ' $arr */ function takesClassConstants(array $arr) : void {} class A {} class B {} takesClassConstants(["A", "B"]);', 'assertions' => [], 'ignored_issues' => ['ArgumentTypeCoercion'], ], 'singleClassConstantAsConstant' => [ 'code' => ' [ 'code' => ' [], ], 'returnClassConstant' => [ 'code' => ' [ 'code' => ' [], 'ignored_issues' => ['LessSpecificReturnStatement', 'MoreSpecificReturnType'], ], 'returnClassConstantArray' => [ 'code' => ' */ function takesClassConstants() : array { return [A::class, B::class]; }', ], 'returnClassConstantArrayAllowCoercion' => [ 'code' => ' */ function takesClassConstants() : array { return ["A", "B"]; }', 'assertions' => [], 'ignored_issues' => ['LessSpecificReturnStatement', 'MoreSpecificReturnType'], ], 'ifClassStringEquals' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [], 'ignored_issues' => ['MixedMethodCall'], ], 'constantArrayOffset' => [ 'code' => ' "bar", ]; } class B {} /** @param class-string $s */ function bar(string $s) : void {} foreach (A::FOO as $class => $_) { bar($class); }', ], 'arrayEquivalence' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' $s */ function foo(string $s) : void {} foo(AChild::class);', ], 'returnClassConstantClassStringParameterized' => [ 'code' => ' $s */ function foo(A $a) : string { return $a::class; }', ], 'returnGetCalledClassClassStringParameterized' => [ 'code' => ' $s */ function foo() : string { return get_called_class(); } }', ], 'returnGetClassClassStringParameterized' => [ 'code' => ' $s */ function foo(A $a) : string { return get_class($a); }', ], 'returnGetParentClassClassStringParameterizedNoArg' => [ 'code' => ' $s */ function foo() : string { return get_parent_class(); } }', ], 'createClassOfTypeFromString' => [ 'code' => ' $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; }', ], 'createClassOfTypeFromStringUsingIsSubclassOf' => [ 'code' => ' $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' => [ 'code' => ' [ 'code' => ' $className */ function foo($className) : void { $className::one(); $className::two(); }', ], 'implicitIntersectionClassString' => [ 'code' => ' $className */ function foo(string $className) : void { $className::two(); if (is_subclass_of($className, Foo::class, true)) { $className::one(); $className::two(); } }', ], 'instanceofClassString' => [ 'code' => ' [ 'code' => ' $class */ private string $class = stdClass::class; public function go(object $object): ?stdClass { $a = $this->class; if ($object instanceof $a) { return $object; } return null; } }', ], 'returnTemplatedClassString' => [ 'code' => ' $shouldBe * @return class-string */ function identity(string $shouldBe) : string { return $shouldBe; } identity(DateTimeImmutable::class)::createFromMutable(new DateTime());', ], 'filterIsObject' => [ 'code' => '|DateTimeInterface $maybe * * @return interface-string */ function Foo($maybe) : string { if (is_object($maybe)) { return get_class($maybe); } return $maybe; }', ], 'filterIsString' => [ 'code' => '|DateTimeInterface $maybe * * @return interface-string */ function Bar($maybe) : string { if (is_string($maybe)) { return $maybe; } return get_class($maybe); }', ], 'mergeLiteralClassStringsWithGeneric' => [ 'code' => ' $literal_classes * @param array> $generic_classes * @return array> */ function foo(array $literal_classes, array $generic_classes) { return array_merge($literal_classes, $generic_classes); }', ], 'mergeGenericClassStringsWithLiteral' => [ 'code' => ' $literal_classes * @param array> $generic_classes * @return array> */ function bar(array $literal_classes, array $generic_classes) { return array_merge($generic_classes, $literal_classes); }', ], 'noCrashWithIsSubclassOfNonExistentVariable' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' B::class]; function foo(): bool { return self::CLASSES["foobar"] === static::class; } } class B extends A {}', ], 'noCrashWhenClassExists' => [ 'code' => ' [ 'code' => ' [ 'code' => ' $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 $className */ function takesExampleClassString(string $className): void {}', ], 'noCrashOnPolyfill' => [ 'code' => ' [ 'code' => ' */ private static $c; /** * @return class-string */ public static function r() : string { return self::$c; } }', ], 'traitClassStringClone' => [ 'code' => ' */ public static function getFactoryClass() { return static::class; } } /** * @psalm-consistent-constructor */ class A { use Factory; public static function factory(): self { $class = static::getFactoryClass(); return new $class; } } /** * @psalm-consistent-constructor */ class B { use Factory; public static function factory(): self { $class = static::getFactoryClass(); return new $class; } }', ], 'staticClassReturn' => [ 'code' => ' [ 'code' => ' [ 'code' => ' [ 'code' => ' */ function a($obj) { $class = $obj::class; return $class; }', ], 'classStringAllowsClasses' => [ 'code' => ' $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' => [ 'code' => ' */ function takesString(string $s) { /** @psalm-suppress ArgumentTypeCoercion */ return new ReflectionClass($s); }', ], 'checkDifferentSubclass' => [ 'code' => ' $s */ function takesAString(string $a): void {} /** @param class-string $s */ function takesBString(string $a): void {} /** @param class-string $s */ function foo(string $s): void { if (is_subclass_of($s, A::class)) { takesAString($s); } if (is_subclass_of($s, B::class)) { takesBString($s); } }', ], 'checkDifferentSubclassAfterNotClassExists' => [ 'code' => ' $s */ function takesAString(string $a): void {} /** @param class-string $s */ function takesBString(string $a): void {} function foo(string $s): void { if (!class_exists($s, false)) { return; } if (is_subclass_of($s, A::class)) { takesAString($s); } if (is_subclass_of($s, B::class)) { takesBString($s); } }', ], 'compareGetClassToLiteralClass' => [ 'code' => ' [ 'code' => '|class-string */ public ?string $bar = null; /** @var class-string */ public ?string $baz = null; } class TypeOne {} class TypeTwo {} $foo = new Foo; $foo->bar = TypeOne::class; $foo->bar = TypeOne::class; $foo->baz = TypeTwo::class; $foo->baz = TypeTwo::class;', ], ]; } public function providerInvalidCodeParse(): iterable { return [ 'arrayOfStringClasses' => [ 'code' => ' $arr */ function takesClassConstants(array $arr) : void {} class A {} class B {} takesClassConstants(["A", "B"]);', 'error_message' => 'ArgumentTypeCoercion', ], 'arrayOfNonExistentStringClasses' => [ 'code' => ' $arr */ function takesClassConstants(array $arr) : void {} /** @psalm-suppress ArgumentTypeCoercion */ takesClassConstants(["A", "B"]);', 'error_message' => 'UndefinedClass', ], 'singleClassConstantWithInvalidDocblock' => [ 'code' => ' 'InvalidDocblock', ], 'returnClassConstantDisallowCoercion' => [ 'code' => ' 'LessSpecificReturnStatement', ], 'returnClassConstantArrayDisallowCoercion' => [ 'code' => ' */ function takesClassConstants() : array { return ["A", "B"]; }', 'error_message' => 'LessSpecificReturnStatement', ], 'returnClassConstantArrayAllowCoercionWithUndefinedClass' => [ 'code' => ' */ function takesClassConstants() : array { return ["A", "B"]; }', 'error_message' => 'UndefinedClass', 'ignored_issues' => ['LessSpecificReturnStatement', 'MoreSpecificReturnType'], ], 'badClassStringConstructor' => [ 'code' => ' 'TooFewArguments', ], 'unknownConstructorCall' => [ 'code' => ' 'MixedMethodCall', ], 'doesNotTakeChildOfClass' => [ 'code' => ' 'InvalidArgument', ], 'createClassOfWrongTypeFromString' => [ 'code' => ' $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', ], ]; } }