Skip to content

Commit 00ffe78

Browse files
pepakrizondrejmirtes
authored andcommitted
Implement DynamicFunctionReturnTypeExtension interface
1 parent fa800a4 commit 00ffe78

18 files changed

+613
-79
lines changed

conf/config.neon

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,31 @@ services:
159159
-
160160
class: PHPStan\Type\FileTypeMapper
161161

162+
-
163+
class: PHPStan\Type\Php\AllArgumentBasedFunctionReturnTypeExtension
164+
tags:
165+
- phpstan.broker.dynamicFunctionReturnTypeExtension
166+
167+
-
168+
class: PHPStan\Type\Php\ArgumentBasedArrayFunctionReturnTypeExtension
169+
tags:
170+
- phpstan.broker.dynamicFunctionReturnTypeExtension
171+
172+
-
173+
class: PHPStan\Type\Php\ArgumentBasedFunctionReturnTypeExtension
174+
tags:
175+
- phpstan.broker.dynamicFunctionReturnTypeExtension
176+
177+
-
178+
class: PHPStan\Type\Php\CallbackBasedArrayFunctionReturnTypeExtension
179+
tags:
180+
- phpstan.broker.dynamicFunctionReturnTypeExtension
181+
182+
-
183+
class: PHPStan\Type\Php\CallbackBasedFunctionReturnTypeExtension
184+
tags:
185+
- phpstan.broker.dynamicFunctionReturnTypeExtension
186+
162187
broker:
163188
class: PHPStan\Broker\Broker
164189
factory: @PHPStan\Broker\BrokerFactory::create

src/Analyser/Scope.php

Lines changed: 9 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -700,89 +700,21 @@ function (Expr\ArrayItem $item): Type {
700700
}
701701

