Skip to content

Commit c871d1f

Browse files
committed
Infer signatures of closures from where their values are sent
2 parents 3bc4b93 + e31e14e commit c871d1f

88 files changed

Lines changed: 7858 additions & 1668 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
@@ -28,5 +28,6 @@ parameters:
2828
checkImportedClassNameCase: true
2929
sortWithoutEffect: true
3030
unresolvedTemplateArguments: true
31+
closureSignaturesFromUsages: true
3132
unusedVariable: true
3233
unusedParameters: true

‎conf/config.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ parameters:
6060
checkImportedClassNameCase: false
6161
sortWithoutEffect: false
6262
unresolvedTemplateArguments: false
63+
closureSignaturesFromUsages: false
6364
unusedVariable: false
6465
unusedParameters: false
6566
fileExtensions:

‎conf/parametersSchema.neon‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ parametersSchema:
5757
checkImportedClassNameCase: bool()
5858
sortWithoutEffect: bool()
5959
unresolvedTemplateArguments: bool()
60+
closureSignaturesFromUsages: bool()
6061
unusedVariable: bool()
6162
unusedParameters: bool()
6263
])

‎src/Analyser/ArgumentsHandler.php‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -736,6 +736,17 @@ public function processArgs(
736736
$gatheredTypes,
737737
$callLike instanceof New_ && $calleeReflection instanceof MethodReflection ? $calleeReflection->getDeclaringClass()->getTemplateTypeMap() : null,
738738
));
739+
if ($this->templateArgumentObserver->carriesClosureSignatureMarkers($gatheredTypes)) {
740+
// the acceptor the closures are sent to: generic-resolved and with
741+
// the intrinsic overrides (array_map's callback) applied
742+
$scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectClosureArguments(
743+
$typeDrivenAcceptorSelection
744+
? $resolvedAcceptor
745+
: $this->selectArgsMetadataAcceptor($nodeScopeResolver, $args, $gatheredTypes, $parametersAcceptors, $namedArgumentsVariants, $gatheredHasName, $gatheredUnpack, $scope),
746+
$gatheredTypes,
747+
($calleeReflection instanceof FunctionReflection || $calleeReflection instanceof ExtendedMethodReflection) && $calleeReflection->isPure()->yes(),
748+
));
749+
}
739750
}
740751

741752
// The by-ref OUT writeback reads the metadata acceptor: it is selected from

