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

Commit 9ae05a4

Browse files
ondrejmirtesclaude
andcommitted
Move single-consumer TypeSpecifier helpers into their handlers
Relocate @internal helpers that were only called from one ExprHandler into that handler as private methods: createArrayDimFetchConditionalExpressionHolder (AssignHandler), createRangeTypes (BinaryOpHandler), specifyTypesFromCallableCall (FuncCallHandler), specifyTypesForFlattenedBooleanAnd + allExpressionsTrackable (BooleanAndHandler), and specifyTypesForFlattenedBooleanOr + augmentBooleanOrTruthyWithConditionalHolders (BooleanOrHandler). Helpers shared by multiple handlers or still used by TypeSpecifier itself (count/asserts/conditional-return/disjunction/boolean-holder helpers and the type-specifying extension getters) stay on TypeSpecifier as public @internal. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent bb18f72 commit 9ae05a4

6 files changed

Lines changed: 328 additions & 319 deletions

File tree

src/Analyser/ExprHandler/AssignHandler.php

Lines changed: 23 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e
157157
$nonNullKeyType = TypeCombinator::removeNull($keyType);
158158
if (!$nonNullKeyType instanceof NeverType) {
159159
$specifiedTypes = $specifiedTypes->unionWith(
160-
$typeSpecifier->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $nonNullKeyType, $arrayType->getIterableValueType()),
160+
$this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $nonNullKeyType, $arrayType->getIterableValueType()),
161161
);
162162
}
163163
}
@@ -217,7 +217,7 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e
217217
$dimFetchType = $arrayType->getIterableValueType();
218218
}
219219
$specifiedTypes = $specifiedTypes->unionWith(
220-
$typeSpecifier->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $narrowedKeyType, $dimFetchType),
220+
$this->createArrayDimFetchConditionalExpressionHolder($expr->var, $arrayArg, $narrowedKeyType, $dimFetchType),
221221
);
222222
}
223223
}
@@ -1033,6 +1033,27 @@ public function processAssignVar(
10331033
return new ExpressionResult($scope, $hasYield, $isAlwaysTerminating, $throwPoints, $impurePoints);
10341034
}
10351035

