expectExceptionMessage('InvalidStringClass'); $this->expectException(\Psalm\Exception\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(\Psalm\Exception\CodeException::class); Config::getInstance()->allow_string_standin_for_class = false; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } /** * @return iterable,error_levels?:string[]}> */ public function providerValidCodeParse(): iterable { return [ 'arrayOfClassConstants' => [ ' $arr */ function takesClassConstants(array $arr) : void {} class A {} class B {} takesClassConstants([A::class, B::class]);', ], 'arrayOfStringClasses' => [ ' $arr */ function takesClassConstants(array $arr) : void {} class A {} class B {} takesClassConstants(["A", "B"]);', 'annotations' => [], 'error_levels' => ['ArgumentTypeCoercion'], ], 'singleClassConstantAsConstant' => [ ' [ ' [], ], 'returnClassConstant' => [ ' [ ' [], 'error_levels' => ['LessSpecificReturnStatement', 'MoreSpecificReturnType'], ], 'returnClassConstantArray' => [ ' */ function takesClassConstants() : array { return [A::class, B::class]; }', ], 'returnClassConstantArrayAllowCoercion' => [ ' */ function takesClassConstants() : array { return ["A", "B"]; }', 'annotations' => [], 'error_levels' => ['LessSpecificReturnStatement', 'MoreSpecificReturnType'], ], 'ifClassStringEquals' => [ ' [ ' [ ' [], 'error_levels' => ['MixedMethodCall'], ], 'constantArrayOffset' => [ ' "bar", ]; } class B {} /** @param class-string $s */ function bar(string $s) : void {} foreach (A::FOO as $class => $_) { bar($class); }', ], 'arrayEquivalence' => [ ' [ ' [ ' [ ' [ ' [ ' $s */ function foo(string $s) : void {} foo(AChild::class);', ], 'returnClassConstantClassStringParameterized' => [ ' $s */ function foo(A $a) : string { return $a::class; }', ], 'returnGetCalledClassClassStringParameterized' => [ ' $s */ function foo() : string { return get_called_class(); } }', ], 'returnGetClassClassStringParameterized' => [ ' $s */ function foo(A $a) : string { return get_class($a); }', ], 'returnGetParentClassClassStringParameterizedNoArg' => [ ' $s */ function foo() : string { return get_parent_class(); } }', ], 'createClassOfTypeFromString' => [ ' $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' => [ ' $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' => [ ' [ ' $className */ function foo($className) : void { $className::one(); $className::two(); }', ], 'implicitIntersectionClassString' => [ ' $className */ function foo(string $className) : void { $className::two(); if (is_subclass_of($className, Foo::class, true)) { $className::one(); $className::two(); } }', ], 'instanceofClassString' => [ ' [ ' $shouldBe * @return class-string */ function identity(string $shouldBe) : string { return $shouldBe; } identity(DateTimeImmutable::class)::createFromMutable(new DateTime());', ], 'filterIsObject' => [ '|DateTimeInterface $maybe * * @return interface-string */ function Foo($maybe) : string { if (is_object($maybe)) { return get_class($maybe); } return $maybe; }', ], 'filterIsString' => [ '|DateTimeInterface $maybe * * @return interface-string */ function Bar($maybe) : string { if (is_string($maybe)) { return $maybe; } return get_class($maybe); }', ], 'mergeLiteralClassStringsWithGeneric' => [ ' $literal_classes * @param array> $generic_classes * @return array> */ function foo(array $literal_classes, array $generic_classes) { return array_merge($literal_classes, $generic_classes); }', ], 'mergeGenericClassStringsWithLiteral' => [ ' $literal_classes * @param array> $generic_classes * @return array> */ function bar(array $literal_classes, array $generic_classes) { return array_merge($generic_classes, $literal_classes); }', ], 'noCrashWithIsSubclassOfNonExistentVariable' => [ ' [ ' [ ' [ ' B::class]; function foo(): bool { return self::CLASSES["foobar"] === static::class; } } class B extends A {}', ], 'noCrashWhenClassExists' => [ ' [ ' [ ' $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' => [ ' [ ' */ private static $c; /** * @return class-string */ public static function r() : string { return self::$c; } }' ], 'traitClassStringClone' => [ ' */ 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' => [ ' [ ' [ ' [ ' */ 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); }', ], ]; } /** * @return iterable */ public function providerInvalidCodeParse(): iterable { return [ 'arrayOfStringClasses' => [ ' $arr */ function takesClassConstants(array $arr) : void {} class A {} class B {} takesClassConstants(["A", "B"]);', 'error_message' => 'ArgumentTypeCoercion', ], 'arrayOfNonExistentStringClasses' => [ ' $arr */ function takesClassConstants(array $arr) : void {} /** @psalm-suppress ArgumentTypeCoercion */ takesClassConstants(["A", "B"]);', 'error_message' => 'UndefinedClass', ], 'singleClassConstantWithInvalidDocblock' => [ ' 'InvalidDocblock', ], 'returnClassConstantDisallowCoercion' => [ ' 'LessSpecificReturnStatement', ], 'returnClassConstantArrayDisallowCoercion' => [ ' */ function takesClassConstants() : array { return ["A", "B"]; }', 'error_message' => 'LessSpecificReturnStatement', ], 'returnClassConstantArrayAllowCoercionWithUndefinedClass' => [ ' */ function takesClassConstants() : array { return ["A", "B"]; }', 'error_message' => 'UndefinedClass', 'error_levels' => ['LessSpecificReturnStatement', 'MoreSpecificReturnType'], ], 'badClassStringConstructor' => [ ' 'TooFewArguments', ], 'unknownConstructorCall' => [ ' 'MixedMethodCall', ], 'doesNotTakeChildOfClass' => [ ' 'InvalidArgument', ], 'createClassOfWrongTypeFromString' => [ ' $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', ], ]; } }