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

Fix #716 - uss string inference to inform property names

This commit is contained in:
Matthew Brown 2018-05-08 21:21:22 -04:00
parent c1440c11dc
commit 5c39fb5ab1
6 changed files with 174 additions and 96 deletions

View File

@ -5,7 +5,7 @@
totallyTyped="true"
strictBinaryOperands="false"
rememberPropertyAssignmentsAfterCall="true"
throwExceptionOnError="1"
throwExceptionOnError="0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config config.xsd"

View File

@ -403,20 +403,32 @@ class AssignmentChecker
$assign_value_type
);
} elseif ($assign_var instanceof PhpParser\Node\Expr\PropertyFetch) {
if (!$assign_var->name instanceof PhpParser\Node\Identifier) {
if (ExpressionChecker::analyze($statements_checker, $assign_var->name, $context) === false) {
return false;
}
}
if ($assign_var->name instanceof PhpParser\Node\Identifier) {
$prop_name = $assign_var->name->name;
} elseif (isset($assign_var->name->inferredType)
&& $assign_var->name->inferredType->isSingleStringLiteral()
) {
$prop_name = $assign_var->name->inferredType->getSingleStringLiteral();
} else {
$prop_name = null;
}
if ($prop_name) {
PropertyAssignmentChecker::analyzeInstance(
$statements_checker,
$assign_var,
$assign_var->name->name,
$prop_name,
$assign_value,
$assign_value_type,
$context
);
} else {
if (ExpressionChecker::analyze($statements_checker, $assign_var->name, $context) === false) {
return false;
}
if (ExpressionChecker::analyze($statements_checker, $assign_var->var, $context) === false) {
return false;
}

View File

@ -50,6 +50,16 @@ class PropertyFetchChecker
return false;
}
if ($stmt->name instanceof PhpParser\Node\Identifier) {
$prop_name = $stmt->name->name;
} elseif (isset($stmt->name->inferredType)
&& $stmt->name->inferredType->isSingleStringLiteral()
) {
$prop_name = $stmt->name->inferredType->getSingleStringLiteral();
} else {
$prop_name = null;
}
$project_checker = $statements_checker->getFileChecker()->project_checker;
$codebase = $project_checker->codebase;
@ -170,7 +180,7 @@ class PropertyFetchChecker
$stmt->inferredType = Type::getNull();
}
if (!$stmt->name instanceof PhpParser\Node\Identifier) {
if (!$prop_name) {
return null;
}
@ -238,7 +248,7 @@ class PropertyFetchChecker
continue;
}
$property_id = $lhs_type_part->value . '::$' . $stmt->name->name;
$property_id = $lhs_type_part->value . '::$' . $prop_name;
if ($stmt_var_id !== '$this'
&& $lhs_type_part->value !== $context->self
@ -257,8 +267,8 @@ class PropertyFetchChecker
) {
$class_storage = $project_checker->classlike_storage_provider->get((string)$lhs_type_part);
if (isset($class_storage->pseudo_property_get_types['$' . $stmt->name->name])) {
$stmt->inferredType = clone $class_storage->pseudo_property_get_types['$' . $stmt->name->name];
if (isset($class_storage->pseudo_property_get_types['$' . $prop_name])) {
$stmt->inferredType = clone $class_storage->pseudo_property_get_types['$' . $prop_name];
continue;
}
@ -326,7 +336,7 @@ class PropertyFetchChecker
(string)$declaring_property_class
);
$property_storage = $declaring_class_storage->properties[$stmt->name->name];
$property_storage = $declaring_class_storage->properties[$prop_name];
if ($property_storage->deprecated) {
if (IssueBuffer::accepts(
@ -345,7 +355,7 @@ class PropertyFetchChecker
if ($class_property_type === false) {
if (IssueBuffer::accepts(
new MissingPropertyType(
'Property ' . $lhs_type_part->value . '::$' . $stmt->name->name
'Property ' . $lhs_type_part->value . '::$' . $prop_name
. ' does not have a declared type',
new CodeLocation($statements_checker->getSource(), $stmt)
),
@ -510,10 +520,20 @@ class PropertyFetchChecker
$stmt->class->inferredType = $fq_class_name ? new Type\Union([new TNamedObject($fq_class_name)]) : null;
}
if ($stmt->name instanceof PhpParser\Node\VarLikeIdentifier) {
$prop_name = $stmt->name->name;
} elseif (isset($stmt->name->inferredType)
&& $stmt->name->inferredType->isSingleStringLiteral()
) {
$prop_name = $stmt->name->inferredType->getSingleStringLiteral();
} else {
$prop_name = null;
}
if ($fq_class_name &&
$context->check_classes &&
$context->check_variables &&
$stmt->name instanceof PhpParser\Node\Identifier &&
$prop_name &&
!ExpressionChecker::isMock($fq_class_name)
) {
$var_id = ExpressionChecker::getVarId(
@ -522,7 +542,7 @@ class PropertyFetchChecker
$statements_checker
);
$property_id = $fq_class_name . '::$' . $stmt->name->name;
$property_id = $fq_class_name . '::$' . $prop_name;
if ($var_id && $context->hasVariable($var_id, $statements_checker)) {
// we don't need to check anything
@ -568,11 +588,11 @@ class PropertyFetchChecker
}
$declaring_property_class = $codebase->properties->getDeclaringClassForProperty(
$fq_class_name . '::$' . $stmt->name->name
$fq_class_name . '::$' . $prop_name
);
$class_storage = $project_checker->classlike_storage_provider->get((string)$declaring_property_class);
$property = $class_storage->properties[$stmt->name->name];
$property = $class_storage->properties[$prop_name];
if ($var_id) {
$context->vars_in_scope[$var_id] = $property->type

View File

@ -724,14 +724,20 @@ class ExpressionChecker
}
}
if ($stmt instanceof PhpParser\Node\Expr\PropertyFetch && $stmt->name instanceof PhpParser\Node\Identifier) {
if ($stmt instanceof PhpParser\Node\Expr\PropertyFetch) {
$object_id = self::getArrayVarId($stmt->var, $this_class_name, $source);
if (!$object_id) {
return null;
}
return $object_id . '->' . $stmt->name;
if ($stmt->name instanceof PhpParser\Node\Identifier) {
return $object_id . '->' . $stmt->name;
} elseif (isset($stmt->name->inferredType) && $stmt->name->inferredType->isSingleStringLiteral()) {
return $object_id . '->' . $stmt->name->inferredType->getSingleStringLiteral();
} else {
return null;
}
}
if ($stmt instanceof PhpParser\Node\Expr\MethodCall

View File

@ -1198,100 +1198,109 @@ class Reconciler
if ($new_var_type === 'int') {
$ints = array_flip(explode(',', $bracketed));
if (isset($existing_var_atomic_types['int'])
&& $existing_var_atomic_types['int'] instanceof Type\Atomic\TLiteralInt
) {
$current_count = count($existing_var_atomic_types['int']->values);
if (isset($existing_var_atomic_types['int'])) {
if ($existing_var_atomic_types['int'] instanceof Type\Atomic\TLiteralInt) {
$current_count = count($existing_var_atomic_types['int']->values);
$existing_var_atomic_types['int']->values = array_intersect_key(
$existing_var_atomic_types['int']->values,
$ints
);
$existing_var_type->bustCache();
$new_count = count($existing_var_atomic_types['int']->values);
if ($key
&& $code_location
&& count($existing_var_atomic_types) === 1
&& ($new_count === 0 || $new_count === $current_count)
) {
self::triggerIssueForImpossible(
$existing_var_type,
$old_var_type_string,
$key,
$new_var_type,
$new_count === $current_count,
$code_location,
$suppressed_issues
$existing_var_atomic_types['int']->values = array_intersect_key(
$existing_var_atomic_types['int']->values,
$ints
);
$existing_var_type->bustCache();
$new_count = count($existing_var_atomic_types['int']->values);
if ($key
&& $code_location
&& count($existing_var_atomic_types) === 1
&& ($new_count === 0 || $new_count === $current_count)
) {
self::triggerIssueForImpossible(
$existing_var_type,
$old_var_type_string,
$key,
$new_var_type,
$new_count === $current_count,
$code_location,
$suppressed_issues
);
}
} else {
/** @psalm-suppress InvalidScalarArgument */
$existing_var_type->addType(new Type\Atomic\TLiteralInt($ints));
}
}
} elseif ($new_var_type === 'string') {
$strings = array_flip(explode('\',\'', substr($bracketed, 1, -1)));
if (isset($existing_var_atomic_types['string'])
&& $existing_var_atomic_types['string'] instanceof Type\Atomic\TLiteralString
) {
$current_count = count($existing_var_atomic_types['string']->values);
if (isset($existing_var_atomic_types['string'])) {
if ($existing_var_atomic_types['string'] instanceof Type\Atomic\TLiteralString) {
$current_count = count($existing_var_atomic_types['string']->values);
$existing_var_atomic_types['string']->values = array_intersect_key(
$existing_var_atomic_types['string']->values,
$strings
);
$existing_var_type->bustCache();
$new_count = count($existing_var_atomic_types['string']->values);
if ($key
&& $code_location
&& count($existing_var_atomic_types) === 1
&& ($new_count === 0 || $new_count === $current_count)
) {
self::triggerIssueForImpossible(
$existing_var_type,
$old_var_type_string,
$key,
$new_var_type,
$new_count === $current_count,
$code_location,
$suppressed_issues
$existing_var_atomic_types['string']->values = array_intersect_key(
$existing_var_atomic_types['string']->values,
$strings
);
$existing_var_type->bustCache();
$new_count = count($existing_var_atomic_types['string']->values);
if ($key
&& $code_location
&& count($existing_var_atomic_types) === 1
&& ($new_count === 0 || $new_count === $current_count)
) {
self::triggerIssueForImpossible(
$existing_var_type,
$old_var_type_string,
$key,
$new_var_type,
$new_count === $current_count,
$code_location,
$suppressed_issues
);
}
} else {
/** @psalm-suppress InvalidScalarArgument */
$existing_var_type->addType(new Type\Atomic\TLiteralString($strings));
}
}
} elseif (substr($new_var_type, 0, 6) === 'float(') {
$floats = array_flip(explode(',', $bracketed));
if (isset($existing_var_atomic_types['float'])
&& $existing_var_atomic_types['float'] instanceof Type\Atomic\TLiteralFloat
) {
$current_count = count($existing_var_atomic_types['float']->values);
if (isset($existing_var_atomic_types['float'])) {
if ($existing_var_atomic_types['float'] instanceof Type\Atomic\TLiteralFloat) {
$current_count = count($existing_var_atomic_types['float']->values);
$existing_var_atomic_types['float']->values = array_intersect_key(
$existing_var_atomic_types['float']->values,
$floats
);
$existing_var_type->bustCache();
$new_count = count($existing_var_atomic_types['float']->values);
if ($key
&& $code_location
&& count($existing_var_atomic_types) === 1
&& ($new_count === 0 || $new_count === $current_count)
) {
self::triggerIssueForImpossible(
$existing_var_type,
$old_var_type_string,
$key,
$new_var_type,
$new_count === $current_count,
$code_location,
$suppressed_issues
$existing_var_atomic_types['float']->values = array_intersect_key(
$existing_var_atomic_types['float']->values,
$floats
);
$existing_var_type->bustCache();
$new_count = count($existing_var_atomic_types['float']->values);
if ($key
&& $code_location
&& count($existing_var_atomic_types) === 1
&& ($new_count === 0 || $new_count === $current_count)
) {
self::triggerIssueForImpossible(
$existing_var_type,
$old_var_type_string,
$key,
$new_var_type,
$new_count === $current_count,
$code_location,
$suppressed_issues
);
}
} else {
/** @psalm-suppress InvalidScalarArgument */
$existing_var_type->addType(new Type\Atomic\TLiteralFloat($floats));
}
}
}

View File

@ -188,6 +188,21 @@ class MagicPropertyTest extends TestCase
'assertions' => [],
'error_level' => ['MixedAssignment', 'MixedTypeCoercion'],
],
'namedPropertyByVariable' => [
'<?php
class A {
/** @var string|null */
public $foo;
public function __get(string $var_name) : ?string {
if ($var_name === "foo") {
return $this->$var_name;
}
return null;
}
}',
],
];
}
@ -457,6 +472,22 @@ class MagicPropertyTest extends TestCase
'error_message' => 'MixedTypeCoercion',
'error_levels' => ['MixedAssignment'],
],
'misnamedPropertyByVariable' => [
'<?php
class B {
/** @var string|null */
public $foo;
public function __get(string $var_name) : ?string {
if ($var_name === "bar") {
return $this->$var_name;
}
return null;
}
}',
'error_message' => 'UndefinedThisPropertyFetch',
],
'directFetchForMagicProperty' => [
'<?php
/**