Skip to content

Commit 050f0e2

Browse files
committed
added Arrays::invoke() and invokeMethod() return type narrowing
1 parent 00d6b2d commit 050f0e2

6 files changed

Lines changed: 225 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,10 @@ Extensions for specific Nette packages use dedicated namespaces: `Nette\PHPStan\
6565

6666
`StringsReturnTypeExtension` (`DynamicStaticMethodReturnTypeExtension`) narrows return types of `Strings::match()`, `matchAll()` and `split()` based on boolean arguments. It resolves `captureOffset`, `unmatchedAsNull`, `patternOrder`, and `lazy` to constant booleans and constructs the precise return type — e.g. `match()` with `captureOffset: true` returns `array<array{string, int<0, max>}>|null` instead of `?array`. When a boolean argument is not a constant, falls back to the declared return type. Config: `extension-nette.neon`.
6767

68+
### ArraysInvokeTypeExtension
69+
70+
`ArraysInvokeTypeExtension` (`DynamicStaticMethodReturnTypeExtension`) narrows return types of `Arrays::invoke()` and `Arrays::invokeMethod()` from `array`. For `invoke()`, it extracts the callable return type from the iterable value type and forwards `...$args` via `ParametersAcceptorSelector::selectFromArgs()` to resolve the correct overload. For `invokeMethod()`, it resolves constant method names on the object type, gets method reflection, and forwards remaining args. Handles `callable(): void` by converting void to null. Falls back to declared return type when callbacks are not callable, method names are not constant strings, or methods don't exist on the object type. Config: `extension-nette.neon`.
71+
6872
### AssertTypeNarrowingExtension
6973

7074
`AssertTypeNarrowingExtension` (`StaticMethodTypeSpecifyingExtension` + `TypeSpecifierAwareExtension`) narrows variable types after `Tester\Assert` assertion calls. Each assertion method is mapped to an equivalent PHP expression that PHPStan already understands, then delegated to `TypeSpecifier::specifyTypesInCondition()`. Supported methods: `null`, `notNull`, `true`, `false`, `truthy`, `falsey`, `same`, `notSame`, and `type` (with built-in type strings like `'string'`, `'int'`, etc. and class/interface names). Config: `extension-nette.neon`.

extension-nette.neon

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ services:
2828
-
2929
class: Nette\PHPStan\Utils\StringsReturnTypeExtension
3030
tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension]
31+
-
32+
class: Nette\PHPStan\Utils\ArraysInvokeTypeExtension
33+
tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension]

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ includes:
4141

4242
<!---->
4343

44-
**Precise return types** — narrows return types of `Strings::match()`, `matchAll()`, `split()`, `Helpers::falseToNull()`, and `Expect::array()` based on the arguments you pass.
44+
**Precise return types** — narrows return types of `Strings::match()`, `matchAll()`, `split()`, `Helpers::falseToNull()`, `Expect::array()`, `Arrays::invoke()`, and `Arrays::invokeMethod()` based on the arguments you pass.
4545

4646
**Removes `|false` and `|null` from PHP functions** — many native functions like `getcwd`, `json_encode`, `preg_split`, `preg_replace`, and [many more](extension-php.neon) include `false` or `null` in their return type even though these error values are unrealistic on modern systems.
4747

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Nette\PHPStan\Utils;
6+
7+
use PhpParser\Node\Expr\StaticCall;
8+
use PHPStan\Analyser\Scope;
9+
use PHPStan\Reflection\MethodReflection;
10+
use PHPStan\Reflection\ParametersAcceptorSelector;
11+
use PHPStan\Type\ArrayType;
12+
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
13+
use PHPStan\Type\NullType;
14+
use PHPStan\Type\Type;
15+
use PHPStan\Type\TypeCombinator;
16+
use function array_slice, count, in_array;
17+
18+
19+
/**
20+
* Narrows return types of Arrays::invoke() and Arrays::invokeMethod()
21+
* by resolving callback/method return types and forwarding arguments.
22+
*/
23+
class ArraysInvokeTypeExtension implements DynamicStaticMethodReturnTypeExtension
24+
{
25+
public function getClass(): string
26+
{
27+
return 'Nette\Utils\Arrays';
28+
}
29+
30+
31+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
32+
{
33+
return in_array($methodReflection->getName(), ['invoke', 'invokeMethod'], true);
34+
}
35+
36+
37+
public function getTypeFromStaticMethodCall(
38+
MethodReflection $methodReflection,
39+
StaticCall $methodCall,
40+
Scope $scope,
41+
): ?Type
42+
{
43+
return match ($methodReflection->getName()) {
44+
'invoke' => $this->resolveInvoke($methodCall, $scope),
45+
'invokeMethod' => $this->resolveInvokeMethod($methodCall, $scope),
46+
default => null,
47+
};
48+
}
49+
50+
51+
private function resolveInvoke(StaticCall $call, Scope $scope): ?Type
52+
{
53+
$args = $call->getArgs();
54+
if ($args === []) {
55+
return null;
56+
}
57+
58+
$callbacksType = $scope->getType($args[0]->value);
59+
$callbackType = $callbacksType->getIterableValueType();
60+
61+
if (!$callbackType->isCallable()->yes()) {
62+
return null;
63+
}
64+
65+
$acceptors = $callbackType->getCallableParametersAcceptors($scope);
66+
$forwardedArgs = array_slice($args, 1);
67+
$selected = ParametersAcceptorSelector::selectFromArgs($scope, $forwardedArgs, $acceptors);
68+
$returnType = self::voidToNull($selected->getReturnType());
69+
70+
return new ArrayType($callbacksType->getIterableKeyType(), $returnType);
71+
}
72+
73+
74+
private function resolveInvokeMethod(StaticCall $call, Scope $scope): ?Type
75+
{
76+
$args = $call->getArgs();
77+
if (count($args) < 2) {
78+
return null;
79+
}
80+
81+
$objectsType = $scope->getType($args[0]->value);
82+
$objectType = $objectsType->getIterableValueType();
83+
84+
$constantStrings = $scope->getType($args[1]->value)->getConstantStrings();
85+
if ($constantStrings === []) {
86+
return null;
87+
}
88+
89+
$forwardedArgs = array_slice($args, 2);
90+
$returnTypes = [];
91+
92+
foreach ($constantStrings as $constantString) {
93+
$methodName = $constantString->getValue();
94+
if (!$objectType->hasMethod($methodName)->yes()) {
95+
return null;
96+
}
97+
98+
$methodReflection = $objectType->getMethod($methodName, $scope);
99+
$selected = ParametersAcceptorSelector::selectFromArgs(
100+
$scope,
101+
$forwardedArgs,
102+
$methodReflection->getVariants(),
103+
);
104+
$returnTypes[] = $selected->getReturnType();
105+
}
106+
107+
$returnType = self::voidToNull(TypeCombinator::union(...$returnTypes));
108+
return new ArrayType($objectsType->getIterableKeyType(), $returnType);
109+
}
110+
111+
112+
private static function voidToNull(Type $type): Type
113+
{
114+
return $type->isVoid()->yes() ? new NullType : $type;
115+
}
116+
}
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\Utils\Arrays;
6+
use function PHPStan\Testing\assertType;
7+
8+
9+
// invoke() — typed closures
10+
11+
12+
/** @param array<callable(): string> $callbacks */
13+
function testInvokeString(array $callbacks): void
14+
{
15+
assertType('array<string>', Arrays::invoke($callbacks));
16+
}
17+
18+
19+
/** @param array<callable(): int> $callbacks */
20+
function testInvokeInt(array $callbacks): void
21+
{
22+
assertType('array<int>', Arrays::invoke($callbacks));
23+
}
24+
25+
26+
// invoke() — string-keyed array preserves keys
27+
28+
29+
/** @param array<string, callable(): string> $callbacks */
30+
function testInvokeStringKeys(array $callbacks): void
31+
{
32+
assertType('array<string, string>', Arrays::invoke($callbacks));
33+
}
34+
35+
36+
// invoke() — forwarding args
37+
38+
39+
/** @param array<callable(string): string> $callbacks */
40+
function testInvokeForwardArgs(array $callbacks): void
41+
{
42+
assertType('array<string>', Arrays::invoke($callbacks, 'hello'));
43+
}
44+
45+
46+
/** @param array<callable(int, int): int> $callbacks */
47+
function testInvokeForwardMultipleArgs(array $callbacks): void
48+
{
49+
assertType('array<int>', Arrays::invoke($callbacks, 1, 2));
50+
}
51+
52+
53+
// invoke() — callable(): void
54+
55+
56+
/** @param list<callable(): void> $callbacks */
57+
function testInvokeVoid(array $callbacks): void
58+
{
59+
assertType('array<int<0, max>, null>', Arrays::invoke($callbacks));
60+
}
61+
62+
63+
// invoke() — typed callable parameter
64+
65+
66+
/** @param array<string, callable(): bool> $callbacks */
67+
function testInvokeTypedCallable(array $callbacks): void
68+
{
69+
assertType('array<string, bool>', Arrays::invoke($callbacks));
70+
}
71+
72+
73+
// invokeMethod() — known class and method
74+
75+
76+
/** @param array<DateTimeImmutable> $objects */
77+
function testInvokeMethodTimestamp(array $objects): void
78+
{
79+
assertType('array<int>', Arrays::invokeMethod($objects, 'getTimestamp'));
80+
}
81+
82+
83+
// invokeMethod() — string keys preserved
84+
85+
86+
/** @param array<string, DateTimeImmutable> $objects */
87+
function testInvokeMethodStringKeys(array $objects): void
88+
{
89+
assertType('array<string, int>', Arrays::invokeMethod($objects, 'getTimestamp'));
90+
}
91+
92+
93+
// invokeMethod() — forwarding args
94+
95+
96+
/** @param array<DateTimeImmutable> $objects */
97+
function testInvokeMethodFormat(array $objects): void
98+
{
99+
assertType('array<string>', Arrays::invokeMethod($objects, 'format', 'Y-m-d'));
100+
}

tests/extensions.phpt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,3 +19,4 @@ TypeAssert::assertTypes(__DIR__ . '/Tester/assert-in-function.php');
1919
// Utils
2020
TypeAssert::assertTypes(__DIR__ . '/Utils/false-to-null-return-type.php');
2121
TypeAssert::assertTypes(__DIR__ . '/Utils/strings-return-type.php');
22+
TypeAssert::assertTypes(__DIR__ . '/Utils/arrays-invoke-return-type.php');

0 commit comments

Comments
 (0)