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

Merge pull request #9841 from kkmuffme/sprintf-basic-return-type-provider

sprintf basic non-empty-string return type provider
This commit is contained in:
orklah 2023-05-31 23:36:03 +02:00 committed by GitHub
commit a762b6c3bb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 222 additions and 0 deletions

View File

@ -39,6 +39,7 @@ use Psalm\Internal\Provider\ReturnTypeProvider\ParseUrlReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\PowReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\RandReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\RoundReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\SprintfReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\StrReplaceReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\StrTrReturnTypeProvider;
use Psalm\Internal\Provider\ReturnTypeProvider\TriggerErrorReturnTypeProvider;
@ -105,6 +106,7 @@ class FunctionReturnTypeProvider
$this->registerClass(MbInternalEncodingReturnTypeProvider::class);
$this->registerClass(DateReturnTypeProvider::class);
$this->registerClass(PowReturnTypeProvider::class);
$this->registerClass(SprintfReturnTypeProvider::class);
}
/**

View File

@ -0,0 +1,96 @@
<?php
namespace Psalm\Internal\Provider\ReturnTypeProvider;
use Psalm\Plugin\EventHandler\Event\FunctionReturnTypeProviderEvent;
use Psalm\Plugin\EventHandler\FunctionReturnTypeProviderInterface;
use Psalm\Type;
use Psalm\Type\Atomic\TClassString;
use Psalm\Type\Atomic\TFloat;
use Psalm\Type\Atomic\TInt;
use Psalm\Type\Atomic\TLiteralString;
use Psalm\Type\Atomic\TNonEmptyString;
use Psalm\Type\Atomic\TNumeric;
use Psalm\Type\Union;
use function array_fill;
use function count;
use function sprintf;
/**
* @internal
*/
class SprintfReturnTypeProvider implements FunctionReturnTypeProviderInterface
{
/**
* @return array<lowercase-string>
*/
public static function getFunctionIds(): array
{
return [
'sprintf',
];
}
public static function getFunctionReturnType(FunctionReturnTypeProviderEvent $event): Union
{
$statements_source = $event->getStatementsSource();
$node_type_provider = $statements_source->getNodeTypeProvider();
$call_args = $event->getCallArgs();
foreach ($call_args as $index => $call_arg) {
$type = $node_type_provider->getType($call_arg->value);
if ($type === null) {
continue;
}
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) !== '') {
return Type::getNonEmptyString();
}
}
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
if ($type->isNonEmptyString() || $type->isInt() || $type->isFloat()) {
return Type::getNonEmptyString();
}
// check for unions of either
$atomic_types = $type->getAtomicTypes();
if ($atomic_types === []) {
continue;
}
foreach ($atomic_types as $atomic_type) {
if ($atomic_type instanceof TNonEmptyString
|| $atomic_type instanceof TClassString
|| ($atomic_type instanceof TLiteralString && $atomic_type->value !== '')
|| $atomic_type instanceof TInt
|| $atomic_type instanceof TFloat
|| $atomic_type instanceof TNumeric) {
// valid non-empty types, potentially there are more though
continue;
}
// empty or generic string
// or other unhandled type
continue 2;
}
return Type::getNonEmptyString();
}
return Type::getString();
}
}

View File

@ -0,0 +1,124 @@
<?php
namespace Psalm\Tests\ReturnTypeProvider;
use Psalm\Tests\TestCase;
use Psalm\Tests\Traits\ValidCodeAnalysisTestTrait;
class SprintfTest extends TestCase
{
use ValidCodeAnalysisTestTrait;
public function providerValidCodeParse(): iterable
{
yield 'sprintfDNonEmpty' => [
'code' => '<?php
$val = sprintf("%d", implode("", array()));
',
'assertions' => [
'$val===' => 'non-empty-string',
],
];
yield 'sprintfFormatNonEmpty' => [
'code' => '<?php
$val = sprintf("%s %s", "", "");
',
'assertions' => [
'$val===' => 'non-empty-string',
],
];
yield 'sprintfArgnumFormatNonEmpty' => [
'code' => '<?php
$val = sprintf("%2\$s %1\$s", "", "");
',
'assertions' => [
'$val===' => 'non-empty-string',
],
];
yield 'sprintfLiteralFormatNonEmpty' => [
'code' => '<?php
$val = sprintf("%s hello", "");
',
'assertions' => [
'$val===' => 'non-empty-string',
],
];
yield 'sprintfStringPlaceholderLiteralIntParamFormatNonEmpty' => [
'code' => '<?php
$val = sprintf("%s", 15);
',
'assertions' => [
'$val===' => 'non-empty-string',
],
];
yield 'sprintfStringPlaceholderIntParamFormatNonEmpty' => [
'code' => '<?php
$val = sprintf("%s", crc32(uniqid()));
',
'assertions' => [
'$val===' => 'non-empty-string',
],
];
yield 'sprintfStringPlaceholderFloatParamFormatNonEmpty' => [
'code' => '<?php
$val = sprintf("%s", microtime(true));
',
'assertions' => [
'$val===' => 'non-empty-string',
],
];
yield 'sprintfStringPlaceholderIntStringParamFormatNonEmpty' => [
'code' => '<?php
$tmp = rand(0, 10) > 5 ? time() : implode("", array()) . "hello";
$val = sprintf("%s", $tmp);
',
'assertions' => [
'$val===' => 'non-empty-string',
],
];
yield 'sprintfStringPlaceholderLiteralStringParamFormat' => [
'code' => '<?php
$val = sprintf("%s", "");
',
'assertions' => [
'$val===' => 'string',
],
];
yield 'sprintfStringPlaceholderStringParamFormat' => [
'code' => '<?php
$val = sprintf("%s", implode("", array()));
',
'assertions' => [
'$val===' => 'string',
],
];
yield 'sprintfStringArgnumPlaceholderStringParamsFormat' => [
'code' => '<?php
$val = sprintf("%2\$s%1\$s", "", implode("", array()));
',
'assertions' => [
'$val===' => 'string',
],
];
yield 'sprintfStringPlaceholderIntStringParamFormat' => [
'code' => '<?php
$tmp = rand(0, 10) > 5 ? time() : implode("", array());
$val = sprintf("%s", $tmp);
',
'assertions' => [
'$val===' => 'string',
],
];
}
}