Skip to content

Commit 15bcdd8

Browse files
committed
added Nette\Schema\Expect::array() return type narrowing
1 parent 79f8cd1 commit 15bcdd8

8 files changed

Lines changed: 143 additions & 1 deletion

File tree

CLAUDE.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,14 @@ Each extension class is registered as a service in NEON with the appropriate tag
3636
- `phpstan.broker.methodsClassReflectionExtension` — magic methods
3737
- `phpstan.broker.typeSpecifyingExtension` — type narrowing
3838

39+
### Namespace conventions
40+
41+
Extensions for specific Nette packages use dedicated namespaces: `Nette\PHPStan\Schema\` for nette/schema, future packages follow the same pattern (`Nette\PHPStan\Forms\`, `Nette\PHPStan\Application\`, etc.). Generic extensions use `Nette\PHPStan\Type\` or `Nette\PHPStan\Analyser\`.
42+
43+
### ExpectArrayReturnTypeExtension
44+
45+
`ExpectArrayReturnTypeExtension` (`DynamicStaticMethodReturnTypeExtension`) narrows the return type of `Expect::array()` from `Structure|Type` to `Structure` or `Type`. It inspects the argument: no argument, null, empty array, or non-Schema values → `Type`; all values implementing `Schema``Structure`; mixed/unknown → fallback to declared union. Config: `extension-schema.neon`.
46+
3947
### NarrowReturnTypeResolver
4048

4149
`NarrowReturnTypeResolver` (`ExpressionTypeResolverExtension`) removes `|false` from return types of native PHP functions and methods where false is trivial or outdated. It handles `FuncCall`, `MethodCall`, and `StaticCall` in a single class. Configuration uses a flat list in NEON — plain names for functions (`json_encode`), `Class::method` notation for methods (`Normalizer::normalize`). It runs before all `DynamicReturnTypeExtension` implementations, delegates to them via `DynamicReturnTypeExtensionRegistry`, and strips `|false` from the result.

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@
1616
"phpstan/phpstan": "^2.1"
1717
},
1818
"require-dev": {
19-
"nette/tester": "^2.6"
19+
"nette/tester": "^2.6",
20+
"nette/schema": "^1.3"
2021
},
2122
"autoload": {
2223
"psr-4": {

extension-schema.neon

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
services:
2+
-
3+
class: Nette\PHPStan\Schema\ExpectArrayReturnTypeExtension
4+
tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension]

extension.neon

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ parameters:
1515

1616
includes:
1717
- extension-narrowReturnType.neon
18+
- extension-schema.neon
1819

1920
services:
2021
-

readme.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,12 @@ $cwd = getcwd();
5555

5656
 <!---->
5757

58+
### Other
59+
60+
- Narrows the return type of `Expect::array()` from `Structure|Type` to `Structure` or `Type` based on the argument
61+
62+
<!---->
63+
5864
### Type Validation Call Ignore
5965

6066
Suppresses `expr.resultUnused` for the runtime type validation pattern commonly used in Nette:
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Nette\PHPStan\Schema;
6+
7+
use Nette\Schema\Elements\Structure;
8+
use Nette\Schema\Elements\Type;
9+
use Nette\Schema\Expect;
10+
use Nette\Schema\Schema;
11+
use PhpParser\Node\Expr\StaticCall;
12+
use PHPStan\Analyser\Scope;
13+
use PHPStan\Reflection\MethodReflection;
14+
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
15+
use PHPStan\Type\ObjectType;
16+
use PHPStan\Type\Type as PhpStanType;
17+
18+
19+
/**
20+
* Narrows the return type of Expect::array() from Structure|Type
21+
* to Structure or Type based on the argument content.
22+
*/
23+
class ExpectArrayReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
24+
{
25+
public function getClass(): string
26+
{
27+
return Expect::class;
28+
}
29+
30+
31+
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
32+
{
33+
return $methodReflection->getName() === 'array';
34+
}
35+
36+
37+
public function getTypeFromStaticMethodCall(
38+
MethodReflection $methodReflection,
39+
StaticCall $methodCall,
40+
Scope $scope,
41+
): ?PhpStanType
42+
{
43+
$args = $methodCall->getArgs();
44+
if ($args === []) {
45+
return new ObjectType(Type::class);
46+
}
47+
48+
$argType = $scope->getType($args[0]->value);
49+
50+
if ($argType->isNull()->yes()) {
51+
return new ObjectType(Type::class);
52+
}
53+
54+
$constantArrays = $argType->getConstantArrays();
55+
if ($constantArrays === []) {
56+
return null;
57+
}
58+
59+
$valueTypes = $constantArrays[0]->getValueTypes();
60+
if ($valueTypes === []) {
61+
return new ObjectType(Type::class);
62+
}
63+
64+
$schemaType = new ObjectType(Schema::class);
65+
$hasSchema = false;
66+
$hasNonSchema = false;
67+
68+
foreach ($valueTypes as $valueType) {
69+
if ($schemaType->isSuperTypeOf($valueType)->yes()) {
70+
$hasSchema = true;
71+
} else {
72+
$hasNonSchema = true;
73+
}
74+
}
75+
76+
if ($hasSchema && !$hasNonSchema) {
77+
return new ObjectType(Structure::class);
78+
}
79+
80+
if ($hasNonSchema && !$hasSchema) {
81+
return new ObjectType(Type::class);
82+
}
83+
84+
return null;
85+
}
86+
}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php declare(strict_types=1);
2+
3+
/**
4+
* Test: Expect::array() return type narrowing.
5+
*/
6+
7+
require __DIR__ . '/../bootstrap.php';
8+
9+
use Nette\PHPStan\Tester\TypeAssert;
10+
11+
TypeAssert::assertTypes(__DIR__ . '/../data/schema/expect-array-return-type.php', [__DIR__ . '/../../extension.neon']);
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\Schema\Elements\Structure;
6+
use Nette\Schema\Elements\Type;
7+
use Nette\Schema\Expect;
8+
use function PHPStan\Testing\assertType;
9+
10+
11+
// No argument → Type
12+
assertType(Type::class, Expect::array());
13+
14+
// Empty array → Type
15+
assertType(Type::class, Expect::array([]));
16+
17+
// Non-Schema values → Type
18+
assertType(Type::class, Expect::array(['key1' => 'val1', 'val3']));
19+
20+
// Schema values (shape definition) → Structure
21+
assertType(Structure::class, Expect::array(['a' => Expect::string()]));
22+
assertType(Structure::class, Expect::array([Expect::int(), Expect::string()]));
23+
24+
// Null argument → Type
25+
assertType(Type::class, Expect::array(null));

0 commit comments

Comments
 (0)