Skip to content

Commit

Permalink
Fix pathinfo($s, PATHINFO_ALL) return type
Browse files Browse the repository at this point in the history
  • Loading branch information
staabm committed Dec 19, 2022
1 parent d28c6ce commit 2feb9a7
Show file tree
Hide file tree
Showing 4 changed files with 76 additions and 12 deletions.
59 changes: 47 additions & 12 deletions src/Type/Php/PathinfoFunctionDynamicReturnTypeExtension.php
Expand Up @@ -5,17 +5,25 @@
use PhpParser\Node;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use function count;
use function sprintf;

class PathinfoFunctionDynamicReturnTypeExtension implements DynamicFunctionReturnTypeExtension
{

public function __construct(private ReflectionProvider $reflectionProvider)
{
}

public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'pathinfo';
Expand All @@ -25,24 +33,51 @@ public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
Node\Expr\FuncCall $functionCall,
Scope $scope,
): Type
): ?Type
{
$argsCount = count($functionCall->getArgs());
if ($argsCount === 0) {
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
} elseif ($argsCount === 1) {
$pathType = $scope->getType($functionCall->getArgs()[0]->value);
return null;
}

$pathType = $scope->getType($functionCall->getArgs()[0]->value);

$builder = ConstantArrayTypeBuilder::createEmpty();
$builder->setOffsetValueType(new ConstantStringType('dirname'), new StringType(), !$pathType->isNonEmptyString()->yes());
$builder->setOffsetValueType(new ConstantStringType('basename'), new StringType());
$builder->setOffsetValueType(new ConstantStringType('extension'), new StringType(), true);
$builder->setOffsetValueType(new ConstantStringType('filename'), new StringType());
$arrayType = $builder->getArray();

if ($argsCount === 1) {
return $arrayType;
} else {
$flagsType = $scope->getType($functionCall->getArgs()[1]->value);
if ($flagsType instanceof ConstantIntegerType) {
if ($flagsType->getValue() === $this->getConstant('PATHINFO_ALL')) {
return $arrayType;
}

$builder = ConstantArrayTypeBuilder::createEmpty();
$builder->setOffsetValueType(new ConstantStringType('dirname'), new StringType(), !$pathType->isNonEmptyString()->yes());
$builder->setOffsetValueType(new ConstantStringType('basename'), new StringType());
$builder->setOffsetValueType(new ConstantStringType('extension'), new StringType(), true);
$builder->setOffsetValueType(new ConstantStringType('filename'), new StringType());
return new StringType();
}
}

return TypeCombinator::union($arrayType, new StringType());
}

private function getConstant(string $constantName): ?int
{
if (!$this->reflectionProvider->hasConstant(new Node\Name($constantName), null)) {
return null;
}

return $builder->getArray();
$constant = $this->reflectionProvider->getConstant(new Node\Name($constantName), null);
$valueType = $constant->getValueType();
if (!$valueType instanceof ConstantIntegerType) {
throw new ShouldNotHappenException(sprintf('Constant %s does not have integer type.', $constantName));
}

return new StringType();
return $valueType->getValue();
}

}
5 changes: 5 additions & 0 deletions tests/PHPStan/Analyser/NodeScopeResolverTest.php
Expand Up @@ -1149,6 +1149,11 @@ public function dataFileAsserts(): iterable

yield from $this->gatherAssertTypes(__DIR__ . '/data/bug-8520.php');
yield from $this->gatherAssertTypes(__DIR__ . '/data/filter-var-dynamic-return-type-extension-regression.php');

if (PHP_VERSION_ID >= 80000) {
yield from $this->gatherAssertTypes(__DIR__ . '/data/pathinfo-php8.php');
}
yield from $this->gatherAssertTypes(__DIR__ . '/data/pathinfo.php');
}

/**
Expand Down
9 changes: 9 additions & 0 deletions tests/PHPStan/Analyser/data/pathinfo-php8.php
@@ -0,0 +1,9 @@
<?php

namespace pathinfo;

use function PHPStan\Testing\assertType;

function doFoo(string $s, int $i) {
assertType('array{dirname?: string, basename: string, extension?: string, filename: string}', pathinfo($s, PATHINFO_ALL));
}
15 changes: 15 additions & 0 deletions tests/PHPStan/Analyser/data/pathinfo.php
@@ -0,0 +1,15 @@
<?php

namespace pathinfoInference;

use function PHPStan\Testing\assertType;

function doFoo(string $s, int $i) {
assertType('array{dirname?: string, basename: string, extension?: string, filename: string}|string', pathinfo($s, $i));
assertType('array{dirname?: string, basename: string, extension?: string, filename: string}', pathinfo($s));

assertType('string', pathinfo($s, PATHINFO_DIRNAME));
assertType('string', pathinfo($s, PATHINFO_BASENAME));
assertType('string', pathinfo($s, PATHINFO_EXTENSION));
assertType('string', pathinfo($s, PATHINFO_FILENAME));
}

0 comments on commit 2feb9a7

Please sign in to comment.