Skip to content

Commit e6876b3

Browse files
committed
feat: get array types working (pass variable around)
1 parent 72d361e commit e6876b3

84 files changed

Lines changed: 966 additions & 209 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/e2e-tests.yml‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,10 @@ jobs:
539539
echo "FAIL: tmp was created by a worker, meaning sys_temp_dir='tmp~1' was incorrectly evaluated to 'tmp'"
540540
exit 1
541541
fi
542+
- script: |
543+
cd e2e/parameter-type-extension
544+
composer install
545+
../../bin/phpstan analyze
542546
543547
steps:
544548
- name: Harden the runner (Audit all outbound calls)
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/vendor
2+
/composer.lock
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
{
2+
"autoload": {
3+
"psr-4": {
4+
"App\\": "src/"
5+
}
6+
}
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
parameters:
2+
ignoreErrors:
3+
-
4+
message: '#^Parameter \#1 \$relations of method App\\Builder\<App\\User\>\:\:with\(\) expects array\<string, Closure\(App\\Relation\<\*, \*, \*\>\)\: mixed\>, array\{car\: Closure\(App\\HasOne\)\: App\\HasOne, monitorable\: Closure\(App\\MorphTo\)\: App\\MorphTo\} given\.$#'
5+
identifier: argument.type
6+
count: 1
7+
path: src/test.php
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
includes:
2+
- phpstan-baseline.neon
3+
parameters:
4+
level: 9
5+
paths:
6+
- src
7+
services:
8+
-
9+
class: App\ParameterTypeExtension
10+
tags:
11+
- phpstan.dynamicMethodParameterTypeExtension
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace App;
6+
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Reflection\MethodReflection;
9+
use PHPStan\Reflection\ParameterReflection;
10+
use PHPStan\Reflection\PassedByReference;
11+
use PHPStan\Type\ClosureType;
12+
use PHPStan\Type\DynamicMethodParameterTypeExtension;
13+
use PHPStan\Type\MixedType;
14+
use PHPStan\Type\ObjectType;
15+
use PHPStan\Type\Type;
16+
use PhpParser\Node\Expr\MethodCall;
17+
18+
final class ParameterTypeExtension implements DynamicMethodParameterTypeExtension
19+
{
20+
public function isMethodSupported(MethodReflection $methodReflection, ParameterReflection $parameter): bool
21+
{
22+
if (! $methodReflection->getDeclaringClass()->is(Builder::class)) {
23+
return false;
24+
}
25+
26+
return $methodReflection->getName() === 'with';
27+
}
28+
29+
public function getTypeFromMethodCall(
30+
MethodReflection $methodReflection,
31+
MethodCall $methodCall,
32+
ParameterReflection $parameter,
33+
Scope $scope,
34+
): Type|null {
35+
$arg = $methodCall->getArgs()[0] ?? null;
36+
if (!$arg) {
37+
return null;
38+
}
39+
40+
$type = $scope->getType($arg->value)->getConstantArrays()[0] ?? null;
41+
if (!$type) {
42+
return null;
43+
}
44+
45+
$model = $scope->getType($methodCall->var)
46+
->getTemplateType(Builder::class, 'TModel')
47+
->getObjectClassNames()[0] ?? null;
48+
if (!$model) {
49+
return null;
50+
}
51+
52+
foreach ($type->getKeyTypes() as $keyType) {
53+
$relationType = $this->getRelationTypeFromModel($model, (string) $keyType->getValue(), $scope);
54+
if (!$relationType) {
55+
continue;
56+
}
57+
58+
$newType = new ClosureType([
59+
new class('test', $relationType) implements ParameterReflection {
60+
public function __construct(private string $name, private Type $type) {}
61+
public function getName(): string
62+
{
63+
return $this->name;
64+
}
65+
public function isOptional(): bool
66+
{
67+
return false;
68+
}
69+
public function getType(): Type
70+
{
71+
return $this->type;
72+
}
73+
public function passedByReference(): PassedByReference
74+
{
75+
return PassedByReference::createNo();
76+
}
77+
public function isVariadic(): bool
78+
{
79+
return false;
80+
}
81+
public function getDefaultValue(): ?Type
82+
{
83+
return null;
84+
}
85+
},
86+
], new MixedType(), false);
87+
88+
$type = $type->setOffsetValueType($keyType, $newType, false);
89+
}
90+
91+
return $type;
92+
}
93+
94+
public function getRelationTypeFromModel(string $model, string $relation, Scope $scope): ?Type
95+
{
96+
$modelType = new ObjectType($model);
97+
98+
if (! $modelType->hasMethod($relation)->yes()) {
99+
return null;
100+
}
101+
102+
$relationType = $modelType->getMethod($relation, $scope)->getVariants()[0]->getReturnType();
103+
104+
if (! (new ObjectType(Relation::class))->isSuperTypeOf($relationType)->yes()) {
105+
return null;
106+
}
107+
108+
return $relationType;
109+
}
110+
}
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace App;
4+
5+
use function PHPStan\Testing\assertType;
6+
7+
abstract class Model {}
8+
9+
class Monitor extends Model {}
10+
11+
class Car extends Model {}
12+
13+
class User extends Model
14+
{
15+
/** @return HasOne<Car, $this> */
16+
public function car(): HasOne
17+
{
18+
return new HasOne(); // @phpstan-ignore return.type
19+
}
20+
21+
/** @return MorphTo<Monitor, $this> */
22+
public function monitorable(): MorphTo
23+
{
24+
return new MorphTo(); // @phpstan-ignore return.type
25+
}
26+
}
27+
28+
/**
29+
* @template TRelatedModel of Model
30+
* @template TDeclaringModel of Model
31+
* @template TResult
32+
*/
33+
class Relation {
34+
/**
35+
* @param list<string> $columns
36+
* @return $this
37+
*/
38+
public function select(array $columns): static
39+
{
40+
return $this;
41+
}
42+
}
43+
44+
/**
45+
* @template TRelatedModel of Model
46+
* @template TDeclaringModel of Model
47+
* @extends Relation<TRelatedModel, TDeclaringModel, ?TRelatedModel>
48+
*/
49+
class HasOne extends Relation {}
50+
51+
/**
52+
* @template TRelatedModel of Model
53+
* @template TDeclaringModel of Model
54+
* @extends Relation<TRelatedModel, TDeclaringModel, ?TRelatedModel>
55+
*/
56+
class MorphTo extends Relation {
57+
/** @return $this */
58+
public function morphWith(): static
59+
{
60+
return $this;
61+
}
62+
}
63+
64+
/** @template TModel of Model */
65+
class Builder
66+
{
67+
/**
68+
* @param array<string, \Closure(Relation<*, *, *>): mixed> $relations
69+
* @return $this
70+
*/
71+
public function with(array $relations): static
72+
{
73+
return $this;
74+
}
75+
}
76+
77+
/** @param Builder<User> $query */
78+
function test(Builder $query): void
79+
{
80+
$query->with([
81+
'car' => function ($r) { assertType('App\HasOne<App\Car, App\User>', $r); },
82+
'monitorable' => function ($r) { assertType('App\MorphTo<App\Monitor, App\User>', $r); },
83+
]);
84+
$query->with([
85+
'car' => fn (HasOne $q) => $q->select(['id']),
86+
'monitorable' => fn (MorphTo $q) => $q->morphWith(),
87+
]);
88+
}

‎src/Analyser/ExprHandler.php‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public function processExpr(
3030
ExpressionResultStorage $storage,
3131
callable $nodeCallback,
3232
ExpressionContext $context,
33+
?Type $overriddenType,
3334
): ExpressionResult;
3435

3536
/**

‎src/Analyser/ExprHandler/ArrayDimFetchHandler.php‎

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
7474
);
7575
}
7676

77-
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult
77+
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context, ?Type $overriddenType): ExpressionResult
7878
{
7979
if ($expr->dim === null) {
80-
$varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep());
80+
$varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $scope, $storage, $nodeCallback, $context->enterDeep(), null);
8181
$scope = $varResult->getScope();
8282

8383
return new ExpressionResult(
@@ -91,8 +91,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
9191
);
9292
}
9393

94-
$dimResult = $nodeScopeResolver->processExprNode($stmt, $expr->dim, $scope, $storage, $nodeCallback, $context->enterDeep());
95-
$varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $dimResult->getScope(), $storage, $nodeCallback, $context->enterDeep());
94+
$dimResult = $nodeScopeResolver->processExprNode($stmt, $expr->dim, $scope, $storage, $nodeCallback, $context->enterDeep(), null);
95+
$varResult = $nodeScopeResolver->processExprNode($stmt, $expr->var, $dimResult->getScope(), $storage, $nodeCallback, $context->enterDeep(), null);
9696
$throwPoints = array_merge($dimResult->getThrowPoints(), $varResult->getThrowPoints());
9797
$impurePoints = array_merge($dimResult->getImpurePoints(), $varResult->getImpurePoints());
9898
$scope = $varResult->getScope();
@@ -106,6 +106,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
106106
$storage,
107107
new NoopNodeCallback(),
108108
$context,
109+
null,
109110
)->getThrowPoints());
110111
}
111112

‎src/Analyser/ExprHandler/ArrayHandler.php‎

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
use PHPStan\Node\LiteralArrayNode;
2424
use PHPStan\Reflection\InitializerExprTypeResolver;
2525
use PHPStan\Type\CallableType;
26+
use PHPStan\Type\Constant\ConstantIntegerType;
2627
use PHPStan\Type\Type;
2728
use PHPStan\Type\TypeCombinator;
2829
use function array_merge;
@@ -70,26 +71,40 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
7071
return $type;
7172
}
7273

73-
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult
74+
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context, ?Type $overriddenType): ExpressionResult
7475
{
7576
$itemNodes = [];
7677
$hasYield = false;
7778
$throwPoints = [];
7879
$impurePoints = [];
7980
$isAlwaysTerminating = false;
81+
$nextAutoIndex = 0;
8082
foreach ($expr->items as $arrayItem) {
8183
$itemNodes[] = new LiteralArrayItem($scope, $arrayItem);
8284
$nodeScopeResolver->callNodeCallback($nodeCallback, $arrayItem, $scope, $storage);
85+
$keyType = new ConstantIntegerType($nextAutoIndex);
8386
if ($arrayItem->key !== null) {
84-
$keyResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->key, $scope, $storage, $nodeCallback, $context->enterDeep());
87+
$keyType = $scope->getType($arrayItem->key);
88+
$keyResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->key, $scope, $storage, $nodeCallback, $context->enterDeep(), null);
8589
$hasYield = $hasYield || $keyResult->hasYield();
8690
$throwPoints = array_merge($throwPoints, $keyResult->getThrowPoints());
8791
$impurePoints = array_merge($impurePoints, $keyResult->getImpurePoints());
8892
$isAlwaysTerminating = $isAlwaysTerminating || $keyResult->isAlwaysTerminating();
8993
$scope = $keyResult->getScope();
9094
}
9195

92-
$valueResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->value, $scope, $storage, $nodeCallback, $context->enterDeep());
96+
$overriddenValueType = null;
97+
if ($overriddenType !== null && $overriddenType->hasOffsetValueType($keyType)->yes()) {
98+
$overriddenValueType = $overriddenType->getOffsetValueType($keyType);
99+
}
100+
101+
if ($arrayItem->key === null) {
102+
$nextAutoIndex++;
103+
} elseif ($keyType instanceof ConstantIntegerType) {
104+
$nextAutoIndex = $keyType->getValue() + 1;
105+
}
106+
107+
$valueResult = $nodeScopeResolver->processExprNode($stmt, $arrayItem->value, $scope, $storage, $nodeCallback, $context->enterDeep(), $overriddenValueType);
93108
$hasYield = $hasYield || $valueResult->hasYield();
94109
$throwPoints = array_merge($throwPoints, $valueResult->getThrowPoints());
95110
$impurePoints = array_merge($impurePoints, $valueResult->getImpurePoints());

0 commit comments

Comments
 (0)