mirror of
https://github.com/danog/psalm.git
synced 2024-11-26 20:34:47 +01:00
Improve handling of variadic parameters
This commit is contained in:
parent
54c76be63b
commit
1348d634ed
@ -435,7 +435,18 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
|
||||
}
|
||||
}
|
||||
|
||||
$context->vars_in_scope['$' . $function_param->name] = $param_type;
|
||||
$var_type = $param_type;
|
||||
|
||||
if ($function_param->is_variadic) {
|
||||
$var_type = new Type\Union([
|
||||
new Type\Atomic\TArray([
|
||||
Type::getInt(),
|
||||
$param_type,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
$context->vars_in_scope['$' . $function_param->name] = $var_type;
|
||||
$context->vars_possibly_in_scope['$' . $function_param->name] = true;
|
||||
|
||||
if ($context->collect_references && $function_param->location) {
|
||||
@ -1019,7 +1030,7 @@ abstract class FunctionLikeAnalyzer extends SourceAnalyzer implements Statements
|
||||
$storage = $this->getFunctionLikeStorage($statements_analyzer);
|
||||
|
||||
foreach ($storage->params as $i => $param) {
|
||||
if ($param->by_ref && isset($context->vars_in_scope['$' . $param->name])) {
|
||||
if ($param->by_ref && isset($context->vars_in_scope['$' . $param->name]) && !$param->is_variadic) {
|
||||
$actual_type = $context->vars_in_scope['$' . $param->name];
|
||||
$param_out_type = $param->type;
|
||||
|
||||
|
@ -665,28 +665,10 @@ class CallAnalyzer
|
||||
new Type\Union([new TArray([Type::getInt(), Type::getMixed()])])
|
||||
);
|
||||
} elseif ($arg->unpack) {
|
||||
if ($arg->value->inferredType->hasArray()) {
|
||||
/** @var Type\Atomic\TArray|Type\Atomic\ObjectLike */
|
||||
$array_atomic_type = $arg->value->inferredType->getTypes()['array'];
|
||||
|
||||
if ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
|
||||
$array_atomic_type = $array_atomic_type->getGenericArrayType();
|
||||
}
|
||||
|
||||
$by_ref_type = Type::combineUnionTypes(
|
||||
$by_ref_type,
|
||||
new Type\Union(
|
||||
[
|
||||
new TArray(
|
||||
[
|
||||
Type::getInt(),
|
||||
clone $array_atomic_type->type_params[1]
|
||||
]
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
$by_ref_type = Type::combineUnionTypes(
|
||||
$by_ref_type,
|
||||
clone $arg->value->inferredType
|
||||
);
|
||||
} else {
|
||||
$by_ref_type = Type::combineUnionTypes(
|
||||
$by_ref_type,
|
||||
@ -1325,17 +1307,19 @@ class CallAnalyzer
|
||||
|
||||
if ($last_param) {
|
||||
if ($argument_offset < count($function_params)) {
|
||||
$by_ref_type = $function_params[$argument_offset]->type;
|
||||
|
||||
if ($by_ref_type && $by_ref_type->isNullable()) {
|
||||
$check_null_ref = false;
|
||||
}
|
||||
|
||||
if (isset($function_storage->param_out_types[$argument_offset])) {
|
||||
$by_ref_out_type = $function_storage->param_out_types[$argument_offset];
|
||||
}
|
||||
$function_param = $function_params[$argument_offset];
|
||||
} else {
|
||||
$by_ref_type = $last_param->type;
|
||||
$function_param = $last_param;
|
||||
}
|
||||
|
||||
$by_ref_type = $function_param->type;
|
||||
|
||||
if (isset($function_storage->param_out_types[$argument_offset])) {
|
||||
$by_ref_out_type = $function_storage->param_out_types[$argument_offset];
|
||||
}
|
||||
|
||||
if ($by_ref_type && $by_ref_type->isNullable()) {
|
||||
$check_null_ref = false;
|
||||
}
|
||||
|
||||
if ($template_types && $by_ref_type) {
|
||||
@ -1364,6 +1348,15 @@ class CallAnalyzer
|
||||
$by_ref_type = $original_by_ref_type;
|
||||
}
|
||||
}
|
||||
|
||||
if ($by_ref_type && $function_param->is_variadic && $arg->unpack) {
|
||||
$by_ref_type = new Type\Union([
|
||||
new Type\Atomic\TArray([
|
||||
Type::getInt(),
|
||||
$by_ref_type,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
$by_ref_type = $by_ref_type ?: Type::getMixed();
|
||||
@ -1406,20 +1399,6 @@ class CallAnalyzer
|
||||
if ($function_param && $function_param->type) {
|
||||
$param_type = clone $function_param->type;
|
||||
|
||||
if ($function_param->is_variadic) {
|
||||
if (!$param_type->hasArray()) {
|
||||
return;
|
||||
}
|
||||
|
||||
$array_atomic_type = $param_type->getTypes()['array'];
|
||||
|
||||
if (!$array_atomic_type instanceof TArray) {
|
||||
return;
|
||||
}
|
||||
|
||||
$param_type = clone $array_atomic_type->type_params[1];
|
||||
}
|
||||
|
||||
if ($existing_generic_params) {
|
||||
$empty_generic_params = [];
|
||||
|
||||
@ -1449,11 +1428,9 @@ class CallAnalyzer
|
||||
if ($arg_type->hasArray()) {
|
||||
/** @var Type\Atomic\TArray|Type\Atomic\ObjectLike */
|
||||
$array_atomic_type = $arg_type->getTypes()['array'];
|
||||
|
||||
if ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
|
||||
$array_atomic_type = $array_atomic_type->getGenericArrayType();
|
||||
}
|
||||
|
||||
$arg_type_param = $array_atomic_type->type_params[1];
|
||||
} else {
|
||||
$arg_type_param = Type::getMixed();
|
||||
@ -1480,32 +1457,6 @@ class CallAnalyzer
|
||||
);
|
||||
|
||||
if ($arg->unpack) {
|
||||
if ($arg_type->hasArray()) {
|
||||
/** @var Type\Atomic\TArray|Type\Atomic\ObjectLike */
|
||||
$array_atomic_type = $arg_type->getTypes()['array'];
|
||||
|
||||
if ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
|
||||
$array_atomic_type = $array_atomic_type->getGenericArrayType();
|
||||
}
|
||||
|
||||
if (self::checkFunctionArgumentType(
|
||||
$statements_analyzer,
|
||||
$array_atomic_type->type_params[1],
|
||||
$fleshed_out_type,
|
||||
$cased_method_id,
|
||||
$argument_offset,
|
||||
new CodeLocation($statements_analyzer->getSource(), $arg->value),
|
||||
$arg->value,
|
||||
$context,
|
||||
$function_param->by_ref,
|
||||
true
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($arg_type->hasMixed()) {
|
||||
if (!$context->collect_initializations
|
||||
&& !$context->collect_mutations
|
||||
@ -1531,22 +1482,33 @@ class CallAnalyzer
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($arg_type->getTypes() as $atomic_type) {
|
||||
if (!$atomic_type->isIterable($codebase)) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidArgument(
|
||||
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id
|
||||
. ' expects array, ' . $atomic_type->getId() . ' provided',
|
||||
new CodeLocation($statements_analyzer->getSource(), $arg->value)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
if ($arg_type->hasArray()) {
|
||||
/** @var Type\Atomic\TArray|Type\Atomic\ObjectLike */
|
||||
$array_atomic_type = $arg_type->getTypes()['array'];
|
||||
|
||||
if ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
|
||||
$array_atomic_type = $array_atomic_type->getGenericArrayType();
|
||||
}
|
||||
|
||||
$arg_type = $array_atomic_type->type_params[1];
|
||||
} else {
|
||||
foreach ($arg_type->getTypes() as $atomic_type) {
|
||||
if (!$atomic_type->isIterable($codebase)) {
|
||||
if (IssueBuffer::accepts(
|
||||
new InvalidArgument(
|
||||
'Argument ' . ($argument_offset + 1) . ' of ' . $cased_method_id
|
||||
. ' expects array, ' . $atomic_type->getId() . ' provided',
|
||||
new CodeLocation($statements_analyzer->getSource(), $arg->value)
|
||||
),
|
||||
$statements_analyzer->getSuppressedIssues()
|
||||
)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (self::checkFunctionArgumentType(
|
||||
@ -1559,7 +1521,8 @@ class CallAnalyzer
|
||||
$arg->value,
|
||||
$context,
|
||||
$function_param->by_ref,
|
||||
$function_param->is_variadic
|
||||
$function_param->is_variadic,
|
||||
$arg->unpack
|
||||
) === false) {
|
||||
return false;
|
||||
}
|
||||
@ -2078,8 +2041,9 @@ class CallAnalyzer
|
||||
CodeLocation $code_location,
|
||||
PhpParser\Node\Expr $input_expr,
|
||||
Context $context,
|
||||
$by_ref = false,
|
||||
$variadic = false
|
||||
bool $by_ref = false,
|
||||
bool $variadic = false,
|
||||
bool $unpack = false
|
||||
) {
|
||||
if ($param_type->hasMixed()) {
|
||||
return null;
|
||||
@ -2446,7 +2410,7 @@ class CallAnalyzer
|
||||
if ($type_match_found
|
||||
&& !$param_type->hasMixed()
|
||||
&& !$param_type->from_docblock
|
||||
&& !$variadic
|
||||
&& !($variadic xor $unpack)
|
||||
&& !$by_ref
|
||||
&& $cased_method_id !== 'echo'
|
||||
) {
|
||||
@ -2471,6 +2435,15 @@ class CallAnalyzer
|
||||
|
||||
$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;
|
||||
}
|
||||
}
|
||||
|
@ -2163,23 +2163,7 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
if ($is_nullable) {
|
||||
$param_type->addType(new Type\Atomic\TNull);
|
||||
}
|
||||
|
||||
if ($param->variadic) {
|
||||
$param_type = new Type\Union([
|
||||
new Type\Atomic\TArray([
|
||||
Type::getInt(),
|
||||
$param_type,
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
} elseif ($param->variadic) {
|
||||
$param_type = new Type\Union([
|
||||
new Type\Atomic\TArray([
|
||||
Type::getInt(),
|
||||
Type::getMixed(),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
$is_optional = $param->default !== null;
|
||||
@ -2338,13 +2322,15 @@ class ReflectorVisitor extends PhpParser\NodeVisitorAbstract implements PhpParse
|
||||
$storage->template_types ?: []
|
||||
);
|
||||
|
||||
if ($docblock_param_variadic) {
|
||||
$new_param_type = new Type\Union([
|
||||
new Type\Atomic\TArray([
|
||||
Type::getInt(),
|
||||
$new_param_type,
|
||||
]),
|
||||
]);
|
||||
if (!$docblock_param_variadic && $storage_param->is_variadic && $new_param_type->hasArray()) {
|
||||
/** @var Type\Atomic\TArray|Type\Atomic\ObjectLike */
|
||||
$array_type = $new_param_type->getTypes()['array'];
|
||||
|
||||
if ($array_type instanceof Type\Atomic\ObjectLike) {
|
||||
$new_param_type = $array_type->getGenericValueType();
|
||||
} else {
|
||||
$new_param_type = $array_type->type_params[1];
|
||||
}
|
||||
}
|
||||
|
||||
$existing_param_type_nullable = $storage_param->is_nullable;
|
||||
|
@ -585,10 +585,16 @@ class AnnotationTest extends TestCase
|
||||
$f = foo();
|
||||
if ($f) {}',
|
||||
],
|
||||
'spreadOperatorArrayAnnotation' => [
|
||||
'spreadOperatorAnnotation' => [
|
||||
'<?php
|
||||
/** @param string[] $s */
|
||||
function foo(string ...$s) : void {}',
|
||||
function foo(string ...$s) : void {}
|
||||
/** @param string ...$s */
|
||||
function bar(string ...$s) : void {}
|
||||
foo("hello", "goodbye");
|
||||
bar("hello", "goodbye");
|
||||
foo(...["hello", "goodbye"]);
|
||||
bar(...["hello", "goodbye"]);',
|
||||
],
|
||||
'valueReturnType' => [
|
||||
'<?php
|
||||
@ -1341,6 +1347,20 @@ class AnnotationTest extends TestCase
|
||||
(new X())->boo([1, 2]);',
|
||||
'error_message' => 'InvalidScalarArgument',
|
||||
],
|
||||
'spreadOperatorArrayAnnotationBadArg' => [
|
||||
'<?php
|
||||
/** @param string[] $s */
|
||||
function foo(string ...$s) : void {}
|
||||
foo(5);',
|
||||
'error_message' => 'InvalidScalarArgument',
|
||||
],
|
||||
'spreadOperatorArrayAnnotationBadSpreadArg' => [
|
||||
'<?php
|
||||
/** @param string[] $s */
|
||||
function foo(string ...$s) : void {}
|
||||
foo(...[5]);',
|
||||
'error_message' => 'InvalidScalarArgument',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
@ -1072,7 +1072,7 @@ class TemporaryUpdateTest extends \Psalm\Tests\TestCase
|
||||
],
|
||||
[
|
||||
getcwd() . DIRECTORY_SEPARATOR . 'A.php' => '<?php
|
||||
function variadic_arguments(string $_foo, ...$bars ) : void {}
|
||||
function variadic_arguments(string $_foo, string ...$bars ) : void {}
|
||||
|
||||
function foo(string $baz, string $qux) : void {
|
||||
variadic_arguments(
|
||||
@ -1082,7 +1082,7 @@ class TemporaryUpdateTest extends \Psalm\Tests\TestCase
|
||||
}',
|
||||
],
|
||||
],
|
||||
'error_positions' => [[238, 238], []],
|
||||
'error_positions' => [[79, 238, 238], []],
|
||||
],
|
||||
'fixClassRef' => [
|
||||
[
|
||||
|
@ -848,6 +848,7 @@ class MethodSignatureTest extends TestCase
|
||||
public function f($f): void {}
|
||||
}',
|
||||
'error_message' => 'MethodSignatureMismatch',
|
||||
['MoreSpecificImplementedParamType']
|
||||
],
|
||||
];
|
||||
}
|
||||
|
@ -85,21 +85,17 @@ class Php56Test extends TestCase
|
||||
$operators = [2, 3];
|
||||
echo add(1, ...$operators);',
|
||||
],
|
||||
'arrayPushArgumentUnpacking' => [
|
||||
'arrayPushArgumentUnpackingWithGoodArg' => [
|
||||
'<?php
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
function a(): array {
|
||||
$a = [];
|
||||
$b = ["foo", "bar"];
|
||||
$a = [];
|
||||
$b = ["foo", "bar"];
|
||||
|
||||
$a[] = "foo";
|
||||
$a[] = "foo";
|
||||
|
||||
array_push($a, ...$b);
|
||||
|
||||
return $a;
|
||||
}',
|
||||
array_push($a, ...$b);',
|
||||
'assertions' => [
|
||||
'$a' => 'non-empty-array<int, string>',
|
||||
],
|
||||
],
|
||||
'arrayMergeArgumentUnpacking' => [
|
||||
'<?php
|
||||
@ -154,11 +150,23 @@ class Php56Test extends TestCase
|
||||
function Foo(string $a, string ...$b) : void {}
|
||||
|
||||
/** @return array<int, string> */
|
||||
function Baz(string ...$b) {
|
||||
Foo(...$b);
|
||||
return $b;
|
||||
function Baz(string ...$c) {
|
||||
Foo(...$c);
|
||||
return $c;
|
||||
}',
|
||||
],
|
||||
'unpackByRefArg' => [
|
||||
'<?php
|
||||
function example (int &...$x): void {}
|
||||
$y = 0;
|
||||
example($y);
|
||||
$z = [0];
|
||||
example(...$z);',
|
||||
'assertions' => [
|
||||
'$y' => 'int',
|
||||
'$z' => 'array<int, int>',
|
||||
],
|
||||
],
|
||||
'exponentiation' => [
|
||||
'<?php
|
||||
$a = 2;
|
||||
@ -237,8 +245,10 @@ class Php56Test extends TestCase
|
||||
function foo(int ...$is) : void {}
|
||||
|
||||
$arr = [1, 2, 3, 4];
|
||||
foo(...$arr);
|
||||
foo(...$arr);',
|
||||
'assertions' => [
|
||||
'$arr' => 'array<int, int>'
|
||||
],
|
||||
],
|
||||
'iterableSplat' => [
|
||||
'<?php
|
||||
|
@ -665,7 +665,7 @@ class TemplateTest extends TestCase
|
||||
[1, 2],
|
||||
];
|
||||
|
||||
$a = splat_proof(... $foo);',
|
||||
$a = splat_proof(...$foo);',
|
||||
'assertions' => [
|
||||
'$a' => 'array<int, int>',
|
||||
],
|
||||
|
@ -19,13 +19,12 @@ class VariadicTest extends TestCase
|
||||
'somefile.php',
|
||||
'<?php
|
||||
/**
|
||||
* @param array<int, int> $a_list
|
||||
* @param int ...$a_list
|
||||
* @return void
|
||||
*/
|
||||
function f(int ...$a_list) {
|
||||
}
|
||||
f(1, 2, "3");
|
||||
'
|
||||
f(1, 2, "3");'
|
||||
);
|
||||
|
||||
$this->analyzeFile('somefile.php', new Context());
|
||||
@ -42,7 +41,7 @@ class VariadicTest extends TestCase
|
||||
/**
|
||||
* @param mixed $req
|
||||
* @param mixed $opt
|
||||
* @param array<int, mixed> $params
|
||||
* @param mixed ...$params
|
||||
* @return array<mixed>
|
||||
*/
|
||||
function f($req, $opt = null, ...$params) {
|
||||
@ -65,7 +64,7 @@ class VariadicTest extends TestCase
|
||||
'variadicArray' => [
|
||||
'<?php
|
||||
/**
|
||||
* @param array<int, int> $a_list
|
||||
* @param int ...$a_list
|
||||
* @return array<int, int>
|
||||
*/
|
||||
function f(int ...$a_list) {
|
||||
|
Loading…
Reference in New Issue
Block a user