Skip to content

Commit 7e1d64f

Browse files
committed
IdenticalHandler
1 parent 2d64aa6 commit 7e1d64f

File tree

6 files changed

+536
-5
lines changed

6 files changed

+536
-5
lines changed

src/Analyser/Generator/ExprHandler/EqualHandler.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,8 @@ private function resolveEqualSpecifiedTypes(
345345
}
346346
}
347347

348+
// todo normalize?
349+
348350
return [
349351
$this->specifiedTypesHelper->create($equalExpr->left, $leftType, TypeSpecifierContext::createTruthy())->setRootExpr($equalExpr)->unionWith(
350352
$this->specifiedTypesHelper->create($equalExpr->right, $rightType, TypeSpecifierContext::createTruthy())->setRootExpr($equalExpr),
Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser\Generator\ExprHandler;
4+
5+
use Countable;
6+
use Generator;
7+
use PhpParser\Node\Expr;
8+
use PhpParser\Node\Expr\BinaryOp\Identical;
9+
use PhpParser\Node\Expr\FuncCall;
10+
use PhpParser\Node\Expr\Variable;
11+
use PhpParser\Node\Name;
12+
use PhpParser\Node\Stmt;
13+
use PHPStan\Analyser\ExpressionContext;
14+
use PHPStan\Analyser\Generator\ExprAnalysisRequest;
15+
use PHPStan\Analyser\Generator\ExprAnalysisResult;
16+
use PHPStan\Analyser\Generator\ExprHandler;
17+
use PHPStan\Analyser\Generator\GeneratorNodeScopeResolver;
18+
use PHPStan\Analyser\Generator\GeneratorScope;
19+
use PHPStan\Analyser\Generator\SpecifiedTypesHelper;
20+
use PHPStan\Analyser\SpecifiedTypes;
21+
use PHPStan\Analyser\TypeSpecifierContext;
22+
use PHPStan\DependencyInjection\AutowiredService;
23+
use PHPStan\Node\Expr\AlwaysRememberedExpr;
24+
use PHPStan\Node\Printer\ExprPrinter;
25+
use PHPStan\Reflection\InitializerExprTypeResolver;
26+
use PHPStan\Reflection\ReflectionProvider;
27+
use PHPStan\Type\Accessory\AccessoryNonEmptyStringType;
28+
use PHPStan\Type\Accessory\AccessoryNonFalsyStringType;
29+
use PHPStan\Type\Accessory\NonEmptyArrayType;
30+
use PHPStan\Type\Constant\ConstantArrayType;
31+
use PHPStan\Type\Constant\ConstantBooleanType;
32+
use PHPStan\Type\Constant\ConstantIntegerType;
33+
use PHPStan\Type\Constant\ConstantStringType;
34+
use PHPStan\Type\IntegerRangeType;
35+
use PHPStan\Type\NeverType;
36+
use PHPStan\Type\ObjectType;
37+
use PHPStan\Type\Type;
38+
use PHPStan\Type\TypeCombinator;
39+
use PHPStan\Type\UnionType;
40+
41+
/**
42+
* @implements ExprHandler<Identical>
43+
* @phpstan-import-type GeneratorTValueType from GeneratorNodeScopeResolver
44+
* @phpstan-import-type GeneratorTSendType from GeneratorNodeScopeResolver
45+
*/
46+
#[AutowiredService]
47+
final class IdenticalHandler implements ExprHandler
48+
{
49+
50+
public function __construct(
51+
private InitializerExprTypeResolver $initializerExprTypeResolver,
52+
private SpecifiedTypesHelper $specifiedTypesHelper,
53+
private ReflectionProvider $reflectionProvider,
54+
private ExprPrinter $exprPrinter,
55+
)
56+
{
57+
}
58+
59+
public function supports(Expr $expr): bool
60+
{
61+
return $expr instanceof Identical;
62+
}
63+
64+
public function analyseExpr(Stmt $stmt, Expr $expr, GeneratorScope $scope, ExpressionContext $context, ?callable $alternativeNodeCallback): Generator
65+
{
66+
$leftResult = yield new ExprAnalysisRequest($stmt, $expr->left, $scope, $context->enterDeep(), $alternativeNodeCallback);
67+
$rightResult = yield new ExprAnalysisRequest($stmt, $expr->right, $leftResult->scope, $context->enterDeep(), $alternativeNodeCallback);
68+
69+
$specifiedTypeGen = $this->resolveIdenticalSpecifiedTypes($expr, $leftResult, $rightResult, $scope);
70+
yield from $specifiedTypeGen;
71+
[$truthyTypes, $falseyTypes] = $specifiedTypeGen->getReturn();
72+
73+
return new ExprAnalysisResult(
74+
$this->getIdenticalType($expr, $leftResult->type, $rightResult->type),
75+
$this->getIdenticalType($expr, $leftResult->nativeType, $rightResult->nativeType),
76+
$rightResult->scope,
77+
hasYield: $leftResult->hasYield || $rightResult->hasYield,
78+
isAlwaysTerminating: $leftResult->isAlwaysTerminating || $rightResult->isAlwaysTerminating,
79+
throwPoints: array_merge($leftResult->throwPoints, $rightResult->throwPoints),
80+
impurePoints: array_merge($leftResult->impurePoints, $rightResult->impurePoints),
81+
specifiedTruthyTypes: $truthyTypes,
82+
specifiedFalseyTypes: $falseyTypes,
83+
specifiedNullTypes: new SpecifiedTypes(),
84+
);
85+
}
86+
87+
private function getIdenticalType(Identical $expr, Type $leftType, Type $rightType): Type
88+
{
89+
if (
90+
$expr->left instanceof Variable
91+
&& is_string($expr->left->name)
92+
&& $expr->right instanceof Variable
93+
&& is_string($expr->right->name)
94+
&& $expr->left->name === $expr->right->name
95+
) {
96+
return new ConstantBooleanType(true);
97+
}
98+
99+
// todo RicherScopeGetTypeHelper
100+
101+
return $this->initializerExprTypeResolver->resolveIdenticalType($leftType, $rightType)->type;
102+
}
103+
104+
/**
105+
* @return Generator<int, GeneratorTValueType, GeneratorTSendType, array{SpecifiedTypes, SpecifiedTypes}>
106+
*/
107+
private function resolveIdenticalSpecifiedTypes(
108+
Identical $identicalExpr,
109+
ExprAnalysisResult $leftResult,
110+
ExprAnalysisResult $rightResult,
111+
GeneratorScope $scope,
112+
): Generator
113+
{
114+
// Normalize to: fn() === expr
115+
$leftExpr = $identicalExpr->left;
116+
$rightExpr = $identicalExpr->right;
117+
if ($rightExpr instanceof FuncCall && !$leftExpr instanceof FuncCall) {
118+
[$leftExpr, $rightExpr] = [$rightExpr, $leftExpr];
119+
}
120+
121+
$unwrappedLeftExpr = $leftExpr;
122+
if ($leftExpr instanceof AlwaysRememberedExpr) {
123+
//$unwrappedLeftExpr = $leftExpr->getExpr();
124+
}
125+
$unwrappedRightExpr = $rightExpr;
126+
if ($rightExpr instanceof AlwaysRememberedExpr) {
127+
//$unwrappedRightExpr = $rightExpr->getExpr();
128+
}
129+
130+
$rightType = $rightResult->type;
131+
132+
// (count($a) === $b)
133+
if (
134+
$unwrappedLeftExpr instanceof FuncCall
135+
&& count($unwrappedLeftExpr->getArgs()) >= 1
136+
&& $unwrappedLeftExpr->name instanceof Name
137+
&& in_array(strtolower((string) $unwrappedLeftExpr->name), ['count', 'sizeof'], true)
138+
&& $rightType->isInteger()->yes()
139+
) {
140+
if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) {
141+
return [
142+
$this->specifiedTypesHelper->create(
143+
$unwrappedLeftExpr->getArgs()[0]->value,
144+
new NeverType(),
145+
TypeSpecifierContext::createTruthy(),
146+
)->setRootExpr($identicalExpr),
147+
$this->specifiedTypesHelper->create(
148+
$unwrappedLeftExpr->getArgs()[0]->value,
149+
new NeverType(),
150+
TypeSpecifierContext::createFalsey(),
151+
)->setRootExpr($identicalExpr),
152+
];
153+
}
154+
155+
$argType = (yield ExprAnalysisRequest::createNoopRequest($unwrappedLeftExpr->getArgs()[0]->value, $scope))->type;
156+
$isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType);
157+
if ($isZero->yes()) {
158+
$falseyNewArgType = new ConstantArrayType([], []);
159+
if (!$argType->isArray()->yes()) {
160+
$truthyNewArgType = new UnionType([
161+
new ObjectType(Countable::class),
162+
new ConstantArrayType([], []),
163+
]);
164+
} else {
165+
$truthyNewArgType = $falseyNewArgType;
166+
}
167+
168+
return [
169+
$this->specifiedTypesHelper->create($unwrappedLeftExpr, $rightType, TypeSpecifierContext::createTruthy())->setRootExpr($identicalExpr)->unionWith(
170+
$this->specifiedTypesHelper->create($unwrappedLeftExpr->getArgs()[0]->value, $truthyNewArgType, TypeSpecifierContext::createTruthy())->setRootExpr($identicalExpr),
171+
),
172+
$this->specifiedTypesHelper->create($unwrappedLeftExpr, $rightType, TypeSpecifierContext::createFalsey())->setRootExpr($identicalExpr)->unionWith(
173+
$this->specifiedTypesHelper->create($unwrappedLeftExpr->getArgs()[0]->value, $falseyNewArgType, TypeSpecifierContext::createFalsey())->setRootExpr($identicalExpr),
174+
),
175+
];
176+
}
177+
178+
$modeArgType = null;
179+
if (count($unwrappedLeftExpr->getArgs()) > 1) {
180+
$modeArgType = yield ExprAnalysisRequest::createNoopRequest($unwrappedLeftExpr->getArgs()[1]->value, $scope);
181+
}
182+
183+
$specifiedTypes = $this->specifiedTypesHelper->specifyTypesForCountFuncCall(
184+
$unwrappedLeftExpr,
185+
$argType,
186+
$rightType,
187+
$modeArgType,
188+
$identicalExpr,
189+
);
190+
if ($specifiedTypes !== null) {
191+
return $specifiedTypes;
192+
}
193+
194+
if ($argType->isArray()->yes()) {
195+
$funcTypes = $this->specifiedTypesHelper->create($unwrappedLeftExpr, $rightType, TypeSpecifierContext::createTruthy())->setRootExpr($identicalExpr);
196+
if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) {
197+
return [
198+
$funcTypes->unionWith(
199+
$this->specifiedTypesHelper->create($unwrappedLeftExpr->getArgs()[0]->value, new NonEmptyArrayType(), TypeSpecifierContext::createTruthy())->setRootExpr($identicalExpr),
200+
),
201+
new SpecifiedTypes(),
202+
];
203+
}
204+
205+
return [
206+
$funcTypes,
207+
new SpecifiedTypes(),
208+
];
209+
}
210+
}
211+
212+
// strlen($a) === $b
213+
if (
214+
$unwrappedLeftExpr instanceof FuncCall
215+
&& count($unwrappedLeftExpr->getArgs()) === 1
216+
&& $unwrappedLeftExpr->name instanceof Name
217+
&& in_array(strtolower((string) $unwrappedLeftExpr->name), ['strlen', 'mb_strlen'], true)
218+
&& $rightType->isInteger()->yes()
219+
) {
220+
if (IntegerRangeType::fromInterval(null, -1)->isSuperTypeOf($rightType)->yes()) {
221+
return [
222+
$this->specifiedTypesHelper->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), TypeSpecifierContext::createTruthy())->setRootExpr($identicalExpr),
223+
$this->specifiedTypesHelper->create($unwrappedLeftExpr->getArgs()[0]->value, new NeverType(), TypeSpecifierContext::createFalsey())->setRootExpr($identicalExpr),
224+
];
225+
}
226+
227+
$isZero = (new ConstantIntegerType(0))->isSuperTypeOf($rightType);
228+
if ($isZero->yes()) {
229+
return [
230+
$this->specifiedTypesHelper->create($unwrappedLeftExpr, $rightType, TypeSpecifierContext::createTruthy())->setRootExpr($identicalExpr)->unionWith(
231+
$this->specifiedTypesHelper->create($unwrappedLeftExpr->getArgs()[0]->value, new ConstantStringType(''), TypeSpecifierContext::createTruthy())->setRootExpr($identicalExpr),
232+
),
233+
$this->specifiedTypesHelper->create($unwrappedLeftExpr, $rightType, TypeSpecifierContext::createFalsey())->setRootExpr($identicalExpr)->unionWith(
234+
$this->specifiedTypesHelper->create($unwrappedLeftExpr->getArgs()[0]->value, new ConstantStringType(''), TypeSpecifierContext::createFalsey())->setRootExpr($identicalExpr),
235+
),
236+
];
237+
}
238+
239+
if (IntegerRangeType::fromInterval(1, null)->isSuperTypeOf($rightType)->yes()) {
240+
$argType = (yield ExprAnalysisRequest::createNoopRequest($unwrappedLeftExpr->getArgs()[0]->value, $scope))->type;
241+
if ($argType->isString()->yes()) {
242+
$funcTypes = $this->specifiedTypesHelper->create($unwrappedLeftExpr, $rightType, TypeSpecifierContext::createTruthy())->setRootExpr($identicalExpr);
243+
244+
$accessory = new AccessoryNonEmptyStringType();
245+
if (IntegerRangeType::fromInterval(2, null)->isSuperTypeOf($rightType)->yes()) {
246+
$accessory = new AccessoryNonFalsyStringType();
247+
}
248+
$valueTypes = $this->specifiedTypesHelper->create($unwrappedLeftExpr->getArgs()[0]->value, $accessory, TypeSpecifierContext::createTruthy())->setRootExpr($identicalExpr);
249+
250+
return [
251+
$funcTypes->unionWith($valueTypes),
252+
new SpecifiedTypes(),
253+
];
254+
}
255+
}
256+
}
257+
258+
// preg_match($a) === $b
259+
if (
260+
$unwrappedLeftExpr instanceof FuncCall
261+
&& $unwrappedLeftExpr->name instanceof Name
262+
&& $unwrappedLeftExpr->name->toLowerString() === 'preg_match'
263+
&& (new ConstantIntegerType(1))->isSuperTypeOf($rightType)->yes()
264+
) {
265+
return [
266+
$leftResult->specifiedTruthyTypes->setRootExpr($identicalExpr),
267+
new SpecifiedTypes(),
268+
];
269+
}
270+
271+
// get_class($a) === 'Foo'
272+
if (
273+
$unwrappedLeftExpr instanceof FuncCall
274+
&& $unwrappedLeftExpr->name instanceof Name
275+
&& in_array(strtolower($unwrappedLeftExpr->name->toString()), ['get_class', 'get_debug_type'], true)
276+
&& isset($unwrappedLeftExpr->getArgs()[0])
277+
) {
278+
if ($rightType instanceof ConstantStringType && $this->reflectionProvider->hasClass($rightType->getValue())) {
279+
return [
280+
$this->specifiedTypesHelper->create(
281+
$unwrappedLeftExpr->getArgs()[0]->value,
282+
new ObjectType($rightType->getValue(), classReflection: $this->reflectionProvider->getClass($rightType->getValue())->asFinal()),
283+
TypeSpecifierContext::createTruthy(),
284+
)->unionWith($this->specifiedTypesHelper->create($leftExpr, $rightType, TypeSpecifierContext::createTruthy()))->setRootExpr($identicalExpr),
285+
new SpecifiedTypes(),
286+
];
287+
}
288+
if ($rightType->getClassStringObjectType()->isObject()->yes()) {
289+
return [
290+
$this->specifiedTypesHelper->create(
291+
$unwrappedLeftExpr->getArgs()[0]->value,
292+
$rightType->getClassStringObjectType(),
293+
TypeSpecifierContext::createTruthy(),
294+
)->unionWith($this->specifiedTypesHelper->create($leftExpr, $rightType, TypeSpecifierContext::createTruthy()))->setRootExpr($identicalExpr),
295+
new SpecifiedTypes(),
296+
];
297+
}
298+
}
299+
300+
301+
return [
302+
new SpecifiedTypes(),
303+
new SpecifiedTypes(),
304+
];
305+
}
306+
307+
}
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser\Generator\ExprHandler;
4+
5+
use Generator;
6+
use PhpParser\Node\Expr;
7+
use PhpParser\Node\Expr\BinaryOp\NotIdentical;
8+
use PhpParser\Node\Expr\BooleanNot;
9+
use PhpParser\Node\Stmt;
10+
use PHPStan\Analyser\ExpressionContext;
11+
use PHPStan\Analyser\Generator\ExprAnalysisRequest;
12+
use PHPStan\Analyser\Generator\ExprAnalysisResult;
13+
use PHPStan\Analyser\Generator\ExprHandler;
14+
use PHPStan\Analyser\Generator\GeneratorScope;
15+
use PHPStan\Analyser\SpecifiedTypes;
16+
use PHPStan\DependencyInjection\AutowiredService;
17+
18+
/**
19+
* @implements ExprHandler<NotIdentical>
20+
*/
21+
#[AutowiredService]
22+
final class NotIdenticalHandler implements ExprHandler
23+
{
24+
25+
public function supports(Expr $expr): bool
26+
{
27+
return $expr instanceof NotIdentical;
28+
}
29+
30+
public function analyseExpr(Stmt $stmt, Expr $expr, GeneratorScope $scope, ExpressionContext $context, ?callable $alternativeNodeCallback): Generator
31+
{
32+
$leftResult = yield new ExprAnalysisRequest($stmt, $expr->left, $scope, $context->enterDeep(), $alternativeNodeCallback);
33+
$rightResult = yield new ExprAnalysisRequest($stmt, $expr->right, $leftResult->scope, $context->enterDeep(), $alternativeNodeCallback);
34+
$booleanNotResult = yield ExprAnalysisRequest::createNoopRequest(new BooleanNot(new Expr\BinaryOp\Identical($expr->left, $expr->right)), $scope);
35+
36+
return new ExprAnalysisResult(
37+
$booleanNotResult->type,
38+
$booleanNotResult->nativeType,
39+
$rightResult->scope,
40+
hasYield: $leftResult->hasYield || $rightResult->hasYield,
41+
isAlwaysTerminating: $leftResult->isAlwaysTerminating || $rightResult->isAlwaysTerminating,
42+
throwPoints: array_merge($leftResult->throwPoints, $rightResult->throwPoints),
43+
impurePoints: array_merge($leftResult->impurePoints, $rightResult->impurePoints),
44+
specifiedTruthyTypes: $booleanNotResult->specifiedTruthyTypes->setRootExpr($expr),
45+
specifiedFalseyTypes: $booleanNotResult->specifiedFalseyTypes->setRootExpr($expr),
46+
specifiedNullTypes: new SpecifiedTypes(),
47+
);
48+
}
49+
50+
}

0 commit comments

Comments
 (0)