Skip to content

Commit 3b094df

Browse files
committed
Bidirectional type narrowing
2 parents 5205fc7 + 66ec5e6 commit 3b094df

163 files changed

Lines changed: 7152 additions & 528 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.

‎conf/bleedingEdge.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,4 @@ parameters:
2727
switchConditionAlwaysFalse: true
2828
checkImportedClassNameCase: true
2929
sortWithoutEffect: true
30+
unresolvedTemplateArguments: true

‎conf/config.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ parameters:
5858
switchConditionAlwaysFalse: false
5959
checkImportedClassNameCase: false
6060
sortWithoutEffect: false
61+
unresolvedTemplateArguments: false
6162
fileExtensions:
6263
- php
6364
checkAdvancedIsset: false

‎conf/parametersSchema.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ parametersSchema:
5656
switchConditionAlwaysFalse: bool()
5757
checkImportedClassNameCase: bool()
5858
sortWithoutEffect: bool()
59+
unresolvedTemplateArguments: bool()
5960
])
6061
fileExtensions: listOf(string())
6162
checkAdvancedIsset: bool()
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php declare(strict_types = 1);
2+
3+
namespace PHPStan\Analyser;
4+
5+
use PHPStan\Reflection\ParameterReflection;
6+
7+
final class ClosureParameterTypes
8+
{
9+
10+
/**
11+
* @param ParameterReflection[]|null $parameters
12+
* @param ParameterReflection[]|null $nativeParameters
13+
*/
14+
public function __construct(
15+
public readonly ?array $parameters,
16+
public readonly ?array $nativeParameters,
17+
)
18+
{
19+
}
20+
21+
}

‎src/Analyser/DirectInternalScopeFactory.php‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@
33
namespace PHPStan\Analyser;
44

55
use PhpParser\Node;
6+
use PHPStan\Analyser\Generics\TemplateArgumentConstraints;
7+
use PHPStan\Analyser\Generics\TemplateArgumentFrame;
68
use PHPStan\DependencyInjection\Container;
79
use PHPStan\DependencyInjection\ExtensionsCollection;
810
use PHPStan\Node\Printer\ExprPrinter;
@@ -64,6 +66,8 @@ public function create(
6466
bool $afterExtractCall = false,
6567
?MutatingScope $parentScope = null,
6668
bool $nativeTypesPromoted = false,
69+
?TemplateArgumentFrame $templateArgumentFrame = null,
70+
?TemplateArgumentConstraints $templateArgumentConstraints = null,
6771
): MutatingScope
6872
{
6973
$className = MutatingScope::class;
@@ -103,6 +107,8 @@ public function create(
103107
$afterExtractCall,
104108
$parentScope,
105109
$nativeTypesPromoted,
110+
$templateArgumentFrame,
111+
$templateArgumentConstraints,
106112
);
107113
}
108114

‎src/Analyser/ExprHandler/ArrayDimFetchHandler.php‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper;
1818
use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper;
1919
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
20+
use PHPStan\Analyser\Generics\TemplateArgumentFrame;
2021
use PHPStan\Analyser\IssetabilityDescriptor;
2122
use PHPStan\Analyser\MutatingScope;
2223
use PHPStan\Analyser\NodeScopeResolver;
@@ -110,7 +111,7 @@ public function composeResult(NodeScopeResolver $nodeScopeResolver, Stmt $stmt,
110111
// flavour); the fabricated node is only the payload dynamic return
111112
// type extensions receive - nothing walks it. Gated by the same
112113
// maybe-ArrayAccess condition, so plain arrays never reach it.
113-
$offsetGetCall = new MethodCall($expr->var, new Identifier('offsetGet'), [new Arg($expr->dim)]);
114+
$offsetGetCall = new MethodCall($expr->var, new Identifier('offsetGet'), [new Arg($expr->dim)], [TemplateArgumentFrame::SYNTHETIC_SITE_ATTRIBUTE => true]);
114115
}
115116

