mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Fix #883 - add @psalm-assert-if-true support to methods
This commit is contained in:
parent
0ef71a49cb
commit
661803a020
@ -180,6 +180,11 @@ class AssertionFinder
|
||||
return;
|
||||
}
|
||||
|
||||
if ($conditional instanceof PhpParser\Node\Expr\MethodCall) {
|
||||
$conditional->assertions = self::processCustomAssertion($conditional, $this_class_name, $source, false);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($conditional instanceof PhpParser\Node\Expr\Empty_) {
|
||||
$var_name = ExpressionChecker::getArrayVarId(
|
||||
$conditional->expr,
|
||||
@ -1419,20 +1424,47 @@ class AssertionFinder
|
||||
) {
|
||||
$if_types[$array_root . '[' . $first_var_name . ']'] = [[$prefix . 'array-key-exists']];
|
||||
}
|
||||
} elseif ($source instanceof StatementsChecker
|
||||
&& $expr->name instanceof PhpParser\Node\Name
|
||||
&& isset($expr->conditionalAssertion)
|
||||
} else {
|
||||
$if_types = self::processCustomAssertion($expr, $this_class_name, $source, $negate);
|
||||
}
|
||||
|
||||
return $if_types;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node\Expr\FuncCall|PhpParser\Node\Expr\MethodCall $expr
|
||||
* @param string|null $this_class_name
|
||||
* @param FileSource $source
|
||||
* @param bool $negate
|
||||
*
|
||||
* @return array<string, array<int, array<int, string>>>
|
||||
*/
|
||||
protected static function processCustomAssertion(
|
||||
$expr,
|
||||
$this_class_name,
|
||||
FileSource $source,
|
||||
$negate = false
|
||||
) {
|
||||
if (!$source instanceof StatementsChecker
|
||||
|| (!isset($expr->ifTrueAssertions) && !isset($expr->ifFalseAssertions))
|
||||
) {
|
||||
$codebase = $source->getFileChecker()->project_checker->codebase;
|
||||
return [];
|
||||
}
|
||||
|
||||
$function_id = ClassLikeChecker::getFQCLNFromNameObject($expr->name, $source->getAliases());
|
||||
$prefix = $negate ? '!' : '';
|
||||
|
||||
$function_storage = $codebase->functions->getStorage(
|
||||
$source,
|
||||
strtolower($function_id)
|
||||
);
|
||||
$first_var_name = isset($expr->args[0]->value)
|
||||
? ExpressionChecker::getArrayVarId(
|
||||
$expr->args[0]->value,
|
||||
$this_class_name,
|
||||
$source
|
||||
)
|
||||
: null;
|
||||
|
||||
foreach ($function_storage->if_true_assertions as $assertion) {
|
||||
$if_types = [];
|
||||
|
||||
if (isset($expr->ifTrueAssertions)) {
|
||||
foreach ($expr->ifTrueAssertions as $assertion) {
|
||||
if (is_int($assertion->var_id) && isset($expr->args[$assertion->var_id])) {
|
||||
if ($assertion->var_id === 0) {
|
||||
$var_name = $first_var_name;
|
||||
@ -1453,10 +1485,12 @@ class AssertionFinder
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (isset($expr->ifFalseAssertions)) {
|
||||
$negated_prefix = !$negate ? '!' : '';
|
||||
|
||||
foreach ($function_storage->if_false_assertions as $assertion) {
|
||||
foreach ($expr->ifFalseAssertions as $assertion) {
|
||||
if (is_int($assertion->var_id) && isset($expr->args[$assertion->var_id])) {
|
||||
if ($assertion->var_id === 0) {
|
||||
$var_name = $first_var_name;
|
||||
|
@ -465,9 +465,12 @@ class FunctionCallChecker extends \Psalm\Checker\Statements\Expression\CallCheck
|
||||
);
|
||||
}
|
||||
|
||||
if ($function_storage->if_true_assertions || $function_storage->if_false_assertions) {
|
||||
/** @psalm-suppress UndefinedPropertyAssignment */
|
||||
$stmt->conditionalAssertion = true;
|
||||
if ($function_storage->if_true_assertions) {
|
||||
$stmt->ifTrueAssertions = $function_storage->if_true_assertions;
|
||||
}
|
||||
|
||||
if ($function_storage->if_false_assertions) {
|
||||
$stmt->ifFalseAssertions = $function_storage->if_false_assertions;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -172,9 +172,11 @@ class MethodCallChecker extends \Psalm\Checker\Statements\Expression\CallChecker
|
||||
if ($class_type) {
|
||||
$return_type = null;
|
||||
|
||||
foreach ($class_type->getTypes() as $class_type_part) {
|
||||
if (!$class_type_part instanceof TNamedObject) {
|
||||
switch (get_class($class_type_part)) {
|
||||
$lhs_types = $class_type->getTypes();
|
||||
|
||||
foreach ($lhs_types as $lhs_type_part) {
|
||||
if (!$lhs_type_part instanceof TNamedObject) {
|
||||
switch (get_class($lhs_type_part)) {
|
||||
case Type\Atomic\TNull::class:
|
||||
case Type\Atomic\TFalse::class:
|
||||
// handled above
|
||||
@ -194,7 +196,7 @@ class MethodCallChecker extends \Psalm\Checker\Statements\Expression\CallChecker
|
||||
case Type\Atomic\TNumericString::class:
|
||||
case Type\Atomic\TClassString::class:
|
||||
case Type\Atomic\TEmptyMixed::class:
|
||||
$invalid_method_call_types[] = (string)$class_type_part;
|
||||
$invalid_method_call_types[] = (string)$lhs_type_part;
|
||||
break;
|
||||
|
||||
case Type\Atomic\TMixed::class:
|
||||
@ -233,9 +235,9 @@ class MethodCallChecker extends \Psalm\Checker\Statements\Expression\CallChecker
|
||||
|
||||
$has_valid_method_call_type = true;
|
||||
|
||||
$fq_class_name = $class_type_part->value;
|
||||
$fq_class_name = $lhs_type_part->value;
|
||||
|
||||
$intersection_types = $class_type_part->getIntersectionTypes();
|
||||
$intersection_types = $lhs_type_part->getIntersectionTypes();
|
||||
|
||||
$is_mock = ExpressionChecker::isMock($fq_class_name);
|
||||
|
||||
@ -480,15 +482,15 @@ class MethodCallChecker extends \Psalm\Checker\Statements\Expression\CallChecker
|
||||
if ($class_storage->template_types) {
|
||||
$class_template_params = [];
|
||||
|
||||
if ($class_type_part instanceof TGenericObject) {
|
||||
if ($lhs_type_part instanceof TGenericObject) {
|
||||
$reversed_class_template_types = array_reverse(array_keys($class_storage->template_types));
|
||||
|
||||
$provided_type_param_count = count($class_type_part->type_params);
|
||||
$provided_type_param_count = count($lhs_type_part->type_params);
|
||||
|
||||
foreach ($reversed_class_template_types as $i => $type_name) {
|
||||
if (isset($class_type_part->type_params[$provided_type_param_count - 1 - $i])) {
|
||||
if (isset($lhs_type_part->type_params[$provided_type_param_count - 1 - $i])) {
|
||||
$class_template_params[$type_name] =
|
||||
$class_type_part->type_params[$provided_type_param_count - 1 - $i];
|
||||
$lhs_type_part->type_params[$provided_type_param_count - 1 - $i];
|
||||
} else {
|
||||
$class_template_params[$type_name] = Type::getMixed();
|
||||
}
|
||||
@ -643,17 +645,25 @@ class MethodCallChecker extends \Psalm\Checker\Statements\Expression\CallChecker
|
||||
|| $codebase->methods->getMethodReturnsByRef($method_id);
|
||||
}
|
||||
|
||||
if (strpos($stmt->name->name, 'assert') === 0) {
|
||||
$assertions = $codebase->methods->getMethodAssertions($method_id);
|
||||
if (count($lhs_types) === 1) {
|
||||
$method_storage = $codebase->methods->getUserMethodStorage($method_id);
|
||||
|
||||
if ($assertions) {
|
||||
if ($method_storage->assertions) {
|
||||
self::applyAssertionsToContext(
|
||||
$assertions,
|
||||
$method_storage->assertions,
|
||||
$stmt->args,
|
||||
$context,
|
||||
$statements_checker
|
||||
);
|
||||
}
|
||||
|
||||
if ($method_storage->if_true_assertions) {
|
||||
$stmt->ifTrueAssertions = $method_storage->if_true_assertions;
|
||||
}
|
||||
|
||||
if ($method_storage->if_false_assertions) {
|
||||
$stmt->ifFalseAssertions = $method_storage->if_false_assertions;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -316,32 +316,6 @@ class Methods
|
||||
return $storage->return_type_location;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $method_id
|
||||
*
|
||||
* @return array<int, \Psalm\Storage\Assertion>
|
||||
*/
|
||||
public function getMethodAssertions($method_id)
|
||||
{
|
||||
$method_id = $this->getDeclaringMethodId($method_id);
|
||||
|
||||
if (!$method_id) {
|
||||
return [];
|
||||
}
|
||||
|
||||
list($fq_class_name) = explode('::', $method_id);
|
||||
|
||||
$fq_class_storage = $this->classlike_storage_provider->get($fq_class_name);
|
||||
|
||||
if (!$fq_class_storage->user_defined && CallMap::inCallMap($method_id)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$storage = $this->getStorage($method_id);
|
||||
|
||||
return $storage->assertions;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $method_id
|
||||
* @param string $declaring_method_id
|
||||
@ -456,6 +430,28 @@ class Methods
|
||||
return $fq_class_name . '::' . $storage->cased_name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $method_id
|
||||
*
|
||||
* @return MethodStorage
|
||||
*/
|
||||
public function getUserMethodStorage($method_id)
|
||||
{
|
||||
$declaring_method_id = $this->getDeclaringMethodId($method_id);
|
||||
|
||||
if (!$declaring_method_id) {
|
||||
throw new \UnexpectedValueException('$storage should not be null for ' . $method_id);
|
||||
}
|
||||
|
||||
$storage = $this->getStorage($declaring_method_id);
|
||||
|
||||
if (!$storage->location) {
|
||||
throw new \UnexpectedValueException('Storage for ' . $method_id . ' is not user-defined');
|
||||
}
|
||||
|
||||
return $storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $method_id
|
||||
*
|
||||
|
@ -406,6 +406,8 @@ return [
|
||||
],
|
||||
'phpparser\\node\\expr\\funccall' => [
|
||||
'args' => 'array<int, PhpParser\Node\Arg>',
|
||||
'ifTrueAssertions' => 'array<int, Psalm\Storage\Assertion>|null',
|
||||
'ifFalseAssertions' => 'array<int, Psalm\Storage\Assertion>|null',
|
||||
],
|
||||
'phpparser\\node\\expr\\new_' => [
|
||||
'args' => 'array<int, PhpParser\Node\Arg>',
|
||||
@ -418,6 +420,8 @@ return [
|
||||
],
|
||||
'phpparser\\node\\expr\\methodcall' => [
|
||||
'args' => 'array<int, PhpParser\Node\Arg>',
|
||||
'ifTrueAssertions' => 'array<int, Psalm\Storage\Assertion>|null',
|
||||
'ifFalseAssertions' => 'array<int, Psalm\Storage\Assertion>|null',
|
||||
],
|
||||
'phpparser\\node\\expr\\staticcall' => [
|
||||
'args' => 'array<int, PhpParser\Node\Arg>',
|
||||
|
@ -255,6 +255,27 @@ class AssertTest extends TestCase
|
||||
}',
|
||||
'error_message' => 'PossiblyNullOperand',
|
||||
],
|
||||
'assertIfTrueMethodCall' => [
|
||||
'<?php
|
||||
class C {
|
||||
/**
|
||||
* @param mixed $p
|
||||
* @psalm-assert-if-true int $p
|
||||
*/
|
||||
public function isInt($p): bool {
|
||||
return is_int($p);
|
||||
}
|
||||
/**
|
||||
* @param mixed $p
|
||||
*/
|
||||
public function doWork($p): void {
|
||||
if ($this->isInt($p)) {
|
||||
strlen($p);
|
||||
}
|
||||
}
|
||||
}',
|
||||
'error_message' => 'InvalidScalarArgument',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user