1
0
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:
Matt Brown 2018-07-11 11:22:07 -04:00
parent 0ef71a49cb
commit 661803a020
6 changed files with 122 additions and 54 deletions

View File

@ -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;

View File

@ -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;
}
}

View File

@ -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;
}
}
}

View File

@ -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
*

View File

@ -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>',

View File

@ -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',
],
];
}
}