remember_property_assignments_after_call = false; $this->addFile( 'somefile.php', 'x = 5; $this->modifyX(); return $this->x; } private function modifyX(): void { $this->x = null; } }' ); $this->analyzeFile('somefile.php', new Context()); } /** * @return void */ public function testForgetPropertyAssignmentsInBranchWithThrow() { Config::getInstance()->remember_property_assignments_after_call = false; $this->addFile( 'somefile.php', 'x = 5; if (rand(0, 1)) { $this->modifyX(); throw new \Exception("bad"); } return $this->x; } private function modifyX(): void { $this->x = null; } }' ); $this->analyzeFile('somefile.php', new Context()); } /** * @return void */ public function testRemovePropertyAfterReassignment() { Config::getInstance()->remember_property_assignments_after_call = false; $this->addFile( 'somefile.php', 'parent = rand(0, 1) ? new A : null; } } $a = new A(); if ($a->parent === null) { throw new \Exception("bad"); } $a = $a->parent;' ); $context = new Context(); $this->analyzeFile('somefile.php', $context); $this->assertSame('A', (string) $context->vars_in_scope['$a']); $this->assertFalse(isset($context->vars_in_scope['$a->parent'])); } /** * @return void */ public function testRemoveClauseAfterReassignment() { Config::getInstance()->remember_property_assignments_after_call = false; $this->addFile( 'somefile.php', 'foo = false; $this->bar(); if ($this->foo === true) {} } private function bar(): void { if (mt_rand(0, 1)) { $this->foo = true; } } }' ); $context = new Context(); $this->analyzeFile('somefile.php', $context); } /** * @return array */ public function providerFileCheckerValidCodeParse() { return [ 'newVarInIf' => [ 'foo = []; } if (!is_array($this->foo)) { // do something } } }', ], 'propertyWithoutTypeSuppressingIssue' => [ 'foo;', 'assertions' => [], 'error_levels' => [ 'MissingPropertyType', 'MixedAssignment', ], ], 'propertyWithoutTypeSuppressingIssueAndAssertingNull' => [ 'foo === null && rand(0,1); echo $this->foo->baz; } }', 'assertions' => [], 'error_levels' => [ 'UndefinedThisPropertyFetch', 'MixedAssignment', 'MixedArgument', 'MixedMethodCall', 'MixedPropertyFetch', ], ], 'sharedPropertyInIf' => [ 'foo; }', 'assertions' => [ '$b' => 'null|int|string', ], ], 'sharedPropertyInElseIf' => [ 'foo; }', 'assertions' => [ '$b' => 'null|int|string', ], ], 'nullablePropertyCheck' => [ 'bb) && $b->bb->aa === "aa") { echo $b->bb->aa; }', ], 'nullablePropertyAfterGuard' => [ 'aa) { $a->aa = "hello"; } echo substr($a->aa, 1);', ], 'nullableStaticPropertyWithIfCheck' => [ ' [ 'name . " - " . $a->class;', ], 'grandparentReflectedProperties' => [ 'ownerDocument;', 'assertions' => [ '$owner' => 'DOMDocument', ], ], 'goodArrayProperties' => [ ' */ public $is = []; } $c = new C1; $c->is = [new A1]; $c->is = [new A1, new A1]; $c->is = [new A1, new B1];', 'assertions' => [], 'error_levels' => ['MixedAssignment'], ], 'issetPropertyDoesNotExist' => [ 'bar)) { }', ], 'notSetInConstructorButHasDefault' => [ ' [ 'foo(); } private function foo(): void { $this->a = 5; } }', ], 'definedInTraitSetInConstructor' => [ 'a = "hello"; } }', ], 'propertySetInNestedPrivateMethod' => [ 'foo(); } private function foo(): void { $this->bar(); } private function bar(): void { $this->a = 5; } }', ], 'propertyArrayIssetAssertion' => [ ' */ public $a = []; private function foo(): void { if (isset($this->a["hello"])) { bar($this->a["hello"]); } } }', ], 'propertyArrayIssetAssertionWithVariableOffset' => [ ' */ public $a = []; private function foo(): void { $b = "hello"; if (!isset($this->a[$b])) { return; } bar($this->a[$b]); } }', ], 'staticPropertyArrayIssetAssertionWithVariableOffset' => [ ' */ public static $a = []; } function foo(): void { $b = "hello"; if (!isset(A::$a[$b])) { return; } bar(A::$a[$b]); }', ], 'staticPropertyArrayIssetAssertionWithVariableOffsetAndElse' => [ ' */ public static $a = []; } function foo(): void { $b = "hello"; if (!isset(A::$a[$b])) { $g = "bar"; } else { bar(A::$a[$b]); $g = "foo"; } bar($g); }', ], 'traitConstructor' => [ 'foo = "hello"; } } class A { use T; }', ], 'abstractClassWithNoConstructor' => [ ' [ 'foo = ""; } } class B extends A { public function __construct() { parent::__construct(); } }', ], 'abstractClassConstructorAndImplicitChildConstructor' => [ 'foo = (string)$bar; } } class B extends A {} class E extends \Exception{}', ], 'notSetInEmptyConstructor' => [ ' [ 'aString = "aa"; echo($this->aString); } } class Descendant extends Base { /** * @var bool */ private $aBool; public function __construct() { parent::__construct(); $this->aBool = true; } }', ], 'extendsClassWithPrivateAndException' => [ 'p = $p; } } final class B extends A {}', ], 'setInAbstractMethod' => [ 'foo(); } } class B extends A { public function foo(): void { $this->bar = "hello"; } }', 'assertions' => [], 'error_levels' => [ 'PropertyNotSetInConstructor' => Config::REPORT_INFO, ], ], 'setInFinalMethod' => [ 'setOptions($opts); } /** * @param string[] $opts * @psalm-param array{a:string,b:string} $opts */ final public function setOptions(array $opts): void { $this->a = $opts["a"] ?? "defaultA"; $this->b = $opts["b"] ?? "defaultB"; } }', ], 'setInFinalClass' => [ 'setOptions($opts); } /** * @param string[] $opts * @psalm-param array{a:string,b:string} $opts */ public function setOptions(array $opts): void { $this->a = $opts["a"] ?? "defaultA"; $this->b = $opts["b"] ?? "defaultB"; } }', ], 'selfPropertyType' => [ 'next = new Node(); } } } $node = new Node(); $next = $node->next', 'assertions' => [ '$next' => 'Node|null', ], ], 'perPropertySuppress' => [ ' [ 'stmts = $stmts; } public function getSubNodeNames() : array { return array("stmts"); } public function getType() : string { return "Stmt_Finally"; } }', 'assertions' => [], 'error_levels' => [ 'MixedTypeCoercion', 'MissingReturnType', ], ], 'privatePropertyAccessible' => [ 'foo = $foo; } private function bar() : void {} } class B extends A { /** @var string */ private $foo; public function __construct(string $foo) { $this->foo = $foo; parent::__construct($foo); } }', ], 'privatePropertyAccessibleDifferentType' => [ 'foo = 5; } private function bar() : void {} } class B extends A { /** @var string */ private $foo; public function __construct(string $foo) { $this->foo = $foo; parent::__construct($foo); } }', ], 'privatePropertyAccessibleInTwoSubclasses' => [ 'prop = 1; } } class C extends A { /** * @var int */ private $prop; public function __construct() { parent::__construct(); $this->prop = 2; } }', ], 'noIssueWhenSuppressingMixedAssignmentForProperty' => [ 'foo = $a; } }', 'assertions' => [], 'error_levels' => [ 'MixedAssignment', ], ], 'propertyAssignmentToMixed' => [ 'foo = $a; }', 'assertions' => [], 'error_levels' => [ 'MixedAssignment', ], ], 'propertySetInBothIfBranches' => [ 'status = 1; } else { $this->status = $in; } } }', ], 'propertySetInPrivateMethodWithIfAndElse' => [ 'foo(); } else { $this->bar(); } } private function foo(): void { $this->a = 5; } private function bar(): void { $this->a = 5; } }', ], 'allowMixedAssignmetWhenDesired' => [ 'mixed = $value; } }', ], 'suppressUndefinedThisPropertyFetch' => [ 'bar = rand(0, 1) ? "hello" : null; } /** @psalm-suppress UndefinedThisPropertyFetch */ public function foo() : void { if ($this->bar === null && rand(0, 1)) {} } }', ], 'suppressUndefinedPropertyFetch' => [ 'bar = rand(0, 1) ? "hello" : null; } } $a = new A(); /** @psalm-suppress UndefinedPropertyFetch */ if ($a->bar === null && rand(0, 1)) {}', ], 'setPropertiesOfSpecialObjects' => [ 'b = "c"; $d = new SimpleXMLElement(""); $d->e = "f";', 'assertions' => [ '$a' => 'stdClass', '$a->b' => 'string', '$d' => 'SimpleXMLElement', '$d->e' => 'mixed', ], ], 'allowLessSpecificReturnTypeForOverriddenMethod' => [ ' [ ' [ 'bar(); echo self::$instance->bat; } } public function bar() : void {} } $a = new A(); if ($a->instance) { $a->instance->bar(); echo $a->instance->bat; }', ], 'nonStaticPropertyMethodCall' => [ 'instance) { $this->instance->bar(); echo $this->instance->bat; } } public function bar() : void {} } $a = new A(); if ($a->instance) { $a->instance->bar(); echo $a->instance->bat; }' ], 'staticPropertyOfStaticTypeMethodCall' => [ 'instance) { $this->instance->bar(); echo $this->instance->bat; } } public function bar() : void {} }' ], 'classStringPropertyType' => [ ' */ public $member = [ InvalidArgumentException::class => 1, ]; }' ], ]; } /** * @return array */ public function providerFileCheckerInvalidCodeParse() { return [ 'undefinedPropertyAssignment' => [ 'foo = "cool";', 'error_message' => 'UndefinedPropertyAssignment', ], 'undefinedPropertyFetch' => [ 'foo;', 'error_message' => 'UndefinedPropertyFetch', ], 'undefinedThisPropertyAssignment' => [ 'foo = "cool"; } }', 'error_message' => 'UndefinedThisPropertyAssignment', ], 'undefinedStaticPropertyAssignment' => [ ' 'UndefinedPropertyAssignment', ], 'undefinedThisPropertyFetch' => [ 'foo; } }', 'error_message' => 'UndefinedThisPropertyFetch', ], 'missingPropertyType' => [ 'foo = 5; } }', 'error_message' => 'MissingPropertyType - src' . DIRECTORY_SEPARATOR . 'somefile.php:3 - Property A::$foo does not have a ' . 'declared type - consider int|null', ], 'missingPropertyTypeWithConstructorInit' => [ 'foo = 5; } }', 'error_message' => 'MissingPropertyType - src' . DIRECTORY_SEPARATOR . 'somefile.php:3 - Property A::$foo does not have a ' . 'declared type - consider int', ], 'missingPropertyTypeWithConstructorInitAndNull' => [ 'foo = 5; } public function makeNull(): void { $this->foo = null; } }', 'error_message' => 'MissingPropertyType - src' . DIRECTORY_SEPARATOR . 'somefile.php:3 - Property A::$foo does not have a ' . 'declared type - consider int|null', ], 'missingPropertyTypeWithConstructorInitAndNullDefault' => [ 'foo = 5; } }', 'error_message' => 'MissingPropertyType - src' . DIRECTORY_SEPARATOR . 'somefile.php:3 - Property A::$foo does not have a ' . 'declared type - consider int|null', ], 'badAssignment' => [ 'foo = 5; } }', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'badStaticAssignment' => [ ' 'InvalidPropertyAssignmentValue', ], 'typeCoercion' => [ 'foo = $a; } } class B extends A {}', 'error_message' => 'TypeCoercion', ], 'mixedTypeCoercion' => [ ' */ public $foo = []; /** @param A[] $arr */ public function barBar(array $arr): void { $this->foo = $arr; } }', 'error_message' => 'MixedTypeCoercion', ], 'staticTypeCoercion' => [ ' 'TypeCoercion', ], 'staticMixedTypeCoercion' => [ ' */ public static $foo = []; /** @param A[] $arr */ public static function barBar(array $arr): void { self::$foo = $arr; } }', 'error_message' => 'MixedTypeCoercion', ], 'possiblyBadAssignment' => [ 'foo = rand(0, 1) ? 5 : "hello"; } }', 'error_message' => 'PossiblyInvalidPropertyAssignmentValue', ], 'possiblyBadStaticAssignment' => [ ' 'PossiblyInvalidPropertyAssignmentValue', ], 'badAssignmentAsWell' => [ 'foo = "bar";', 'error_message' => 'InvalidPropertyAssignment', ], 'badFetch' => [ 'foo;', 'error_message' => 'InvalidPropertyFetch', ], 'possiblyBadFetch' => [ ' 3 ? "hello" : new stdClass; echo $a->foo;', 'error_message' => 'PossiblyInvalidPropertyFetch', ], 'mixedPropertyFetch' => [ 'foo;', 'error_message' => 'MixedPropertyFetch', 'error_levels' => [ 'MissingPropertyType', 'MixedAssignment', ], ], 'mixedPropertyAssignment' => [ 'foo = "hello";', 'error_message' => 'MixedPropertyAssignment', 'error_levels' => [ 'MissingPropertyType', 'MixedAssignment', ], ], 'possiblyNullablePropertyAssignment' => [ 'foo = "hello";', 'error_message' => 'PossiblyNullPropertyAssignment', ], 'nullablePropertyAssignment' => [ 'foo = "hello";', 'error_message' => 'NullPropertyAssignment', ], 'possiblyNullablePropertyFetch' => [ 'foo;', 'error_message' => 'PossiblyNullPropertyFetch', ], 'nullablePropertyFetch' => [ 'foo;', 'error_message' => 'NullPropertyFetch', ], 'badArrayProperty' => [ ' */ public $bb; } $c = new C; $c->bb = [new A, new B];', 'error_message' => 'InvalidPropertyAssignmentValue', ], 'possiblyBadArrayProperty' => [ 'bb = ["hello", "world"];', 'error_message' => 'PossiblyInvalidPropertyAssignmentValue', ], 'notSetInEmptyConstructor' => [ ' 'PropertyNotSetInConstructor', ], 'noConstructor' => [ ' 'MissingConstructor', ], 'abstractClassInheritsNoConstructor' => [ ' 'MissingConstructor', ], 'abstractClassInheritsPrivateConstructor' => [ 'foo = "hello"; } } class B extends A {}', 'error_message' => 'InaccessibleMethod', ], 'classInheritsPrivateConstructorWithImplementedConstructor' => [ 'foo = "hello"; } } class B extends A { public function __construct() {} }', 'error_message' => 'PropertyNotSetInConstructor', ], 'notSetInAllBranchesOfIf' => [ 'a = 5; } } }', 'error_message' => 'PropertyNotSetInConstructor', ], 'propertySetInProtectedMethod' => [ 'foo(); } protected function foo(): void { $this->a = 5; } }', 'error_message' => 'PropertyNotSetInConstructor', ], 'definedInTraitNotSetInEmptyConstructor' => [ ' 'PropertyNotSetInConstructor', ], 'propertySetInPrivateMethodWithIf' => [ 'foo(); } } private function foo(): void { $this->a = 5; } }', 'error_message' => 'PropertyNotSetInConstructor', ], 'privatePropertySameNameNotSetInConstructor' => [ 'b = "foo"; } } class B extends A { /** @var string */ private $b; }', 'error_message' => 'PropertyNotSetInConstructor', ], 'privateMethodCalledInParentConstructor' => [ 'publicMethod(); } public function publicMethod() : void { $this->privateMethod(); } private function privateMethod() : void {} }', 'error_message' => 'PropertyNotSetInConstructor', ], 'privatePropertySetInParentConstructorReversedOrder' => [ 'b = "foo"; } } }', 'error_message' => 'PropertyNotSetInConstructor', ], 'privatePropertySetInParentConstructor' => [ 'b = "foo"; } } } class B extends A { /** @var string */ private $b; } ', 'error_message' => 'InaccessibleProperty', ], 'undefinedPropertyClass' => [ ' 'UndefinedClass', ], 'abstractClassWithNoConstructorButChild' => [ ' 'PropertyNotSetInConstructor', ], 'badAssignmentToUndefinedVars' => [ '$y = 4;', 'error_message' => 'UndefinedGlobalVariable', ], 'echoUndefinedPropertyFetch' => [ '$y;', 'error_message' => 'UndefinedGlobalVariable', ], 'toStringPropertyAssignment' => [ 'foo = new B;', 'error_message' => 'ImplicitToStringCast', ], 'noInfiniteLoop' => [ 'doThing(); } private function doThing(): void { if (rand(0, 1)) { $this->doOtherThing(); } } private function doOtherThing(): void { if (rand(0, 1)) { $this->doThing(); } } }', 'error_message' => 'PropertyNotSetInConstructor', ], 'invalidPropertyDefault' => [ ' 'InvalidPropertyAssignmentValue', ], 'prohibitMixedAssignmentNormally' => [ 'mixed = $value; } }', 'error_message' => 'MixedAssignment', ], 'assertPropertyTypeHasImpossibleType' => [ 'foo)) {}', 'error_message' => 'DocblockTypeContradiction', ], 'impossiblePropertyCheck' => [ 'bar = new Bar(); } public function getBar(): void { if (!$this->bar) {} } }', 'error_message' => 'DocblockTypeContradiction', ], 'staticPropertyOfStaticTypeMethodCallWithUndefinedMethod' => [ 'instance) { $this->instance->bar(); } } } class B extends A { public function bar() : void {} }', 'error_message' => 'UndefinedMethod', ], ]; } }