Skip to content

Commit 6002ee4

Browse files
committed
Merge branch '2.1.x' into m21
2 parents 46f88a3 + 53123fa commit 6002ee4

Some content is hidden

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

45 files changed

+1573
-70
lines changed

src/Analyser/ExprHandler/AssignOpHandler.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Analyser\ExpressionResult;
1414
use PHPStan\Analyser\ExpressionResultStorage;
1515
use PHPStan\Analyser\ExprHandler;
16+
use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper;
1617
use PHPStan\Analyser\InternalThrowPoint;
1718
use PHPStan\Analyser\MutatingScope;
1819
use PHPStan\Analyser\NodeScopeResolver;
@@ -22,6 +23,7 @@
2223
use PHPStan\Type\Constant\ConstantIntegerType;
2324
use PHPStan\Type\ObjectType;
2425
use PHPStan\Type\Type;
26+
use function array_merge;
2527
use function get_class;
2628
use function sprintf;
2729

@@ -35,6 +37,7 @@ final class AssignOpHandler implements ExprHandler
3537
public function __construct(
3638
private AssignHandler $assignHandler,
3739
private InitializerExprTypeResolver $initializerExprTypeResolver,
40+
private ImplicitToStringCallHelper $implicitToStringCallHelper,
3841
)
3942
{
4043
}
@@ -85,19 +88,25 @@ static function (MutatingScope $scope) use ($stmt, $expr, $nodeCallback, $contex
8588
}
8689
$scope = $assignResult->getScope();
8790
$throwPoints = $assignResult->getThrowPoints();
91+
$impurePoints = $assignResult->getImpurePoints();
8892
if (
8993
($expr instanceof Expr\AssignOp\Div || $expr instanceof Expr\AssignOp\Mod) &&
9094
!$scope->getType($expr->expr)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no()
9195
) {
9296
$throwPoints[] = InternalThrowPoint::createExplicit($scope, new ObjectType(DivisionByZeroError::class), $expr, false);
9397
}
98+
if ($expr instanceof Expr\AssignOp\Concat) {
99+
$toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope);
100+
$throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints());
101+
$impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints());
102+
}
94103

95104
return new ExpressionResult(
96105
$scope,
97106
hasYield: $assignResult->hasYield(),
98107
isAlwaysTerminating: $assignResult->isAlwaysTerminating(),
99108
throwPoints: $throwPoints,
100-
impurePoints: $assignResult->getImpurePoints(),
109+
impurePoints: $impurePoints,
101110
truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
102111
falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
103112
);

src/Analyser/ExprHandler/BinaryOpHandler.php

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Analyser\ExpressionResult;
1414
use PHPStan\Analyser\ExpressionResultStorage;
1515
use PHPStan\Analyser\ExprHandler;
16+
use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper;
1617
use PHPStan\Analyser\InternalThrowPoint;
1718
use PHPStan\Analyser\MutatingScope;
1819
use PHPStan\Analyser\NodeScopeResolver;
@@ -42,6 +43,7 @@ public function __construct(
4243
private InitializerExprTypeResolver $initializerExprTypeResolver,
4344
private RicherScopeGetTypeHelper $richerScopeGetTypeHelper,
4445
private PhpVersion $phpVersion,
46+
private ImplicitToStringCallHelper $implicitToStringCallHelper,
4547
)
4648
{
4749
}
@@ -62,20 +64,27 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
6264
$leftResult = $nodeScopeResolver->processExprNode($stmt, $expr->left, $scope, $storage, $nodeCallback, $context->enterDeep());
6365
$rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $leftResult->getScope(), $storage, $nodeCallback, $context->enterDeep());
6466
$throwPoints = array_merge($leftResult->getThrowPoints(), $rightResult->getThrowPoints());
67+
$impurePoints = array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints());
6568
if (
6669
($expr instanceof BinaryOp\Div || $expr instanceof BinaryOp\Mod) &&
6770
!$leftResult->getScope()->getType($expr->right)->toNumber()->isSuperTypeOf(new ConstantIntegerType(0))->no()
6871
) {
6972
$throwPoints[] = InternalThrowPoint::createExplicit($leftResult->getScope(), new ObjectType(DivisionByZeroError::class), $expr, false);
7073
}
74+
if ($expr instanceof BinaryOp\Concat) {
75+
$leftToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->left, $scope);
76+
$rightToStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->right, $leftResult->getScope());
77+
$throwPoints = array_merge($throwPoints, $leftToStringResult->getThrowPoints(), $rightToStringResult->getThrowPoints());
78+
$impurePoints = array_merge($impurePoints, $leftToStringResult->getImpurePoints(), $rightToStringResult->getImpurePoints());
79+
}
7180
$scope = $rightResult->getScope();
7281

