Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
d81067d
Bidirectional type narrowing initial implementation
ondrejmirtes Sep 1, 2026
2edf14d
Add regression tests for issues fixed by unresolved template arguments
ondrejmirtes Sep 1, 2026
168c594
Add regression test for #8031
ondrejmirtes Sep 1, 2026
20c3947
Pin bug-15169-calls to the unresolved template argument semantics
ondrejmirtes Sep 6, 2026
8fd2168
Pin the native flavour of template arguments (currently failing)
ondrejmirtes Sep 6, 2026
5fad23a
Avoid retaining call ASTs in template argument caches
ondrejmirtes Sep 6, 2026
3b1a688
Preserve inference constraints from arrow function arguments
ondrejmirtes Sep 6, 2026
a87e6be
Preserve inference constraints from terminating expression branches
ondrejmirtes Sep 6, 2026
1f5f1e0
Preserve inference constraints when foreach scope does not escape
ondrejmirtes Sep 6, 2026
fba5263
Preserve inference constraints from unreachable for loop bodies
ondrejmirtes Sep 6, 2026
c359956
Resolve dependent template bounds and defaults completely
ondrejmirtes Sep 6, 2026
dfd955c
Preserve unresolved template identity in finite type comparisons
ondrejmirtes Sep 6, 2026
6fd7b53
Keep PHPDoc template inference out of native construction types
ondrejmirtes Sep 6, 2026
90fb324
Ignore diagnostic call markers when deciding statement replay
ondrejmirtes Sep 6, 2026
8906711
Reuse analysed bodies for array_map and immediately invoked closures
ondrejmirtes Sep 7, 2026
95ed4fe
Carry template resolution policy in immutable analysis contexts
ondrejmirtes Sep 7, 2026
186e52f
Keep template argument inference compatible with PHP 7.4
ondrejmirtes Sep 7, 2026
d59d8c4
Respect template bounds when inferring from later arguments
ondrejmirtes Sep 7, 2026
8da57e4
Use template bounds for unconstrained generic call arguments
ondrejmirtes Sep 7, 2026
2c6d582
Preserve template defaults when generic calls add no constraints
ondrejmirtes Sep 7, 2026
952645b
Use generic parameter defaults to constrain empty nested collections
ondrejmirtes Sep 7, 2026
194bd0e
Use template bounds for objects passed to impure mixed parameters
ondrejmirtes Sep 7, 2026
5e5139b
Preserve unknown ArrayObject values when constructed from an object
ondrejmirtes Sep 7, 2026
87a4b79
Preserve inferred arguments when return types omit generic arguments
ondrejmirtes Sep 7, 2026
2416411
Use a complete PHP version requirement in Bug14396Test
ondrejmirtes Sep 7, 2026
b379e3d
Update levels expectations for inferred SplObjectStorage values
ondrejmirtes Sep 7, 2026
a82b7e9
Preserve bounds for unconstrained template arguments
ondrejmirtes Sep 7, 2026
66ec5e6
Solve linked invariant template arguments together
ondrejmirtes Sep 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions conf/bleedingEdge.neon
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,4 @@ parameters:
switchConditionAlwaysFalse: true
checkImportedClassNameCase: true
sortWithoutEffect: true
unresolvedTemplateArguments: true
1 change: 1 addition & 0 deletions conf/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ parameters:
switchConditionAlwaysFalse: false
checkImportedClassNameCase: false
sortWithoutEffect: false
unresolvedTemplateArguments: false
fileExtensions:
- php
checkAdvancedIsset: false
Expand Down
1 change: 1 addition & 0 deletions conf/parametersSchema.neon
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ parametersSchema:
switchConditionAlwaysFalse: bool()
checkImportedClassNameCase: bool()
sortWithoutEffect: bool()
unresolvedTemplateArguments: bool()
])
fileExtensions: listOf(string())
checkAdvancedIsset: bool()
Expand Down
21 changes: 21 additions & 0 deletions src/Analyser/ClosureParameterTypes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser;

use PHPStan\Reflection\ParameterReflection;

final class ClosureParameterTypes
{

/**
* @param ParameterReflection[]|null $parameters
* @param ParameterReflection[]|null $nativeParameters
*/
public function __construct(
public readonly ?array $parameters,
public readonly ?array $nativeParameters,
)
{
}

}
6 changes: 6 additions & 0 deletions src/Analyser/DirectInternalScopeFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
namespace PHPStan\Analyser;

