Skip to content

Commit

Permalink
Allow plain assertions (@psalm-assert) about $this (fixes #3105) (#3108)
Browse files Browse the repository at this point in the history
* Allow plain assertions (@psalm-assert) about $this (fixes #3105)

* Fix multiple assertion combining

* Fix multiple assertion combining for $this again

* Add test for multiple assertion combining for $this again
  • Loading branch information
m0003r committed Apr 9, 2020
1 parent 2a7be23 commit 4d1be3f
Show file tree
Hide file tree
Showing 3 changed files with 210 additions and 1 deletion.
Expand Up @@ -21,8 +21,9 @@
use Psalm\IssueBuffer;
use Psalm\Type;
use Psalm\Type\Atomic\TNamedObject;
use function is_string;
use function count;
use function is_string;
use function array_reduce;

/**
* @internal
Expand Down Expand Up @@ -166,6 +167,7 @@ public static function analyze(

$result = new AtomicMethodCallAnalysisResult();

$possible_new_class_types = [];
foreach ($lhs_types as $lhs_type_part) {
AtomicMethodCallAnalyzer::analyze(
$statements_analyzer,
Expand All @@ -181,6 +183,23 @@ public static function analyze(
$lhs_var_id,
$result
);
if (isset($context->vars_in_scope[$lhs_var_id])
&& ($possible_new_class_type = $context->vars_in_scope[$lhs_var_id]) instanceof Type\Union
&& !$possible_new_class_type->equals($class_type)) {
$possible_new_class_types[] = $context->vars_in_scope[$lhs_var_id];
}
}

if (count($possible_new_class_types) > 0) {
$class_type = array_reduce(
$possible_new_class_types,
function (?Type\Union $type_1, Type\Union $type_2) use ($codebase): Type\Union {
if ($type_1 === null) {
return $type_2;
}
return Type::combineUnionTypes($type_1, $type_2, $codebase);
}
);
}

if ($result->invalid_method_call_types) {
Expand Down
Expand Up @@ -3670,6 +3670,8 @@ protected static function applyAssertionsToContext(
}
} elseif (isset($context->vars_in_scope[$assertion->var_id])) {
$assertion_var_id = $assertion->var_id;
} elseif ($assertion->var_id === '$this' && !is_null($thisName)) {
$assertion_var_id = $thisName;
} elseif (strpos($assertion->var_id, '$this->') === 0 && !is_null($thisName)) {
$assertion_var_id = $thisName . str_replace('$this->', '->', $assertion->var_id);
}
Expand Down
188 changes: 188 additions & 0 deletions tests/AssertAnnotationTest.php
Expand Up @@ -412,6 +412,30 @@ function assertIntOrFoo($b) : void {
if (!is_int($a)) $a->bar();',
],
'assertThisType' => [
'<?php
class Type {
/**
* @psalm-assert FooType $this
*/
public function isFoo() : bool {
if (!$this instanceof FooType) {
throw new \Exception();
}
return true;
}
}
class FooType extends Type {
public function bar(): void {}
}
function takesType(Type $t) : void {
$t->isFoo();
$t->bar();
}'
],
'assertThisTypeIfTrue' => [
'<?php
class Type {
Expand All @@ -433,6 +457,145 @@ function takesType(Type $t) : void {
}
}'
],
'assertThisTypeCombined' => [
'<?php
class Type {
/**
* @psalm-assert FooType $this
*/
public function assertFoo() : void {
if (!$this instanceof FooType) {
throw new \Exception();
}
}
/**
* @psalm-assert BarType $this
*/
public function assertBar() : void {
if (!$this instanceof BarType) {
throw new \Exception();
}
}
}
interface FooType {
public function foo(): void;
}
interface BarType {
public function bar(): void;
}
function takesType(Type $t) : void {
$t->assertFoo();
$t->assertBar();
$t->foo();
$t->bar();
}'
],
'assertThisTypeSimpleCombined' => [
'<?php
class Type {
/**
* @psalm-assert FooType $this
*/
public function assertFoo() : void {
if (!$this instanceof FooType) {
throw new \Exception();
}
return;
}
/**
* @psalm-assert BarType $this
*/
public function assertBar() : void {
if (!$this instanceof BarType) {
throw new \Exception();
}
return;
}
}
interface FooType {
public function foo(): void;
}
interface BarType {
public function bar(): void;
}
/** @param Type&FooType $t */
function takesType(Type $t) : void {
$t->assertBar();
$t->foo();
$t->bar();
}'
],
'assertThisTypeIfTrueCombined' => [
'<?php
class Type {
/**
* @psalm-assert-if-true FooType $this
*/
public function assertFoo() : bool {
return $this instanceof FooType;
}
/**
* @psalm-assert-if-true BarType $this
*/
public function assertBar() : bool {
return $this instanceof BarType;
}
}
interface FooType {
public function foo(): void;
}
interface BarType {
public function bar(): void;
}
function takesType(Type $t) : void {
if ($t->assertFoo() && $t->assertBar()) {
$t->foo();
$t->bar();
}
}'
],
'assertThisTypeSimpleAndIfTrueCombined' => [
'<?php
class Type {
/**
* @psalm-assert BarType $this
* @psalm-assert-if-true FooType $this
*/
public function isFoo() : bool {
if (!$this instanceof BarType) {
throw new \Exception();
}
return $this instanceof FooType;
}
}
interface FooType {
public function foo(): void;
}
interface BarType {
public function bar(): void;
}
function takesType(Type $t) : void {
if ($t->isFoo()) {
$t->foo();
}
$t->bar();
}'
],
'assertThisTypeSwitchTrue' => [
'<?php
class Type {
Expand Down Expand Up @@ -1116,6 +1279,31 @@ function takesString(string $s) : void {
}',
'error_message' => 'DocblockTypeContradiction',
],
'assertThisType' => [
'<?php
class Type {
/**
* @psalm-assert FooType $this
*/
public function isFoo() : bool {
if (!$this instanceof FooType) {
throw new \Exception();
}
return true;
}
}
class FooType extends Type {
public function bar(): void {}
}
function takesType(Type $t) : void {
$t->bar();
$t->isFoo();
}',
'error_message' => 'UndefinedMethod',
],
];
}
}

0 comments on commit 4d1be3f

Please sign in to comment.