7382
return new ExpressionResult(
7483
$scope,
7584
hasYield: $leftResult->hasYield() || $rightResult->hasYield(),
7685
isAlwaysTerminating: $leftResult->isAlwaysTerminating() || $rightResult->isAlwaysTerminating(),
7786
throwPoints: $throwPoints,
78-
impurePoints: array_merge($leftResult->getImpurePoints(), $rightResult->getImpurePoints()),
87+
impurePoints: $impurePoints,
7988
truthyScopeCallback: static fn (): MutatingScope => $scope->filterByTruthyValue($expr),
8089
falseyScopeCallback: static fn (): MutatingScope => $scope->filterByFalseyValue($expr),
8190
);

src/Analyser/ExprHandler/CastStringHandler.php

Lines changed: 6 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,18 @@
44

55
use PhpParser\Node\Expr;
66
use PhpParser\Node\Expr\Cast;
7-
use PhpParser\Node\Identifier;
87
use PhpParser\Node\Stmt;
98
use PHPStan\Analyser\ExpressionContext;
109
use PHPStan\Analyser\ExpressionResult;
1110
use PHPStan\Analyser\ExpressionResultStorage;
1211
use PHPStan\Analyser\ExprHandler;
13-
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
14-
use PHPStan\Analyser\ImpurePoint;
12+
use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper;
1513
use PHPStan\Analyser\MutatingScope;
1614
use PHPStan\Analyser\NodeScopeResolver;
1715
use PHPStan\DependencyInjection\AutowiredService;
18-
use PHPStan\Php\PhpVersion;
1916
use PHPStan\Reflection\InitializerExprTypeResolver;
2017
use PHPStan\Type\Type;
21-
use function sprintf;
18+
use function array_merge;
2219

