use_phpdoc_method_without_magic_or_parent = true; $this->addFile( 'somefile.php', 'getString(); $child->setInteger(4); /** @psalm-suppress MixedAssignment */ $b = $child->setString(5); $c = $child->getBool("hello"); $d = $child->getArray(); $e = $child->getCallable();' ); $this->analyzeFile('somefile.php', new Context()); } public function testPhpDocMethodWhenTemplated(): void { Config::getInstance()->use_phpdoc_method_without_magic_or_parent = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', new Context()); } public function testAnnotationWithoutCallConfig(): void { $this->expectExceptionMessage('UndefinedMethod'); $this->expectException(CodeException::class); Config::getInstance()->use_phpdoc_method_without_magic_or_parent = false; $this->addFile( 'somefile.php', 'getString();' ); $context = new Context(); $this->analyzeFile('somefile.php', $context); } public function testOverrideParentClassRetunType(): void { Config::getInstance()->use_phpdoc_method_without_magic_or_parent = true; $this->addFile( 'somefile.php', 'analyzeFile('somefile.php', $context); $this->assertSame('Child', (string) $context->vars_in_scope['$child']); } public function testOverrideExceptionMethodReturn(): void { Config::getInstance()->use_phpdoc_method_without_magic_or_parent = true; $this->addFile( 'somefile.php', 'getCode(); }' ); $context = new Context(); $this->analyzeFile('somefile.php', $context); } /** * @return iterable,error_levels?:string[]}> */ public function providerValidCodeParse(): iterable { return [ 'validSimpleAnnotations' => [ 'getString(); $child->setInteger(4); /** @psalm-suppress MixedAssignment */ $b = $child->setString(5); $c = $child->getBool("hello"); $d = $child->getArray(); $e = $child->getCallable(); $child->setMixed("hello"); $child->setMixed(4); $child->setImplicitMixed("hello"); $child->setImplicitMixed(4);', 'assertions' => [ '$a' => 'string', '$b' => 'mixed', '$c' => 'bool', '$d' => 'array', '$e' => 'callable():string', ], ], 'validAnnotationWithDefault' => [ 'setArray(["boo"]); $child->setArray(["boo"], 8);', ], 'validAnnotationWithByRefParam' => [ 'configure("foo", $array);', ], 'validAnnotationWithNonEmptyDefaultArray' => [ 'setArray(["boo"]); $child->setArray(["boo"]);', ], 'validAnnotationWithNonEmptyDefaultOldStyleArray' => [ 'setArray(["boo"]); $child->setArray(["boo"]);', ], 'validStaticAnnotationWithDefault' => [ ' [ '$a' => 'string', ], ], 'validAnnotationWithVariadic' => [ 'setInts(1, 2, 3, 4);', ], 'validUnionAnnotations' => [ 'setBool("hello", true); $c = $child->setBool("hello", "true"); $child->setAnotherArray(["boo"]);', 'assertions' => [ '$b' => 'bool', '$c' => 'bool', ], ], 'namespacedValidAnnotations' => [ 'setBool("hello", true); $c = $child->setBool("hello", "true");', ], 'globalMethod' => [ ' [ 'work()); } }', ], 'magicMethodOverridesParentWithMoreSpecificType' => [ ' [ 'hasMany("User", ["id" => "user_id"]) ->viaTable("account_to_user", ["account_id" => "id"]); return $query; } }', ], 'magicMethodReturnSelf' => [ 'getThis();', [ '$a' => 'C', '$b' => 'C', ], ], 'allowMagicMethodStatic' => [ 'getStatic(); $d = (new D)->getStatic();', [ '$c' => 'C', '$d' => 'D', ], ], 'validSimplePsalmAnnotations' => [ 'getString(); $child->setInteger(4);', 'assertions' => [ '$a' => 'string', ], ], 'overrideMethodAnnotations' => [ 'getString(); $child->setInteger(4);', 'assertions' => [ '$a' => 'string', ], ], 'alwaysAllowAnnotationOnInterface' => [ 'sayHello();', ], 'inheritInterfacePseudoMethodsFromParent' => [ 'getClassMetadata()); test(concreteEm()->getClassMetadata()); test2(em()->getOtherMetadata()); test2(concreteEm()->getOtherMetadata());', ], 'fullyQualifiedParam' => [ 'setInteger(function() : void {}); }', ], 'allowMethodsNamedBooleanAndInteger' => [ 'boolean(5); $child->integer(5);' ], 'overrideWithSelfBeforeMethodName' => [ ' [ ' [ 'foo();' ], 'allowFinalOverrider' => [ ' [ ' getAll():\IteratorAggregate */ class Foo { private \IteratorAggregate $items; /** * @psalm-suppress MixedReturnTypeCoercion */ public function getAll(): \IteratorAggregate { return $this->items; } public function __construct(\IteratorAggregate $foos) { $this->items = $foos; } } /** * @psalm-suppress MixedReturnTypeCoercion * @method \IteratorAggregate getAll():\IteratorAggregate */ class Bar { private \IteratorAggregate $items; /** * @psalm-suppress MixedReturnTypeCoercion */ public function getAll(): \IteratorAggregate { return $this->items; } public function __construct(\IteratorAggregate $foos) { $this->items = $foos; } }' ], 'parseFloatInDefault' => [ 'randomInt(); }' ], 'negativeInDefault' => [ 'foo();' ], 'namespacedNegativeInDefault' => [ 'foo(); }' ], 'namespacedUnion' => [ 'bar(new \DateTime(), new Cache());' ], ]; } /** * @return iterable */ public function providerInvalidCodeParse(): iterable { return [ 'annotationWithBadDocblock' => [ ' 'InvalidDocblock', ], 'annotationWithByRefParam' => [ ' 'InvalidDocblock', ], 'annotationWithSealed' => [ 'getString(); $child->foo();', 'error_message' => 'UndefinedMagicMethod - src' . DIRECTORY_SEPARATOR . 'somefile.php:13:29 - Magic method Child::foo does not exist', ], 'annotationInvalidArg' => [ 'setString("five");', 'error_message' => 'InvalidScalarArgument', ], 'unionAnnotationInvalidArg' => [ 'setBool("hello", 5);', 'error_message' => 'InvalidScalarArgument', ], 'validAnnotationWithInvalidVariadicCall' => [ 'setInts([1, 2, 3]);', 'error_message' => 'InvalidArgument', ], 'magicMethodOverridesParentWithDifferentReturnType' => [ ' 'ImplementedReturnTypeMismatch - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:33', ], 'magicMethodOverridesParentWithDifferentParamType' => [ ' 'ImplementedParamTypeMismatch - src' . DIRECTORY_SEPARATOR . 'somefile.php:11:33', ], 'parseBadMethodAnnotation' => [ ' 'InvalidDocblock', ], 'methodwithDash' => [ ' 'InvalidDocblock', ], 'methodWithAmpersandAndSpace' => [ ' 'InvalidDocblock', ], 'inheritSealedMethods' => [ 'foo();', 'error_message' => 'UndefinedMagicMethod', ], 'lonelyMethod' => [ ' 'InvalidDocblock', ], ]; } public function testSealAllMethodsWithoutFoo(): void { Config::getInstance()->seal_all_methods = true; $this->addFile( 'somefile.php', 'foo(); ' ); $error_message = 'UndefinedMagicMethod'; $this->expectException(CodeException::class); $this->expectExceptionMessage($error_message); $this->analyzeFile('somefile.php', new Context()); } public function testSealAllMethodsWithFoo(): void { Config::getInstance()->seal_all_methods = true; $this->addFile( 'somefile.php', 'foo(); ' ); $this->analyzeFile('somefile.php', new Context()); } public function testSealAllMethodsWithFooInSubclass(): void { Config::getInstance()->seal_all_methods = true; $this->addFile( 'somefile.php', 'foo(); ' ); $this->analyzeFile('somefile.php', new Context()); } public function testSealAllMethodsWithFooAnnotated(): void { Config::getInstance()->seal_all_methods = true; $this->addFile( 'somefile.php', 'foo(); ' ); $this->analyzeFile('somefile.php', new Context()); } public function testSealAllMethodsSetToFalse(): void { Config::getInstance()->seal_all_methods = false; $this->addFile( 'somefile.php', 'foo(); ' ); $this->analyzeFile('somefile.php', new Context()); } public function testIntersectionTypeWhenMagicMethodDoesNotExistButIsProvidedBySecondType(): void { $this->addFile( 'somefile.php', 'otherMethod(); ' ); $this->analyzeFile('somefile.php', new Context()); } public function testIntersectionTypeWhenMethodDoesNotExistOnEither(): void { $this->addFile( 'somefile.php', 'nonExistantMethod(); ' ); $error_message = 'UndefinedMagicMethod'; $this->expectException(CodeException::class); $this->expectExceptionMessage($error_message); $this->analyzeFile('somefile.php', new Context()); } }