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:
parent
c1440c11dc
commit
5c39fb5ab1
@ -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"
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
/**
|
||||
|
Loading…
Reference in New Issue
Block a user