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

Add support for intersection types

Fixes #140
This commit is contained in:
Matthew Brown 2017-04-15 20:36:40 -04:00
parent 2bdd062400
commit f67e92023b
4 changed files with 157 additions and 6 deletions

View File

@ -704,6 +704,7 @@ class CallChecker
}
$config = Config::getInstance();
$file_checker = $statements_checker->getFileChecker();
if ($class_type && is_string($stmt->name)) {
$return_type = null;
@ -751,6 +752,8 @@ class CallChecker
$fq_class_name = $class_type_part->value;
$intersection_types = $class_type_part->getIntersectionTypes();
$is_mock = ExpressionChecker::isMock($fq_class_name);
$has_mock = $has_mock || $is_mock;
@ -771,7 +774,7 @@ class CallChecker
} else {
$does_class_exist = ClassLikeChecker::checkFullyQualifiedClassLikeName(
$fq_class_name,
$statements_checker->getFileChecker(),
$file_checker,
new CodeLocation($statements_checker->getSource(), $stmt),
$statements_checker->getSuppressedIssues()
);
@ -787,6 +790,18 @@ class CallChecker
}
$method_id = $fq_class_name . '::' . strtolower($stmt->name);
if ($intersection_types && !MethodChecker::methodExists($method_id, $file_checker)) {
foreach ($intersection_types as $intersection_type) {
$method_id = $intersection_type->value . '::' . strtolower($stmt->name);
$fq_class_name = $intersection_type->value;
if (MethodChecker::methodExists($method_id, $file_checker)) {
break;
}
}
}
$cased_method_id = $fq_class_name . '::' . $stmt->name;
$does_method_exist = MethodChecker::checkMethodExists(

View File

@ -360,8 +360,36 @@ class TypeChecker
return $new_type;
}
if (!InterfaceChecker::interfaceExists($new_var_type, $file_checker) &&
$code_location &&
if (InterfaceChecker::interfaceExists($new_var_type, $file_checker)) {
$new_type_part = new TNamedObject($new_var_type);
$acceptable_atomic_types = [];
foreach ($existing_var_type->types as $existing_var_type_part) {
if (TypeChecker::isAtomicContainedBy(
$existing_var_type_part,
$new_type_part,
$file_checker,
$scalar_type_match_found,
$type_coerced,
$atomic_to_string_cast
)) {
$acceptable_atomic_types[] = $existing_var_type_part;
continue;
}
if ($existing_var_type_part instanceof TNamedObject &&
ClassChecker::classExists($existing_var_type_part->value, $file_checker)
) {
$existing_var_type_part->addIntersectionType($new_type_part);
$acceptable_atomic_types[] = $existing_var_type_part;
}
}
if ($acceptable_atomic_types) {
return new Type\Union($acceptable_atomic_types);
}
} elseif ($code_location &&
!$new_type->isMixed() &&
!$existing_var_type->from_docblock
) {
@ -476,8 +504,27 @@ class TypeChecker
$atomic_to_string_cast
);
if ($is_atomic_contained_by === true) {
if ($is_atomic_contained_by) {
$type_match_found = true;
} elseif (!$type_coerced &&
$input_type_part instanceof TNamedObject &&
$intersection_types = $input_type_part->getIntersectionTypes()
) {
foreach ($intersection_types as $intersection_type) {
$is_atomic_contained_by = self::isAtomicContainedBy(
$intersection_type,
$container_type_part,
$file_checker,
$scalar_type_match_found,
$type_coerced,
$atomic_to_string_cast
);
if ($is_atomic_contained_by) {
$type_match_found = true;
break;
}
}
}
if ($atomic_to_string_cast !== true) {

View File

@ -1,13 +1,21 @@
<?php
namespace Psalm\Type\Atomic;
class TNamedObject extends \Psalm\Type\Atomic
use \Psalm\Type\Atomic;
use \Psalm\Type\Union;
class TNamedObject extends Atomic
{
/**
* @var string
*/
public $value;
/**
* @var TNamedObject[]|null
*/
public $extra_types;
/**
* @param string $value the name of the object
*/
@ -48,4 +56,21 @@ class TNamedObject extends \Psalm\Type\Atomic
return '\\' . $this->value;
}
/**
* @param TNamedObject $type
* @return void
*/
public function addIntersectionType(TNamedObject $type)
{
$this->extra_types[] = $type;
}
/**
* @return TNamedObject[]|null
*/
public function getIntersectionTypes()
{
return $this->extra_types;
}
}

View File

@ -1068,7 +1068,7 @@ class TypeReconciliationTest extends PHPUnit_Framework_TestCase
$file_checker->visitAndAnalyzeMethods($context);
}
/**
/**
* @return void
*/
public function testTernaryByRefVarInConditional()
@ -1089,4 +1089,68 @@ class TypeReconciliationTest extends PHPUnit_Framework_TestCase
$context = new Context();
$file_checker->visitAndAnalyzeMethods($context);
}
/**
* @return void
*/
public function testPossibleInstanceof()
{
$this->markTestSkipped('Currently broken');
$stmts = self::$parser->parse('<?php
interface I1 {}
interface I2 {}
class A
{
public function foo() : void {
if ($this instanceof I1 || $this instanceof I2) {}
}
}
');
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
$context = new Context();
$file_checker->visitAndAnalyzeMethods($context);
}
/**
* @return void
*/
public function testIntersection()
{
$stmts = self::$parser->parse('<?php
interface I {
public function bat() : void;
}
function takesI(I $i) : void {}
function takesA(A $a) : void {}
class A {
public function foo() : void {
if ($this instanceof I) {
$this->bar();
$this->bat();
takesA($this);
takesI($this);
}
}
protected function bar() : void {}
}
class B extends A implements I {
public function bat() : void {}
}
');
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
$context = new Context();
$file_checker->visitAndAnalyzeMethods($context);
}
}