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:
commit
a762b6c3bb
@ -39,6 +39,7 @@ use Psalm\Internal\Provider\ReturnTypeProvider\ParseUrlReturnTypeProvider;
|
|||||||
use Psalm\Internal\Provider\ReturnTypeProvider\PowReturnTypeProvider;
|
use Psalm\Internal\Provider\ReturnTypeProvider\PowReturnTypeProvider;
|
||||||
use Psalm\Internal\Provider\ReturnTypeProvider\RandReturnTypeProvider;
|
use Psalm\Internal\Provider\ReturnTypeProvider\RandReturnTypeProvider;
|
||||||
use Psalm\Internal\Provider\ReturnTypeProvider\RoundReturnTypeProvider;
|
use Psalm\Internal\Provider\ReturnTypeProvider\RoundReturnTypeProvider;
|
||||||
|
use Psalm\Internal\Provider\ReturnTypeProvider\SprintfReturnTypeProvider;
|
||||||
use Psalm\Internal\Provider\ReturnTypeProvider\StrReplaceReturnTypeProvider;
|
use Psalm\Internal\Provider\ReturnTypeProvider\StrReplaceReturnTypeProvider;
|
||||||
use Psalm\Internal\Provider\ReturnTypeProvider\StrTrReturnTypeProvider;
|
use Psalm\Internal\Provider\ReturnTypeProvider\StrTrReturnTypeProvider;
|
||||||
use Psalm\Internal\Provider\ReturnTypeProvider\TriggerErrorReturnTypeProvider;
|
use Psalm\Internal\Provider\ReturnTypeProvider\TriggerErrorReturnTypeProvider;
|
||||||
@ -105,6 +106,7 @@ class FunctionReturnTypeProvider
|
|||||||
$this->registerClass(MbInternalEncodingReturnTypeProvider::class);
|
$this->registerClass(MbInternalEncodingReturnTypeProvider::class);
|
||||||
$this->registerClass(DateReturnTypeProvider::class);
|
$this->registerClass(DateReturnTypeProvider::class);
|
||||||
$this->registerClass(PowReturnTypeProvider::class);
|
$this->registerClass(PowReturnTypeProvider::class);
|
||||||
|
$this->registerClass(SprintfReturnTypeProvider::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
124
tests/ReturnTypeProvider/SprintfTest.php
Normal file
124
tests/ReturnTypeProvider/SprintfTest.php
Normal 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',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user