use PhpParser\Node;
use PHPStan\Analyser\Generics\TemplateArgumentConstraints;
use PHPStan\Analyser\Generics\TemplateArgumentFrame;
use PHPStan\DependencyInjection\Container;
use PHPStan\DependencyInjection\ExtensionsCollection;
use PHPStan\Node\Printer\ExprPrinter;
Expand Down Expand Up @@ -64,6 +66,8 @@ public function create(
bool $afterExtractCall = false,
?MutatingScope $parentScope = null,
bool $nativeTypesPromoted = false,
?TemplateArgumentFrame $templateArgumentFrame = null,
?TemplateArgumentConstraints $templateArgumentConstraints = null,
): MutatingScope
{
$className = MutatingScope::class;
Expand Down Expand Up @@ -103,6 +107,8 @@ public function create(
$afterExtractCall,
$parentScope,
$nativeTypesPromoted,
$templateArgumentFrame,
$templateArgumentConstraints,
);
}

Expand Down
3 changes: 2 additions & 1 deletion src/Analyser/ExprHandler/ArrayDimFetchHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper;
use PHPStan\Analyser\ExprHandler\Helper\MethodCallReturnTypeHelper;
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
use PHPStan\Analyser\Generics\TemplateArgumentFrame;
use PHPStan\Analyser\IssetabilityDescriptor;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
Expand Down Expand Up @@ -110,7 +111,7 @@ public function composeResult(NodeScopeResolver $nodeScopeResolver, Stmt $stmt,
// flavour); the fabricated node is only the payload dynamic return
// type extensions receive - nothing walks it. Gated by the same
// maybe-ArrayAccess condition, so plain arrays never reach it.
$offsetGetCall = new MethodCall($expr->var, new Identifier('offsetGet'), [new Arg($expr->dim)]);
$offsetGetCall = new MethodCall($expr->var, new Identifier('offsetGet'), [new Arg($expr->dim)], [TemplateArgumentFrame::SYNTHETIC_SITE_ATTRIBUTE => true]);
}

