🌐 US-Proxy
class="logged-out env-production page-responsive" style="word-wrap: break-word;" >
Skip to content

Commit bb18f72

Browse files
ondrejmirtesclaude
andcommitted
Dispatch specifyTypesInCondition through ExprHandler::specifyTypes
Move each Expr-specific branch out of TypeSpecifier::specifyTypesInCondition into the matching ExprHandler implementation and dispatch via the phpstan.exprHandler tag, mirroring MutatingScope::resolveType. Add specifyTypes() to the ExprHandler interface, inject the Container into TypeSpecifier, and keep the shared narrowing helpers on TypeSpecifier as public @internal so handlers can call back into them. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 5997afc commit bb18f72

71 files changed

Lines changed: 1985 additions & 1258 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.

phpstan-baseline.neon

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ parameters:
4242
count: 1
4343
path: src/Analyser/ExprHandler/BooleanNotHandler.php
4444

45+
-
46+
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
47+
identifier: phpstanApi.instanceofType
48+
count: 2
49+
path: src/Analyser/ExprHandler/IssetHandler.php
50+
4551
-
4652
rawMessage: 'Only numeric types are allowed in pre-increment, float|int|string|null given.'
4753
identifier: preInc.nonNumeric
@@ -108,12 +114,6 @@ parameters:
108114
count: 5
109115
path: src/Analyser/TypeSpecifier.php
110116

111-
-
112-
rawMessage: 'Doing instanceof PHPStan\Type\Constant\ConstantStringType is error-prone and deprecated. Use Type::getConstantStrings() instead.'
113-
identifier: phpstanApi.instanceofType
114-
count: 2
115-
path: src/Analyser/TypeSpecifier.php
116-
117117
-
118118
rawMessage: 'Template type TNodeType is declared as covariant, but occurs in contravariant position in parameter node of method PHPStan\Collectors\Collector::processNode().'
119119
identifier: generics.variance

src/Analyser/ExprHandler.php

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,14 @@ public function processExpr(
3737
*/
3838
public function resolveType(MutatingScope $scope, Expr $expr): Type;
3939

40+
/**
41+
* @param T $expr
42+
*/
43+
public function specifyTypes(
44+
TypeSpecifier $typeSpecifier,
45+
Scope $scope,
46+
Expr $expr,
47+
TypeSpecifierContext $context,
48+
): SpecifiedTypes;
49+
4050
}

src/Analyser/ExprHandler/ArrayDimFetchHandler.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
use PHPStan\Analyser\MutatingScope;
1818
use PHPStan\Analyser\NodeScopeResolver;
1919
use PHPStan\Analyser\NoopNodeCallback;
20+
use PHPStan\Analyser\Scope;
21+
use PHPStan\Analyser\SpecifiedTypes;
22+
use PHPStan\Analyser\TypeSpecifier;
23+
use PHPStan\Analyser\TypeSpecifierContext;
2024
use PHPStan\DependencyInjection\AutowiredService;
2125
use PHPStan\Node\Expr\TypeExpr;
2226
use PHPStan\Type\NeverType;
@@ -116,4 +120,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
116120
);
117121
}
118122

123+
public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes
124+
{
125+
return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context);
126+
}
127+
119128
}

src/Analyser/ExprHandler/ArrayHandler.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@
1414
use PHPStan\Analyser\ExprHandler;
1515
use PHPStan\Analyser\MutatingScope;
1616
use PHPStan\Analyser\NodeScopeResolver;
17+
use PHPStan\Analyser\Scope;
18+
use PHPStan\Analyser\SpecifiedTypes;
19+
use PHPStan\Analyser\TypeSpecifier;
20+
use PHPStan\Analyser\TypeSpecifierContext;
1721
use PHPStan\DependencyInjection\AutowiredService;
1822
use PHPStan\Node\LiteralArrayItem;
1923
use PHPStan\Node\LiteralArrayNode;
@@ -103,4 +107,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
103107
);
104108
}
105109

110+
public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes
111+
{
112+
return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context);
113+
}
114+
106115
}

src/Analyser/ExprHandler/ArrowFunctionHandler.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,10 @@
1212
use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver;
1313
use PHPStan\Analyser\MutatingScope;
1414
use PHPStan\Analyser\NodeScopeResolver;
15+
use PHPStan\Analyser\Scope;
16+
use PHPStan\Analyser\SpecifiedTypes;
17+
use PHPStan\Analyser\TypeSpecifier;
18+
use PHPStan\Analyser\TypeSpecifierContext;
1519
use PHPStan\DependencyInjection\AutowiredService;
1620
use PHPStan\Type\Type;
1721

@@ -51,4 +55,9 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
5155
return $this->closureTypeResolver->getClosureType($scope, $expr);
5256
}
5357

58+
public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes
59+
{
60+
return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context);
61+
}
62+
5463
}

src/Analyser/ExprHandler/AssignHandler.php