1036+
private function createArrayDimFetchConditionalExpressionHolder(
1037+
Expr\Variable $keyVar,
1038+
Expr $arrayArg,
1039+
Type $narrowedKeyType,
1040+
Type $dimFetchType,
1041+
): SpecifiedTypes
1042+
{
1043+
$dimFetch = new ArrayDimFetch($arrayArg, $keyVar);
1044+
$dimFetchString = $this->exprPrinter->printExpr($dimFetch);
1045+
$keyExprString = $this->exprPrinter->printExpr($keyVar);
1046+
1047+
$holder = new ConditionalExpressionHolder(
1048+
[$keyExprString => ExpressionTypeHolder::createYes($keyVar, $narrowedKeyType)],
1049+
ExpressionTypeHolder::createYes($dimFetch, $dimFetchType),
1050+
);
1051+
1052+
return (new SpecifiedTypes([], []))->setNewConditionalExpressionHolders([
1053+
$dimFetchString => [$holder->getKey() => $holder],
1054+
]);
1055+
}
1056+
10361057
private function unwrapAssign(Expr $expr): Expr
10371058
{
10381059
if ($expr instanceof Assign) {

src/Analyser/ExprHandler/BinaryOpHandler.php

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
use PHPStan\Analyser\TypeSpecifier;
2727
use PHPStan\Analyser\TypeSpecifierContext;
2828
use PHPStan\DependencyInjection\AutowiredService;
29+
use PHPStan\Node\Printer\ExprPrinter;
2930
use PHPStan\Php\PhpVersion;
3031
use PHPStan\Reflection\InitializerExprTypeResolver;
3132
use PHPStan\ShouldNotHappenException;
@@ -37,6 +38,7 @@
3738
use PHPStan\Type\Constant\ConstantBooleanType;
3839
use PHPStan\Type\Constant\ConstantIntegerType;
3940
use PHPStan\Type\IntegerRangeType;
41+
use PHPStan\Type\IntegerType;
4042
use PHPStan\Type\ObjectType;
4143
use PHPStan\Type\Type;
4244
use PHPStan\Type\TypeCombinator;
@@ -61,6 +63,7 @@ public function __construct(
6163
private RicherScopeGetTypeHelper $richerScopeGetTypeHelper,
6264
private PhpVersion $phpVersion,
6365
private ImplicitToStringCallHelper $implicitToStringCallHelper,
66+
private ExprPrinter $exprPrinter,
6467
)
6568
{
6669
}
@@ -459,21 +462,21 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e
459462

460463
if ($leftType instanceof ConstantIntegerType) {
461464
if ($expr->right instanceof Expr\PostInc) {
462-
$result = $result->unionWith($typeSpecifier->createRangeTypes(
465+
$result = $result->unionWith($this->createRangeTypes(
463466
$expr,
464467
$expr->right->var,
465468
IntegerRangeType::fromInterval($leftType->getValue(), null, $offset + 1),
466469
$context,
467470
));
468471
} elseif ($expr->right instanceof Expr\PostDec) {
469-
$result = $result->unionWith($typeSpecifier->createRangeTypes(
472+
$result = $result->unionWith($this->createRangeTypes(
470473
$expr,
471474
$expr->right->var,
472475
IntegerRangeType::fromInterval($leftType->getValue(), null, $offset - 1),
473476
$context,
474477
));
475478
} elseif ($expr->right instanceof Expr\PreInc || $expr->right instanceof Expr\PreDec) {
476-
$result = $result->unionWith($typeSpecifier->createRangeTypes(
479+
$result = $result->unionWith($this->createRangeTypes(
477480
$expr,
478481
$expr->right->var,
479482
IntegerRangeType::fromInterval($leftType->getValue(), null, $offset),
@@ -485,21 +488,21 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e
485488
$rightType = $scope->getType($expr->right);
486489
if ($rightType instanceof ConstantIntegerType) {
487490
if ($expr->left instanceof Expr\PostInc) {
488-
$result = $result->unionWith($typeSpecifier->createRangeTypes(
491+
$result = $result->unionWith($this->createRangeTypes(
489492
$expr,
490493
$expr->left->var,
491494
IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset + 1),
492495
$context,
493496
));
494497
} elseif ($expr->left instanceof Expr\PostDec) {
495-
$result = $result->unionWith($typeSpecifier->createRangeTypes(
498+
$result = $result->unionWith($this->createRangeTypes(
496499
$expr,
497500
$expr->left->var,
498501
IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset - 1),
499502
$context,
500503
));
501504
} elseif ($expr->left instanceof Expr\PreInc || $expr->left instanceof Expr\PreDec) {
502-
$result = $result->unionWith($typeSpecifier->createRangeTypes(
505+
$result = $result->unionWith($this->createRangeTypes(
503506
$expr,
504507
$expr->left->var,
505508
IntegerRangeType::fromInterval(null, $rightType->getValue(), -$offset),
@@ -566,4 +569,21 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e
566569
return $typeSpecifier->specifyDefaultTypes($scope, $expr, $context);
567570
}
568571

572+
private function createRangeTypes(?Expr $rootExpr, Expr $expr, Type $type, TypeSpecifierContext $context): SpecifiedTypes
573+
{
574+
$sureNotTypes = [];
575+
576+
if ($type instanceof IntegerRangeType || $type instanceof ConstantIntegerType) {
577+
$exprString = $this->exprPrinter->printExpr($expr);
578+
if ($context->false()) {
579+
$sureNotTypes[$exprString] = [$expr, $type];
580+
} elseif ($context->true()) {
581+
$inverted = TypeCombinator::remove(new IntegerType(), $type);
582+
$sureNotTypes[$exprString] = [$expr, $inverted];
583+
}
584+
}
585+
586+
return (new SpecifiedTypes(sureNotTypes: $sureNotTypes))->setRootExpr($rootExpr);
587+
}
588+
569589
}

src/Analyser/ExprHandler/BooleanAndHandler.php

Lines changed: 87 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,10 @@
2626
use PHPStan\Type\Constant\ConstantBooleanType;
2727
use PHPStan\Type\NeverType;
2828
use PHPStan\Type\Type;
29+
use PHPStan\Type\TypeCombinator;
2930
use function array_merge;
31+
use function array_reverse;
32+
use function is_string;
3033

3134
/**
3235
* @implements ExprHandler<BooleanAnd|LogicalAnd>
@@ -89,7 +92,7 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e
8992
$context->true()
9093
&& self::getBooleanExpressionDepth($expr) > self::BOOLEAN_EXPRESSION_MAX_PROCESS_DEPTH
9194
) {
92-
return $typeSpecifier->specifyTypesForFlattenedBooleanAnd($scope, $expr, $context);
95+
return $this->specifyTypesForFlattenedBooleanAnd($typeSpecifier, $scope, $expr, $context);
9396
}
9497

9598
$leftTypes = $typeSpecifier->specifyTypesInCondition($scope, $expr->left, $context)->setRootExpr($expr);
@@ -119,13 +122,13 @@ public function specifyTypes(TypeSpecifier $typeSpecifier, Scope $scope, Expr $e
119122
// from the truthy narrowing instead, swapping sure/sureNot types.
120123
if ($leftTypesForHolders->getSureTypes() === [] && $leftTypesForHolders->getSureNotTypes() === []) {
121124
$truthyLeftTypes = $typeSpecifier->specifyTypesInCondition($scope, $expr->left, TypeSpecifierContext::createTruthy());
122-
if ($typeSpecifier->allExpressionsTrackable($truthyLeftTypes)) {
125+
if ($this->allExpressionsTrackable($truthyLeftTypes)) {
123126
$leftTypesForHolders = new SpecifiedTypes($truthyLeftTypes->getSureNotTypes(), $truthyLeftTypes->getSureTypes());
124127
}
125128
}
126129
if ($rightTypesForHolders->getSureTypes() === [] && $rightTypesForHolders->getSureNotTypes() === []) {
127130
$truthyRightTypes = $typeSpecifier->specifyTypesInCondition($rightScope, $expr->right, TypeSpecifierContext::createTruthy());
128-
if ($typeSpecifier->allExpressionsTrackable($truthyRightTypes)) {
131+
if ($this->allExpressionsTrackable($truthyRightTypes)) {
129132
$rightTypesForHolders = new SpecifiedTypes($truthyRightTypes->getSureNotTypes(), $truthyRightTypes->getSureTypes());
130133
}
131134
}
@@ -161,6 +164,87 @@ public static function getBooleanExpressionDepth(Expr $expr, int $depth = 0): in
161164
return $depth;
162165
}
163166

167+
/**
168+
* Flatten a deep BooleanAnd chain into leaf expressions and process them
169+
* without recursive filterByTruthyValue calls.
170+
*
171+
* @param BooleanAnd|LogicalAnd $expr
172+
*/
173+
private function specifyTypesForFlattenedBooleanAnd(
174+
TypeSpecifier $typeSpecifier,
175+
MutatingScope $scope,
176+
Expr $expr,
177+
TypeSpecifierContext $context,
178+
): SpecifiedTypes
179+
{
180+
$arms = [];
181+
$current = $expr;
182+
while ($current instanceof BooleanAnd || $current instanceof LogicalAnd) {
183+
$arms[] = $current->right;
184+
$current = $current->left;
185+
}
186+
$arms[] = $current;
187+
$arms = array_reverse($arms);
188+
189+
// Truthy: all arms are true → union all SpecifiedTypes.
190+
// Collect per-expression types first, then build unions once
191+
// to avoid O(N²) from incremental growth.
192+
/** @var array<string, array{Expr, list<Type>}> $sureTypesPerExpr */
193+
$sureTypesPerExpr = [];
194+
/** @var array<string, array{Expr, list<Type>}> $sureNotTypesPerExpr */
195+
$sureNotTypesPerExpr = [];
196+
197+
foreach ($arms as $arm) {
198+
$armTypes = $typeSpecifier->specifyTypesInCondition($scope, $arm, $context);
199+
foreach ($armTypes->getSureTypes() as $exprString => [$exprNode, $type]) {
200+
$sureTypesPerExpr[$exprString][0] = $exprNode;
201+
$sureTypesPerExpr[$exprString][1][] = $type;
202+
}
203+
foreach ($armTypes->getSureNotTypes() as $exprString => [$exprNode, $type]) {
204+
$sureNotTypesPerExpr[$exprString][0] = $exprNode;
205+
$sureNotTypesPerExpr[$exprString][1][] = $type;
206+
}
207+
}
208+
209+
$sureTypes = [];
210+
foreach ($sureTypesPerExpr as $exprString => [$exprNode, $types]) {
211+
$sureTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)];
212+
}
213+
$sureNotTypes = [];
214+
foreach ($sureNotTypesPerExpr as $exprString => [$exprNode, $types]) {
215+
$sureNotTypes[$exprString] = [$exprNode, TypeCombinator::union(...$types)];
216+
}
217+
218+
return (new SpecifiedTypes($sureTypes, $sureNotTypes))->setRootExpr($expr);
219+
}
220+
221+
private function allExpressionsTrackable(SpecifiedTypes $types): bool
222+
{
223+
foreach ($types->getSureTypes() as [$expr]) {
224+
if (!$this->isTrackableExpression($expr)) {
225+
return false;
226+
}
227+
}
228+
foreach ($types->getSureNotTypes() as [$expr]) {
229+
if (!$this->isTrackableExpression($expr)) {
230+
return false;
231+
}
232+
}
233+
234+
return $types->getSureTypes() !== [] || $types->getSureNotTypes() !== [];
235+
}
236+
237+
private function isTrackableExpression(Expr $expr): bool
238+
{
239+
if ($expr instanceof Expr\Variable) {
240+
return is_string($expr->name);
241+
}
242+
243+
return $expr instanceof Expr\PropertyFetch
244+
|| $expr instanceof Expr\ArrayDimFetch
245+
|| $expr instanceof Expr\StaticPropertyFetch;
246+
}
247+
164248
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult
165249
{
166250
$leftResult = $nodeScopeResolver->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep());

0 commit comments

Comments
 (0)