mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Support int offsets in dealing with type inference
This commit is contained in:
parent
327e3150a2
commit
73b1ab1411
@ -510,7 +510,11 @@ class AssignmentChecker
|
||||
|
||||
$class_property_type = Type::getMixed();
|
||||
} else {
|
||||
$class_property_type = ExpressionChecker::fleshOutTypes($class_property_type, [], $lhs_type_part->value);
|
||||
$class_property_type = ExpressionChecker::fleshOutTypes(
|
||||
clone $class_property_type,
|
||||
[],
|
||||
$lhs_type_part->value
|
||||
);
|
||||
}
|
||||
|
||||
$class_property_types[] = $class_property_type;
|
||||
|
@ -660,9 +660,12 @@ class FetchChecker
|
||||
$statements_checker->getAliasedClasses()
|
||||
);
|
||||
|
||||
$keyed_array_var_id = $array_var_id && $stmt->dim instanceof PhpParser\Node\Scalar\String_
|
||||
? $array_var_id . '[\'' . $stmt->dim->value . '\']'
|
||||
: null;
|
||||
$keyed_array_var_id = ExpressionChecker::getArrayVarId(
|
||||
$stmt,
|
||||
$statements_checker->getFQCLN(),
|
||||
$statements_checker->getNamespace(),
|
||||
$statements_checker->getAliasedClasses()
|
||||
);
|
||||
|
||||
if ($stmt->dim && ExpressionChecker::check($statements_checker, $stmt->dim, $context) === false) {
|
||||
return false;
|
||||
|
@ -923,7 +923,11 @@ class ExpressionChecker
|
||||
$stmt->dim instanceof PhpParser\Node\Scalar\LNumber)
|
||||
) {
|
||||
$root_var_id = self::getArrayVarId($stmt->var, $this_class_name, $namespace, $aliased_classes);
|
||||
return $root_var_id ? $root_var_id . '[\'' . $stmt->dim->value . '\']' : null;
|
||||
$offset = $stmt->dim instanceof PhpParser\Node\Scalar\String_
|
||||
? '\'' . $stmt->dim->value . '\''
|
||||
: $stmt->dim->value;
|
||||
|
||||
return $root_var_id ? $root_var_id . '[' . $offset . ']' : null;
|
||||
}
|
||||
|
||||
return self::getVarId($stmt, $this_class_name, $namespace, $aliased_classes);
|
||||
|
@ -1092,6 +1092,10 @@ class TypeChecker
|
||||
? clone $existing_types[$key]
|
||||
: self::getValueForKey($key, $existing_types);
|
||||
|
||||
if ($result_type && empty($result_type->types)) {
|
||||
throw new \InvalidArgumentException('Union::$types cannot be empty after get value for ' . $key);
|
||||
}
|
||||
|
||||
foreach ($new_type_parts as $new_type_part) {
|
||||
$result_type = self::reconcileTypes(
|
||||
(string) $new_type_part,
|
||||
@ -1205,9 +1209,9 @@ class TypeChecker
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return Type::getMixed();
|
||||
}
|
||||
|
||||
return Type::getMixed();
|
||||
}
|
||||
|
||||
return $existing_var_type;
|
||||
@ -1315,7 +1319,7 @@ class TypeChecker
|
||||
*/
|
||||
protected static function getArrayValueForKey($key, array &$existing_keys)
|
||||
{
|
||||
$key_parts = preg_split('/(\'\]|\[\')/', $key, -1, PREG_SPLIT_NO_EMPTY);
|
||||
$key_parts = preg_split('/(\]|\[)/', $key, -1, PREG_SPLIT_NO_EMPTY);
|
||||
|
||||
if (count($key_parts) === 1) {
|
||||
return isset($existing_keys[$key_parts[0]]) ? clone $existing_keys[$key_parts[0]] : null;
|
||||
@ -1330,30 +1334,35 @@ class TypeChecker
|
||||
|
||||
// for an expression like $obj->key1->key2
|
||||
for ($i = 1; $i < count($key_parts); $i++) {
|
||||
$new_base_key = $base_key . '[\'' . $key_parts[$i] . '\']';
|
||||
$new_base_key = $base_key . '[' . $key_parts[$i] . ']';
|
||||
|
||||
if (!isset($existing_keys[$new_base_key])) {
|
||||
/** @var Type\Union|null */
|
||||
$new_base_type = null;
|
||||
|
||||
foreach ($existing_keys[$base_key]->types as $existing_key_type_part) {
|
||||
if (!$existing_key_type_part->isObjectLike()) {
|
||||
if ($existing_key_type_part instanceof Type\Generic) {
|
||||
$new_base_type_candidate = clone $existing_key_type_part->type_params[1];
|
||||
} elseif (!$existing_key_type_part instanceof Type\ObjectLike) {
|
||||
return null;
|
||||
}
|
||||
} else {
|
||||
$array_properties = $existing_key_type_part->properties;
|
||||
|
||||
/** @var Type\ObjectLike $existing_key_type_part */
|
||||
$array_properties = $existing_key_type_part->properties;
|
||||
$key_parts_key = str_replace('\'', '', $key_parts[$i]);
|
||||
|
||||
if (!isset($array_properties[$key_parts[$i]])) {
|
||||
return null;
|
||||
if (!isset($array_properties[$key_parts_key])) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$new_base_type_candidate = clone $array_properties[$key_parts_key];
|
||||
}
|
||||
|
||||
if (!$new_base_type) {
|
||||
$new_base_type = clone $array_properties[$key_parts[$i]];
|
||||
$new_base_type = $new_base_type_candidate;
|
||||
} else {
|
||||
$new_base_type = Type::combineUnionTypes(
|
||||
$new_base_type,
|
||||
clone $array_properties[$key_parts[$i]]
|
||||
$new_base_type_candidate
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -86,4 +86,11 @@ class Generic extends Atomic
|
||||
) .
|
||||
'>';
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this->type_params as &$type_param) {
|
||||
$type_param = clone $type_param;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -92,4 +92,11 @@ class ObjectLike extends Atomic
|
||||
|
||||
return Type::combineTypes($all_types);
|
||||
}
|
||||
|
||||
public function __clone()
|
||||
{
|
||||
foreach ($this->properties as $key => &$property) {
|
||||
$property = clone $property;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,6 +4,7 @@ namespace Psalm\Tests;
|
||||
use PhpParser\ParserFactory;
|
||||
use PHPUnit_Framework_TestCase;
|
||||
use Psalm\Context;
|
||||
use Psalm\Type;
|
||||
|
||||
class ArrayAssignmentTest extends PHPUnit_Framework_TestCase
|
||||
{
|
||||
@ -493,4 +494,79 @@ class ArrayAssignmentTest extends PHPUnit_Framework_TestCase
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
|
||||
public function testInstanceOfStringOffset()
|
||||
{
|
||||
$stmts = self::$parser->parse('<?php
|
||||
class A {
|
||||
public function foo() : void { }
|
||||
}
|
||||
function bar (array $a) : void {
|
||||
if ($a["a"] instanceof A) {
|
||||
$a["a"]->foo();
|
||||
}
|
||||
}
|
||||
');
|
||||
$file_checker = new \Psalm\Checker\FileChecker('somefile.php', $stmts);
|
||||
$context = new Context('somefile.php');
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
|
||||
public function testInstanceOfIntOffset()
|
||||
{
|
||||
$context = new Context('somefile.php');
|
||||
$stmts = self::$parser->parse('<?php
|
||||
class A {
|
||||
public function foo() : void { }
|
||||
}
|
||||
function bar (array $a) : void {
|
||||
if ($a[0] instanceof A) {
|
||||
$a[0]->foo();
|
||||
}
|
||||
}
|
||||
');
|
||||
|
||||
$file_checker = new \Psalm\Checker\FileChecker('somefile.php', $stmts);
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
|
||||
public function testNotEmptyStringOffset()
|
||||
{
|
||||
$context = new Context('somefile.php');
|
||||
$stmts = self::$parser->parse('<?php
|
||||
/**
|
||||
* @param array<string> $a
|
||||
*/
|
||||
function bar (array $a) : string {
|
||||
if ($a["bat"]) {
|
||||
return $a["bat"];
|
||||
}
|
||||
|
||||
return "blah";
|
||||
}
|
||||
');
|
||||
|
||||
$file_checker = new \Psalm\Checker\FileChecker('somefile.php', $stmts);
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
|
||||
public function testNotEmptyIntOffset()
|
||||
{
|
||||
$context = new Context('somefile.php');
|
||||
$stmts = self::$parser->parse('<?php
|
||||
/**
|
||||
* @param array<string> $a
|
||||
*/
|
||||
function bar (array $a) : string {
|
||||
if ($a[0]) {
|
||||
return $a[0];
|
||||
}
|
||||
|
||||
return "blah";
|
||||
}
|
||||
');
|
||||
|
||||
$file_checker = new \Psalm\Checker\FileChecker('somefile.php', $stmts);
|
||||
$file_checker->check(true, true, $context);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user