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 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)) {}', ], ]; } /** * @return array */ public function providerFileCheckerInvalidCodeParse() { return [ 'undefinedPropertyAssignment' => [ 'foo = "cool";', 'error_message' => 'UndefinedPropertyAssignment', ], 'undefinedPropertyFetch' => [ 'foo;', 'error_message' => 'UndefinedPropertyFetch', ], 'undefinedThisPropertyAssignment' => [ 'foo = "cool"; } }', 'error_message' => 'UndefinedThisPropertyAssignment', ], '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 null|int', ], '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 null|int', ], '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', ], 'possiblyBadAssignment' => [ 'foo = rand(0, 1) ? 5 : "hello"; } }', 'error_message' => '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', ], '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', ], ]; } }