use_phpdoc_property_without_magic_or_parent = true; $this->addFile( 'somefile.php', 'hello;' ); $this->analyzeFile('somefile.php', new Context()); } public function providerValidCodeParse(): iterable { return [ 'propertyDocblock' => [ 'code' => 'foo = "hello";', ], 'propertyOfTypeClassDocblock' => [ 'code' => 'foo = new PropertyType();', ], 'propertySealedDocblockDefinedPropertyFetch' => [ 'code' => 'foo;', ], /** * With a magic setter and no annotations specifying properties or types, we can * set anything we want on any variable name. The magic setter is trusted to figure * it out. */ 'magicSetterUndefinedPropertyNoAnnotation' => [ 'code' => '__set("foo", new stdClass()); } }', ], /** * With a magic getter and no annotations specifying properties or types, we can * get anything we want with any variable name. The magic getter is trusted to figure * it out. */ 'magicGetterUndefinedPropertyNoAnnotation' => [ 'code' => '__get("foo"); } }', ], /** * The property $foo is defined as a string with the `@property` annotation. We * use the magic setter to set it to a string, so everything is cool. */ 'magicSetterValidAssignmentType' => [ 'code' => '__set("foo", "value"); } }', ], 'propertyDocblockAssignmentToMixed' => [ 'code' => '__set("foo", $b); }', 'assertions' => [], 'ignored_issues' => ['MixedAssignment', 'MixedPropertyTypeCoercion'], ], 'namedPropertyByVariable' => [ 'code' => '$var_name; } return null; } }', ], 'getPropertyExplicitCall' => [ 'code' => '__get("test"); } }', ], 'inheritedGetPropertyExplicitCall' => [ 'code' => '__get("test"); } }', ], 'undefinedThisPropertyFetchWithMagic' => [ 'code' => 'name; } public function goodGet2(): void { echo $this->otherName; } } $a = new A(); echo $a->name; echo $a->otherName;', ], 'psalmUndefinedThisPropertyFetchWithMagic' => [ 'code' => 'name; } public function goodGet2(): void { echo $this->otherName; } } $a = new A(); echo $a->name; echo $a->otherName;', ], 'directFetchForMagicProperty' => [ 'code' => 'test; } }', ], 'magicPropertyFetchOnProtected' => [ 'code' => 'foo = "bar"; echo $c->foo;', 'assertions' => [], 'ignored_issues' => ['MixedArgument'], ], 'dontAssumeNonNullAfterPossibleMagicFetch' => [ 'code' => 'foo; if ($c) {} }', 'assertions' => [], 'ignored_issues' => ['PossiblyNullPropertyFetch'], ], 'accessInMagicGet' => [ 'code' => 'other; case "other": return "foo"; } return "default"; } }', 'assertions' => [], 'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'], ], 'overrideInheritedProperty' => [ 'code' => 'service = $service; } } /** @property ConcreteService $service */ class FooBar extends Foo { public function __construct(ConcreteService $concreteService) { parent::__construct($concreteService); } public function doSomething(): void { $this->service->getById(123); } }', ], 'magicInterfacePropertyRead' => [ 'code' => 'foo; }', ], 'phanMagicInterfacePropertyRead' => [ 'code' => 'foo; }', ], 'magicInterfacePropertyWrite' => [ 'code' => 'foo = "hello"; }', ], 'psalmMagicInterfacePropertyWrite' => [ 'code' => 'foo = "hello"; }', ], 'psalmPropertyDocblock' => [ 'code' => 'foo = "hello";', ], 'overridePropertyAnnotations' => [ 'code' => 'foo = "hello";', ], 'overrideWithReadWritePropertyAnnotations' => [ 'code' => 'foo = []; $a = new A(); $a->takesString($a->foo);', ], 'removeAssertionsAfterCall' => [ 'code' => 'a)) { $this->a = ["something"]; /** * @psalm-suppress MixedArrayAccess * @psalm-suppress MixedArgument */ echo $this->a[0]; } } }' ], 'magicPropertyDefinedOnTrait' => [ 'code' => 'props[$field]; } /** * @param mixed $value */ public function __set(string $field, $value) : void { $this->props[$field] = $value; } } /** * @property mixed $email * @property mixed $password * @property mixed $last_login_at */ trait UserFields {} $record = new UserRecord(); $record->email; $record->password; $record->last_login_at = new DateTimeImmutable("now");' ], 'reconcileMagicProperties' => [ 'code' => 'props["a"] = "hello"; $this->props["b"] = "goodbye"; } /** * @psalm-mutation-free */ public function __get(string $prop){ return $this->props[$prop] ?? null; } /** @param mixed $b */ public function __set(string $a, $b){ $this->props[$a] = $b; } public function bar(): string { if (is_null($this->a) || is_null($this->b)) { } else { return $this->b; } return "hello"; } }' ], 'propertyReadIsExpanded' => [ 'code' => 'type; ', 'assertions' => [ '$a===' => '1|2', ], ], 'propertyWriteIsExpanded' => [ 'code' => 'type = A::TYPE_B; ', ], 'impureMethodTest' => [ 'code' => ' $errors * * @psalm-seal-properties */ final class OrganizationObject { public function __get(string $key) { return []; } /** * @param mixed $a */ public function __set(string $key, $a): void { } public function updateErrors(): void { /** @var array */ $errors = []; $this->errors = $errors; } /** @return array */ public function updateStatus(): array { $_ = $this->errors; $this->updateErrors(); $errors = $this->errors; return $errors; } }' ] ]; } public function providerInvalidCodeParse(): iterable { return [ 'annotationWithoutGetter' => [ 'code' => 'is_protected; } }', 'error_message' => 'UndefinedThisPropertyFetch', ], 'propertyDocblockInvalidAssignment' => [ 'code' => 'foo = 5;', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'propertyInvalidClassAssignment' => [ 'code' => 'foo = new SomeOtherPropertyType();', 'error_message' => 'InvalidPropertyAssignmentValue - src' . DIRECTORY_SEPARATOR . 'somefile.php:29:31 - $a->foo with declared type' . ' \'Bar\PropertyType\' cannot', ], 'propertyWriteDocblockInvalidAssignment' => [ 'code' => 'foo = 5;', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'psalmPropertyWriteDocblockInvalidAssignment' => [ 'code' => 'foo = 5;', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'propertySealedDocblockUndefinedPropertyAssignment' => [ 'code' => 'bar = 5;', 'error_message' => 'UndefinedMagicPropertyAssignment', ], 'propertySealedDocblockDefinedPropertyAssignment' => [ 'code' => 'foo = 5;', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'propertyReadInvalidFetch' => [ 'code' => 'foo);', 'error_message' => 'InvalidArgument', ], 'psalmPropertyReadInvalidFetch' => [ 'code' => 'foo);', 'error_message' => 'InvalidArgument', ], 'propertySealedDocblockUndefinedPropertyFetch' => [ 'code' => 'bar;', 'error_message' => 'UndefinedMagicPropertyFetch', ], /** * The property $foo is not defined on the object, but accessed with the magic setter. * This is an error because `@psalm-seal-properties` is specified on the class block. */ 'magicSetterUndefinedProperty' => [ 'code' => '__set("foo", "value"); } }', 'error_message' => 'UndefinedThisPropertyAssignment', ], /** * The property $foo is not defined on the object, but accessed with the magic getter. * This is an error because `@psalm-seal-properties` is specified on the class block. */ 'magicGetterUndefinedProperty' => [ 'code' => '__get("foo"); } }', 'error_message' => 'UndefinedThisPropertyFetch', ], /** * The property $foo is defined as a string with the `@property` annotation, but * the magic setter is used to set it to an object. */ 'magicSetterInvalidAssignmentType' => [ 'code' => '__set("foo", new stdClass()); } }', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'propertyDocblockAssignmentToMixed' => [ 'code' => '__set("foo", $b); }', 'error_message' => 'MixedPropertyTypeCoercion', 'ignored_issues' => ['MixedAssignment'], ], 'magicInterfacePropertyWrongProperty' => [ 'code' => 'bar; }', 'error_message' => 'UndefinedMagicPropertyFetch', ], 'psalmMagicInterfacePropertyWrongProperty' => [ 'code' => 'bar; }', 'error_message' => 'UndefinedMagicPropertyFetch', ], 'magicInterfaceWrongPropertyWrite' => [ 'code' => 'bar = "hello"; }', 'error_message' => 'UndefinedMagicPropertyAssignment', ], 'psalmMagicInterfaceWrongPropertyWrite' => [ 'code' => 'bar = "hello"; }', 'error_message' => 'UndefinedMagicPropertyAssignment', ], 'propertyDocblockOnProperty' => [ 'code' => ' 'InvalidDocblock' ], ]; } public function testSealAllMethodsWithoutFoo(): void { Config::getInstance()->seal_all_properties = true; $this->addFile( 'somefile.php', 'foo; ' ); $error_message = 'UndefinedMagicPropertyFetch'; $this->expectException(CodeException::class); $this->expectExceptionMessage($error_message); $this->analyzeFile('somefile.php', new Context()); } }