1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-30 04:39:00 +01:00
psalm/tests/MagicPropertyTest.php

1217 lines
43 KiB
PHP
Raw Normal View History

<?php
namespace Psalm\Tests;
use Psalm\Config;
use Psalm\Context;
use Psalm\Exception\CodeException;
2021-12-04 21:55:53 +01:00
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
2021-06-08 04:55:21 +02:00
use const DIRECTORY_SEPARATOR;
class MagicPropertyTest extends TestCase
{
2021-12-04 21:55:53 +01:00
use InvalidCodeAnalysisTestTrait;
use ValidCodeAnalysisTestTrait;
public function testPhpDocPropertyWithoutGet(): void
{
Config::getInstance()->use_phpdoc_property_without_magic_or_parent = true;
$this->addFile(
'somefile.php',
'<?php
/**
* @property string $hello
*/
class Child {}
$child = new Child();
2022-12-18 17:15:15 +01:00
$a = $child->hello;',
);
$this->analyzeFile('somefile.php', new Context());
}
public function providerValidCodeParse(): iterable
{
return [
'propertyDocblock' => [
'code' => '<?php
namespace Bar;
/**
* @property string $foo
*/
class A {
/** @param string $name */
public function __get($name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/**
* @param string $name
* @param mixed $value
*/
public function __set($name, $value): void {
}
}
$a = new A();
$a->foo = "hello";',
],
'propertyOfTypeClassDocblock' => [
'code' => '<?php
namespace Bar;
class PropertyType {}
/**
* @property PropertyType $foo
*/
class A {
/** @param string $name */
public function __get($name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/**
* @param string $name
* @param mixed $value
*/
public function __set($name, $value): void {
}
}
$a = new A();
$a->foo = new PropertyType();',
],
'propertySealedDocblockDefinedPropertyFetch' => [
'code' => '<?php
namespace Bar;
/**
* @property string $foo
* @psalm-seal-properties
*/
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
}
$a = new A();
echo $a->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' => '<?php
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
public function goodSet(): void {
$this->__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' => '<?php
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
public function goodGet(): void {
echo $this->__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' => '<?php
/**
* @property string $foo
*/
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
public function goodSet(): void {
$this->__set("foo", "value");
}
}',
],
'propertyDocblockAssignmentToMixed' => [
'code' => '<?php
/**
* @property string $foo
*/
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
}
/** @param mixed $b */
function foo($b) : void {
$a = new A();
$a->__set("foo", $b);
}',
'assertions' => [],
Add support for strict arrays, fix type alias intersection, fix array_is_list assertion on non-lists (#8395) * Immutable CodeLocation * Remove excess clones * Remove external clones * Remove leftover clones * Fix final clone issue * Immutable storages * Refactoring * Fixes * Fixes * Fix * Fix * Fixes * Simplify * Fixes * Fix * Fixes * Update * Fix * Cache global types * Fix * Update * Update * Fixes * Fixes * Refactor * Fixes * Fix * Fix * More caching * Fix * Fix * Update * Update * Fix * Fixes * Update * Refactor * Update * Fixes * Break one more test * Fix * FIx * Fix * Fix * Fix * Fix * Improve performance and readability * Equivalent logic * Fixes * Revert * Revert "Revert" This reverts commit f9175100c8452c80559234200663fd4c4f4dd889. * Fix * Fix reference bug * Make default TypeVisitor immutable * Bugfix * Remove clones * Partial refactoring * Refactoring * Fixes * Fix * Fixes * Fixes * cs-fix * Fix final bugs * Add test * Misc fixes * Update * Fixes * Experiment with removing different property * revert "Experiment with removing different property" This reverts commit ac1156e077fc4ea633530d51096d27b6e88bfdf9. * Uniform naming * Uniform naming * Hack hotfix * Clean up $_FILES ref #8621 * Undo hack, try fixing properly * Helper method * Remove redundant call * Partially fix bugs * Cleanup * Change defaults * Fix bug * Fix (?, hope this doesn't break anything else) * cs-fix * Review fixes * Bugfix * Bugfix * Improve logic * Add support for list{} and callable-list{} types, properly implement array_is_list assertions (fixes #8389) * Default to sealed arrays * Fix array_merge bug * Fixes * Fix * Sealed type checks * Properly infer properties-of and get_object_vars on final classes * Fix array_map zipping * Fix tests * Fixes * Fixes * Fix more stuff * Recursively resolve type aliases * Fix typo * Fixes * Fix array_is_list assertion on keyed array * Add BC docs * Fixes * fix * Update * Update * Update * Update * Seal arrays with count assertions * Fix #8528 * Fix * Update * Improve sealed array foreach logic * get_object_vars on template properties * Fix sealed array assertion reconciler logic * Improved reconciler * Add tests * Single source of truth for test types * Fix tests * Fixup tests * Fixup tests * Fixup tests * Update * Fix tests * Fix tests * Final fixes * Fixes * Use list syntax only when needed * Fix tests * Cs-fix * Update docs * Update docs * Update docs * Update docs * Update docs * Document missing types * Update docs * Improve class-string-map docs * Update * Update * I love working on psalm :) * Keep arrays unsealed by default * Fixup tests * Fix syntax mistake * cs-fix * Fix typo * Re-import missing types * Keep strict types only in return types * argc/argv fixes * argc/argv fixes * Fix test * Comment-out valinor code, pinging @romm pls merge https://github.com/CuyZ/Valinor/pull/246 so we can add valinor to the psalm docs :)
2022-11-05 22:34:42 +01:00
'ignored_issues' => ['MixedAssignment', 'MixedPropertyTypeCoercion'],
],
'namedPropertyByVariable' => [
'code' => '<?php
class A {
/** @var string|null */
public $foo;
public function __get(string $var_name) : ?string {
if ($var_name === "foo") {
return $this->$var_name;
}
return null;
}
}',
],
2018-05-09 04:01:05 +02:00
'getPropertyExplicitCall' => [
'code' => '<?php
2018-05-09 04:01:05 +02:00
class A {
public function __get(string $name) {}
/**
* @param mixed $value
*/
public function __set(string $name, $value) {}
}
/**
* @property string $test
*/
class B extends A {
public function test(): string {
return $this->__get("test");
}
}',
],
'inheritedGetPropertyExplicitCall' => [
'code' => '<?php
/**
* @property string $test
*/
class A {
public function __get(string $name) {}
/**
* @param mixed $value
*/
public function __set(string $name, $value) {}
}
class B extends A {
public function test(): string {
return $this->__get("test");
}
}',
],
'undefinedThisPropertyFetchWithMagic' => [
'code' => '<?php
/**
* @property-read string $name
* @property string $otherName
*/
class A {
public function __get(string $name): void {
}
public function goodGet(): void {
echo $this->name;
}
public function goodGet2(): void {
echo $this->otherName;
}
}
$a = new A();
echo $a->name;
echo $a->otherName;',
],
2020-03-24 23:32:57 +01:00
'psalmUndefinedThisPropertyFetchWithMagic' => [
'code' => '<?php
2020-03-24 23:32:57 +01:00
/**
* @psalm-property-read string $name
* @property string $otherName
*/
class A {
public function __get(string $name): void {
}
public function goodGet(): void {
echo $this->name;
}
public function goodGet2(): void {
echo $this->otherName;
}
}
$a = new A();
echo $a->name;
echo $a->otherName;',
],
'directFetchForMagicProperty' => [
'code' => '<?php
/**
* @property string $test
*/
class C {
public function __get(string $name)
{
}
/**
* @param mixed $value
*/
public function __set(string $name, $value)
{
}
public function test(): string
{
return $this->test;
}
}',
],
'magicPropertyFetchOnProtected' => [
'code' => '<?php
class C {
/** @var string */
protected $foo = "foo";
public function __get(string $name) {}
/**
* @param mixed $value
*/
public function __set(string $name, $value)
{
}
}
$c = new C();
$c->foo = "bar";
echo $c->foo;',
'assertions' => [],
Add support for strict arrays, fix type alias intersection, fix array_is_list assertion on non-lists (#8395) * Immutable CodeLocation * Remove excess clones * Remove external clones * Remove leftover clones * Fix final clone issue * Immutable storages * Refactoring * Fixes * Fixes * Fix * Fix * Fixes * Simplify * Fixes * Fix * Fixes * Update * Fix * Cache global types * Fix * Update * Update * Fixes * Fixes * Refactor * Fixes * Fix * Fix * More caching * Fix * Fix * Update * Update * Fix * Fixes * Update * Refactor * Update * Fixes * Break one more test * Fix * FIx * Fix * Fix * Fix * Fix * Improve performance and readability * Equivalent logic * Fixes * Revert * Revert "Revert" This reverts commit f9175100c8452c80559234200663fd4c4f4dd889. * Fix * Fix reference bug * Make default TypeVisitor immutable * Bugfix * Remove clones * Partial refactoring * Refactoring * Fixes * Fix * Fixes * Fixes * cs-fix * Fix final bugs * Add test * Misc fixes * Update * Fixes * Experiment with removing different property * revert "Experiment with removing different property" This reverts commit ac1156e077fc4ea633530d51096d27b6e88bfdf9. * Uniform naming * Uniform naming * Hack hotfix * Clean up $_FILES ref #8621 * Undo hack, try fixing properly * Helper method * Remove redundant call * Partially fix bugs * Cleanup * Change defaults * Fix bug * Fix (?, hope this doesn't break anything else) * cs-fix * Review fixes * Bugfix * Bugfix * Improve logic * Add support for list{} and callable-list{} types, properly implement array_is_list assertions (fixes #8389) * Default to sealed arrays * Fix array_merge bug * Fixes * Fix * Sealed type checks * Properly infer properties-of and get_object_vars on final classes * Fix array_map zipping * Fix tests * Fixes * Fixes * Fix more stuff * Recursively resolve type aliases * Fix typo * Fixes * Fix array_is_list assertion on keyed array * Add BC docs * Fixes * fix * Update * Update * Update * Update * Seal arrays with count assertions * Fix #8528 * Fix * Update * Improve sealed array foreach logic * get_object_vars on template properties * Fix sealed array assertion reconciler logic * Improved reconciler * Add tests * Single source of truth for test types * Fix tests * Fixup tests * Fixup tests * Fixup tests * Update * Fix tests * Fix tests * Final fixes * Fixes * Use list syntax only when needed * Fix tests * Cs-fix * Update docs * Update docs * Update docs * Update docs * Update docs * Document missing types * Update docs * Improve class-string-map docs * Update * Update * I love working on psalm :) * Keep arrays unsealed by default * Fixup tests * Fix syntax mistake * cs-fix * Fix typo * Re-import missing types * Keep strict types only in return types * argc/argv fixes * argc/argv fixes * Fix test * Comment-out valinor code, pinging @romm pls merge https://github.com/CuyZ/Valinor/pull/246 so we can add valinor to the psalm docs :)
2022-11-05 22:34:42 +01:00
'ignored_issues' => ['MixedArgument'],
],
'dontAssumeNonNullAfterPossibleMagicFetch' => [
'code' => '<?php
class C {
public function __get(string $name) : string {
return "hello";
}
}
function foo(?C $c) : void {
echo $c->foo;
if ($c) {}
}',
'assertions' => [],
Add support for strict arrays, fix type alias intersection, fix array_is_list assertion on non-lists (#8395) * Immutable CodeLocation * Remove excess clones * Remove external clones * Remove leftover clones * Fix final clone issue * Immutable storages * Refactoring * Fixes * Fixes * Fix * Fix * Fixes * Simplify * Fixes * Fix * Fixes * Update * Fix * Cache global types * Fix * Update * Update * Fixes * Fixes * Refactor * Fixes * Fix * Fix * More caching * Fix * Fix * Update * Update * Fix * Fixes * Update * Refactor * Update * Fixes * Break one more test * Fix * FIx * Fix * Fix * Fix * Fix * Improve performance and readability * Equivalent logic * Fixes * Revert * Revert "Revert" This reverts commit f9175100c8452c80559234200663fd4c4f4dd889. * Fix * Fix reference bug * Make default TypeVisitor immutable * Bugfix * Remove clones * Partial refactoring * Refactoring * Fixes * Fix * Fixes * Fixes * cs-fix * Fix final bugs * Add test * Misc fixes * Update * Fixes * Experiment with removing different property * revert "Experiment with removing different property" This reverts commit ac1156e077fc4ea633530d51096d27b6e88bfdf9. * Uniform naming * Uniform naming * Hack hotfix * Clean up $_FILES ref #8621 * Undo hack, try fixing properly * Helper method * Remove redundant call * Partially fix bugs * Cleanup * Change defaults * Fix bug * Fix (?, hope this doesn't break anything else) * cs-fix * Review fixes * Bugfix * Bugfix * Improve logic * Add support for list{} and callable-list{} types, properly implement array_is_list assertions (fixes #8389) * Default to sealed arrays * Fix array_merge bug * Fixes * Fix * Sealed type checks * Properly infer properties-of and get_object_vars on final classes * Fix array_map zipping * Fix tests * Fixes * Fixes * Fix more stuff * Recursively resolve type aliases * Fix typo * Fixes * Fix array_is_list assertion on keyed array * Add BC docs * Fixes * fix * Update * Update * Update * Update * Seal arrays with count assertions * Fix #8528 * Fix * Update * Improve sealed array foreach logic * get_object_vars on template properties * Fix sealed array assertion reconciler logic * Improved reconciler * Add tests * Single source of truth for test types * Fix tests * Fixup tests * Fixup tests * Fixup tests * Update * Fix tests * Fix tests * Final fixes * Fixes * Use list syntax only when needed * Fix tests * Cs-fix * Update docs * Update docs * Update docs * Update docs * Update docs * Document missing types * Update docs * Improve class-string-map docs * Update * Update * I love working on psalm :) * Keep arrays unsealed by default * Fixup tests * Fix syntax mistake * cs-fix * Fix typo * Re-import missing types * Keep strict types only in return types * argc/argv fixes * argc/argv fixes * Fix test * Comment-out valinor code, pinging @romm pls merge https://github.com/CuyZ/Valinor/pull/246 so we can add valinor to the psalm docs :)
2022-11-05 22:34:42 +01:00
'ignored_issues' => ['PossiblyNullPropertyFetch'],
],
'accessInMagicGet' => [
'code' => '<?php
class X {
public function __get(string $name) : string {
switch ($name) {
case "a":
return $this->other;
case "other":
return "foo";
}
return "default";
}
}',
'assertions' => [],
Add support for strict arrays, fix type alias intersection, fix array_is_list assertion on non-lists (#8395) * Immutable CodeLocation * Remove excess clones * Remove external clones * Remove leftover clones * Fix final clone issue * Immutable storages * Refactoring * Fixes * Fixes * Fix * Fix * Fixes * Simplify * Fixes * Fix * Fixes * Update * Fix * Cache global types * Fix * Update * Update * Fixes * Fixes * Refactor * Fixes * Fix * Fix * More caching * Fix * Fix * Update * Update * Fix * Fixes * Update * Refactor * Update * Fixes * Break one more test * Fix * FIx * Fix * Fix * Fix * Fix * Improve performance and readability * Equivalent logic * Fixes * Revert * Revert "Revert" This reverts commit f9175100c8452c80559234200663fd4c4f4dd889. * Fix * Fix reference bug * Make default TypeVisitor immutable * Bugfix * Remove clones * Partial refactoring * Refactoring * Fixes * Fix * Fixes * Fixes * cs-fix * Fix final bugs * Add test * Misc fixes * Update * Fixes * Experiment with removing different property * revert "Experiment with removing different property" This reverts commit ac1156e077fc4ea633530d51096d27b6e88bfdf9. * Uniform naming * Uniform naming * Hack hotfix * Clean up $_FILES ref #8621 * Undo hack, try fixing properly * Helper method * Remove redundant call * Partially fix bugs * Cleanup * Change defaults * Fix bug * Fix (?, hope this doesn't break anything else) * cs-fix * Review fixes * Bugfix * Bugfix * Improve logic * Add support for list{} and callable-list{} types, properly implement array_is_list assertions (fixes #8389) * Default to sealed arrays * Fix array_merge bug * Fixes * Fix * Sealed type checks * Properly infer properties-of and get_object_vars on final classes * Fix array_map zipping * Fix tests * Fixes * Fixes * Fix more stuff * Recursively resolve type aliases * Fix typo * Fixes * Fix array_is_list assertion on keyed array * Add BC docs * Fixes * fix * Update * Update * Update * Update * Seal arrays with count assertions * Fix #8528 * Fix * Update * Improve sealed array foreach logic * get_object_vars on template properties * Fix sealed array assertion reconciler logic * Improved reconciler * Add tests * Single source of truth for test types * Fix tests * Fixup tests * Fixup tests * Fixup tests * Update * Fix tests * Fix tests * Final fixes * Fixes * Use list syntax only when needed * Fix tests * Cs-fix * Update docs * Update docs * Update docs * Update docs * Update docs * Document missing types * Update docs * Improve class-string-map docs * Update * Update * I love working on psalm :) * Keep arrays unsealed by default * Fixup tests * Fix syntax mistake * cs-fix * Fix typo * Re-import missing types * Keep strict types only in return types * argc/argv fixes * argc/argv fixes * Fix test * Comment-out valinor code, pinging @romm pls merge https://github.com/CuyZ/Valinor/pull/246 so we can add valinor to the psalm docs :)
2022-11-05 22:34:42 +01:00
'ignored_issues' => ['MixedReturnStatement', 'MixedInferredReturnType'],
],
'overrideInheritedProperty' => [
'code' => '<?php
interface ServiceInterface {}
class ConcreteService implements ServiceInterface {
public function getById(int $i) : void {}
}
class Foo
{
/** @var ServiceInterface */
protected $service;
public function __construct(ServiceInterface $service)
{
$this->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);
}
2019-03-23 19:27:54 +01:00
}',
],
'magicInterfacePropertyRead' => [
'code' => '<?php
/**
* @property-read string $foo
* @psalm-seal-properties
*/
interface GetterSetter {
/** @return mixed */
public function __get(string $key);
/** @param mixed $value */
public function __set(string $key, $value) : void;
}
/** @psalm-suppress NoInterfaceProperties */
function getFoo(GetterSetter $o) : string {
return $o->foo;
}',
],
2020-03-24 23:32:57 +01:00
'phanMagicInterfacePropertyRead' => [
'code' => '<?php
2020-03-24 23:32:57 +01:00
/**
* @psalm-property-read string $foo
* @psalm-seal-properties
*/
interface GetterSetter {
/** @return mixed */
public function __get(string $key);
/** @param mixed $value */
public function __set(string $key, $value) : void;
}
/** @psalm-suppress NoInterfaceProperties */
function getFoo(GetterSetter $o) : string {
return $o->foo;
}',
],
'magicInterfacePropertyWrite' => [
'code' => '<?php
/**
* @property-write string $foo
* @psalm-seal-properties
*/
interface GetterSetter {
/** @return mixed */
public function __get(string $key);
/** @param mixed $value */
public function __set(string $key, $value) : void;
}
/** @psalm-suppress NoInterfaceProperties */
function getFoo(GetterSetter $o) : void {
$o->foo = "hello";
}',
],
2020-03-24 23:32:57 +01:00
'psalmMagicInterfacePropertyWrite' => [
'code' => '<?php
2020-03-24 23:32:57 +01:00
/**
* @psalm-property-write string $foo
* @psalm-seal-properties
*/
interface GetterSetter {
/** @return mixed */
public function __get(string $key);
/** @param mixed $value */
public function __set(string $key, $value) : void;
}
/** @psalm-suppress NoInterfaceProperties */
function getFoo(GetterSetter $o) : void {
$o->foo = "hello";
}',
],
'psalmPropertyDocblock' => [
'code' => '<?php
namespace Bar;
/**
* @psalm-property string $foo
*/
class A {
/** @param string $name */
public function __get($name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/**
* @param string $name
* @param mixed $value
*/
public function __set($name, $value): void {
}
}
$a = new A();
$a->foo = "hello";',
],
'overridePropertyAnnotations' => [
'code' => '<?php
namespace Bar;
/**
* @property int $foo
* @psalm-property string $foo
*/
class A {
/** @param string $name */
public function __get($name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/**
* @param string $name
* @param mixed $value
*/
public function __set($name, $value): void {
}
}
$a = new A();
$a->foo = "hello";',
],
'overrideWithReadWritePropertyAnnotations' => [
'code' => '<?php
namespace Bar;
/**
* @psalm-property int $foo
* @property-read string $foo
* @property-write array $foo
*/
class A {
/** @param string $name */
public function __get($name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/**
* @param string $name
* @param mixed $value
*/
public function __set($name, $value): void {
}
public function takesString(string $s): void {}
}
$a = new A();
$a->foo = [];
$a = new A();
$a->takesString($a->foo);',
],
'removeAssertionsAfterCall' => [
'code' => '<?php
class C {
/**
* @return mixed
*/
public function __get(string $name) {
return rand();
}
/**
* @param mixed $value
* @return void
*/
public function __set(string $name, $value) {}
public function main() : void {
if (!isset($this->a)) {
$this->a = ["something"];
/**
* @psalm-suppress MixedArrayAccess
* @psalm-suppress MixedArgument
*/
echo $this->a[0];
}
}
2022-12-18 17:15:15 +01:00
}',
],
'magicPropertyDefinedOnTrait' => [
'code' => '<?php
class UserRecord
{
use UserFields;
private array $props = [];
public function __get(string $field)
{
return $this->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;
2022-12-18 17:15:15 +01:00
$record->last_login_at = new DateTimeImmutable("now");',
],
'reconcileMagicProperties' => [
'code' => '<?php
/**
* @property string|null $a A
* @property string|null $b B
*/
class Foo
{
private array $props = [];
public function __construct() {
$this->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";
}
2022-12-18 17:15:15 +01:00
}',
],
'propertyReadIsExpanded' => [
'code' => '<?php
/** @property self::TYPE_* $type */
class A {
public const TYPE_A = 1;
public const TYPE_B = 2;
public function __get(string $_prop) {}
/** @param mixed $_value */
public function __set(string $_prop, $_value) {}
}
$a = (new A)->type;
',
'assertions' => [
'$a===' => '1|2',
],
],
'propertyWriteIsExpanded' => [
'code' => '<?php
/** @property self::TYPE_* $type */
class A {
public const TYPE_A = 1;
public const TYPE_B = 2;
public function __get(string $_prop) {}
/** @param mixed $_value */
public function __set(string $_prop, $_value) {}
}
$a = (new A);
$a->type = A::TYPE_B;
',
],
2022-11-24 19:31:53 +01:00
'impureMethodTest' => [
'code' => '<?php
/**
* @property array<string, string> $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<string, string> */
$errors = [];
$this->errors = $errors;
}
/** @return array<string, string> */
public function updateStatus(): array {
$_ = $this->errors;
$this->updateErrors();
$errors = $this->errors;
return $errors;
}
2022-12-18 17:15:15 +01:00
}',
],
];
}
public function providerInvalidCodeParse(): iterable
{
return [
'annotationWithoutGetter' => [
'code' => '<?php
/**
* @property bool $is_protected
*/
final class Page {
public function isProtected(): bool
{
return $this->is_protected;
}
}',
'error_message' => 'UndefinedThisPropertyFetch',
],
'propertyDocblockInvalidAssignment' => [
'code' => '<?php
/**
* @property string $foo
*/
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
}
$a = new A();
$a->foo = 5;',
'error_message' => 'InvalidPropertyAssignmentValue',
],
'propertyInvalidClassAssignment' => [
'code' => '<?php
namespace Bar;
class PropertyType {}
class SomeOtherPropertyType {}
/**
* @property PropertyType $foo
*/
class A {
/** @param string $name */
public function __get($name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/**
* @param string $name
* @param mixed $value
*/
public function __set($name, $value): void {
}
}
$a = new A();
$a->foo = new SomeOtherPropertyType();',
2019-02-27 22:00:44 +01:00
'error_message' => 'InvalidPropertyAssignmentValue - src' . DIRECTORY_SEPARATOR . 'somefile.php:29:31 - $a->foo with declared type'
. ' \'Bar\PropertyType\' cannot',
],
'propertyWriteDocblockInvalidAssignment' => [
'code' => '<?php
/**
* @property-write string $foo
*/
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
}
$a = new A();
$a->foo = 5;',
'error_message' => 'InvalidPropertyAssignmentValue',
],
2020-03-24 23:32:57 +01:00
'psalmPropertyWriteDocblockInvalidAssignment' => [
'code' => '<?php
2020-03-24 23:32:57 +01:00
/**
* @psalm-property-write string $foo
*/
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
}
$a = new A();
$a->foo = 5;',
'error_message' => 'InvalidPropertyAssignmentValue',
],
'propertySealedDocblockUndefinedPropertyAssignment' => [
'code' => '<?php
/**
* @property string $foo
* @psalm-seal-properties
*/
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
}
$a = new A();
$a->bar = 5;',
2020-01-06 03:58:18 +01:00
'error_message' => 'UndefinedMagicPropertyAssignment',
],
'propertySealedDocblockDefinedPropertyAssignment' => [
'code' => '<?php
/**
* @property string $foo
* @psalm-seal-properties
*/
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
}
$a = new A();
$a->foo = 5;',
'error_message' => 'InvalidPropertyAssignmentValue',
],
'propertyReadInvalidFetch' => [
'code' => '<?php
/**
* @property-read string $foo
*/
class A {
/** @return mixed */
public function __get(string $name) {
if ($name === "foo") {
return "hello";
}
}
}
$a = new A();
echo count($a->foo);',
'error_message' => 'InvalidArgument',
],
2020-03-24 23:32:57 +01:00
'psalmPropertyReadInvalidFetch' => [
'code' => '<?php
2020-03-24 23:32:57 +01:00
/**
* @psalm-property-read string $foo
*/
class A {
/** @return mixed */
public function __get(string $name) {
if ($name === "foo") {
return "hello";
}
}
}
$a = new A();
echo count($a->foo);',
'error_message' => 'InvalidArgument',
],
'propertySealedDocblockUndefinedPropertyFetch' => [
'code' => '<?php
/**
* @property string $foo
* @psalm-seal-properties
*/
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
}
$a = new A();
echo $a->bar;',
2020-01-06 03:58:18 +01:00
'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' => '<?php
/**
* @psalm-seal-properties
*/
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
public function badSet(): void {
$this->__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' => '<?php
/**
* @psalm-seal-properties
*/
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
public function badGet(): void {
$this->__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' => '<?php
/**
* @property string $foo
*/
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
public function badSet(): void {
$this->__set("foo", new stdClass());
}
}',
'error_message' => 'InvalidPropertyAssignmentValue',
],
'propertyDocblockAssignmentToMixed' => [
'code' => '<?php
/**
* @property string $foo
*/
class A {
public function __get(string $name): ?string {
if ($name === "foo") {
return "hello";
}
return null;
}
/** @param mixed $value */
public function __set(string $name, $value): void {
}
}
/** @param mixed $b */
function foo($b) : void {
$a = new A();
$a->__set("foo", $b);
}',
'error_message' => 'MixedPropertyTypeCoercion',
'ignored_issues' => ['MixedAssignment'],
],
'magicInterfacePropertyWrongProperty' => [
'code' => '<?php
/**
* @property-read string $foo
* @psalm-seal-properties
*/
interface GetterSetter {
/** @return mixed */
public function __get(string $key);
/** @param mixed $value */
public function __set(string $key, $value) : void;
}
/** @psalm-suppress NoInterfaceProperties */
function getBar(GetterSetter $o) : string {
return $o->bar;
}',
'error_message' => 'UndefinedMagicPropertyFetch',
],
2020-03-24 23:32:57 +01:00
'psalmMagicInterfacePropertyWrongProperty' => [
'code' => '<?php
2020-03-24 23:32:57 +01:00
/**
* @psalm-property-read string $foo
* @psalm-seal-properties
*/
interface GetterSetter {
/** @return mixed */
public function __get(string $key);
/** @param mixed $value */
public function __set(string $key, $value) : void;
}
/** @psalm-suppress NoInterfaceProperties */
function getBar(GetterSetter $o) : string {
return $o->bar;
}',
'error_message' => 'UndefinedMagicPropertyFetch',
],
'magicInterfaceWrongPropertyWrite' => [
'code' => '<?php
/**
* @property-write string $foo
* @psalm-seal-properties
*/
interface GetterSetter {
/** @return mixed */
public function __get(string $key);
/** @param mixed $value */
public function __set(string $key, $value) : void;
}
2020-03-24 23:32:57 +01:00
/** @psalm-suppress NoInterfaceProperties */
function getFoo(GetterSetter $o) : void {
$o->bar = "hello";
}',
'error_message' => 'UndefinedMagicPropertyAssignment',
],
'psalmMagicInterfaceWrongPropertyWrite' => [
'code' => '<?php
2020-03-24 23:32:57 +01:00
/**
* @psalm-property-write string $foo
* @psalm-seal-properties
*/
interface GetterSetter {
/** @return mixed */
public function __get(string $key);
/** @param mixed $value */
public function __set(string $key, $value) : void;
}
/** @psalm-suppress NoInterfaceProperties */
function getFoo(GetterSetter $o) : void {
$o->bar = "hello";
}',
'error_message' => 'UndefinedMagicPropertyAssignment',
],
'propertyDocblockOnProperty' => [
'code' => '<?php
2021-12-04 21:55:53 +01:00
class A {
/** @property string[] */
public array $arr;
}',
2022-12-18 17:15:15 +01:00
'error_message' => 'InvalidDocblock',
],
];
}
public function testSealAllMethodsWithoutFoo(): void
{
Config::getInstance()->seal_all_properties = true;
$this->addFile(
'somefile.php',
'<?php
class A {
public function __get(string $name) {}
}
class B extends A {}
$b = new B();
$result = $b->foo;
2022-12-18 17:15:15 +01:00
',
);
$error_message = 'UndefinedMagicPropertyFetch';
$this->expectException(CodeException::class);
$this->expectExceptionMessage($error_message);
$this->analyzeFile('somefile.php', new Context());
}
}