mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
check args for sprintf + printf
* check args for (s)printf for Invalid/TooMany/TooFew * don't change the return type when we cannot infer it more specifically (nullable return) * fix https://github.com/vimeo/psalm/issues/9874 * implement https://github.com/vimeo/psalm/issues/9817 * add tests
This commit is contained in:
parent
e3f589d79e
commit
3179498643
@ -2,6 +2,12 @@
|
||||
|
||||
namespace Psalm\Internal\Provider\ReturnTypeProvider;
|
||||
|
||||
use ArgumentCountError;
|
||||
use ValueError;
|
||||
use Psalm\Issue\InvalidArgument;
|
||||
use Psalm\Issue\TooFewArguments;
|
||||
use Psalm\Issue\TooManyArguments;
|
||||
use Psalm\IssueBuffer;
|
||||
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
|
||||
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
|
||||
use Psalm\Type;
|
||||
@ -28,16 +34,32 @@ class SprintfReturnTypeProvider implements FunctionReturnTypeProviderInterface
|
||||
public static function getFunctionIds(): array
|
||||
{
|
||||
return [
|
||||
'printf',
|
||||
'sprintf',
|
||||
];
|
||||
}
|
||||
|
||||
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): Union
|
||||
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): ?Union
|
||||
{
|
||||
$statements_source = $event->getStatementsSource();
|
||||
$node_type_provider = $statements_source->getNodeTypeProvider();
|
||||
|
||||
$call_args = $event->getCallArgs();
|
||||
|
||||
// it makes no sense to use sprintf/printf when there is only 1 arg (the format)
|
||||
// as it wouldn't have any placeholders
|
||||
if (count($call_args) === 1) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new TooFewArguments(
|
||||
'Too few arguments for ' . $event->getFunctionId() . ', expecting at least 2 arguments',
|
||||
$event->getCodeLocation(),
|
||||
$event->getFunctionId(),
|
||||
),
|
||||
$statements_source->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$node_type_provider = $statements_source->getNodeTypeProvider();
|
||||
foreach ($call_args as $index => $call_arg) {
|
||||
$type = $node_type_provider->getType($call_arg->value);
|
||||
if ($type === null) {
|
||||
@ -45,23 +67,120 @@ class SprintfReturnTypeProvider implements FunctionReturnTypeProviderInterface
|
||||
}
|
||||
|
||||
if ($index === 0 && $type->isSingleStringLiteral()) {
|
||||
// use empty string dummies to check if the format itself produces a non-empty return value
|
||||
// faster than validating the pattern and checking all args separately
|
||||
$dummy = array_fill(0, count($call_args) - 1, '');
|
||||
if (sprintf($type->getSingleStringLiteral()->value, ...$dummy) !== '') {
|
||||
$args_count = count($call_args) - 1;
|
||||
$dummy = array_fill(0, $args_count, '');
|
||||
|
||||
// check if we have enough/too many arguments and a valid format
|
||||
$result = null;
|
||||
$initial_result = null;
|
||||
while (count($dummy) > -1) {
|
||||
try {
|
||||
// before PHP 8, an uncatchable Warning is thrown if too few arguments are passed, which is ignored and handled below instead
|
||||
$result = @sprintf($type->getSingleStringLiteral()->value, ...$dummy);
|
||||
if ($initial_result === null) {
|
||||
$initial_result = $result;
|
||||
}
|
||||
} catch(ValueError $value_error) {
|
||||
// PHP 8
|
||||
// the format is invalid
|
||||
IssueBuffer::maybeAdd(
|
||||
new InvalidArgument(
|
||||
'Argument 1 of ' . $event->getFunctionId() . ' is invalid - ' . $value_error->getMessage(),
|
||||
$event->getCodeLocation(),
|
||||
$event->getFunctionId(),
|
||||
),
|
||||
$statements_source->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
return null;
|
||||
} catch(ArgumentCountError $error) {
|
||||
// PHP 8
|
||||
if (count($dummy) >= $args_count) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new TooFewArguments(
|
||||
'Too few arguments for ' . $event->getFunctionId(),
|
||||
$event->getCodeLocation(),
|
||||
$event->getFunctionId(),
|
||||
),
|
||||
$statements_source->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
// we are in the next iteration, so we have 1 placeholder less here
|
||||
// otherwise we would have reported an error above already
|
||||
if (count($dummy) + 1 === $args_count) {
|
||||
break;
|
||||
}
|
||||
|
||||
IssueBuffer::maybeAdd(
|
||||
new TooManyArguments(
|
||||
'Too many arguments for the number of placeholders in ' . $event->getFunctionId(),
|
||||
$event->getCodeLocation(),
|
||||
$event->getFunctionId(),
|
||||
),
|
||||
$statements_source->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
// PHP 7
|
||||
if ($result === false && count($dummy) >= $args_count) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new TooFewArguments(
|
||||
'Too few arguments for ' . $event->getFunctionId(),
|
||||
$event->getCodeLocation(),
|
||||
$event->getFunctionId(),
|
||||
),
|
||||
$statements_source->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
return Type::getFalse();
|
||||
}
|
||||
|
||||
// PHP 7
|
||||
if ($result === false && count($dummy) + 1 !== $args_count) {
|
||||
IssueBuffer::maybeAdd(
|
||||
new TooManyArguments(
|
||||
'Too many arguments for the number of placeholders in ' . $event->getFunctionId(),
|
||||
$event->getCodeLocation(),
|
||||
$event->getFunctionId(),
|
||||
),
|
||||
$statements_source->getSuppressedIssues(),
|
||||
);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
array_pop($dummy);
|
||||
}
|
||||
|
||||
if ($event->getFunctionId() === 'printf') {
|
||||
// printf only has the format validated above
|
||||
// don't change the return type
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($initial_result !== null && $initial_result !== false && $initial_result !== '') {
|
||||
return Type::getNonEmptyString();
|
||||
}
|
||||
}
|
||||
|
||||
if ($index === 0 && $event->getFunctionId() === 'printf') {
|
||||
// printf only has the format validated above
|
||||
// don't change the return type
|
||||
return null;
|
||||
}
|
||||
|
||||
if ($index === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// if the function has more arguments than the pattern has placeholders, this could be a false positive
|
||||
// if the param is not used in the pattern
|
||||
// however we would need to analyze the format arg to check that
|
||||
// can be done eventually to also implement https://github.com/vimeo/psalm/issues/9818
|
||||
// and https://github.com/vimeo/psalm/issues/9817
|
||||
// however this is already reported above and returned, so this cannot happen
|
||||
if ($type->isNonEmptyString() || $type->isInt() || $type->isFloat()) {
|
||||
return Type::getNonEmptyString();
|
||||
}
|
||||
@ -91,6 +210,6 @@ class SprintfReturnTypeProvider implements FunctionReturnTypeProviderInterface
|
||||
return Type::getNonEmptyString();
|
||||
}
|
||||
|
||||
return Type::getString();
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -3,10 +3,12 @@
|
||||
namespace Psalm\Tests\ReturnTypeProvider;
|
||||
|
||||
use Psalm\Tests\TestCase;
|
||||
use Psalm\Tests\Traits\InvalidCodeAnalysisTestTrait;
|
||||
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
|
||||
|
||||
class SprintfTest extends TestCase
|
||||
{
|
||||
use InvalidCodeAnalysisTestTrait;
|
||||
use ValidCodeAnalysisTestTrait;
|
||||
|
||||
public function providerValidCodeParse(): iterable
|
||||
@ -120,5 +122,68 @@ class SprintfTest extends TestCase
|
||||
'$val===' => 'string',
|
||||
],
|
||||
];
|
||||
|
||||
yield 'printfSimple' => [
|
||||
'code' => '<?php
|
||||
$val = printf("%s", "hello");
|
||||
',
|
||||
'assertions' => [
|
||||
'$val===' => 'int<0, max>',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
public function providerInvalidCodeParse(): iterable
|
||||
{
|
||||
return [
|
||||
'sprintfOnlyFormat' => [
|
||||
'code' => '<?php
|
||||
$x = sprintf("hello");
|
||||
',
|
||||
'error_message' => 'TooFewArguments',
|
||||
],
|
||||
'sprintfTooFewArguments' => [
|
||||
'code' => '<?php
|
||||
$x = sprintf("%s hello %d", "a");
|
||||
',
|
||||
'error_message' => 'TooFewArguments',
|
||||
],
|
||||
'sprintfTooManyArguments' => [
|
||||
'code' => '<?php
|
||||
$x = sprintf("%s hello", "a", "b");
|
||||
',
|
||||
'error_message' => 'TooManyArguments',
|
||||
],
|
||||
'sprintfInvalidFormat' => [
|
||||
'code' => '<?php
|
||||
$x = sprintf(\'"%" hello\', "a");
|
||||
',
|
||||
'error_message' => 'InvalidArgument',
|
||||
],
|
||||
'printfOnlyFormat' => [
|
||||
'code' => '<?php
|
||||
printf("hello");
|
||||
',
|
||||
'error_message' => 'TooFewArguments',
|
||||
],
|
||||
'printfTooFewArguments' => [
|
||||
'code' => '<?php
|
||||
printf("%s hello %d", "a");
|
||||
',
|
||||
'error_message' => 'TooFewArguments',
|
||||
],
|
||||
'printfTooManyArguments' => [
|
||||
'code' => '<?php
|
||||
printf("%s hello", "a", "b");
|
||||
',
|
||||
'error_message' => 'TooManyArguments',
|
||||
],
|
||||
'printfInvalidFormat' => [
|
||||
'code' => '<?php
|
||||
printf(\'"%" hello\', "a");
|
||||
',
|
||||
'error_message' => 'InvalidArgument',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user