2320
/**
2421
* @implements ExprHandler<Cast\String_>
@@ -29,8 +26,7 @@ final class CastStringHandler implements ExprHandler
2926

3027
public function __construct(
3128
private InitializerExprTypeResolver $initializerExprTypeResolver,
32-
private PhpVersion $phpVersion,
33-
private MethodThrowPointHelper $methodThrowPointHelper,
29+
private ImplicitToStringCallHelper $implicitToStringCallHelper,
3430
)
3531
{
3632
}
@@ -46,31 +42,9 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
4642
$impurePoints = $exprResult->getImpurePoints();
4743
$throwPoints = $exprResult->getThrowPoints();
4844

49-
$exprType = $scope->getType($expr->expr);
50-
$toStringMethod = $scope->getMethodReflection($exprType, '__toString');
51-
if ($toStringMethod !== null) {
52-
if (!$toStringMethod->hasSideEffects()->no()) {
53-
$impurePoints[] = new ImpurePoint(
54-
$scope,
55-
$expr,
56-
'methodCall',
57-
sprintf('call to method %s::%s()', $toStringMethod->getDeclaringClass()->getDisplayName(), $toStringMethod->getName()),
58-
$toStringMethod->isPure()->no(),
59-
);
60-
}
61-
62-
if ($this->phpVersion->throwsOnStringCast()) {
63-
$throwPoint = $this->methodThrowPointHelper->getThrowPoint(
64-
$toStringMethod,
65-
$toStringMethod->getOnlyVariant(),
66-
new Expr\MethodCall($expr->expr, new Identifier('__toString')),
67-
$scope,
68-
);
69-
if ($throwPoint !== null) {
70-
$throwPoints[] = $throwPoint;
71-
}
72-
}
73-
}
45+
$toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope);
46+
$throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints());
47+
$impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints());
7448

7549
$scope = $exprResult->getScope();
7650

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser\ExprHandler\Helper;
4+
5+
use PhpParser\Node\Expr;
6+
use PhpParser\Node\Identifier;
7+
use PHPStan\Analyser\ExpressionResult;
8+
use PHPStan\Analyser\ImpurePoint;
9+
use PHPStan\Analyser\MutatingScope;
10+
use PHPStan\DependencyInjection\AutowiredService;
11+
use PHPStan\Php\PhpVersion;
12+
use function sprintf;
13+
14+
#[AutowiredService]
15+
final class ImplicitToStringCallHelper
16+
{
17+
18+
public function __construct(
19+
private PhpVersion $phpVersion,
20+
private MethodThrowPointHelper $methodThrowPointHelper,
21+
)
22+
{
23+
}
24+
25+
public function processImplicitToStringCall(Expr $expr, MutatingScope $scope): ExpressionResult
26+
{
27+
$throwPoints = [];
28+
$impurePoints = [];
29+
30+
$exprType = $scope->getType($expr);
31+
$toStringMethod = $scope->getMethodReflection($exprType, '__toString');
32+
if ($toStringMethod === null) {
33+
return new ExpressionResult(
34+
$scope,
35+
hasYield: false,
36+
isAlwaysTerminating: false,
37+
throwPoints: [],
38+
impurePoints: [],
39+
);
40+
}
41+
42+
if (!$toStringMethod->hasSideEffects()->no()) {
43+
$impurePoints[] = new ImpurePoint(
44+
$scope,
45+
$expr,
46+
'methodCall',
47+
sprintf('call to method %s::%s()', $toStringMethod->getDeclaringClass()->getDisplayName(), $toStringMethod->getName()),
48+
$toStringMethod->isPure()->no(),
49+
);
50+
}
51+
52+
if ($this->phpVersion->throwsOnStringCast()) {
53+
$throwPoint = $this->methodThrowPointHelper->getThrowPoint(
54+
$toStringMethod,
55+
$toStringMethod->getOnlyVariant(),
56+
new Expr\MethodCall($expr, new Identifier('__toString')),
57+
$scope,
58+
);
59+
if ($throwPoint !== null) {
60+
$throwPoints[] = $throwPoint;
61+
}
62+
}
63+
64+
return new ExpressionResult(
65+
$scope,
66+
hasYield: false,
67+
isAlwaysTerminating: false,
68+
throwPoints: $throwPoints,
69+
impurePoints: $impurePoints,
70+
);
71+
}
72+
73+
}

src/Analyser/ExprHandler/Helper/MethodCallReturnTypeHelper.php

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\DependencyInjection\AutowiredService;
1010
use PHPStan\DependencyInjection\Type\DynamicReturnTypeExtensionRegistryProvider;
1111
use PHPStan\Reflection\ParametersAcceptorSelector;
12+
use PHPStan\Type\ObjectType;
1213
use PHPStan\Type\Type;
1314
use PHPStan\Type\TypeCombinator;
1415
use function count;
@@ -52,7 +53,9 @@ public function methodCallReturnType(
5253
}
5354

5455
$resolvedTypes = [];
55-
foreach ($typeWithMethod->getObjectClassNames() as $className) {
56+
$allClassNames = $typeWithMethod->getObjectClassNames();
57+
$handledClassNames = [];
58+
foreach ($allClassNames as $className) {
5659
if ($normalizedMethodCall instanceof MethodCall) {
5760
foreach ($this->dynamicReturnTypeExtensionRegistryProvider->getRegistry()->getDynamicMethodReturnTypeExtensionsForClass($className) as $dynamicMethodReturnTypeExtension) {
5861
if (!$dynamicMethodReturnTypeExtension->isMethodSupported($methodReflection)) {
@@ -65,6 +68,7 @@ public function methodCallReturnType(
6568
}
6669

6770
$resolvedTypes[] = $resolvedType;
71+
$handledClassNames[] = $className;
6872
}
6973
} else {
7074
foreach ($this->dynamicReturnTypeExtensionRegistryProvider->getRegistry()->getDynamicStaticMethodReturnTypeExtensionsForClass($className) as $dynamicStaticMethodReturnTypeExtension) {
@@ -82,11 +86,29 @@ public function methodCallReturnType(
8286
}
8387

8488
$resolvedTypes[] = $resolvedType;
89+
$handledClassNames[] = $className;
8590
}
8691
}
8792
}
8893

8994
if (count($resolvedTypes) > 0) {
95+
if (count($allClassNames) !== count($handledClassNames)) {
96+
$remainingType = $typeWithMethod;
97+
foreach ($handledClassNames as $handledClassName) {
98+
$remainingType = TypeCombinator::remove($remainingType, new ObjectType($handledClassName));
99+
}
100+
if ($remainingType->hasMethod($methodName)->yes()) {
101+
$remainingMethod = $remainingType->getMethod($methodName, $scope);
102+
$remainingParametersAcceptor = ParametersAcceptorSelector::selectFromArgs(
103+
$scope,
104+
$methodCall->getArgs(),
105+
$remainingMethod->getVariants(),
106+
$remainingMethod->getNamedArgumentsVariants(),
107+
);
108+
$resolvedTypes[] = $remainingParametersAcceptor->getReturnType();
109+
}
110+
}
111+
90112
return VoidToNullTypeTransformer::transform(TypeCombinator::union(...$resolvedTypes), $methodCall);
91113
}
92114

src/Analyser/ExprHandler/InterpolatedStringHandler.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use PHPStan\Analyser\ExpressionResult;
1111
use PHPStan\Analyser\ExpressionResultStorage;
1212
use PHPStan\Analyser\ExprHandler;
13+
use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper;
1314
use PHPStan\Analyser\MutatingScope;
1415
use PHPStan\Analyser\NodeScopeResolver;
1516
use PHPStan\DependencyInjection\AutowiredService;
@@ -27,6 +28,7 @@ final class InterpolatedStringHandler implements ExprHandler
2728

2829
public function __construct(
2930
private InitializerExprTypeResolver $initializerExprTypeResolver,
31+
private ImplicitToStringCallHelper $implicitToStringCallHelper,
3032
)
3133
{
3234
}
@@ -50,6 +52,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
5052
$hasYield = $hasYield || $partResult->hasYield();
5153
$throwPoints = array_merge($throwPoints, $partResult->getThrowPoints());
5254
$impurePoints = array_merge($impurePoints, $partResult->getImpurePoints());
55+
56+
$toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($part, $scope);
57+
$throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints());
58+
$impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints());
59+
5360
$isAlwaysTerminating = $isAlwaysTerminating || $partResult->isAlwaysTerminating();
5461
$scope = $partResult->getScope();
5562
}

src/Analyser/ExprHandler/PrintHandler.php

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use PHPStan\Analyser\ExpressionResult;
1010
use PHPStan\Analyser\ExpressionResultStorage;
1111
use PHPStan\Analyser\ExprHandler;
12+
use PHPStan\Analyser\ExprHandler\Helper\ImplicitToStringCallHelper;
1213
use PHPStan\Analyser\ImpurePoint;
1314
use PHPStan\Analyser\MutatingScope;
1415
use PHPStan\Analyser\NodeScopeResolver;
@@ -24,6 +25,12 @@
2425
final class PrintHandler implements ExprHandler
2526
{
2627

28+
public function __construct(
29+
private ImplicitToStringCallHelper $implicitToStringCallHelper,
30+
)
31+
{
32+
}
33+
2734
public function supports(Expr $expr): bool
2835
{
2936
return $expr instanceof Print_;
@@ -37,14 +44,21 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
3744
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult
3845
{
3946
$exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $scope, $storage, $nodeCallback, $context->enterDeep());
47+
$throwPoints = $exprResult->getThrowPoints();
48+
$impurePoints = $exprResult->getImpurePoints();
49+
50+
$toStringResult = $this->implicitToStringCallHelper->processImplicitToStringCall($expr->expr, $scope);
51+
$throwPoints = array_merge($throwPoints, $toStringResult->getThrowPoints());
52+
$impurePoints = array_merge($impurePoints, $toStringResult->getImpurePoints());
53+
4054
$scope = $exprResult->getScope();
4155

4256
return new ExpressionResult(
4357
$scope,
4458
hasYield: $exprResult->hasYield(),
4559
isAlwaysTerminating: $exprResult->isAlwaysTerminating(),
46-
throwPoints: $exprResult->getThrowPoints(),
47-
impurePoints: array_merge($exprResult->getImpurePoints(), [new ImpurePoint($scope, $expr, 'print', 'print', true)]),
60+
throwPoints: $throwPoints,
61+
impurePoints: array_merge($impurePoints, [new ImpurePoint($scope, $expr, 'print', 'print', true)]),
4862
);
4963
}
5064

src/Analyser/ExprHandler/PropertyFetchHandler.php

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,14 @@
3333
final class PropertyFetchHandler implements ExprHandler
3434
{
3535

36+
/**
37+
* Representative property name used when resolving dynamic property access ($obj->{$expr}).
38+
* The actual name doesn't matter — it just needs to be non-empty so that
39+
* PropertiesClassReflectionExtensions (e.g. SimpleXMLElement) that accept
40+
* any property name can return the correct type.
41+
*/
42+
private const DYNAMIC_PROPERTY_NAME = '__phpstan_dynamic_property';
43+
3644
public function __construct(
3745
private PhpVersion $phpVersion,
3846
private PropertyReflectionFinder $propertyReflectionFinder,
@@ -130,6 +138,14 @@ public function resolveType(MutatingScope $scope, Expr $expr): Type
130138
);
131139
}
132140

141+
if ($nameType->isString()->yes()) {
142+
$fetchedOnType = $scope->getType($expr->var);
143+
$returnType = $this->propertyFetchType($scope, $fetchedOnType, self::DYNAMIC_PROPERTY_NAME, $expr);
144+
if ($returnType !== null) {
145+
return NullsafeShortCircuitingHelper::getType($scope, $expr->var, $returnType);
146+
}
147+
}
148+
133149
return new MixedType();
134150
}
135151

0 commit comments

Comments
 (0)