Skip to content

Commit ff5a28e

Browse files
ondrejmirtesclaude
andcommitted
Derive ArrayAccess and __set simulation throw points directly instead of walking synthetic calls
The offsetGet/offsetSet/offsetExists/offsetUnset and __set simulations built a synthetic MethodCall and processed it through processExprNode() with a NoopNodeCallback just to read the resulting throw points. The new MethodThrowPointHelper::getThrowPointsForCallOnType() derives them directly from the receiver type's method reflection; the fabricated node is only the throw-point anchor and the payload dynamic throw-type extensions receive - nothing processes it. The __set simulation no longer re-walks the real receiver, whose throw points the main walk already collected. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01DaBZjgksga4c5s6Q9FniY7
1 parent 1d345a6 commit ff5a28e

5 files changed

Lines changed: 63 additions & 34 deletions

File tree

src/Analyser/ExprHandler/ArrayDimFetchHandler.php

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@
1414
use PHPStan\Analyser\ExpressionResultFactory;
1515
use PHPStan\Analyser\ExpressionResultStorage;
1616
use PHPStan\Analyser\ExprHandler;
17+
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
1718
use PHPStan\Analyser\ExprHandler\Helper\NullsafeShortCircuitingHelper;
1819
use PHPStan\Analyser\IssetabilityDescriptor;
1920
use PHPStan\Analyser\MutatingScope;
2021
use PHPStan\Analyser\NodeScopeResolver;
21-
use PHPStan\Analyser\NoopNodeCallback;
2222
use PHPStan\Analyser\Scope;
2323
use PHPStan\Analyser\SpecifiedTypes;
2424
use PHPStan\Analyser\TypeSpecifier;
@@ -37,7 +37,10 @@
3737
final class ArrayDimFetchHandler implements ExprHandler
3838
{
3939

40-
public function __construct(private ExpressionResultFactory $expressionResultFactory)
40+
public function __construct(
41+
private ExpressionResultFactory $expressionResultFactory,
42+
private MethodThrowPointHelper $methodThrowPointHelper,
43+
)
4144
{
4245
}
4346

@@ -123,14 +126,12 @@ public function composeResult(NodeScopeResolver $nodeScopeResolver, Stmt $stmt,
123126

124127
$varType = $varResult->getType();
125128
if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) {
126-
$throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode(
127-
$stmt,
128-
new MethodCall(new TypeExpr($varType), 'offsetGet'),
129+
$throwPoints = array_merge($throwPoints, $this->methodThrowPointHelper->getThrowPointsForCallOnType(
129130
$scope,
130-
$storage,
131-
new NoopNodeCallback(),
132131
$context,
133-
)->getThrowPoints());
132+
$varType,
133+
new MethodCall(new TypeExpr($varType), 'offsetGet'),
134+
));
134135
}
135136

136137
return $this->expressionResultFactory->create(

src/Analyser/ExprHandler/AssignHandler.php

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
use PHPStan\Analyser\ExpressionResultStorage;
3030
use PHPStan\Analyser\ExpressionTypeHolder;
3131
use PHPStan\Analyser\ExprHandler;
32+
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
3233
use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper;
3334
use PHPStan\Analyser\ImpurePoint;
3435
use PHPStan\Analyser\InternalThrowPoint;
@@ -105,6 +106,7 @@ public function __construct(
105106
private ArrayDimFetchHandler $arrayDimFetchHandler,
106107
private PropertyFetchHandler $propertyFetchHandler,
107108
private StaticPropertyFetchHandler $staticPropertyFetchHandler,
109+
private MethodThrowPointHelper $methodThrowPointHelper,
108110
)
109111
{
110112
}
@@ -1080,14 +1082,12 @@ public function applyWrite(
10801082
&& !$setVarType->isArray()->yes()
10811083
&& !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($setVarType)->no()
10821084
) {
1083-
$throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode(
1084-
$stmt,
1085-
new MethodCall(new TypeExpr($setVarType), 'offsetSet'),
1085+
$throwPoints = array_merge($throwPoints, $this->methodThrowPointHelper->getThrowPointsForCallOnType(
10861086
$scope,
1087-
$storage,
1088-
new NoopNodeCallback(),
10891087
$context,
1090-
)->getThrowPoints());
1088+
$setVarType,
1089+
new MethodCall(new TypeExpr($setVarType), 'offsetSet'),
1090+
));
10911091
}
10921092
} elseif ($kind === PreparedAssignTarget::KIND_PROPERTY_FETCH) {
10931093
if (!$var instanceof PropertyFetch) {
@@ -1173,16 +1173,15 @@ public function applyWrite(
11731173
$assignedExprType = $scope->getType($assignedExpr);
11741174
$nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage);
11751175
$scope = $scope->assignExpression($var, $assignedExprType, $scope->getNativeType($assignedExpr));
1176-
// simulate dynamic property assign by __set to get throw points
1176+
// simulate dynamic property assign by __set to get throw points;
1177+
// the receiver's own throw points were already collected by its walk
11771178
if (!$propertyHolderType->hasMethod('__set')->no()) {
1178-
$throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode(
1179-
$stmt,
1180-
new MethodCall($var->var, '__set'),
1179+
$throwPoints = array_merge($throwPoints, $this->methodThrowPointHelper->getThrowPointsForCallOnType(
11811180
$scope,
1182-
$storage,
1183-
new NoopNodeCallback(),
11841181
$context,
1185-
)->getThrowPoints());
1182+
$propertyHolderType,
1183+
new MethodCall($var->var, '__set'),
1184+
));
11861185
}
11871186
}
11881187

