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

Coerce mixed values when passed as arguments

This commit is contained in:
Brown 2019-05-21 11:51:41 -04:00
parent a1eb191f57
commit 4ecf370900
6 changed files with 143 additions and 44 deletions

View File

@ -1586,6 +1586,15 @@ class CallAnalyzer
$static_fq_class_name
);
$fleshed_out_signature_type = $function_param->signature_type
? ExpressionAnalyzer::fleshOutType(
$codebase,
$function_param->signature_type,
$self_fq_class_name,
$static_fq_class_name
)
: null;
if ($arg->unpack) {
if ($arg_type->hasMixed()) {
if (!$context->collect_initializations
@ -1647,6 +1656,7 @@ class CallAnalyzer
$statements_analyzer,
$arg_type,
$fleshed_out_type,
$fleshed_out_signature_type,
$cased_method_id,
$argument_offset,
new CodeLocation($statements_analyzer->getSource(), $arg->value),
@ -2171,6 +2181,7 @@ class CallAnalyzer
StatementsAnalyzer $statements_analyzer,
Type\Union $input_type,
Type\Union $param_type,
?Type\Union $signature_param_type,
$cased_method_id,
$argument_offset,
CodeLocation $code_location,
@ -2229,6 +2240,21 @@ class CallAnalyzer
// fall through
}
if (!$by_ref
&& !($variadic xor $unpack)
&& $cased_method_id !== 'echo'
) {
self::coerceValueAfterGatekeeperArgument(
$statements_analyzer,
$input_type,
$input_expr,
$param_type,
$signature_param_type,
$context,
$unpack
);
}
return null;
}
@ -2587,49 +2613,73 @@ class CallAnalyzer
}
if ($type_match_found
&& !$param_type->hasMixed()
&& !$param_type->from_docblock
&& !($variadic xor $unpack)
&& !$by_ref
&& !($variadic xor $unpack)
&& $cased_method_id !== 'echo'
) {
$var_id = ExpressionAnalyzer::getVarId(
self::coerceValueAfterGatekeeperArgument(
$statements_analyzer,
$input_type,
$input_expr,
$statements_analyzer->getFQCLN(),
$statements_analyzer
$param_type,
$signature_param_type,
$context,
$unpack
);
if ($var_id) {
if ($input_type->isNullable() && !$param_type->isNullable()) {
$input_type->removeType('null');
}
if ($input_type->getId() === $param_type->getId()) {
$input_type->from_docblock = false;
foreach ($input_type->getTypes() as $atomic_type) {
$atomic_type->from_docblock = false;
}
}
$context->removeVarFromConflictingClauses($var_id, null, $statements_analyzer);
if ($unpack) {
$input_type = new Type\Union([
new TArray([
Type::getInt(),
$input_type
]),
]);
}
$context->vars_in_scope[$var_id] = $input_type;
}
}
return null;
}
private static function coerceValueAfterGatekeeperArgument(
StatementsAnalyzer $statements_analyzer,
Type\Union $input_type,
PhpParser\Node\Expr $input_expr,
Type\Union $param_type,
?Type\Union $signature_param_type,
Context $context,
bool $unpack
) : void {
if ($param_type->hasMixed() || ($param_type->from_docblock && !$input_type->isMixed())) {
return;
}
$var_id = ExpressionAnalyzer::getVarId(
$input_expr,
$statements_analyzer->getFQCLN(),
$statements_analyzer
);
if ($var_id) {
if ($input_type->isNullable() && !$param_type->isNullable()) {
$input_type->removeType('null');
}
if ($input_type->getId() === $param_type->getId()) {
$input_type->from_docblock = false;
foreach ($input_type->getTypes() as $atomic_type) {
$atomic_type->from_docblock = false;
}
} elseif ($input_type->isMixed() && $signature_param_type) {
$input_type = clone $signature_param_type;
}
$context->removeVarFromConflictingClauses($var_id, null, $statements_analyzer);
if ($unpack) {
$input_type = new Type\Union([
new TArray([
Type::getInt(),
$input_type
]),
]);
}
$context->vars_in_scope[$var_id] = $input_type;
}
}
/**
* @param PhpParser\Node\Scalar\String_|PhpParser\Node\Expr\Array_|PhpParser\Node\Expr\BinaryOp\Concat
* $callable_arg

View File

@ -454,6 +454,7 @@ class StatementsAnalyzer extends SourceAnalyzer implements StatementsSource
$this,
$expr->inferredType,
Type::getString(),
null,
'echo',
(int)$i,
new CodeLocation($this->getSource(), $expr),

View File

@ -56,12 +56,12 @@ class CallMap
$call_map_functions[] = $call_map[$call_map_key . '\'' . $i];
}
$function_type_options = [];
$function_param_options = [];
foreach ($call_map_functions as $call_map_function_args) {
array_shift($call_map_function_args);
$function_types = [];
$function_params = [];
/** @var string $arg_name - key type changed with above array_shift */
foreach ($call_map_function_args as $arg_name => $arg_type) {
@ -88,7 +88,7 @@ class CallMap
? Type::parseString($arg_type)
: Type::getMixed();
$function_types[] = new FunctionLikeParameter(
$function_param = new FunctionLikeParameter(
$arg_name,
$by_reference,
$param_type,
@ -98,12 +98,16 @@ class CallMap
false,
$variadic
);
$function_param->signature_type = null;
$function_params[] = $function_param;
}
$function_type_options[] = $function_types;
$function_param_options[] = $function_params;
}
return $function_type_options;
return $function_param_options;
}
/**

View File

@ -435,7 +435,7 @@ abstract class Type
$tree_type = $tree_type instanceof Union ? $tree_type : new Union([$tree_type]);
return new FunctionLikeParameter(
$param = new FunctionLikeParameter(
'',
false,
$tree_type,
@ -445,6 +445,11 @@ abstract class Type
false,
$is_variadic
);
// type is not authoratative
$param->signature_type = null;
return $param;
},
$parse_tree->children
);

View File

@ -112,17 +112,21 @@ class ParamTypeManipulationTest extends FileManipulationTest
],
'noParamTypeButConcatAndStringUsage' => [
'<?php
function takesString(string $s) : void {}
function fooFoo($a): void {
echo $a . "foo";
echo substr($a, 4, 2);
echo takesString($a);
}',
'<?php
function takesString(string $s) : void {}
/**
* @param string $a
*/
function fooFoo($a): void {
echo $a . "foo";
echo substr($a, 4, 2);
echo takesString($a);
}',
'7.1',
['MissingParamType'],
@ -130,16 +134,20 @@ class ParamTypeManipulationTest extends FileManipulationTest
],
'noParamTypeButConcatAndStringUsageReversed' => [
'<?php
function takesString(string $s) : void {}
function fooFoo($a): void {
echo substr($a, 4, 2);
echo takesString($a);
echo $a . "foo";
}',
'<?php
function takesString(string $s) : void {}
/**
* @param string $a
*/
function fooFoo($a): void {
echo substr($a, 4, 2);
echo takesString($a);
echo $a . "foo";
}',
'7.1',

View File

@ -1646,6 +1646,21 @@ class FunctionCallTest extends TestCase
);
}',
],
'noImplicitAssignmentToStringFromMixedWithDocblockTypes' => [
'<?php
/** @param string $s */
function takesString($s) : void {}
function takesInt(int $i) : void {}
/**
* @param mixed $s
* @psalm-suppress MixedArgument
*/
function bar($s) : void {
takesString($s);
takesInt($s);
}',
],
];
}
@ -2167,7 +2182,23 @@ class FunctionCallTest extends TestCase
$a = rand(0, 1) ? null : 5;
/** @psalm-suppress MixedArgument */
foo((int) $a);',
'InvalidPassByReference',
'error_message' => 'InvalidPassByReference',
],
'implicitAssignmentToStringFromMixed' => [
'<?php
/** @param "a"|"b" $s */
function takesString(string $s) : void {}
function takesInt(int $i) : void {}
/**
* @param mixed $s
* @psalm-suppress MixedArgument
*/
function bar($s) : void {
takesString($s);
takesInt($s);
}',
'error_message' => 'InvalidScalarArgument'
],
];
}