return $this->expressionResultFactory->create(
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/ExprHandler/ArrowFunctionHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ public function supports(Expr $expr): bool

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

// A plain typeCallback recursing through getClosureType() would re-walk
Expand Down
34 changes: 33 additions & 1 deletion src/Analyser/ExprHandler/AssignHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use PHPStan\Analyser\ExprHandler\Helper\MethodThrowPointHelper;
use PHPStan\Analyser\ExprHandler\Helper\NonNullabilityHelper;
use PHPStan\Analyser\ExprHandler\Helper\VirtualExprResultHelper;
use PHPStan\Analyser\Generics\TemplateArgumentObserver;
use PHPStan\Analyser\ImpurePoint;
use PHPStan\Analyser\InternalThrowPoint;
use PHPStan\Analyser\MutatingScope;
Expand Down Expand Up @@ -102,6 +103,7 @@ final class AssignHandler implements ExprHandler
{

public function __construct(
private TemplateArgumentObserver $templateArgumentObserver,
private VarAnnotationProcessor $varAnnotationProcessor,
private PhpVersion $phpVersion,
private ExprPrinter $exprPrinter,
Expand Down Expand Up @@ -238,6 +240,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$scope = $this->varAnnotationProcessor->processVarAnnotation($scope, $vars, $stmt, $varChangedScope);
if (!$varChangedScope) {
$scope = $nodeScopeResolver->processStmtVarAnnotation($scope, $storage, $stmt, null, $nodeCallback);
} else {
// the @var tag is a declared type the assigned value flows into
$templateArgumentFrame = $nodeScopeResolver->observingTemplateArgumentFrame($scope);
if ($templateArgumentFrame !== null) {
foreach ($vars as $var) {
if ($scope->hasVariableType($var)->no()) {
continue;
}
$scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectSend($scope->getVariableType($var), $assignedExprResult->getType()));
}
}
}
}

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

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

Expand Down Expand Up @@ -1349,6 +1365,12 @@ public function applyWrite(
&& !$setVarType->isArray()->yes()
&& !(new ObjectType(ArrayAccess::class))->isSuperTypeOf($setVarType)->no()
) {
$scope = $scope->addTemplateArgumentConstraints($nodeScopeResolver->collectOffsetSetUsage(
$scope,
$setVarType,
$offsetTypes[count($offsetTypes) - 1][0],
$writtenValueType,
));
$throwPoints = array_merge($throwPoints, $this->methodThrowPointHelper->getThrowPointsForCallOnType(
$scope,
$context,
Expand Down Expand Up @@ -1378,6 +1400,10 @@ public function applyWrite(
if ($propertyName !== null && $propertyHolderType->hasInstanceProperty($propertyName)->yes()) {
$propertyReflection = $propertyHolderType->getInstanceProperty($propertyName, $scope);
$assignedExprType = $this->readAssignedValueType($nodeScopeResolver, $assignedValueResult, $assignedExpr, $scope);
$templateArgumentFrame = $nodeScopeResolver->observingTemplateArgumentFrame($scope);
if ($templateArgumentFrame !== null) {
$scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectSend($propertyReflection->getWritableType(), $assignedExprType));
}
$nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage);
if ($propertyReflection->canChangeTypeAfterAssignment()) {
if ($propertyReflection->hasNativeType()) {
Expand Down Expand Up @@ -1469,6 +1495,12 @@ public function applyWrite(
if ($propertyName !== null) {
$propertyReflection = $scope->getStaticPropertyReflection($propertyHolderType, $propertyName);
$assignedExprType = $this->readAssignedValueType($nodeScopeResolver, $assignedValueResult, $assignedExpr, $scope);
if ($propertyReflection !== null) {
$templateArgumentFrame = $nodeScopeResolver->observingTemplateArgumentFrame($scope);
if ($templateArgumentFrame !== null) {
$scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectSend($propertyReflection->getWritableType(), $assignedExprType));
}
}
$nodeScopeResolver->callNodeCallback($nodeCallback, new PropertyAssignNode($var, $assignedExpr, $isAssignOp), $scopeBeforeAssignEval, $storage);
if ($propertyReflection !== null && $propertyReflection->canChangeTypeAfterAssignment()) {
if ($propertyReflection->hasNativeType()) {
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/ExprHandler/BooleanAndHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $leftTruthyScope, $storage, $nodeCallback, $context);
$rightExprType = $rightResult->getType();
if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
$leftMergedWithRightScope = $leftResult->getFalseyScope();
$leftMergedWithRightScope = $leftResult->getFalseyScope()->addTemplateArgumentConstraints($rightResult->getScope()->getTemplateArgumentConstraints());
} else {
$leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope());
}
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/ExprHandler/BooleanOrHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
$rightResult = $nodeScopeResolver->processExprNode($stmt, $expr->right, $leftFalseyScope, $storage, $nodeCallback, $context);
$rightExprType = $rightResult->getType();
if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
$leftMergedWithRightScope = $leftResult->getTruthyScope();
$leftMergedWithRightScope = $leftResult->getTruthyScope()->addTemplateArgumentConstraints($rightResult->getScope()->getTemplateArgumentConstraints());
} else {
$leftMergedWithRightScope = $leftResult->getScope()->mergeWith($rightResult->getScope());
}
Expand Down
2 changes: 1 addition & 1 deletion src/Analyser/ExprHandler/CoalesceHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex

$rightExprType = $rightResult->getType();
if ($rightExprType instanceof NeverType && $rightExprType->isExplicit()) {
$scope = $scope->applySpecifiedTypes($leftIssetTypes);
$scope = $scope->applySpecifiedTypes($leftIssetTypes)->addTemplateArgumentConstraints($rightResult->getScope()->getTemplateArgumentConstraints());
} else {
$scope = $scope->applySpecifiedTypes($leftIssetTypes)->mergeWith($rightResult->getScope());
}
Expand Down
9 changes: 5 additions & 4 deletions src/Analyser/ExprHandler/FuncCallHandler.php
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
use PHPStan\Analyser\ExprHandler\Helper\DynamicReturnTypeStoragePrimer;
use PHPStan\Analyser\ExprHandler\Helper\EarlyTerminatingCallHelper;
use PHPStan\Analyser\ExprHandler\Helper\FuncCallScopeEffectsHelper;
use PHPStan\Analyser\Generics\TemplateArgumentFrame;
use PHPStan\Analyser\ImpurePoint;
use PHPStan\Analyser\InternalThrowPoint;
use PHPStan\Analyser\MutatingScope;
Expand Down Expand Up @@ -208,8 +209,8 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
// properties array resolve from stored results instead of unprocessed
// nodes; processArgs() below processes them again as clone()'s arguments,
// so the NoopNodeCallback here avoids duplicate node-callbacks.
$cloneObjectArgResult = $nodeScopeResolver->processExprNode($stmt, $normalizedExpr->getArgs()[0]->value, $scope, $storage, new NoopNodeCallback(), $context->enterDeep());
$clonePropertiesArgResult = $nodeScopeResolver->processExprNode($stmt, $normalizedExpr->getArgs()[1]->value, $scope, $storage, new NoopNodeCallback(), $context->enterDeep());
$cloneObjectArgResult = $nodeScopeResolver->processExprNode($stmt, $normalizedExpr->getArgs()[0]->value, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()->withoutTemplateArgumentResolution());
$clonePropertiesArgResult = $nodeScopeResolver->processExprNode($stmt, $normalizedExpr->getArgs()[1]->value, $scope, $storage, new NoopNodeCallback(), $context->enterDeep()->withoutTemplateArgumentResolution());
$clonePropertiesArgType = $clonePropertiesArgResult->getType();
// the cloned type is composed from the object argument's result -
// no synthetic Clone_ walk
Expand Down Expand Up @@ -660,7 +661,7 @@ private function resolveReturnType(NodeScopeResolver $nodeScopeResolver, Mutatin
}
}

return $parametersAcceptor->getReturnType();
return TemplateArgumentFrame::returnTypeOfCall($parametersAcceptor, $reflectionScope, $expr);
}

if (!$this->reflectionProvider->hasFunction($expr->name, $reflectionScope)) {
Expand Down Expand Up @@ -732,7 +733,7 @@ private function resolveReturnType(NodeScopeResolver $nodeScopeResolver, Mutatin

// the typeCallback keeps void; ExpressionResult projects void->null for
// value reads, getKeepVoidType() keeps it
return $parametersAcceptor->getReturnType();
return TemplateArgumentFrame::returnTypeOfCall($parametersAcceptor, $reflectionScope, $expr);
}

/**
Expand Down
63 changes: 63 additions & 0 deletions src/Analyser/ExprHandler/Helper/ClosureParameterResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php declare(strict_types = 1);

namespace PHPStan\Analyser\ExprHandler\Helper;

use PhpParser\Node;
use PHPStan\Analyser\ClosureParameterTypes;
use PHPStan\Analyser\ExpressionResultStorage;
use PHPStan\Analyser\MutatingScope;
use PHPStan\Analyser\NodeScopeResolver;
use PHPStan\DependencyInjection\AutowiredService;
use PHPStan\Parser\ArrayMapArgVisitor;
use PHPStan\Parser\ImmediatelyInvokedClosureVisitor;
use PHPStan\Reflection\PassedByReference;
use PHPStan\Reflection\Php\DummyParameter;
use PHPStan\Type\Type;

/** Resolve the same contextual parameters for body analysis and closure type inference. */
#[AutowiredService]
final class ClosureParameterResolver
{

public function __construct(private NodeScopeResolver $nodeScopeResolver)
{
}

/** @param Node\Arg[]|null $callArgs */
public function resolve(
MutatingScope $scope,
Node\Expr\Closure|Node\Expr\ArrowFunction $expr,
?ExpressionResultStorage $storage,
?array $callArgs,
?Type $passedToType,
?Type $nativePassedToType,
): ClosureParameterTypes
{
$arrayMapArgs = $expr->getAttribute(ArrayMapArgVisitor::ATTRIBUTE_NAME);
$immediatelyInvokedArgs = $expr->getAttribute(ImmediatelyInvokedClosureVisitor::ARGS_ATTRIBUTE_NAME);
$intrinsicArgs = $arrayMapArgs ?? $immediatelyInvokedArgs;
if ($intrinsicArgs === null) {
return new ClosureParameterTypes(
$this->nodeScopeResolver->createCallableParameters($scope, $expr, $callArgs, $passedToType),
$this->nodeScopeResolver->createNativeCallableParameters($scope, $expr, $callArgs, $nativePassedToType),
);
}

$parameters = [];
$nativeParameters = [];
foreach ($intrinsicArgs as $arg) {
$result = $storage !== null ? $storage->findExpressionResult($arg->value) : null;
$type = $result !== null ? $result->getType() : $this->nodeScopeResolver->readScopeStateOrSyntheticType($arg->value, $scope);
$nativeType = $result !== null ? $result->getNativeType() : $this->nodeScopeResolver->readScopeStateOrSyntheticType($arg->value, $scope->doNotTreatPhpDocTypesAsCertain());
if ($arrayMapArgs !== null) {
$type = $type->getIterableValueType();
$nativeType = $nativeType->getIterableValueType();
}
$parameters[] = new DummyParameter('item', $type, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
$nativeParameters[] = new DummyParameter('item', $nativeType, optional: false, passedByReference: PassedByReference::createNo(), variadic: false, defaultValue: null);
}

return new ClosureParameterTypes($parameters, $nativeParameters);
}

}
Loading
Loading