mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Coerce mixed values when passed as arguments
This commit is contained in:
parent
a1eb191f57
commit
4ecf370900
@ -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
|
||||
|
@ -454,6 +454,7 @@ class StatementsAnalyzer extends SourceAnalyzer implements StatementsSource
|
||||
$this,
|
||||
$expr->inferredType,
|
||||
Type::getString(),
|
||||
null,
|
||||
'echo',
|
||||
(int)$i,
|
||||
new CodeLocation($this->getSource(), $expr),
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
);
|
||||
|
@ -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',
|
||||
|
@ -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'
|
||||
],
|
||||
];
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user