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());' ], 'magicMethodInheritance' => [ 'foo()); consumeInt($b->bar());' ], 'magicMethodInheritanceOnInterface' => [ 'foo());' ], 'magicStaticMethodInheritance' => [ ' [ ' [ 'create([])); $d = new BlahModel(); consumeBlah($d->create([]));' ], 'returnThisShouldKeepGenerics' => [ ' $a */ $a = new A(); $b = $a->foo(); /** @var I $i */ $c = $i->foo();', [ '$b' => 'A', '$c' => 'I', ] ], 'genericsOfInheritedMethodsShouldBeResolved' => [ ' */ class A implements I { public function __call(string $name, array $args) {} } /** * @template E * @extends I */ interface I2 extends I {} class B {} /** * @template E * @method E get() */ class C { public function __call(string $name, array $args) {} } /** * @template E * @extends C */ class D extends C {} /** @var A $a */ $a = new A(); $b = $a->get(); /** @var I2 $i */ $c = $i->get(); /** @var D $d */ $d = new D(); $e = $d->get();', [ '$b' => 'B', '$c' => 'B', '$e' => 'B', ] ], ]; } /** * @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', ], 'magicParentCallShouldNotPolluteContext' => [ ' 'UndefinedVariable', ] ]; } 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()); } }