116117
return $this->expressionResultFactory->create(

‎src/Analyser/ExprHandler/ArrowFunctionHandler.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ public function supports(Expr $expr): bool
3939

4040
public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult
4141
{
42-
$arrowFunctionResult = $nodeScopeResolver->processArrowFunctionNode($stmt, $expr, $scope, $storage, $nodeCallback, null);
42+
$arrowFunctionResult = $nodeScopeResolver->processArrowFunctionNode($stmt, $expr, $scope, $storage, $nodeCallback, null, null, $context);
4343
$result = $arrowFunctionResult->getExpressionResult();
4444

4545
// A plain typeCallback recursing through getClosureType() would re-walk

‎src/Analyser/ExprHandler/AssignHandler.php‎

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
3636
use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper;
3737
use PHPStan\Analyser\ExprHandler\Helper\VirtualExprResultHelper;
38+
use PHPStan\Analyser\Generics\TemplateArgumentObserver;
3839
use PHPStan\Analyser\ImpurePoint;
3940
use PHPStan\Analyser\InternalThrowPoint;
4041
use PHPStan\Analyser\MutatingScope;
@@ -102,6 +103,7 @@ final class AssignHandler implements ExprHandler
102103
{
103104

104105
public function __construct(
106+
private TemplateArgumentObserver $templateArgumentObserver,
105107
private VarAnnotationProcessor $varAnnotationProcessor,
106108
private PhpVersion $phpVersion,
107109
private ExprPrinter $exprPrinter,
@@ -238,6 +240,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
238240
$scope = $this->varAnnotationProcessor->processVarAnnotation($scope, $vars, $stmt, $varChangedScope);
239241
if (!$varChangedScope) {
240242
$scope = $nodeScopeResolver->processStmtVarAnnotation($scope, $storage, $stmt, null, $nodeCallback);
243+
} else {
244+
// the @var tag is a declared type the assigned value flows into
245+
$templateArgumentFrame = $nodeScopeResolver->observingTemplateArgumentFrame($scope);
246+
if ($templateArgumentFrame !== null) {
247+
foreach ($vars as $var) {
248+
if ($scope->hasVariableType($var)->no()) {
249+
continue;
250+
}
251+
$scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectSend($scope->getVariableType($var), $assignedExprResult->getType()));
252+
}
253+
}
241254
}
242255
}
243256

@@ -1104,7 +1117,7 @@ public function applyWrite(
11041117
if ($if === null) {
11051118
$if = $assignedExpr->cond;
11061119
}
1107-
$condScope = $nodeScopeResolver->processExprNode($stmt, $assignedExpr->cond, $scope, $storage->duplicate(), new NoopNodeCallback(), ExpressionContext::createDeep())->getScope();
1120+
$condScope = $nodeScopeResolver->processExprNode($stmt, $assignedExpr->cond, $scope, $storage->duplicate(), new NoopNodeCallback(), ExpressionContext::createDeep(resolveTemplateArguments: false))->getScope();
11081121
$truthySpecifiedTypes = $this->defaultNarrowingHelper->specifyTypesForNode($condScope, $assignedExpr->cond, TypeSpecifierContext::createTruthy());
11091122
$falseySpecifiedTypes = $this->defaultNarrowingHelper->specifyTypesForNode($condScope, $assignedExpr->cond, TypeSpecifierContext::createFalsey());
11101123
$truthyScope = $condScope->applySpecifiedTypes($truthySpecifiedTypes);
@@ -1257,6 +1270,9 @@ public function applyWrite(
12571270
$nativeScopeBeforeAssignEval = $scopeBeforeAssignEval->doNotTreatPhpDocTypesAsCertain();
12581271
$valueToWrite = $this->readAssignedValueType($nodeScopeResolver, $storedValueResult, $assignedExpr, $scopeBeforeAssignEval);
12591272
$nativeValueToWrite = $this->readAssignedValueType($nodeScopeResolver, $storedValueResult, $assignedExpr, $nativeScopeBeforeAssignEval);
1273+
// the value the write puts in, before the chain walk below rebuilds
1274+
// $valueToWrite into the containers of the enclosing dimensions
1275+
$writtenValueType = $valueToWrite;
12601276

12611277
[$varType, $varNativeType] = $this->resolveContainerTypesAfterAssignedExprEval($nodeScopeResolver, $var, $varResult, $scope, $scopeBeforeAssignEval, $storage);
12621278

@@ -1349,6 +1365,12 @@ public function applyWrite(
13491365
&& !$setVarType->isArray()->yes()
13501366
&& !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($setVarType)->no()
13511367
) {
1368+
$scope = $scope->addTemplateArgumentConstraints($nodeScopeResolver->collectOffsetSetUsage(
1369+
$scope,
1370+
$setVarType,
1371+
$offsetTypes[count($offsetTypes) - 1][0],
1372+
$writtenValueType,
1373+
));
13521374
$throwPoints = array_merge($throwPoints, $this->methodThrowPointHelper->getThrowPointsForCallOnType(
13531375
$scope,
13541376
$context,
@@ -1378,6 +1400,10 @@ public function applyWrite(
13781400
if ($propertyName !== null && $propertyHolderType->hasInstanceProperty($propertyName)->yes()) {
13791401
$propertyReflection = $propertyHolderType->getInstanceProperty($propertyName, $scope);
13801402
$assignedExprType = $this->readAssignedValueType($nodeScopeResolver, $assignedValueResult, $assignedExpr, $scope);
1403+
$templateArgumentFrame = $nodeScopeResolver->observingTemplateArgumentFrame($scope);
1404+
if ($templateArgumentFrame !== null) {
1405+
$scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectSend($propertyReflection->getWritableType(), $assignedExprType));
1406+
}
13811407
$nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage);
13821408
if ($propertyReflection->canChangeTypeAfterAssignment()) {
13831409
if ($propertyReflection->hasNativeType()) {
@@ -1469,6 +1495,12 @@ public function applyWrite(
14691495
if ($propertyName !== null) {
14701496
$propertyReflection = $scope->getStaticPropertyReflection($propertyHolderType, $propertyName);
14711497
$assignedExprType = $this->readAssignedValueType($nodeScopeResolver, $assignedValueResult, $assignedExpr, $scope);
1498+
if ($propertyReflection !== null) {
1499+
$templateArgumentFrame = $nodeScopeResolver->observingTemplateArgumentFrame($scope);
1500+
if ($templateArgumentFrame !== null) {
1501+
$scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectSend($propertyReflection->getWritableType(), $assignedExprType));
1502+
}
1503+
}
14721504
$nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage);
14731505
if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) {
14741506
if ($propertyReflection->hasNativeType()) {

‎src/Analyser/ExprHandler/BooleanAndHandler.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
5050
$rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $leftTruthyScope, $storage, $nodeCallback, $context);
5151
$rightExprType = $rightResult->getType();
5252
if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
53-
$leftMergedWithRightScope = $leftResult->getFalseyScope();
53+
$leftMergedWithRightScope = $leftResult->getFalseyScope()->addTemplateArgumentConstraints($rightResult->getScope()->getTemplateArgumentConstraints());
5454
} else {
5555
$leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope());
5656
}

‎src/Analyser/ExprHandler/BooleanOrHandler.php‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
6868
$rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $leftFalseyScope, $storage, $nodeCallback, $context);
6969
$rightExprType = $rightResult->getType();
7070
if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
71-
$leftMergedWithRightScope = $leftResult->getTruthyScope();
71+
$leftMergedWithRightScope = $leftResult->getTruthyScope()->addTemplateArgumentConstraints($rightResult->getScope()->getTemplateArgumentConstraints());
7272
} else {
7373
$leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope());
7474
}

0 commit comments

Comments
 (0)