702702
if ($node instanceof FuncCall && $node->name instanceof Name) {
703-
$arrayFunctionsThatDependOnClosureReturnType = [
704-
'array_map' => 0,
705-
'array_reduce' => 1,
706-
];
707-
$functionName = strtolower((string) $node->name);
708-
if (
709-
isset($arrayFunctionsThatDependOnClosureReturnType[$functionName])
710-
&& isset($node->args[$arrayFunctionsThatDependOnClosureReturnType[$functionName]])
711-
&& $node->args[$arrayFunctionsThatDependOnClosureReturnType[$functionName]]->value instanceof Expr\Closure
712-
) {
713-
$closure = $node->args[$arrayFunctionsThatDependOnClosureReturnType[$functionName]]->value;
714-
$anonymousFunctionType = $this->getFunctionType($closure->returnType, $closure->returnType === null, false);
715-
if ($functionName === 'array_reduce') {
716-
return $anonymousFunctionType;
717-
}
718-
719-
return new ArrayType(
720-
$anonymousFunctionType,
721-
true
722-
);
723-
}
724-
725-
$arrayFunctionsThatDependOnArgumentType = [
726-
'array_filter' => 0,
727-
'array_unique' => 0,
728-
'array_reverse' => 0,
729-
];
730-
if (
731-
isset($arrayFunctionsThatDependOnArgumentType[$functionName])
732-
&& isset($node->args[$arrayFunctionsThatDependOnArgumentType[$functionName]])
733-
) {
734-
$argumentValue = $node->args[$arrayFunctionsThatDependOnArgumentType[$functionName]]->value;
735-
return $this->getType($argumentValue);
736-
}
737-
738-
$arrayFunctionsThatCreateArrayBasedOnArgumentType = [
739-
'array_fill' => 2,
740-
'array_fill_keys' => 1,
741-
];
742-
if (
743-
isset($arrayFunctionsThatCreateArrayBasedOnArgumentType[$functionName])
744-
&& isset($node->args[$arrayFunctionsThatCreateArrayBasedOnArgumentType[$functionName]])
745-
) {
746-
$argumentValue = $node->args[$arrayFunctionsThatCreateArrayBasedOnArgumentType[$functionName]]->value;
747-
748-
return new ArrayType($this->getType($argumentValue), true);
703+
if (!$this->broker->hasFunction($node->name, $this)) {
704+
return new ErrorType();
749705
}
750706

751-
$functionsThatCombineAllArgumentTypes = [
752-
'min' => '',
753-
'max' => '',
754-
];
755-
if (
756-
isset($functionsThatCombineAllArgumentTypes[$functionName])
757-
&& isset($node->args[0])
758-
) {
759-
if ($node->args[0]->unpack) {
760-
$argumentType = $this->getType($node->args[0]->value);
761-
if ($argumentType instanceof ArrayType) {
762-
return $argumentType->getItemType();
763-
}
764-
}
765-
766-
if (count($node->args) === 1) {
767-
$argumentType = $this->getType($node->args[0]->value);
768-
if ($argumentType instanceof ArrayType) {
769-
return $argumentType->getItemType();
770-
}
771-
}
707+
$functionReflection = $this->broker->getFunction($node->name, $this);
772708

773-
$argumentTypes = [];
774-
foreach ($node->args as $arg) {
775-
$argumentTypes[] = $this->getType($arg->value);
709+
foreach ($this->broker->getDynamicFunctionReturnTypeExtensions() as $dynamicFunctionReturnTypeExtension) {
710+
if (!$dynamicFunctionReturnTypeExtension->isFunctionSupported($functionReflection)) {
711+
continue;
776712
}
777713

778-
return TypeCombinator::union(...$argumentTypes);
779-
}
780-
781-
if (!$this->broker->hasFunction($node->name, $this)) {
782-
return new ErrorType();
714+
return $dynamicFunctionReturnTypeExtension->getTypeFromFunctionCall($functionReflection, $node, $this);
783715
}
784716

785-
return $this->broker->getFunction($node->name, $this)->getReturnType();
717+
return $functionReflection->getReturnType();
786718
}
787719

788720
return new MixedType();
@@ -1109,7 +1041,7 @@ private function isParameterValueNullable(Node\Param $parameter): bool
11091041
* @param bool $isVariadic
11101042
* @return Type
11111043
*/
1112-
private function getFunctionType($type = null, bool $isNullable, bool $isVariadic): Type
1044+
public function getFunctionType($type = null, bool $isNullable, bool $isVariadic): Type
11131045
{
11141046
if ($isNullable) {
11151047
return TypeCombinator::addNull(

src/Broker/Broker.php

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ class Broker
2525
/** @var \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] */
2626
private $dynamicStaticMethodReturnTypeExtensions = [];
2727

28+
/** @var \PHPStan\Type\DynamicFunctionReturnTypeExtension[] */
29+
private $dynamicFunctionReturnTypeExtensions = [];
30+
2831
/** @var \PHPStan\Reflection\ClassReflection[] */
2932
private $classReflections = [];
3033

@@ -48,6 +51,7 @@ class Broker
4851
* @param \PHPStan\Reflection\MethodsClassReflectionExtension[] $methodsClassReflectionExtensions
4952
* @param \PHPStan\Type\DynamicMethodReturnTypeExtension[] $dynamicMethodReturnTypeExtensions
5053
* @param \PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] $dynamicStaticMethodReturnTypeExtensions
54+
* @param \PHPStan\Type\DynamicFunctionReturnTypeExtension[] $dynamicFunctionReturnTypeExtensions
5155
* @param \PHPStan\Reflection\FunctionReflectionFactory $functionReflectionFactory
5256
* @param \PHPStan\Type\FileTypeMapper $fileTypeMapper
5357
*/
@@ -56,6 +60,7 @@ public function __construct(
5660
array $methodsClassReflectionExtensions,
5761
array $dynamicMethodReturnTypeExtensions,
5862
array $dynamicStaticMethodReturnTypeExtensions,
63+
array $dynamicFunctionReturnTypeExtensions,
5964
FunctionReflectionFactory $functionReflectionFactory,
6065
FileTypeMapper $fileTypeMapper
6166
)
@@ -76,6 +81,10 @@ public function __construct(
7681
$this->dynamicStaticMethodReturnTypeExtensions[$dynamicStaticMethodReturnTypeExtension->getClass()][] = $dynamicStaticMethodReturnTypeExtension;
7782
}
7883

84+
foreach ($dynamicFunctionReturnTypeExtensions as $functionReturnTypeExtension) {
85+
$this->dynamicFunctionReturnTypeExtensions[] = $functionReturnTypeExtension;
86+
}
87+
7988
$this->functionReflectionFactory = $functionReflectionFactory;
8089
$this->fileTypeMapper = $fileTypeMapper;
8190

@@ -106,6 +115,14 @@ public function getDynamicStaticMethodReturnTypeExtensionsForClass(string $class
106115
return $this->getDynamicExtensionsForType($this->dynamicStaticMethodReturnTypeExtensions, $className);
107116
}
108117

118+
/**
119+
* @return \PHPStan\Type\DynamicFunctionReturnTypeExtension[]
120+
*/
121+
public function getDynamicFunctionReturnTypeExtensions(): array
122+
{
123+
return $this->dynamicFunctionReturnTypeExtensions;
124+
}
125+
109126
/**
110127
* @param \PHPStan\Type\DynamicMethodReturnTypeExtension[]|\PHPStan\Type\DynamicStaticMethodReturnTypeExtension[] $extensions
111128
* @param string $className

src/Broker/BrokerFactory.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class BrokerFactory
1616
const METHODS_CLASS_REFLECTION_EXTENSION_TAG = 'phpstan.broker.methodsClassReflectionExtension';
1717
const DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicMethodReturnTypeExtension';
1818
const DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicStaticMethodReturnTypeExtension';
19+
const DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG = 'phpstan.broker.dynamicFunctionReturnTypeExtension';
1920

2021
/** @var \Nette\DI\Container */
2122
private $container;
@@ -43,6 +44,7 @@ public function create(): Broker
4344
array_merge([$phpClassReflectionExtension], $tagToService($this->container->findByTag(self::METHODS_CLASS_REFLECTION_EXTENSION_TAG)), [$annotationsMethodsClassReflectionExtension]),
4445
$tagToService($this->container->findByTag(self::DYNAMIC_METHOD_RETURN_TYPE_EXTENSION_TAG)),
4546
$tagToService($this->container->findByTag(self::DYNAMIC_STATIC_METHOD_RETURN_TYPE_EXTENSION_TAG)),
47+
$tagToService($this->container->findByTag(self::DYNAMIC_FUNCTION_RETURN_TYPE_EXTENSION_TAG)),
4648
$this->container->getByType(FunctionReflectionFactory::class),
4749
$this->container->getByType(FileTypeMapper::class)
4850
);

src/Command/ErrorsConsoleStyle.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ public function table(array $headers, array $rows)
3333
$maxHeaderWidth = strlen($headers[0]);
3434
foreach ($rows as $row) {
3535
$length = strlen($row[0]);
36-
if ($maxHeaderWidth === null || $length > $maxHeaderWidth) {
36+
if ($maxHeaderWidth === 0 || $length > $maxHeaderWidth) {
3737
$maxHeaderWidth = $length;
3838
}
3939
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\FunctionReflection;
8+
9+
interface DynamicFunctionReturnTypeExtension
10+
{
11+
12+
public function isFunctionSupported(FunctionReflection $functionReflection): bool;
13+
14+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type;
15+
16+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Type\ArrayType;
9+
use PHPStan\Type\Type;
10+
use PHPStan\Type\TypeCombinator;
11+
12+
class AllArgumentBasedFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
13+
{
14+
15+
/** @var string[] */
16+
private $functionNames = [
17+
'min' => '',
18+
'max' => '',
19+
];
20+
21+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
22+
{
23+
return isset($this->functionNames[strtolower($functionReflection->getName())]);
24+
}
25+
26+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
27+
{
28+
if (!isset($functionCall->args[0])) {
29+
return $functionReflection->getReturnType();
30+
}
31+
32+
if ($functionCall->args[0]->unpack) {
33+
$argumentType = $scope->getType($functionCall->args[0]->value);
34+
if ($argumentType instanceof ArrayType) {
35+
return $argumentType->getItemType();
36+
}
37+
}
38+
39+
if (count($functionCall->args) === 1) {
40+
$argumentType = $scope->getType($functionCall->args[0]->value);
41+
if ($argumentType instanceof ArrayType) {
42+
return $argumentType->getItemType();
43+
}
44+
}
45+
46+
$argumentTypes = [];
47+
foreach ($functionCall->args as $arg) {
48+
$argumentTypes[] = $scope->getType($arg->value);
49+
}
50+
51+
return TypeCombinator::union(...$argumentTypes);
52+
}
53+
54+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Type\ArrayType;
9+
use PHPStan\Type\Type;
10+
11+
class ArgumentBasedArrayFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
12+
{
13+
14+
/** @var int[] */
15+
private $functionNames = [
16+
'array_fill' => 2,
17+
'array_fill_keys' => 1,
18+
];
19+
20+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
21+
{
22+
return isset($this->functionNames[strtolower($functionReflection->getName())]);
23+
}
24+
25+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
26+
{
27+
$argumentPosition = $this->functionNames[strtolower($functionReflection->getName())];
28+
29+
if (!isset($functionCall->args[$argumentPosition])) {
30+
return $functionReflection->getReturnType();
31+
}
32+
33+
$argumentValue = $functionCall->args[$argumentPosition]->value;
34+
return new ArrayType($scope->getType($argumentValue), true);
35+
}
36+
37+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Type\Php;
4+
5+
use PhpParser\Node\Expr\FuncCall;
6+
use PHPStan\Analyser\Scope;
7+
use PHPStan\Reflection\FunctionReflection;
8+
use PHPStan\Type\Type;
9+
10+
class ArgumentBasedFunctionReturnTypeExtension implements \PHPStan\Type\DynamicFunctionReturnTypeExtension
11+
{
12+
13+
/** @var int[] */
14+
private $functionNames = [
15+
'array_filter' => 0,
16+
'array_unique' => 0,
17+
'array_reverse' => 0,
18+
];
19+
20+
public function isFunctionSupported(FunctionReflection $functionReflection): bool
21+
{
22+
return isset($this->functionNames[strtolower($functionReflection->getName())]);
23+
}
24+
25+
public function getTypeFromFunctionCall(FunctionReflection $functionReflection, FuncCall $functionCall, Scope $scope): Type
26+
{
27+
$argumentPosition = $this->functionNames[strtolower($functionReflection->getName())];
28+
29+
if (!isset($functionCall->args[$argumentPosition])) {
30+
return $functionReflection->getReturnType();
31+
}
32+
33+
$argumentValue = $functionCall->args[$argumentPosition]->value;
34+
return $scope->getType($argumentValue);
35+
}
36+
37+
}

0 commit comments

Comments
 (0)