Skip to content

Commit

Permalink
SscanfFunctionDynamicReturnTypeExtension return type extension
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm committed Jun 17, 2022
1 parent 4482fe4 commit e1a22b2
Show file tree
Hide file tree
Showing 3 changed files with 108 additions and 2 deletions.
5 changes: 5 additions & 0 deletions conf/config.neon
Expand Up @@ -1667,6 +1667,11 @@ services:
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\SscanfFunctionDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension

-
class: PHPStan\Type\Php\StrvalFamilyFunctionReturnTypeExtension
tags:
Expand Down
69 changes: 69 additions & 0 deletions src/Type/Php/SscanfFunctionDynamicReturnTypeExtension.php
@@ -0,0 +1,69 @@
<?php declare(strict_types = 1);

namespace PHPStan\Type\Php;

use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\FloatType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function in_array;
use function preg_match_all;

class SscanfFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return in_array($functionReflection->getName(), ['sscanf', 'fscanf'], true);
}

public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope,
): ?Type
{
$args = $functionCall->getArgs();
if (count($args) !== 2) {
return null;
}

$formatType = $scope->getType($args[1]->value);

if (!$formatType instanceof ConstantStringType) {
return null;
}

if (preg_match_all('/%[cdeEfosux]{1}/', $formatType->getValue(), $matches) > 0) {
$arrayBuilder = ConstantArrayTypeBuilder::createEmpty();

for ($i = 0; $i < count($matches[0]); $i++) {
$type = new StringType();

if (in_array($matches[0][$i], ['%d', '%o', '%u', '%x'], true)) {
$type = new IntegerType();
}

if (in_array($matches[0][$i], ['%e', '%E', '%f'], true)) {
$type = new FloatType();
}

$arrayBuilder->setOffsetValueType(new ConstantIntegerType($i), $type);
}

return TypeCombinator::addNull($arrayBuilder->getArray());
}

return null;
}

}
36 changes: 34 additions & 2 deletions tests/PHPStan/Analyser/data/sscanf.php
@@ -1,6 +1,38 @@
<?php

namespace Sscanf;

use function PHPStan\Testing\assertType;

assertType('int|null', sscanf('20-20', '%d-%d', $first, $second));
assertType('array|null', sscanf('20-20', '%d-%d'));
function foo(string $s) {
assertType('int|null', sscanf('20-20', '%d-%d', $first, $second));
assertType('array{int, int}|null', sscanf('20-20', '%d-%d'));
}

function sscanfFormatInference(string $s) {
assertType('int|null', sscanf($s, $s, $first, $second));
assertType('array|null', sscanf($s, $s));

assertType('array{string}|null', sscanf($s, '%c'));
assertType('array{int}|null', sscanf($s, '%d'));
assertType('array{float}|null', sscanf($s, '%e'));
assertType('array{float}|null', sscanf($s, '%E'));
assertType('array{float}|null', sscanf($s, '%f'));
assertType('array{int}|null', sscanf($s, '%o'));
assertType('array{string}|null', sscanf($s, '%s'));
assertType('array{int}|null', sscanf($s, '%u'));
assertType('array{int}|null', sscanf($s, '%x'));

$mandate = "January 01 2000";
list($month, $day, $year) = sscanf($mandate, "%s %d %d");
assertType('string', $month);
assertType('int', $day);
assertType('int', $year);
}

function fscanfFormatInference($r) {
list($month, $day, $year) = fscanf($r, "%s %d %d");
assertType('string', $month);
assertType('int', $day);
assertType('int', $year);
}

0 comments on commit e1a22b2

Please sign in to comment.