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

Allow plain assertions (@psalm-assert) about $this (fixes #3105) (#3108)

* 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
This commit is contained in:
m0003r 2020-04-09 15:15:07 +03:00 committed by GitHub
parent 2a7be233bb
commit 4d1be3f0c4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 210 additions and 1 deletions

View File

@ -21,8 +21,9 @@ use Psalm\Issue\UndefinedMethod;
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
@ -166,6 +167,7 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
$result = new AtomicMethodCallAnalysisResult();
$possible_new_class_types = [];
foreach ($lhs_types as $lhs_type_part) {
AtomicMethodCallAnalyzer::analyze(
$statements_analyzer,
@ -181,6 +183,23 @@ class MethodCallAnalyzer extends \Psalm\Internal\Analyzer\Statements\Expression\
$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) {

View File

@ -3670,6 +3670,8 @@ class CallAnalyzer
}
} 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);
}

View File

@ -412,6 +412,30 @@ class AssertAnnotationTest extends TestCase
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 {
@ -433,6 +457,145 @@ class AssertAnnotationTest extends TestCase
}
}'
],
'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 {
@ -1116,6 +1279,31 @@ class AssertAnnotationTest extends TestCase
}',
'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',
],
];
}
}