src/Analyser/ExprHandler/Helper/MethodThrowPointHelper.php

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use PhpParser\Node\Expr\MethodCall;
66
use PhpParser\Node\Expr\StaticCall;
7+
use PhpParser\Node\Identifier;
78
use PHPStan\Analyser\ExpressionContext;
89
use PHPStan\Analyser\InternalThrowPoint;
910
use PHPStan\Analyser\MutatingScope;
@@ -13,10 +14,13 @@
1314
use PHPStan\DependencyInjection\ExtensionsCollection;
1415
use PHPStan\Reflection\MethodReflection;
1516
use PHPStan\Reflection\ParametersAcceptor;
17+
use PHPStan\Reflection\ParametersAcceptorSelector;
18+
use PHPStan\ShouldNotHappenException;
1619
use PHPStan\Type\DynamicMethodThrowTypeExtension;
1720
use PHPStan\Type\DynamicStaticMethodThrowTypeExtension;
1821
use PHPStan\Type\NeverType;
1922
use PHPStan\Type\ObjectType;
23+
use PHPStan\Type\Type;
2024
use ReflectionFunction;
2125
use ReflectionMethod;
2226
use Throwable;
@@ -107,4 +111,31 @@ public function getThrowPoint(
107111
return null;
108112
}
109113

114+
/**
115+
* The throw points of invoking a method on an already-priced receiver
116+
* type - what walking a synthetic MethodCall used to produce, without the
117+
* walk. The method call node is only the throw-point anchor and the
118+
* payload dynamic throw-type extensions receive; nothing processes it.
119+
*
120+
* @return list<InternalThrowPoint>
121+
*/
122+
public function getThrowPointsForCallOnType(MutatingScope $scope, ExpressionContext $context, Type $calledOnType, MethodCall $methodCall): array
123+
{
124+
if (!$methodCall->name instanceof Identifier) {
125+
throw new ShouldNotHappenException();
126+
}
127+
128+
$methodReflection = $scope->getMethodReflection($calledOnType, $methodCall->name->toString());
129+
if ($methodReflection === null) {
130+
return [InternalThrowPoint::createImplicit($scope, $methodCall)];
131+
}
132+
133+
$throwPoint = $this->getThrowPoint($methodReflection, ParametersAcceptorSelector::combineAcceptors($methodReflection->getVariants()), $methodCall, $scope, $context);
134+
if ($throwPoint === null) {
135+
return [];
136+
}
137+
138+
return [$throwPoint];
139+
}
140+
110141
}

src/Analyser/ExprHandler/IssetHandler.php

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@
1818
use PHPStan\Analyser\ExpressionResultFactory;
1919
use PHPStan\Analyser\ExpressionResultStorage;
2020
use PHPStan\Analyser\ExprHandler;
21+
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
2122
use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper;
2223
use PHPStan\Analyser\MutatingScope;
2324
use PHPStan\Analyser\NodeScopeResolver;
24-
use PHPStan\Analyser\NoopNodeCallback;
2525
use PHPStan\Analyser\Scope;
2626
use PHPStan\Analyser\SpecifiedTypes;
2727
use PHPStan\Analyser\TypeSpecifier;
@@ -62,6 +62,7 @@ final class IssetHandler implements ExprHandler
6262
public function __construct(
6363
private NonNullabilityHelper $nonNullabilityHelper,
6464
private ExpressionResultFactory $expressionResultFactory,
65+
private MethodThrowPointHelper $methodThrowPointHelper,
6566
)
6667
{
6768
}
@@ -375,14 +376,12 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
375376
continue;
376377
}
377378

378-
$throwPoints = array_merge($throwPoints, $nodeScopeResolver->processExprNode(
379-
$stmt,
380-
new MethodCall(new TypeExpr($varType), 'offsetExists'),
379+
$throwPoints = array_merge($throwPoints, $this->methodThrowPointHelper->getThrowPointsForCallOnType(
381380
$scope,
382-
$storage,
383-
new NoopNodeCallback(),
384381
$context,
385-
)->getThrowPoints());
382+
$varType,
383+
new MethodCall(new TypeExpr($varType), 'offsetExists'),
384+
));
386385
}
387386
foreach (array_reverse($expr->vars) as $var) {
388387
$scope = $nodeScopeResolver->lookForUnsetAllowedUndefinedExpressions($scope, $var);

src/Analyser/NodeScopeResolver.php

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
use PhpParser\NodeVisitorAbstract;
5353
use PHPStan\Analyser\ExprHandler\AssignHandler;
5454
use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper;
55+
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
5556
use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass;
5657
use PHPStan\BetterReflection\Reflection\ReflectionEnum;
5758
use PHPStan\BetterReflection\Reflector\Reflector;
@@ -2397,14 +2398,12 @@ public function processStmtNode(
23972398
if ($var instanceof ArrayDimFetch && $var->dim !== null) {
23982399
$varType = $scope->getType($var->var);
23992400
if (!$varType->isArray()->yes() && !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($varType)->no()) {
2400-
$throwPoints = array_merge($throwPoints, $this->processExprNode(
2401-
$stmt,
2402-
new MethodCall(new TypeExpr($varType), 'offsetUnset'),
2401+
$throwPoints = array_merge($throwPoints, $this->container->getByType(MethodThrowPointHelper::class)->getThrowPointsForCallOnType(
24032402
$scope,
2404-
$storage,
2405-
new NoopNodeCallback(),
24062403
ExpressionContext::createDeep(),
2407-
)->getThrowPoints());
2404+
$varType,
2405+
new MethodCall(new TypeExpr($varType), 'offsetUnset'),
2406+
));
24082407
}
24092408

24102409
$clonedVar = $this->deepNodeCloner->cloneNode($var->var);

0 commit comments

Comments
 (0)