‎src/Analyser/ClosureProcessor.php‎

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
use PHPStan\Analyser\ExprHandler\ArrowFunctionHandler;
1111
use PHPStan\Analyser\ExprHandler\Helper\ClosureParameterResolver;
1212
use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver;
13+
use PHPStan\Analyser\ExprHandler\Helper\ContextualClosureParameterResolver;
14+
use PHPStan\Analyser\Generics\ClosureSignatureInference;
1315
use PHPStan\DependencyInjection\AutowiredService;
1416
use PHPStan\DependencyInjection\Container;
1517
use PHPStan\Node\ClosureReturnStatementsNode;
@@ -51,6 +53,8 @@ public function __construct(
5153
private ExpressionResultFactory $expressionResultFactory,
5254
private ClosureParameterResolver $closureParameterResolver,
5355
private ClosureTypeResolver $closureTypeResolver,
56+
private ContextualClosureParameterResolver $contextualClosureParameterResolver,
57+
private ClosureSignatureInference $closureSignatureInference,
5458
)
5559
{
5660
}
@@ -107,6 +111,12 @@ private function processClosureNodeInternal(
107111
$parameterTypes = $this->closureParameterResolver->resolve($scope, $expr, $storage, $closureCallArgs, $passedToType, $nativePassedToType);
108112
$callableParameters = $parameterTypes->parameters;
109113
$nativeCallableParameters = $parameterTypes->nativeParameters;
114+
[$expectedReturnType, $nativeExpectedReturnType] = $this->contextualClosureParameterResolver->resolveExpectedReturnTypes($scope, $expr, $passedToType, $nativePassedToType);
115+
// the closure's own body may invoke it (use (&$self)): its sites must
116+
// exist before the body is observed
117+
if ($this->closureSignatureInference->isObserving($scope)) {
118+
$scope = $scope->addTemplateArgumentConstraints($this->closureSignatureInference->collectSites($scope, $this->closureTypeResolver->getClosureType($scope, $expr, true, $storage)));
119+
}
110120

111121
$useScope = $scope;
112122
foreach ($expr->uses as $use) {
@@ -219,7 +229,7 @@ private function processClosureNodeInternal(
219229
if (count($byRefUses) === 0) {
220230
$nodeScopeResolver->pushNodeGatherer($closureStmtsGatherer);
221231
try {
222-
$statementResult = $nodeScopeResolver->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments()));
232+
$statementResult = $nodeScopeResolver->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments())->withExpectedReturnType($expectedReturnType, $nativeExpectedReturnType));
223233
} finally {
224234
$nodeScopeResolver->popNodeGatherer();
225235
}
@@ -266,7 +276,7 @@ private function processClosureNodeInternal(
266276
// loops walk single-pass here and only the final walk below (top-level)
267277
// runs their full convergence - otherwise every closure-convergence
268278
// pass would re-converge every inner loop from scratch
269-
$intermediaryClosureScopeResult = $nodeScopeResolver->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $bodyRecording, StatementContext::createDeep(resolveTemplateArguments: false));
279+
$intermediaryClosureScopeResult = $nodeScopeResolver->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $bodyRecording, StatementContext::createDeep(resolveTemplateArguments: false)->withExpectedReturnType($expectedReturnType, $nativeExpectedReturnType));
270280
// the candidate to replace the final walk when this pass's entry
271281
// turns out to be the fixpoint
272282
if ($bodyRecording instanceof RecordingNodeCallback) {
@@ -320,7 +330,7 @@ private function processClosureNodeInternal(
320330
$nodeScopeResolver->replayRecording($replayBodyRecording, $nodeCallback, $originalStorage, $closureScope);
321331
$statementResult = $replayPassResult;
322332
} else {
323-
$statementResult = $nodeScopeResolver->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments()));
333+
$statementResult = $nodeScopeResolver->processStmtNodesInternal($expr, $expr->stmts, $closureScope, $storage, $nodeCallback, StatementContext::createTopLevel($context->shouldResolveTemplateArguments())->withExpectedReturnType($expectedReturnType, $nativeExpectedReturnType));
324334
}
325335
} finally {
326336
$nodeScopeResolver->popNodeGatherer();
@@ -445,6 +455,7 @@ public function processArrowFunctionNode(
445455
$parameterTypes = $this->closureParameterResolver->resolve($scope, $expr, $storage, $arrowFunctionCallArgs, $passedToType, $nativePassedToType);
446456
$callableParameters = $parameterTypes->parameters;
447457
$nativeCallableParameters = $parameterTypes->nativeParameters;
458+
[$expectedReturnType, $nativeExpectedReturnType] = $this->contextualClosureParameterResolver->resolveExpectedReturnTypes($scope, $expr, $passedToType, $nativePassedToType);
448459
$arrowFunctionScope = $scope->enterArrowFunction($expr, $callableParameters, $nativeCallableParameters);
449460
if ($arrowFunctionScope->getAnonymousFunctionReflection() === null) {
450461
throw new ShouldNotHappenException();
@@ -482,7 +493,7 @@ public function processArrowFunctionNode(
482493

483494
$nodeScopeResolver->pushNodeGatherer($arrowFunctionStmtsGatherer);
484495
try {
485-
$exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel($context->shouldResolveTemplateArguments()));
496+
$exprResult = $nodeScopeResolver->processExprNode($stmt, $expr->expr, $arrowFunctionScope, $storage, $nodeCallback, ExpressionContext::createTopLevel($context->shouldResolveTemplateArguments())->enterPassedToType($expectedReturnType, $nativeExpectedReturnType));
486497
} finally {
487498
$nodeScopeResolver->popNodeGatherer();
488499
}

‎src/Analyser/ExprHandler/ArrowFunctionHandler.php‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Analyser\ExprHandler;
1414
use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver;
1515
use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper;
16+
use PHPStan\Analyser\Generics\ClosureSignatureInference;
1617
use PHPStan\Analyser\MutatingScope;
1718
use PHPStan\Analyser\NodeScopeResolver;
1819
use PHPStan\Analyser\TypeSpecifierContext;
@@ -34,6 +35,7 @@ public function __construct(
3435
private ExpressionResultFactory $expressionResultFactory,
3536
private DefaultNarrowingHelper $defaultNarrowingHelper,
3637
private ClosureProcessor $closureProcessor,
38+
private ClosureSignatureInference $closureSignatureInference,
3739
)
3840
{
3941
}
@@ -84,7 +86,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
8486
);
8587

8688
return $this->expressionResultFactory->create(
87-
$result->getScope(),
89+
$result->getScope()->addTemplateArgumentConstraints($this->closureSignatureInference->collectSites($scope, $type)),
8890
beforeScope: $scope,
8991
expr: $expr,
9092
variableFlow: $result->getVariableFlow(),

‎src/Analyser/ExprHandler/AssignHandler.php‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1480,6 +1480,15 @@ public function applyWrite(
14801480
}
14811481
}
14821482

1483+
if (
1484+
(!$var instanceof Variable || !is_string($var->name) || in_array($var->name, Scope::SUPERGLOBAL_VARIABLES, true))
1485+
&& $nodeScopeResolver->observingTemplateArgumentFrame($scope) !== null
1486+
) {
1487+
// an offset of a property or a superglobal: whoever reads it later
1488+
// can invoke the closures written into it with anything
1489+
$scope = $scope->addTemplateArgumentConstraints($this->templateArgumentObserver->collectEscape($writtenValueType));
1490+
}
1491+
14831492
foreach ($additionalExpressions as $k => $additionalExpression) {
14841493
[$expr, $type] = $additionalExpression;
14851494
$nativeType = $type;
@@ -1848,6 +1857,7 @@ private function collectOffsetSetUsage(NodeScopeResolver $nodeScopeResolver, Mut
18481857
}
18491858

18501859
$constraints = $constraints->merge($this->templateArgumentObserver->collectArgument($parameters[1]->getType(), $valueType));
1860+
$constraints = $constraints->merge($this->templateArgumentObserver->collectClosureArgument($parameters[1]->getType(), $valueType));
18511861
return $constraints;
18521862
}
18531863

‎src/Analyser/ExprHandler/ClosureHandler.php‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
use PHPStan\Analyser\ExprHandler;
1414
use PHPStan\Analyser\ExprHandler\Helper\ClosureTypeResolver;
1515
use PHPStan\Analyser\ExprHandler\Helper\DefaultNarrowingHelper;
16+
use PHPStan\Analyser\Generics\ClosureSignatureInference;
1617
use PHPStan\Analyser\MutatingScope;
1718
use PHPStan\Analyser\NodeScopeResolver;
1819
use PHPStan\Analyser\TypeSpecifierContext;
@@ -34,6 +35,7 @@ public function __construct(
3435
private ExpressionResultFactory $expressionResultFactory,
3536
private DefaultNarrowingHelper $defaultNarrowingHelper,
3637
private ClosureProcessor $closureProcessor,
38+
private ClosureSignatureInference $closureSignatureInference,
3739
)
3840
{
3941
}
@@ -77,7 +79,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
7779
$nativeType = $type;
7880

7981
return $this->expressionResultFactory->create(
80-
$processClosureResult->applyByRefUseScope($processClosureResult->getScope()),
82+
$processClosureResult->applyByRefUseScope($processClosureResult->getScope())->addTemplateArgumentConstraints($this->closureSignatureInference->collectSites($scope, $type)),
8183
beforeScope: $scope,
8284
expr: $expr,
8385
variableFlow: self::getVariableFlow($expr),

‎src/Analyser/ExprHandler/FuncCallHandler.php‎

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
use PHPStan\Analyser\ExprHandler\Helper\DynamicReturnTypeStoragePrimer;
2828
use PHPStan\Analyser\ExprHandler\Helper\EarlyTerminatingCallHelper;
2929
use PHPStan\Analyser\ExprHandler\Helper\FuncCallScopeEffectsHelper;
30+
use PHPStan\Analyser\Generics\ClosureSignatureInference;
3031
use PHPStan\Analyser\Generics\TemplateArgumentFrame;
3132
use PHPStan\Analyser\ImpurePoint;
3233
use PHPStan\Analyser\InternalThrowPoint;
@@ -52,21 +53,26 @@
5253
use PHPStan\Reflection\Callables\SimpleThrowPoint;
5354
use PHPStan\Reflection\ExtendedParametersAcceptor;
5455
use PHPStan\Reflection\FunctionReflection;
56+
use PHPStan\Reflection\Native\NativeParameterReflection;
5557
use PHPStan\Reflection\ParametersAcceptor;
5658
use PHPStan\Reflection\ParametersAcceptorSelector;
59+
use PHPStan\Reflection\PassedByReference;
5760
use PHPStan\Reflection\ReflectionProvider;
5861
use PHPStan\Rules\Comparison\ImpossibleCheckTypeHelper;
5962
use PHPStan\ShouldNotHappenException;
6063
use PHPStan\Turbo\ShadowedByTurboExtension;
6164
use PHPStan\Type\Accessory\HasPropertyType;
6265
use PHPStan\Type\Accessory\NonEmptyArrayType;
66+
use PHPStan\Type\CallableType;
67+
use PHPStan\Type\ClosureType;
6368
use PHPStan\Type\Constant\ConstantBooleanType;
6469
use PHPStan\Type\DynamicFunctionThrowTypeExtension;
6570
use PHPStan\Type\DynamicReturnTypeExtensionRegistry;
6671
use PHPStan\Type\ErrorType;
6772
use PHPStan\Type\Generic\TemplateTypeHelper;
6873
use PHPStan\Type\Generic\TemplateTypeVariance;
6974
use PHPStan\Type\Generic\TemplateTypeVarianceMap;
75+
use PHPStan\Type\MixedType;
7076
use PHPStan\Type\NeverType;
7177
use PHPStan\Type\ObjectType;
7278
use PHPStan\Type\Type;
@@ -112,6 +118,7 @@ public function __construct(
112118
private ArgumentsHandler $argumentsHandler,
113119
private ClosureProcessor $closureProcessor,
114120
private AssignHandler $assignHandler,
121+
private ClosureSignatureInference $closureSignatureInference,
115122
)
116123
{
117124
}
@@ -341,6 +348,17 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex
341348
}
342349
}
343350
$resolvedParametersAcceptor = $argsResult->getResolvedParametersAcceptor();
351+
if (
352+
$resolvedParametersAcceptor !== null
353+
&& $nameResult !== null
354+
&& $expr->name instanceof Expr\Variable
355+
&& is_string($expr->name->name)
356+
) {
357+
$invokedClosureType = $this->resolveInvokedClosureType($nodeScopeResolver, $normalizedExpr, $expr->name->name, $nameResult->getType(), $argsResult->getScope(), $storage);
358+
if ($invokedClosureType !== null) {
359+
$resolvedParametersAcceptor = $invokedClosureType->getCallableParametersAcceptors($scope)[0];
360+
}
361+
}
344362
// arguments walked ahead of the callee: the scope already carries them
345363
// and the callee walk's own effects (a closure's by-ref uses) on top
346364
if ($argumentsWalkedAhead === null) {
@@ -764,6 +782,63 @@ private function resolveReturnType(NodeScopeResolver $nodeScopeResolver, Mutatin
764782
return TemplateArgumentFrame::returnTypeOfCall($parametersAcceptor, $reflectionScope, $expr);
765783
}
766784

785+
/**
786+
* A closure whose signature is inferred from its invocations (see
787+
* ClosureSignatureInference) joins the arguments of all of them; invoked
788+
* with narrower ones, it returns what its body returns for these. The
789+
* variable holds the closure it was assigned when the type it holds is the
790+
* type the closure was created with.
791+
*/
792+
private function resolveInvokedClosureType(NodeScopeResolver $nodeScopeResolver, FuncCall $call, string $name, Type $calleeType, MutatingScope $scope, ExpressionResultStorage $storage): ?ClosureType
793+
{
794+
if (!$calleeType instanceof ClosureType || !$this->closureSignatureInference->infersInvocationReturnType($scope, $calleeType)) {
795+
return null;
796+
}
797+
$parameters = $calleeType->getParameters();
798+
$args = $call->getArgs();
799+
if ($args === [] || count($args) > count($parameters)) {
800+
return null;
801+
}
802+
803+
$narrower = false;
804+
$callableParameters = [];
805+
foreach ($parameters as $i => $parameter) {
806+
if ($parameter->isVariadic() || !$parameter->passedByReference()->no()) {
807+
return null;
808+
}
809+
$type = $parameter->getType();
810+
if (isset($args[$i])) {
811+
if ($args[$i]->name !== null || $args[$i]->unpack) {
812+
return null;
813+
}
814+
$argumentType = $nodeScopeResolver->readTypeOfMaybeStored($args[$i]->value, $scope);
815+
// an argument the signature rejects is reported against it
816+
if (!$type->isSuperTypeOf($argumentType)->yes()) {
817+
return null;
818+
}
819+
if (!$argumentType->isSuperTypeOf($type)->yes()) {
820+
$narrower = true;
821+
$type = $argumentType;
822+
}
823+
}
824+
$callableParameters[] = new NativeParameterReflection($parameter->getName(), $parameter->isOptional(), $type, PassedByReference::createNo(), false, null);
825+
}
826+
if (!$narrower) {
827+
return null;
828+
}
829+
830+
foreach ($this->closureSignatureInference->findAssignedClosures($scope, $name) as $closure) {
831+
$creationResult = $storage->findExpressionResult($closure);
832+
if ($creationResult === null || !$creationResult->getType()->equals($calleeType)) {
833+
continue;
834+
}
835+
836+
return $this->closureTypeResolver->getClosureType($creationResult->getBeforeScope(), $closure, false, $storage, new CallableType($callableParameters, new MixedType(), false));
837+
}
838+
839+
return null;
840+
}
841+
767842
/**
768843
* Ported inside-out from the old TypeResolvingExprHandler::specifyTypes(): the
769844
* FunctionTypeSpecifyingExtensions, conditional-return-type and @phpstan-assert

0 commit comments

Comments
 (0)