forked from vimeo/psalm
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
sprintf basic non-empty-string return type provider
Fix vimeo#9819 This PR is a starting point for improving the sprintf return type and eventually validate the format, param types and param count. (see vimeo#9817, vimeo#9818)
- Loading branch information
Showing
3 changed files
with
222 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
96 changes: 96 additions & 0 deletions
96
src/Psalm/Internal/Provider/ReturnTypeProvider/SprintfReturnTypeProvider.php
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 === array()) { | ||
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 types | ||
continue; | ||
} | ||
|
||
// empty or generic string | ||
// or other unhandled type | ||
continue 2; | ||
} | ||
|
||
return Type::getNonEmptyString(); | ||
} | ||
|
||
return Type::getString(); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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", sleep(0)); | ||
', | ||
'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', | ||
], | ||
]; | ||
} | ||
} |