Lines changed: 180 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
use PHPStan\Type\Accessory\HasOffsetValueType;
5858
use PHPStan\Type\Accessory\NonEmptyArrayType;
5959
use PHPStan\Type\Constant\ConstantArrayType;
60+
use PHPStan\Type\Constant\ConstantBooleanType;
6061
use PHPStan\Type\Constant\ConstantIntegerType;
6162
use PHPStan\Type\Constant\ConstantStringType;
6263
use PHPStan\Type\ConstantTypeHelper;
@@ -65,6 +66,7 @@
6566
use PHPStan\Type\IntegerType;
6667
use PHPStan\Type\MixedType;
6768
use PHPStan\Type\NeverType;
69+
use PHPStan\Type\NullType;
6870
use PHPStan\Type\ObjectType;
6971
use PHPStan\Type\StaticTypeFactory;
7072
use PHPStan\Type\Type;
@@ -107,6 +109,184 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
107109
return $scope->getType($expr->expr);
108110
}
109111

112+
public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes
113+
{
114+
if (!$expr instanceof Assign) {
115+
return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context);
116+
}
117+
118+
if (!$scope instanceof MutatingScope) {
119+
throw new ShouldNotHappenException();
120+
}
121+
122+
if ($context->null()) {
123+
$specifiedTypes = $typeSpecifier->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->expr, $context)->setRootExpr($expr);
124+
$specifiedTypes = $specifiedTypes->removeExpr($this->exprPrinter->printExpr($expr->var));
125+
} else {
126+
$specifiedTypes = $typeSpecifier->specifyTypesInCondition($scope->exitFirstLevelStatements(), $expr->var, $context)->setRootExpr($expr);
127+
}
128+
129+
// infer $arr[$key] after $key = array_key_first/last($arr)
130+
if (
131+
$expr->expr instanceof FuncCall
132+
&& $expr->expr->name instanceof Name
133+
&& !$expr->expr->isFirstClassCallable()
134+
&& in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true)
135+
&& count($expr->expr->getArgs()) >= 1
136+
) {
137+
$arrayArg = $expr->expr->getArgs()[0]->value;
138+
$arrayType = $scope->getType($arrayArg);
139+
140+
if ($arrayType->isArray()->yes()) {
141+
if ($context->true()) {
142+
$specifiedTypes = $specifiedTypes->unionWith(
143+
$typeSpecifier->create($arrayArg, new NonEmptyArrayType(), TypeSpecifierContext::createTrue(), $scope),
144+
);
145+
$isNonEmpty = true;
146+
} else {
147+
$isNonEmpty = $arrayType->isIterableAtLeastOnce()->yes();
148+
}
149+
150+
if ($isNonEmpty) {
151+
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
152+
$specifiedTypes = $specifiedTypes->unionWith(
153+
$typeSpecifier->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope),
154+
);
155+
} elseif ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) {
156+
$keyType = $scope->getType($expr->expr);
157+
$nonNullKeyType = TypeCombinator::removeNull($keyType);
158+
if (!$nonNullKeyType instanceof NeverType) {
159+
$specifiedTypes = $specifiedTypes->unionWith(
160+
$typeSpecifier->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $nonNullKeyType, $arrayType->getIterableValueType()),
161+
);
162+
}
163+
}
164+
}
165+
}
166+
167+
// infer $arr[$key] after $key = array_search($needle, $arr) or $key = array_find_key($arr, $callback)
168+
if (
169+
$expr->expr instanceof FuncCall
170+
&& $expr->expr->name instanceof Name
171+
&& !$expr->expr->isFirstClassCallable()
172+
&& count($expr->expr->getArgs()) >= 2
173+
) {
174+
$funcName = $expr->expr->name->toLowerString();
175+
$arrayArg = null;
176+
$sentinelType = null;
177+
$isStrictArraySearch = false;
178+
179+
if ($funcName === 'array_search') {
180+
$arrayArg = $expr->expr->getArgs()[1]->value;
181+
$sentinelType = new ConstantBooleanType(false);
182+
$isStrictArraySearch = count($expr->expr->getArgs()) >= 3 && $scope->getType($expr->expr->getArgs()[2]->value)->isTrue()->yes();
183+
} elseif ($funcName === 'array_find_key') {
184+
$arrayArg = $expr->expr->getArgs()[0]->value;
185+
$sentinelType = new NullType();
186+
}
187+
188+
if ($arrayArg !== null) {
189+
$arrayType = $scope->getType($arrayArg);
190+
191+
if ($arrayType->isArray()->yes()) {
192+
if ($context->true()) {
193+
$specifiedTypes = $specifiedTypes->unionWith(
194+
$typeSpecifier->create($arrayArg, new NonEmptyArrayType(), TypeSpecifierContext::createTrue(), $scope),
195+
);
196+
197+
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
198+
199+
if ($isStrictArraySearch) {
200+
$needleType = $scope->getType($expr->expr->getArgs()[0]->value);
201+
$dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType());
202+
} else {
203+
$dimFetchType = $arrayType->getIterableValueType();
204+
}
205+
206+
$specifiedTypes = $specifiedTypes->unionWith(
207+
$typeSpecifier->create($dimFetch, $dimFetchType, TypeSpecifierContext::createTrue(), $scope),
208+
);
209+
} elseif ($expr->var instanceof Expr\Variable && is_string($expr->var->name)) {
210+
$keyType = $scope->getType($expr->expr);
211+
$narrowedKeyType = TypeCombinator::remove($keyType, $sentinelType);
212+
if (!$narrowedKeyType instanceof NeverType) {
213+
if ($isStrictArraySearch) {
214+
$needleType = $scope->getType($expr->expr->getArgs()[0]->value);
215+
$dimFetchType = TypeCombinator::intersect($needleType, $arrayType->getIterableValueType());
216+
} else {
217+
$dimFetchType = $arrayType->getIterableValueType();
218+
}
219+
$specifiedTypes = $specifiedTypes->unionWith(
220+
$typeSpecifier->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $narrowedKeyType, $dimFetchType),
221+
);
222+
}
223+
}
224+
}
225+
}
226+
}
227+
228+
if ($context->null()) {
229+
// infer $arr[$key] after $key = array_rand($arr)
230+
if (
231+
$expr->expr instanceof FuncCall
232+
&& $expr->expr->name instanceof Name
233+
&& !$expr->expr->isFirstClassCallable()
234+
&& in_array($expr->expr->name->toLowerString(), ['array_rand'], true)
235+
&& count($expr->expr->getArgs()) >= 1
236+
) {
237+
$numArg = null;
238+
$args = $expr->expr->getArgs();
239+
$arrayArg = $args[0]->value;
240+
if (count($args) > 1) {
241+
$numArg = $args[1]->value;
242+
}
243+
$one = new ConstantIntegerType(1);
244+
$arrayType = $scope->getType($arrayArg);
245+
246+
if (
247+
$arrayType->isArray()->yes()
248+
&& $arrayType->isIterableAtLeastOnce()->yes()
249+
&& ($numArg === null || $one->isSuperTypeOf($scope->getType($numArg))->yes())
250+
) {
251+
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
252+
253+
return $specifiedTypes->unionWith(
254+
$typeSpecifier->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope),
255+
);
256+
}
257+
}
258+
259+
// infer $list[$count] after $count = count($list) - 1
260+
if (
261+
$expr->expr instanceof Expr\BinaryOp\Minus
262+
&& $expr->expr->left instanceof FuncCall
263+
&& $expr->expr->left->name instanceof Name
264+
&& !$expr->expr->left->isFirstClassCallable()
265+
&& $expr->expr->right instanceof Node\Scalar\Int_
266+
&& $expr->expr->right->value === 1
267+
&& in_array($expr->expr->left->name->toLowerString(), ['count', 'sizeof'], true)
268+
&& count($expr->expr->left->getArgs()) >= 1
269+
) {
270+
$arrayArg = $expr->expr->left->getArgs()[0]->value;
271+
$arrayType = $scope->getType($arrayArg);
272+
if (
273+
$arrayType->isList()->yes()
274+
&& $arrayType->isIterableAtLeastOnce()->yes()
275+
) {
276+
$dimFetch = new ArrayDimFetch($arrayArg, $expr->var);
277+
278+
return $specifiedTypes->unionWith(
279+
$typeSpecifier->create($dimFetch, $arrayType->getIterableValueType(), TypeSpecifierContext::createTrue(), $scope),
280+
);
281+
}
282+
}
283+
284+
return $specifiedTypes;
285+
}
286+
287+
return $specifiedTypes;
288+
}
289+
110290
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult
111291
{
112292
$result = $this->processAssignVar(

src/Analyser/ExprHandler/AssignOpHandler.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@
1717
use PHPStan\Analyser\InternalThrowPoint;
1818
use PHPStan\Analyser\MutatingScope;
1919
use PHPStan\Analyser\NodeScopeResolver;
20+
use PHPStan\Analyser\Scope;
21+
use PHPStan\Analyser\SpecifiedTypes;
22+
use PHPStan\Analyser\TypeSpecifier;
23+
use PHPStan\Analyser\TypeSpecifierContext;
2024
use PHPStan\DependencyInjection\AutowiredService;
2125
use PHPStan\Reflection\InitializerExprTypeResolver;
2226
use PHPStan\ShouldNotHappenException;
@@ -171,4 +175,9 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
171175
throw new ShouldNotHappenException(sprintf('Unhandled %s', get_class($expr)));
172176
}
173177

178+
public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $expr, TypeSpecifierContext $context): SpecifiedTypes
179+
{
180+
return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context);
181+
}
182+
174183
}

0 commit comments

Comments
 (0)