Skip to content

Commit

Permalink
Merge pull request #9841 from kkmuffme/sprintf-basic-return-type-prov…
Browse files Browse the repository at this point in the history
…ider

sprintf basic non-empty-string return type provider
  • Loading branch information
orklah committed May 31, 2023
2 parents 5b2efad + 8606d55 commit a762b6c
Show file tree
Hide file tree
Showing 3 changed files with 222 additions and 0 deletions.
2 changes: 2 additions & 0 deletions src/Psalm/Internal/Provider/FunctionReturnTypeProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
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;
Expand Down Expand Up @@ -105,6 +106,7 @@ public function __construct()
$this->registerClass(MbInternalEncodingReturnTypeProvider::class);
$this->registerClass(DateReturnTypeProvider::class);
$this->registerClass(PowReturnTypeProvider::class);
$this->registerClass(SprintfReturnTypeProvider::class);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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 changes: 124 additions & 0 deletions tests/ReturnTypeProvider/SprintfTest.php
Original file line number Diff line number Diff line change
@@ -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',
],
];
}
}

0 comments on commit a762b6c

Please sign in to comment.