diff --git a/build/PHPStan/Build/RequiredPhpVersionVisitor.php b/build/PHPStan/Build/RequiredPhpVersionVisitor.php index c71aa7d638e..873a0374f61 100644 --- a/build/PHPStan/Build/RequiredPhpVersionVisitor.php +++ b/build/PHPStan/Build/RequiredPhpVersionVisitor.php @@ -120,6 +120,10 @@ public function enterNode(Node $node): ?Node } } + if ($node instanceof Node\Expr\CallLike && $node->isPartialFunctionApplication() && !$node->isFirstClassCallable()) { + $this->require(static fn (PhpVersion $phpVersion) => $phpVersion->supportsPartialFunctionApplication(), 'partial function application', $node); + } + if ($node instanceof Node\Arg && $node->name !== null) { $this->require(static fn (PhpVersion $phpVersion) => $phpVersion->supportsNamedArguments(), 'named arguments', $node); } @@ -211,7 +215,7 @@ private function findPhpVersion(callable $callable): int { $minorVersionIterator = new PhpMinorVersionIterator( new PhpVersion(70400), - new PhpVersion(80500), + new PhpVersion(80600), ); foreach ($minorVersionIterator as $phpVersion) { diff --git a/build/collision-detector.json b/build/collision-detector.json index fbdee3556b9..d920bf8d16b 100644 --- a/build/collision-detector.json +++ b/build/collision-detector.json @@ -18,6 +18,10 @@ "../tests/PHPStan/Levels/data/stubs/function.php", "../tests/PHPStan/Rules/Properties/data/abstract-final-property-hook-parse-error.php", "../tests/PHPStan/Rules/Properties/data/final-property-hooks.php", - "../tests/PHPStan/Rules/Cast/data/void-cast.php" + "../tests/PHPStan/Rules/Cast/data/void-cast.php", + "../tests/PHPStan/Analyser/nsrt/partial-function-application.php", + "../tests/PHPStan/Rules/DeadCode/data/partial-function-application-unused-private-method.php", + "../tests/PHPStan/Rules/Functions/data/partial-function-application.php", + "../tests/PHPStan/Rules/Functions/data/partial-function-application-php-versions.php" ] } diff --git a/src/Analyser/ExprHandler/AssignHandler.php b/src/Analyser/ExprHandler/AssignHandler.php index bd62f071ea6..1e24f6cab7d 100644 --- a/src/Analyser/ExprHandler/AssignHandler.php +++ b/src/Analyser/ExprHandler/AssignHandler.php @@ -342,7 +342,7 @@ private function captureAssignedCallArgResults(Expr $assignedExpr, ExpressionRes } elseif ($assignedExpr instanceof Expr\BinaryOp\Minus && $assignedExpr->left instanceof FuncCall) { $call = $assignedExpr->left; } - if ($call === null || $call->isFirstClassCallable()) { + if ($call === null || $call->isPartialFunctionApplication()) { return []; } @@ -418,7 +418,7 @@ private function createSpecifyTypesCallback(Assign $expr, ExpressionResult $assi if ( $expr->expr instanceof FuncCall && $expr->expr->name instanceof Name - && !$expr->expr->isFirstClassCallable() + && !$expr->expr->isPartialFunctionApplication() && in_array($expr->expr->name->toLowerString(), ['array_key_first', 'array_key_last'], true) && count($expr->expr->getArgs()) >= 1 ) { @@ -456,7 +456,7 @@ private function createSpecifyTypesCallback(Assign $expr, ExpressionResult $assi if ( $expr->expr instanceof FuncCall && $expr->expr->name instanceof Name - && !$expr->expr->isFirstClassCallable() + && !$expr->expr->isPartialFunctionApplication() && count($expr->expr->getArgs()) >= 2 ) { $funcName = $expr->expr->name->toLowerString(); @@ -518,7 +518,7 @@ private function createSpecifyTypesCallback(Assign $expr, ExpressionResult $assi if ( $expr->expr instanceof FuncCall && $expr->expr->name instanceof Name - && !$expr->expr->isFirstClassCallable() + && !$expr->expr->isPartialFunctionApplication() && in_array($expr->expr->name->toLowerString(), ['array_rand'], true) && count($expr->expr->getArgs()) >= 1 ) { @@ -549,7 +549,7 @@ private function createSpecifyTypesCallback(Assign $expr, ExpressionResult $assi $expr->expr instanceof Expr\BinaryOp\Minus && $expr->expr->left instanceof FuncCall && $expr->expr->left->name instanceof Name - && !$expr->expr->left->isFirstClassCallable() + && !$expr->expr->left->isPartialFunctionApplication() && $expr->expr->right instanceof Node\Scalar\Int_ && $expr->expr->right->value === 1 && in_array($expr->expr->left->name->toLowerString(), ['count', 'sizeof'], true) @@ -2450,7 +2450,7 @@ private function processInArrayForConditionalExpressionsAfterAssign( if ( !$assignedExpr->name instanceof Name || $assignedExpr->name->toLowerString() !== 'in_array' - || $assignedExpr->isFirstClassCallable() + || $assignedExpr->isPartialFunctionApplication() || !$assignedType->isTrue()->maybe() ) { return $conditionalExpressions; diff --git a/src/Analyser/ExprHandler/BinaryOpHandler.php b/src/Analyser/ExprHandler/BinaryOpHandler.php index 362e7b27ab4..3516491d958 100644 --- a/src/Analyser/ExprHandler/BinaryOpHandler.php +++ b/src/Analyser/ExprHandler/BinaryOpHandler.php @@ -123,11 +123,11 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // never collects) $specifySubResults = []; $specifySubExprs = []; - if ($expr->right instanceof Expr\FuncCall && !$expr->right->isFirstClassCallable() && isset($expr->right->getArgs()[0])) { + if ($expr->right instanceof Expr\FuncCall && !$expr->right->isPartialFunctionApplication() && isset($expr->right->getArgs()[0])) { $specifySubExprs[] = $expr->right->getArgs()[0]->value; } elseif ($expr->right instanceof BinaryOp\Minus) { $specifySubExprs[] = $expr->right->right; - if ($expr->right->left instanceof Expr\FuncCall && !$expr->right->left->isFirstClassCallable() && isset($expr->right->left->getArgs()[0])) { + if ($expr->right->left instanceof Expr\FuncCall && !$expr->right->left->isPartialFunctionApplication() && isset($expr->right->left->getArgs()[0])) { $specifySubExprs[] = $expr->right->left->getArgs()[0]->value; } } @@ -368,7 +368,7 @@ static function () use ($expr, $nativeTypesPromoted, $typeCallback): Type { if ( $expr->left instanceof Expr\FuncCall && $expr->left->name instanceof Name - && !$expr->left->isFirstClassCallable() + && !$expr->left->isPartialFunctionApplication() && in_array(strtolower((string) $expr->left->name), ['count', 'sizeof', 'strlen', 'mb_strlen', 'preg_match'], true) && count($expr->left->getArgs()) >= 1 && ( @@ -426,7 +426,7 @@ static function () use ($expr, $nativeTypesPromoted, $typeCallback): Type { !$context->null() && $expr->right instanceof Expr\FuncCall && $expr->right->name instanceof Name - && !$expr->right->isFirstClassCallable() + && !$expr->right->isPartialFunctionApplication() && in_array(strtolower((string) $expr->right->name), ['count', 'sizeof'], true) && count($expr->right->getArgs()) >= 1 && $leftType->isInteger()->yes() @@ -530,7 +530,7 @@ static function () use ($expr, $nativeTypesPromoted, $typeCallback): Type { && $expr->right instanceof BinaryOp\Minus && $expr->right->left instanceof Expr\FuncCall && $expr->right->left->name instanceof Name - && !$expr->right->left->isFirstClassCallable() + && !$expr->right->left->isPartialFunctionApplication() && in_array(strtolower((string) $expr->right->left->name), ['count', 'sizeof'], true) && count($expr->right->left->getArgs()) >= 1 // constant offsets are handled via HasOffsetType/HasOffsetValueType @@ -557,7 +557,7 @@ static function () use ($expr, $nativeTypesPromoted, $typeCallback): Type { !$context->null() && $expr->right instanceof Expr\FuncCall && $expr->right->name instanceof Name - && !$expr->right->isFirstClassCallable() + && !$expr->right->isPartialFunctionApplication() && in_array(strtolower((string) $expr->right->name), ['preg_match'], true) && count($expr->right->getArgs()) >= 3 && ( @@ -575,7 +575,7 @@ static function () use ($expr, $nativeTypesPromoted, $typeCallback): Type { !$context->null() && $expr->right instanceof Expr\FuncCall && $expr->right->name instanceof Name - && !$expr->right->isFirstClassCallable() + && !$expr->right->isPartialFunctionApplication() && in_array(strtolower((string) $expr->right->name), ['strlen', 'mb_strlen'], true) && count($expr->right->getArgs()) === 1 && $leftType->isInteger()->yes() diff --git a/src/Analyser/ExprHandler/FuncCallHandler.php b/src/Analyser/ExprHandler/FuncCallHandler.php index 4ff33f80889..961625e9308 100644 --- a/src/Analyser/ExprHandler/FuncCallHandler.php +++ b/src/Analyser/ExprHandler/FuncCallHandler.php @@ -133,7 +133,7 @@ public function __construct( public function supports(Expr $expr): bool { - return $expr instanceof FuncCall && !$expr->isFirstClassCallable(); + return $expr instanceof FuncCall && !$expr->isPartialFunctionApplication(); } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult @@ -468,7 +468,7 @@ public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Ex // for find_key: an empty result does not mean an empty array) if ( $expr->name instanceof Name - && !$expr->isFirstClassCallable() + && !$expr->isPartialFunctionApplication() && isset($normalizedExpr->getArgs()[0]) && $type->isNull()->yes() ) { diff --git a/src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php b/src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php index f762a308775..a2f770813d1 100644 --- a/src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php +++ b/src/Analyser/ExprHandler/Helper/DefaultNarrowingHelper.php @@ -93,7 +93,7 @@ public function __construct( */ public function specifyTypesForNode(Scope $scope, Expr $node, TypeSpecifierContext $context): SpecifiedTypes { - if ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) { + if ($node instanceof Expr\CallLike && $node->isPartialFunctionApplication()) { return (new SpecifiedTypes([], []))->setRootExpr($node); } diff --git a/src/Analyser/ExprHandler/Helper/IdenticalNarrowingHelper.php b/src/Analyser/ExprHandler/Helper/IdenticalNarrowingHelper.php index 1c2d71ca200..9b3b453722b 100644 --- a/src/Analyser/ExprHandler/Helper/IdenticalNarrowingHelper.php +++ b/src/Analyser/ExprHandler/Helper/IdenticalNarrowingHelper.php @@ -434,8 +434,8 @@ private function specifyGeneral( // one non-empty side makes both non-empty if ( $context->true() - && $unwrappedLeft->name instanceof Name && in_array($unwrappedLeft->name->toLowerString(), ['count', 'sizeof'], true) && !$unwrappedLeft->isFirstClassCallable() && isset($unwrappedLeft->getArgs()[0]) - && $unwrappedRight->name instanceof Name && in_array($unwrappedRight->name->toLowerString(), ['count', 'sizeof'], true) && !$unwrappedRight->isFirstClassCallable() && isset($unwrappedRight->getArgs()[0]) + && $unwrappedLeft->name instanceof Name && in_array($unwrappedLeft->name->toLowerString(), ['count', 'sizeof'], true) && !$unwrappedLeft->isPartialFunctionApplication() && isset($unwrappedLeft->getArgs()[0]) + && $unwrappedRight->name instanceof Name && in_array($unwrappedRight->name->toLowerString(), ['count', 'sizeof'], true) && !$unwrappedRight->isPartialFunctionApplication() && isset($unwrappedRight->getArgs()[0]) ) { if ($leftArgResult === null || $rightArgResult === null) { return null; @@ -795,7 +795,7 @@ private function specifyEqualAgainstConstantSide( if ( $unwrappedSubject instanceof Expr\FuncCall && $unwrappedSubject->name instanceof Name - && !$unwrappedSubject->isFirstClassCallable() + && !$unwrappedSubject->isPartialFunctionApplication() && isset($unwrappedSubject->getArgs()[0]) ) { $funcName = $unwrappedSubject->name->toLowerString(); @@ -837,7 +837,7 @@ private function specifyFuncCallFamilies( ?ExpressionResult $argResult, ): SpecifiedTypes|false|null { - if (!($call->name instanceof Name) || $call->isFirstClassCallable() || !isset($call->getArgs()[0])) { + if (!($call->name instanceof Name) || $call->isPartialFunctionApplication() || !isset($call->getArgs()[0])) { return false; } @@ -1115,7 +1115,7 @@ private function specifyFuncCallFamilies( public function captureFirstArgResult(Expr $side, ExpressionResultStorage $storage): ?ExpressionResult { $unwrapped = $side instanceof AlwaysRememberedExpr ? $side->getExpr() : $side; - if (!$unwrapped instanceof Expr\FuncCall || $unwrapped->isFirstClassCallable() || !isset($unwrapped->getArgs()[0])) { + if (!$unwrapped instanceof Expr\FuncCall || $unwrapped->isPartialFunctionApplication() || !isset($unwrapped->getArgs()[0])) { return null; } diff --git a/src/Analyser/ExprHandler/MethodCallHandler.php b/src/Analyser/ExprHandler/MethodCallHandler.php index 79c730ef67e..ca16103cb99 100644 --- a/src/Analyser/ExprHandler/MethodCallHandler.php +++ b/src/Analyser/ExprHandler/MethodCallHandler.php @@ -87,7 +87,7 @@ public function __construct( public function supports(Expr $expr): bool { - return $expr instanceof MethodCall && !$expr->isFirstClassCallable(); + return $expr instanceof MethodCall && !$expr->isPartialFunctionApplication(); } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult diff --git a/src/Analyser/ExprHandler/NewHandler.php b/src/Analyser/ExprHandler/NewHandler.php index 61244aa9fbe..c8e1eac1f67 100644 --- a/src/Analyser/ExprHandler/NewHandler.php +++ b/src/Analyser/ExprHandler/NewHandler.php @@ -110,7 +110,7 @@ public function __construct( public function supports(Expr $expr): bool { - return $expr instanceof New_ && !$expr->isFirstClassCallable(); + return $expr instanceof New_ && !$expr->isPartialFunctionApplication(); } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult diff --git a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php index ebbc5946959..45b06f7413c 100644 --- a/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php +++ b/src/Analyser/ExprHandler/NullsafeMethodCallHandler.php @@ -52,7 +52,7 @@ public function __construct( public function supports(Expr $expr): bool { - return $expr instanceof NullsafeMethodCall; + return $expr instanceof NullsafeMethodCall && !$expr->isPartialFunctionApplication(); } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult diff --git a/src/Analyser/ExprHandler/StaticCallHandler.php b/src/Analyser/ExprHandler/StaticCallHandler.php index a90ba1d98b2..f6a32745c26 100644 --- a/src/Analyser/ExprHandler/StaticCallHandler.php +++ b/src/Analyser/ExprHandler/StaticCallHandler.php @@ -88,7 +88,7 @@ public function __construct( public function supports(Expr $expr): bool { - return $expr instanceof StaticCall && !$expr->isFirstClassCallable(); + return $expr instanceof StaticCall && !$expr->isPartialFunctionApplication(); } public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult diff --git a/src/Analyser/ExprHandler/Virtual/PartialFunctionApplicationNodeHandler.php b/src/Analyser/ExprHandler/Virtual/PartialFunctionApplicationNodeHandler.php new file mode 100644 index 00000000000..aae8d7da0f8 --- /dev/null +++ b/src/Analyser/ExprHandler/Virtual/PartialFunctionApplicationNodeHandler.php @@ -0,0 +1,126 @@ + + */ +#[AutowiredService] +final class PartialFunctionApplicationNodeHandler implements ExprHandler +{ + + public function __construct( + private ExpressionResultFactory $expressionResultFactory, + private DefaultNarrowingHelper $defaultNarrowingHelper, + ) + { + } + + public function supports(Expr $expr): bool + { + return $expr instanceof PartialFunctionApplicationNode; + } + + public function processExpr(NodeScopeResolver $nodeScopeResolver, Stmt $stmt, Expr $expr, MutatingScope $scope, ExpressionResultStorage $storage, callable $nodeCallback, ExpressionContext $context): ExpressionResult + { + $beforeScope = $scope; + $hasYield = false; + $isAlwaysTerminating = false; + $throwPoints = []; + $impurePoints = []; + $variableFlows = []; + $dependencies = []; + foreach ($this->getEvaluatedExprs($expr->getOriginalNode()) as $evaluatedExpr) { + $result = $nodeScopeResolver->processExprNode($stmt, $evaluatedExpr, $scope, $storage, $nodeCallback, ExpressionContext::createDeep($context->shouldResolveTemplateArguments())); + $scope = $result->getScope(); + $hasYield = $hasYield || $result->hasYield(); + $isAlwaysTerminating = $isAlwaysTerminating || $result->isAlwaysTerminating(); + $throwPoints = array_merge($throwPoints, $result->getThrowPoints()); + $impurePoints = array_merge($impurePoints, $result->getImpurePoints()); + $variableFlows[] = $result->getVariableFlow(); + $dependencies[] = $result->getDependencies(); + } + + $result = $this->expressionResultFactory->create( + $scope, + beforeScope: $beforeScope, + expr: $expr, + variableFlow: VariableFlow::sequence(...$variableFlows), + hasYield: $hasYield, + isAlwaysTerminating: $isAlwaysTerminating, + throwPoints: $throwPoints, + impurePoints: $impurePoints, + typeCallback: static fn (bool $nativeTypesPromoted): Type => new ObjectType(Closure::class), + specifyTypesCallback: fn (TypeSpecifierContext $context, bool $nativeTypesPromoted) => $this->defaultNarrowingHelper->specifyDefaultTypes($expr, $context), + ); + + return $result->withDependencies(Dependencies::merge(...$dependencies)); + } + + /** + * The callee's own expressions and the bound argument values, in the order + * PHP evaluates them. + * + * @return list + */ + private function getEvaluatedExprs(Expr\CallLike $call): array + { + $exprs = []; + if ($call instanceof Expr\FuncCall) { + if ($call->name instanceof Expr) { + $exprs[] = $call->name; + } + } elseif ($call instanceof Expr\MethodCall || $call instanceof Expr\NullsafeMethodCall) { + $exprs[] = $call->var; + if ($call->name instanceof Expr) { + $exprs[] = $call->name; + } + } elseif ($call instanceof Expr\StaticCall) { + if ($call->class instanceof Expr) { + $exprs[] = $call->class; + } + if ($call->name instanceof Expr) { + $exprs[] = $call->name; + } + } elseif ($call instanceof Expr\New_) { + if ($call->class instanceof Expr) { + $exprs[] = $call->class; + } + } + + foreach ($call->getRawArgs() as $arg) { + if (!$arg instanceof Arg) { + continue; + } + + $exprs[] = $arg->value; + } + + return $exprs; + } + +} diff --git a/src/Analyser/ExprHandlerRegistry.php b/src/Analyser/ExprHandlerRegistry.php index 190c5171d4b..9e9353faa49 100644 --- a/src/Analyser/ExprHandlerRegistry.php +++ b/src/Analyser/ExprHandlerRegistry.php @@ -39,7 +39,7 @@ public static function resolve(Expr $expr, Container $container): ?ExprHandler { $cacheKey = get_class($expr); if ($expr instanceof Expr\CallLike) { - $cacheKey .= '|' . $expr->isFirstClassCallable(); + $cacheKey .= '|' . $expr->isPartialFunctionApplication(); if ($expr instanceof Expr\New_) { $cacheKey .= '|' . get_class($expr->class); } diff --git a/src/Analyser/ExpressionResult.php b/src/Analyser/ExpressionResult.php index 9f69558d9b1..5fac78b1e8f 100644 --- a/src/Analyser/ExpressionResult.php +++ b/src/Analyser/ExpressionResult.php @@ -527,13 +527,13 @@ private function projectsVoidToNull(bool $nativeTypesPromoted): bool $expr = $this->expr; if ($expr instanceof Expr\FuncCall) { - return $expr->name instanceof Node\Name && !$expr->isFirstClassCallable(); + return $expr->name instanceof Node\Name && !$expr->isPartialFunctionApplication(); } return ($expr instanceof Expr\MethodCall || $expr instanceof Expr\NullsafeMethodCall || $expr instanceof Expr\StaticCall) - && !$expr->isFirstClassCallable(); + && !$expr->isPartialFunctionApplication(); } /** diff --git a/src/Analyser/MutatingScope.php b/src/Analyser/MutatingScope.php index 85867200a20..46741f1d055 100644 --- a/src/Analyser/MutatingScope.php +++ b/src/Analyser/MutatingScope.php @@ -1097,11 +1097,13 @@ private function resolveType(string $exprString, Expr $node): Type return $expressionType; } - // NodeScopeResolver intercepts a first-class callable CallLike before the - // ExprHandler dispatch - no handler supports the original node, its closure - // type lives on the stored result's typeCallback (see the *CallableNode - // handlers), mirroring TypeSpecifier::specifyTypesInCondition(). - if ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) { + // NodeScopeResolver intercepts a first-class callable or a partial + // function application before the ExprHandler dispatch - no handler + // supports the original node, its closure type lives on the stored + // result's typeCallback (see the *CallableNode and + // PartialFunctionApplicationNode handlers), mirroring + // TypeSpecifier::specifyTypesInCondition(). + if ($node instanceof Expr\CallLike && $node->isPartialFunctionApplication()) { return $this->resolveTypeOfNewWorldHandlerNode($node); } @@ -1213,7 +1215,7 @@ private function getCurrentTypesOfSpecifiedExpr(Expr $expr): ?array // argument-less instance calls: the shape @phpstan-assert subjects // take (synthetic per-build nodes, never stored - a walk per // application otherwise) - || ($expr instanceof Expr\MethodCall && $expr->name instanceof Identifier && !$expr->isFirstClassCallable() && $expr->getArgs() === []) + || ($expr instanceof Expr\MethodCall && $expr->name instanceof Identifier && !$expr->isPartialFunctionApplication() && $expr->getArgs() === []) ) && ($exprResult === null || !$exprResult->containsNullsafe()) ) { @@ -1567,7 +1569,7 @@ public function getKeepVoidType(Expr $node): Type && !$node instanceof MethodCall && !$node instanceof Expr\NullsafeMethodCall && !$node instanceof Expr\StaticCall - ) || $node->isFirstClassCallable() + ) || $node->isPartialFunctionApplication() ) ) { return $this->getScopeStateType($node); @@ -2641,7 +2643,7 @@ private function expressionTypeIsUnchangeable(ExpressionTypeHolder $typeHolder): $type = $typeHolder->getType(); return $expr instanceof FuncCall - && !$expr->isFirstClassCallable() + && !$expr->isPartialFunctionApplication() && $expr->name instanceof FullyQualified && in_array( $expr->name->toLowerString(), @@ -3499,7 +3501,7 @@ private function resolveScopeStateType(Expr $expr, bool $native): Type if ( $expr instanceof Expr\NullsafeMethodCall && $expr->name instanceof Identifier - && !$expr->isFirstClassCallable() + && !$expr->isPartialFunctionApplication() && $expr->getArgs() === [] ) { $attributes = $expr->getAttributes(); @@ -3517,7 +3519,7 @@ private function resolveScopeStateType(Expr $expr, bool $native): Type if ( $expr instanceof Expr\MethodCall && $expr->name instanceof Identifier - && !$expr->isFirstClassCallable() + && !$expr->isPartialFunctionApplication() && $expr->getArgs() === [] ) { $storage = $this->services->expressionResultStorageStack->getCurrent(); diff --git a/src/Analyser/NodeScopeResolver.php b/src/Analyser/NodeScopeResolver.php index b85f94a6b81..1b5b47f50dd 100644 --- a/src/Analyser/NodeScopeResolver.php +++ b/src/Analyser/NodeScopeResolver.php @@ -39,6 +39,7 @@ use PHPStan\Node\InstantiationCallableNode; use PHPStan\Node\MethodCallableNode; use PHPStan\Node\MethodCallExpressionNode; +use PHPStan\Node\PartialFunctionApplicationNode; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Node\StaticMethodCallExpressionNode; use PHPStan\Reflection\Php\PhpMethodFromParserNodeReflection; @@ -564,7 +565,7 @@ public function processExprOnDemand(Expr $expr, MutatingScope $scope, Expression // throw stays: real source nodes must have a handler. if ( ExprHandlerRegistry::resolve($expr, $this->container) === null - && !($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) + && !($expr instanceof Expr\CallLike && $expr->isPartialFunctionApplication()) ) { $mixed = new MixedType(); return $this->expressionResultFactory->create( @@ -815,8 +816,10 @@ private function processExprNodeInternal( ExpressionContext $context, ): ExpressionResult { - if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { - if ($expr instanceof FuncCall) { + if ($expr instanceof Expr\CallLike && $expr->isPartialFunctionApplication()) { + if (!$expr->isFirstClassCallable()) { + $newExpr = new PartialFunctionApplicationNode($expr); + } elseif ($expr instanceof FuncCall) { $newExpr = new FunctionCallableNode($expr->name, $expr); } elseif ($expr instanceof MethodCall) { $newExpr = new MethodCallableNode($expr->var, $expr->name, $expr); @@ -825,7 +828,8 @@ private function processExprNodeInternal( } elseif ($expr instanceof New_ && !$expr->class instanceof Class_) { $newExpr = new InstantiationCallableNode($expr->class, $expr); } else { - throw new ShouldNotHappenException(); + // $a?->b(...) and new class (...) {} do not compile + $newExpr = new PartialFunctionApplicationNode($expr); } $newExprResult = $this->processExprNode($stmt, $newExpr, $scope, $storage, $nodeCallback, $context); @@ -838,8 +842,7 @@ private function processExprNodeInternal( throwPoints: $newExprResult->getThrowPoints(), impurePoints: $newExprResult->getImpurePoints(), variableFlow: $newExprResult->getVariableFlow(), - // the first-class callable closure type lives on the *CallableNode - // result; delegate so getType() of the original CallLike answers from it + // the closure type lives on the result of the virtual node; delegate so getType() of the original CallLike answers from it typeCallback: static fn (bool $nativeTypesPromoted): Type => ($nativeTypesPromoted ? $newExprResult->getNativeType() : $newExprResult->getType()), specifyTypesCallback: SpecifiedTypes::emptySpecifyCallback(), dependencies: $newExprResult->getDependencies(), diff --git a/src/Analyser/StmtHandler/ForHandler.php b/src/Analyser/StmtHandler/ForHandler.php index 7a95e2c5710..f5cac51afb4 100644 --- a/src/Analyser/StmtHandler/ForHandler.php +++ b/src/Analyser/StmtHandler/ForHandler.php @@ -81,7 +81,7 @@ private function inferForLoopExpressions(NodeScopeResolver $nodeScopeResolver, F && $lastCondExpr->left instanceof Variable && $lastCondExpr->right instanceof FuncCall && $lastCondExpr->right->name instanceof Name - && !$lastCondExpr->right->isFirstClassCallable() + && !$lastCondExpr->right->isPartialFunctionApplication() && in_array($lastCondExpr->right->name->toLowerString(), ['count', 'sizeof'], true) && count($lastCondExpr->right->getArgs()) > 0 && $lastCondExpr->right->getArgs()[0]->value instanceof Variable @@ -106,7 +106,7 @@ private function inferForLoopExpressions(NodeScopeResolver $nodeScopeResolver, F && $lastCondExpr->right instanceof Variable && $lastCondExpr->left instanceof FuncCall && $lastCondExpr->left->name instanceof Name - && !$lastCondExpr->left->isFirstClassCallable() + && !$lastCondExpr->left->isPartialFunctionApplication() && in_array($lastCondExpr->left->name->toLowerString(), ['count', 'sizeof'], true) && count($lastCondExpr->left->getArgs()) > 0 && $lastCondExpr->left->getArgs()[0]->value instanceof Variable diff --git a/src/Analyser/StmtHandler/ForeachHandler.php b/src/Analyser/StmtHandler/ForeachHandler.php index 2ba5323bf7b..3476a6a47d5 100644 --- a/src/Analyser/StmtHandler/ForeachHandler.php +++ b/src/Analyser/StmtHandler/ForeachHandler.php @@ -644,7 +644,7 @@ private function enterForeach(NodeScopeResolver $nodeScopeResolver, MutatingScop if ( $stmt->expr instanceof FuncCall && $stmt->expr->name instanceof Name - && !$stmt->expr->isFirstClassCallable() + && !$stmt->expr->isPartialFunctionApplication() && $stmt->expr->name->toLowerString() === 'array_keys' && $stmt->valueVar instanceof Variable ) { diff --git a/src/Analyser/TypeSpecifier.php b/src/Analyser/TypeSpecifier.php index 835844bc4a6..b3816021a37 100644 --- a/src/Analyser/TypeSpecifier.php +++ b/src/Analyser/TypeSpecifier.php @@ -68,7 +68,7 @@ public function specifyTypesInCondition( TypeSpecifierContext $context, ): SpecifiedTypes { - if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { + if ($expr instanceof Expr\CallLike && $expr->isPartialFunctionApplication()) { return (new SpecifiedTypes([], []))->setRootExpr($expr); } diff --git a/src/Analyser/VariableFlowBuilder.php b/src/Analyser/VariableFlowBuilder.php index 2f9be103c60..5089fff1949 100644 --- a/src/Analyser/VariableFlowBuilder.php +++ b/src/Analyser/VariableFlowBuilder.php @@ -23,7 +23,7 @@ public static function throws(Expr $expr, array $throwPoints): ?VariableFlow // a callback the callee invokes immediately throws through the call - // its throw points are re-created on the callback argument node $callbackArguments = []; - if ($expr instanceof Expr\CallLike && !$expr->isFirstClassCallable()) { + if ($expr instanceof Expr\CallLike && !$expr->isPartialFunctionApplication()) { foreach ($expr->getArgs() as $arg) { if (!$arg->value instanceof Expr\Closure && !$arg->value instanceof Expr\ArrowFunction) { continue; diff --git a/src/Analyser/VolatileExpressionHelper.php b/src/Analyser/VolatileExpressionHelper.php index b54b2767abc..e4d2b150c13 100644 --- a/src/Analyser/VolatileExpressionHelper.php +++ b/src/Analyser/VolatileExpressionHelper.php @@ -140,7 +140,7 @@ public static function invalidateNegativeExistenceChecks( if ( !$expr instanceof FuncCall - || $expr->isFirstClassCallable() + || $expr->isPartialFunctionApplication() || !$expr->name instanceof Name || !in_array($expr->name->toLowerString(), $functionNames, true) || $exprTypeHolder->getType()->isTrue()->yes() diff --git a/src/Node/ClassStatementsGatherer.php b/src/Node/ClassStatementsGatherer.php index 71a56858423..02c18f0ec89 100644 --- a/src/Node/ClassStatementsGatherer.php +++ b/src/Node/ClassStatementsGatherer.php @@ -187,6 +187,13 @@ private function gatherNodes(Node $node, Scope $scope): void $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($node->getOriginalNode(), $scope); return; } + if ($node instanceof PartialFunctionApplicationNode) { + $originalNode = $node->getOriginalNode(); + if ($originalNode instanceof MethodCall || $originalNode instanceof StaticCall) { + $this->methodCalls[] = new \PHPStan\Node\Method\MethodCall($originalNode, $scope); + } + return; + } if ($node instanceof MethodReturnStatementsNode) { $this->returnStatementNodes[strtolower($node->getMethodName())] = $node; return; diff --git a/src/Node/PartialFunctionApplicationNode.php b/src/Node/PartialFunctionApplicationNode.php new file mode 100644 index 00000000000..8080b9b3c65 --- /dev/null +++ b/src/Node/PartialFunctionApplicationNode.php @@ -0,0 +1,48 @@ +getAttributes(); + unset($attributes[ExprPrinter::ATTRIBUTE_CACHE_KEY]); + parent::__construct($attributes); + } + + public function getOriginalNode(): CallLike + { + return $this->originalNode; + } + + #[Override] + public function getType(): string + { + return 'PHPStan_Node_PartialFunctionApplicationNode'; + } + + /** + * @return string[] + */ + #[Override] + public function getSubNodeNames(): array + { + return []; + } + +} diff --git a/src/Node/Printer/Printer.php b/src/Node/Printer/Printer.php index bc868a46c52..65fffeb0fdd 100644 --- a/src/Node/Printer/Printer.php +++ b/src/Node/Printer/Printer.php @@ -28,6 +28,7 @@ use PHPStan\Node\InstantiationCallableNode; use PHPStan\Node\IssetExpr; use PHPStan\Node\MethodCallableNode; +use PHPStan\Node\PartialFunctionApplicationNode; use PHPStan\Node\StaticMethodCallableNode; use PHPStan\Type\VerbosityLevel; use function preg_match; @@ -209,4 +210,9 @@ protected function pPHPStan_Node_InstantiationCallableNode(InstantiationCallable return sprintf('__phpstanInstantiationCallable(%s)', $this->p($expr->getOriginalNode())); } + protected function pPHPStan_Node_PartialFunctionApplicationNode(PartialFunctionApplicationNode $expr): string // phpcs:ignore + { + return sprintf('__phpstanPartialFunctionApplication(%s)', $this->p($expr->getOriginalNode())); + } + } diff --git a/src/Parser/ArrayFilterArgVisitor.php b/src/Parser/ArrayFilterArgVisitor.php index 09b11ff3ad6..314443e24ba 100644 --- a/src/Parser/ArrayFilterArgVisitor.php +++ b/src/Parser/ArrayFilterArgVisitor.php @@ -18,7 +18,7 @@ final class ArrayFilterArgVisitor extends NodeVisitorAbstract #[Override] public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isPartialFunctionApplication()) { $functionName = $node->name->toLowerString(); if ($functionName === 'array_filter') { $args = $node->getArgs(); diff --git a/src/Parser/ArrayFindArgVisitor.php b/src/Parser/ArrayFindArgVisitor.php index ca52b483b72..72511d3c71e 100644 --- a/src/Parser/ArrayFindArgVisitor.php +++ b/src/Parser/ArrayFindArgVisitor.php @@ -19,7 +19,7 @@ final class ArrayFindArgVisitor extends NodeVisitorAbstract #[Override] public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isPartialFunctionApplication()) { $functionName = $node->name->toLowerString(); if (in_array($functionName, ['array_all', 'array_any', 'array_find', 'array_find_key'], true)) { $args = $node->getArgs(); diff --git a/src/Parser/ArrayMapArgVisitor.php b/src/Parser/ArrayMapArgVisitor.php index b0b35180d63..6309200f842 100644 --- a/src/Parser/ArrayMapArgVisitor.php +++ b/src/Parser/ArrayMapArgVisitor.php @@ -20,7 +20,7 @@ final class ArrayMapArgVisitor extends NodeVisitorAbstract #[Override] public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isPartialFunctionApplication()) { $functionName = $node->name->toLowerString(); if ($functionName === 'array_map') { $args = $node->getArgs(); diff --git a/src/Parser/ArrayWalkArgVisitor.php b/src/Parser/ArrayWalkArgVisitor.php index 0cc945a67a0..892d0cf9a6c 100644 --- a/src/Parser/ArrayWalkArgVisitor.php +++ b/src/Parser/ArrayWalkArgVisitor.php @@ -18,7 +18,7 @@ final class ArrayWalkArgVisitor extends NodeVisitorAbstract #[Override] public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isPartialFunctionApplication()) { $functionName = $node->name->toLowerString(); if ($functionName === 'array_walk') { $args = $node->getArgs(); diff --git a/src/Parser/ArrowFunctionArgVisitor.php b/src/Parser/ArrowFunctionArgVisitor.php index 6d95d250c3d..1af34a5777e 100644 --- a/src/Parser/ArrowFunctionArgVisitor.php +++ b/src/Parser/ArrowFunctionArgVisitor.php @@ -23,7 +23,7 @@ public function enterNode(Node $node): ?Node return null; } - if ($node->isFirstClassCallable()) { + if ($node->isPartialFunctionApplication()) { return null; } diff --git a/src/Parser/ClosureArgVisitor.php b/src/Parser/ClosureArgVisitor.php index 94a9213cc3c..90688f4b634 100644 --- a/src/Parser/ClosureArgVisitor.php +++ b/src/Parser/ClosureArgVisitor.php @@ -23,7 +23,7 @@ public function enterNode(Node $node): ?Node return null; } - if ($node->isFirstClassCallable()) { + if ($node->isPartialFunctionApplication()) { return null; } diff --git a/src/Parser/ClosureBindArgVisitor.php b/src/Parser/ClosureBindArgVisitor.php index 93c22b7c615..887d0810452 100644 --- a/src/Parser/ClosureBindArgVisitor.php +++ b/src/Parser/ClosureBindArgVisitor.php @@ -26,7 +26,7 @@ public function enterNode(Node $node): ?Node && $node->class->toLowerString() === 'closure' && $node->name instanceof Identifier && $node->name->toLowerString() === 'bind' - && !$node->isFirstClassCallable() + && !$node->isPartialFunctionApplication() ) { $args = $node->getArgs(); if (count($args) > 1) { diff --git a/src/Parser/ClosureBindToVarVisitor.php b/src/Parser/ClosureBindToVarVisitor.php index 9243e6d7848..db865a434b1 100644 --- a/src/Parser/ClosureBindToVarVisitor.php +++ b/src/Parser/ClosureBindToVarVisitor.php @@ -23,7 +23,7 @@ public function enterNode(Node $node): ?Node $node instanceof Node\Expr\MethodCall && $node->name instanceof Identifier && $node->name->toLowerString() === 'bindto' - && !$node->isFirstClassCallable() + && !$node->isPartialFunctionApplication() ) { $args = $node->getArgs(); if (isset($args[0])) { diff --git a/src/Parser/CurlSetOptArgVisitor.php b/src/Parser/CurlSetOptArgVisitor.php index 748adc8b14e..2fc09a7ccbb 100644 --- a/src/Parser/CurlSetOptArgVisitor.php +++ b/src/Parser/CurlSetOptArgVisitor.php @@ -18,7 +18,7 @@ final class CurlSetOptArgVisitor extends NodeVisitorAbstract #[Override] public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isPartialFunctionApplication()) { $functionName = $node->name->toLowerString(); if ($functionName === 'curl_setopt') { $args = $node->getArgs(); diff --git a/src/Parser/CurlSetOptArrayArgVisitor.php b/src/Parser/CurlSetOptArrayArgVisitor.php index 694fc983632..67af6330e7b 100644 --- a/src/Parser/CurlSetOptArrayArgVisitor.php +++ b/src/Parser/CurlSetOptArrayArgVisitor.php @@ -18,7 +18,7 @@ final class CurlSetOptArrayArgVisitor extends NodeVisitorAbstract #[Override] public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isPartialFunctionApplication()) { $functionName = $node->name->toLowerString(); if ($functionName === 'curl_setopt_array') { $args = $node->getArgs(); diff --git a/src/Parser/ImmediatelyInvokedClosureVisitor.php b/src/Parser/ImmediatelyInvokedClosureVisitor.php index 800942a52d4..264de7b0304 100644 --- a/src/Parser/ImmediatelyInvokedClosureVisitor.php +++ b/src/Parser/ImmediatelyInvokedClosureVisitor.php @@ -22,7 +22,7 @@ public function enterNode(Node $node): ?Node if ( $node instanceof Node\Expr\FuncCall && ($node->name instanceof Node\Expr\Closure || $node->name instanceof Node\Expr\ArrowFunction) - && !$node->isFirstClassCallable() + && !$node->isPartialFunctionApplication() ) { $node->name->setAttribute(self::ATTRIBUTE_NAME, true); $node->name->setAttribute(self::ARGS_ATTRIBUTE_NAME, $node->args); diff --git a/src/Parser/ImplodeArgVisitor.php b/src/Parser/ImplodeArgVisitor.php index ed3008f6c13..5e315cd39f3 100644 --- a/src/Parser/ImplodeArgVisitor.php +++ b/src/Parser/ImplodeArgVisitor.php @@ -19,7 +19,7 @@ final class ImplodeArgVisitor extends NodeVisitorAbstract #[Override] public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isFirstClassCallable()) { + if ($node instanceof Node\Expr\FuncCall && $node->name instanceof Node\Name && !$node->isPartialFunctionApplication()) { $functionName = $node->name->toLowerString(); if (in_array($functionName, ['implode', 'join'], true)) { $args = $node->getArgs(); diff --git a/src/Parser/ReversePipeTransformerVisitor.php b/src/Parser/ReversePipeTransformerVisitor.php index 519bf06e69b..c2affaab741 100644 --- a/src/Parser/ReversePipeTransformerVisitor.php +++ b/src/Parser/ReversePipeTransformerVisitor.php @@ -15,7 +15,7 @@ final class ReversePipeTransformerVisitor extends NodeVisitorAbstract #[Override] public function enterNode(Node $node): ?Node { - if ($node instanceof Node\Expr\FuncCall && !$node->isFirstClassCallable()) { + if ($node instanceof Node\Expr\FuncCall && !$node->isPartialFunctionApplication()) { $attributes = $node->getAttributes(); $origPipeAttributes = $attributes[PipeTransformerVisitor::ORIGINAL_PIPE_ATTRIBUTE_NAME] ?? []; $origVariadicPlaceholderAttributes = $attributes[PipeTransformerVisitor::ORIGINAL_VARIADIC_PLACEHOLDER_ATTRIBUTE_NAME] ?? []; @@ -39,7 +39,7 @@ public function enterNode(Node $node): ?Node } } - if ($node instanceof Node\Expr\MethodCall && !$node->isFirstClassCallable()) { + if ($node instanceof Node\Expr\MethodCall && !$node->isPartialFunctionApplication()) { $attributes = $node->getAttributes(); $origPipeAttributes = $attributes[PipeTransformerVisitor::ORIGINAL_PIPE_ATTRIBUTE_NAME] ?? []; $origVariadicPlaceholderAttributes = $attributes[PipeTransformerVisitor::ORIGINAL_VARIADIC_PLACEHOLDER_ATTRIBUTE_NAME] ?? []; @@ -55,7 +55,7 @@ public function enterNode(Node $node): ?Node } } - if ($node instanceof Node\Expr\StaticCall && !$node->isFirstClassCallable()) { + if ($node instanceof Node\Expr\StaticCall && !$node->isPartialFunctionApplication()) { $attributes = $node->getAttributes(); $origPipeAttributes = $attributes[PipeTransformerVisitor::ORIGINAL_PIPE_ATTRIBUTE_NAME] ?? []; $origVariadicPlaceholderAttributes = $attributes[PipeTransformerVisitor::ORIGINAL_VARIADIC_PLACEHOLDER_ATTRIBUTE_NAME] ?? []; diff --git a/src/Php/PhpVersion.php b/src/Php/PhpVersion.php index 6667a5916d9..ddf0f8d898f 100644 --- a/src/Php/PhpVersion.php +++ b/src/Php/PhpVersion.php @@ -123,6 +123,11 @@ public function supportsPipeOperator(): bool return $this->versionId >= 80500; } + public function supportsPartialFunctionApplication(): bool + { + return $this->versionId >= 80600; + } + public function deprecatesRequiredParameterAfterOptional(): bool { return $this->versionId >= 80000; diff --git a/src/Php/PhpVersions.php b/src/Php/PhpVersions.php index 84bd2c3f5f2..808181b9924 100644 --- a/src/Php/PhpVersions.php +++ b/src/Php/PhpVersions.php @@ -158,6 +158,11 @@ public function supportsFirstClassCallables(): TrinaryLogic return IntegerRangeType::fromInterval(80100, null)->isSuperTypeOf($this->phpVersions)->result; } + public function supportsPartialFunctionApplication(): TrinaryLogic + { + return IntegerRangeType::fromInterval(80600, null)->isSuperTypeOf($this->phpVersions)->result; + } + public function supportsReadOnlyClasses(): TrinaryLogic { return IntegerRangeType::fromInterval(80200, null)->isSuperTypeOf($this->phpVersions)->result; diff --git a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php index 70b6bb71bb7..a697585233d 100644 --- a/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php +++ b/src/Reflection/BetterReflection/SourceLocator/CachingVisitor.php @@ -96,7 +96,7 @@ public function enterNode(Node $node): ?int return NodeVisitor::DONT_TRAVERSE_CHILDREN; } - if ($node instanceof Node\Expr\FuncCall && !$node->isFirstClassCallable()) { + if ($node instanceof Node\Expr\FuncCall && !$node->isPartialFunctionApplication()) { try { ConstantNodeChecker::assertValidDefineFunctionCall($node); } catch (InvalidConstantNode) { diff --git a/src/Reflection/InitializerExprTypeResolver.php b/src/Reflection/InitializerExprTypeResolver.php index abf5384f2a4..6674259bee8 100644 --- a/src/Reflection/InitializerExprTypeResolver.php +++ b/src/Reflection/InitializerExprTypeResolver.php @@ -223,6 +223,9 @@ public function getType(Expr $expr, InitializerExprContext $context): Type if ($expr instanceof Expr\CallLike && $expr->isFirstClassCallable()) { return $this->getFirstClassCallableType($expr, $context, false); } + if ($expr instanceof Expr\CallLike && $expr->isPartialFunctionApplication()) { + return new ObjectType(Closure::class); + } if ($expr instanceof Expr\Closure && $expr->static) { $parameters = []; $isVariadic = false; diff --git a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php index 9a4947d5216..262a11554e0 100644 --- a/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php +++ b/src/Rules/Arrays/NonexistentOffsetInArrayDimFetchRule.php @@ -112,7 +112,7 @@ public function processNode(Node $node, Scope $scope): array if ( $node->dim instanceof Node\Expr\FuncCall - && !$node->dim->isFirstClassCallable() + && !$node->dim->isPartialFunctionApplication() && $node->dim->name instanceof Node\Name && in_array($node->dim->name->toLowerString(), ['array_key_first', 'array_key_last'], true) && count($node->dim->getArgs()) >= 1 diff --git a/src/Rules/Comparison/FunctionCallConstantConditionHelper.php b/src/Rules/Comparison/FunctionCallConstantConditionHelper.php index 567632db258..7f6076c39e3 100644 --- a/src/Rules/Comparison/FunctionCallConstantConditionHelper.php +++ b/src/Rules/Comparison/FunctionCallConstantConditionHelper.php @@ -47,7 +47,7 @@ public function isTypeCheckCandidate(Expr $expr): bool $expr instanceof FuncCall || $expr instanceof MethodCall || $expr instanceof StaticCall - ) && !$expr->isFirstClassCallable(); + ) && !$expr->isPartialFunctionApplication(); } /** diff --git a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php index 49af429e6de..04a0fbaa7b3 100644 --- a/src/Rules/Comparison/ImpossibleCheckTypeHelper.php +++ b/src/Rules/Comparison/ImpossibleCheckTypeHelper.php @@ -147,7 +147,7 @@ private function getSpecifiedType( ): ?bool { if ($node instanceof FuncCall) { - if ($node->isFirstClassCallable()) { + if ($node->isPartialFunctionApplication()) { return null; } $args = $node->getArgs(); @@ -573,7 +573,7 @@ public function doNotTreatPhpDocTypesAsCertain(): self private function determineContext(Scope $scope, Expr $node): TypeSpecifierContext { - if ($node instanceof Expr\CallLike && $node->isFirstClassCallable()) { + if ($node instanceof Expr\CallLike && $node->isPartialFunctionApplication()) { return TypeSpecifierContext::createTruthy(); } diff --git a/src/Rules/DeadCode/PossiblyPureCallTransitivePurityResolver.php b/src/Rules/DeadCode/PossiblyPureCallTransitivePurityResolver.php index 627f49e27f4..09984127af6 100644 --- a/src/Rules/DeadCode/PossiblyPureCallTransitivePurityResolver.php +++ b/src/Rules/DeadCode/PossiblyPureCallTransitivePurityResolver.php @@ -152,7 +152,7 @@ public function getPureCallableKeys(CollectedDataNode $node): array private function resolveCall(Node $expr, Scope $scope): ?array { if ($expr instanceof Node\Expr\FuncCall) { - if ($expr->isFirstClassCallable()) { + if ($expr->isPartialFunctionApplication()) { return null; } if (!$expr->name instanceof Node\Name) { @@ -166,7 +166,7 @@ private function resolveCall(Node $expr, Scope $scope): ?array } if ($expr instanceof Node\Expr\MethodCall || $expr instanceof Node\Expr\NullsafeMethodCall) { - if ($expr->isFirstClassCallable()) { + if ($expr->isPartialFunctionApplication()) { return null; } if (!$expr->name instanceof Node\Identifier) { @@ -207,7 +207,7 @@ private function resolveCall(Node $expr, Scope $scope): ?array } if ($expr instanceof Node\Expr\StaticCall) { - if ($expr->isFirstClassCallable()) { + if ($expr->isPartialFunctionApplication()) { return null; } if (!$expr->name instanceof Node\Identifier) { diff --git a/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php b/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php index 237b09cb643..0c06fe2bd5e 100644 --- a/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php +++ b/src/Rules/DeadCode/PossiblyPureFuncCallCollector.php @@ -30,7 +30,7 @@ public function processNode(Node $node, Scope $scope) $expr = $node->expr; if ($expr instanceof Node\Expr\BinaryOp\Pipe) { if ($expr->right instanceof Node\Expr\FuncCall) { - if (!$expr->right->isFirstClassCallable()) { + if (!$expr->right->isPartialFunctionApplication()) { return null; } @@ -42,7 +42,7 @@ public function processNode(Node $node, Scope $scope) if (!$expr instanceof Node\Expr\FuncCall) { return null; } - if ($expr->isFirstClassCallable()) { + if ($expr->isPartialFunctionApplication()) { return null; } if (!$expr->name instanceof Node\Name) { diff --git a/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php b/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php index 6d63e769248..61905f75881 100644 --- a/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php +++ b/src/Rules/DeadCode/PossiblyPureMethodCallCollector.php @@ -29,7 +29,7 @@ public function processNode(Node $node, Scope $scope) $expr = $node->expr; if ($expr instanceof Node\Expr\BinaryOp\Pipe) { if ($expr->right instanceof Node\Expr\MethodCall || $expr->right instanceof Node\Expr\NullsafeMethodCall) { - if (!$expr->right->isFirstClassCallable()) { + if (!$expr->right->isPartialFunctionApplication()) { return null; } @@ -42,7 +42,7 @@ public function processNode(Node $node, Scope $scope) if (!$expr instanceof Node\Expr\MethodCall && !$expr instanceof Node\Expr\NullsafeMethodCall) { return null; } - if ($expr->isFirstClassCallable()) { + if ($expr->isPartialFunctionApplication()) { return null; } if (!$expr->name instanceof Node\Identifier) { diff --git a/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php b/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php index 782bf82cfdb..bb0554d2ae7 100644 --- a/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php +++ b/src/Rules/DeadCode/PossiblyPureStaticCallCollector.php @@ -29,7 +29,7 @@ public function processNode(Node $node, Scope $scope) $expr = $node->expr; if ($expr instanceof Node\Expr\BinaryOp\Pipe) { if ($expr->right instanceof Node\Expr\StaticCall) { - if (!$expr->right->isFirstClassCallable()) { + if (!$expr->right->isPartialFunctionApplication()) { return null; } @@ -38,7 +38,7 @@ public function processNode(Node $node, Scope $scope) $expr = $expr->right->expr; } } - if (!$expr instanceof Node\Expr\StaticCall || $expr->isFirstClassCallable()) { + if (!$expr instanceof Node\Expr\StaticCall || $expr->isPartialFunctionApplication()) { return null; } if (!$expr->name instanceof Node\Identifier) { diff --git a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php index 9e38f44bb12..0b5ab1aa0e8 100644 --- a/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php +++ b/src/Rules/Functions/CallToFunctionStatementWithNoDiscardRule.php @@ -41,7 +41,7 @@ public function processNode(Node $node, Scope $scope): array if ($funcCall instanceof Node\Expr\BinaryOp\Pipe) { if ($funcCall->right instanceof Node\Expr\FuncCall) { - if (!$funcCall->right->isFirstClassCallable()) { + if (!$funcCall->right->isPartialFunctionApplication()) { return []; } @@ -55,7 +55,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($funcCall->isFirstClassCallable()) { + if ($funcCall->isPartialFunctionApplication()) { return []; } diff --git a/src/Rules/Functions/PartialFunctionApplicationRule.php b/src/Rules/Functions/PartialFunctionApplicationRule.php new file mode 100644 index 00000000000..5e3ad4b3c53 --- /dev/null +++ b/src/Rules/Functions/PartialFunctionApplicationRule.php @@ -0,0 +1,57 @@ + + */ +#[RegisteredRule(level: 0)] +final class PartialFunctionApplicationRule implements Rule +{ + + public function getNodeType(): string + { + return PartialFunctionApplicationNode::class; + } + + public function processNode(Node $node, Scope $scope): array + { + $originalNode = $node->getOriginalNode(); + if ($originalNode instanceof Node\Expr\New_) { + return [ + RuleErrorBuilder::message('Cannot create callable from the new operator.') + ->identifier('callable.notSupported') + ->nonIgnorable() + ->build(), + ]; + } + + if ($originalNode instanceof Node\Expr\NullsafeMethodCall) { + return [ + RuleErrorBuilder::message('Cannot combine nullsafe operator with callable creation.') + ->identifier('callable.notSupported') + ->nonIgnorable() + ->build(), + ]; + } + + if (!$scope->getPhpVersion()->supportsPartialFunctionApplication()->yes()) { + return [ + RuleErrorBuilder::message('Partial function application is supported only on PHP 8.6 and later.') + ->identifier('callable.notSupported') + ->nonIgnorable() + ->build(), + ]; + } + + return []; + } + +} diff --git a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php index 75dc2cabac0..a6d4f6e4237 100644 --- a/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToMethodStatementWithNoDiscardRule.php @@ -42,7 +42,7 @@ public function processNode(Node $node, Scope $scope): array if ($methodCall instanceof Node\Expr\BinaryOp\Pipe) { if ($methodCall->right instanceof Node\Expr\MethodCall || $methodCall->right instanceof Node\Expr\NullsafeMethodCall) { - if (!$methodCall->right->isFirstClassCallable()) { + if (!$methodCall->right->isPartialFunctionApplication()) { return []; } @@ -61,7 +61,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($methodCall->isFirstClassCallable()) { + if ($methodCall->isPartialFunctionApplication()) { return []; } diff --git a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php index 3e68ea13c53..939d8243133 100644 --- a/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php +++ b/src/Rules/Methods/CallToStaticMethodStatementWithNoDiscardRule.php @@ -45,7 +45,7 @@ public function processNode(Node $node, Scope $scope): array if ($methodCall instanceof Node\Expr\BinaryOp\Pipe) { if ($methodCall->right instanceof Node\Expr\StaticCall) { - if (!$methodCall->right->isFirstClassCallable()) { + if (!$methodCall->right->isPartialFunctionApplication()) { return []; } @@ -62,7 +62,7 @@ public function processNode(Node $node, Scope $scope): array return []; } - if ($methodCall->isFirstClassCallable()) { + if ($methodCall->isPartialFunctionApplication()) { return []; } diff --git a/src/Type/Regex/RegexExpressionHelper.php b/src/Type/Regex/RegexExpressionHelper.php index dada8e2c5c9..1b4cb31eb2c 100644 --- a/src/Type/Regex/RegexExpressionHelper.php +++ b/src/Type/Regex/RegexExpressionHelper.php @@ -49,7 +49,7 @@ public function resolve(Expr $expr): Type // this allows us to turn string concatenations with preg_quote() into static analyzable strings. if ( $expr instanceof Expr\FuncCall - && !$expr->isFirstClassCallable() + && !$expr->isPartialFunctionApplication() && $expr->name instanceof Name && $expr->name->toLowerString() === 'preg_quote' ) { diff --git a/tests/PHPStan/Analyser/nsrt/partial-function-application.php b/tests/PHPStan/Analyser/nsrt/partial-function-application.php new file mode 100644 index 00000000000..7d4f2f152ac --- /dev/null +++ b/tests/PHPStan/Analyser/nsrt/partial-function-application.php @@ -0,0 +1,44 @@ += 8.6 + +declare(strict_types = 1); + +namespace PartialFunctionApplicationTypes; + +use function PHPStan\Testing\assertType; + +class Foo +{ + + public function doFoo(int $a, string $b): string + { + return $b . $a; + } + + public static function doBar(int $a, int ...$rest): int + { + return $a; + } + +} + +/** + * @param list $strings + */ +function doFoo(Foo $foo, string $name, array $strings): void +{ + assertType('Closure', str_replace(' ', '-', ?)); + assertType('Closure', $foo->doFoo(1, ?)); + assertType('Closure', $foo->doFoo(b: ?, a: 1)); + assertType('Closure', Foo::doBar(?, ...)); + assertType('Closure', $name(?)); + assertType('Closure', strlen('abc', ...)); + assertType('Closure', array_map(strtoupper(...), ?)); + assertType('list', array_map(strtoupper(?), $strings)); + + $i = 0; + $f = str_repeat('a', $i++, ...); + assertType('1', $i); + + $f = $foo->doFoo(...)(?, 'b'); + assertType('Closure', $f); +} diff --git a/tests/PHPStan/Build/RequiredPhpVersionCommentTest.php b/tests/PHPStan/Build/RequiredPhpVersionCommentTest.php index 18895c3f3de..9700e55b544 100644 --- a/tests/PHPStan/Build/RequiredPhpVersionCommentTest.php +++ b/tests/PHPStan/Build/RequiredPhpVersionCommentTest.php @@ -151,6 +151,8 @@ public static function dataDetectedVersion(): iterable yield 'property hook' => [' 1; } }', 80400]; yield 'asymmetric visibility' => [' [' strlen(...);', 80500]; + yield 'partial function application' => [' ['bar(1, ...);', 80600]; } #[DataProvider('dataDetectedVersion')] diff --git a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php index ccf290cc5f9..ecfde9a0136 100644 --- a/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php +++ b/tests/PHPStan/Rules/DeadCode/UnusedPrivateMethodRuleTest.php @@ -78,6 +78,11 @@ public function testFirstClassCallable(): void $this->analyse([__DIR__ . '/data/callable-unused-private-method.php'], []); } + public function testPartialFunctionApplication(): void + { + $this->analyse([__DIR__ . '/data/partial-function-application-unused-private-method.php'], []); + } + #[RequiresPhp('>= 8.1.0')] public function testEnums(): void { diff --git a/tests/PHPStan/Rules/DeadCode/data/partial-function-application-unused-private-method.php b/tests/PHPStan/Rules/DeadCode/data/partial-function-application-unused-private-method.php new file mode 100644 index 00000000000..8c28fa26443 --- /dev/null +++ b/tests/PHPStan/Rules/DeadCode/data/partial-function-application-unused-private-method.php @@ -0,0 +1,28 @@ += 8.6 + +namespace PartialFunctionApplicationUnusedPrivateMethod; + +final class Foo +{ + + public function doFoo(): \Closure + { + return $this->usedByPartialApplication(1, ?); + } + + public function doBar(): \Closure + { + return self::usedStatically(?); + } + + private function usedByPartialApplication(int $a, int $b): int + { + return $a + $b; + } + + private static function usedStatically(int $a): int + { + return $a; + } + +} diff --git a/tests/PHPStan/Rules/Functions/PartialFunctionApplicationRuleConfigPhpTest.php b/tests/PHPStan/Rules/Functions/PartialFunctionApplicationRuleConfigPhpTest.php new file mode 100644 index 00000000000..ea8ee0671af --- /dev/null +++ b/tests/PHPStan/Rules/Functions/PartialFunctionApplicationRuleConfigPhpTest.php @@ -0,0 +1,40 @@ + + */ +class PartialFunctionApplicationRuleConfigPhpTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PartialFunctionApplicationRule(); + } + + public function testRule(): void + { + $this->analyse([__DIR__ . '/data/partial-function-application-php-versions.php'], [ + [ + 'Partial function application is supported only on PHP 8.6 and later.', + 17, + ], + [ + 'Partial function application is supported only on PHP 8.6 and later.', + 20, + ], + ]); + } + + public static function getAdditionalConfigFiles(): array + { + return [ + __DIR__ . '/data/partial-function-application-php-version-range.neon', + ]; + } + +} diff --git a/tests/PHPStan/Rules/Functions/PartialFunctionApplicationRuleTest.php b/tests/PHPStan/Rules/Functions/PartialFunctionApplicationRuleTest.php new file mode 100644 index 00000000000..a4fc4b96205 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/PartialFunctionApplicationRuleTest.php @@ -0,0 +1,48 @@ + + */ +class PartialFunctionApplicationRuleTest extends RuleTestCase +{ + + protected function getRule(): Rule + { + return new PartialFunctionApplicationRule(); + } + + public function testRule(): void + { + $errors = []; + if (PHP_VERSION_ID < 80600) { + foreach ([11, 30, 31, 32, 33, 34] as $line) { + $errors[] = [ + 'Partial function application is supported only on PHP 8.6 and later.', + $line, + ]; + } + } + + $errors[] = [ + 'Cannot combine nullsafe operator with callable creation.', + 36, + ]; + $errors[] = [ + 'Cannot combine nullsafe operator with callable creation.', + 37, + ]; + $errors[] = [ + 'Cannot create callable from the new operator.', + 38, + ]; + + $this->analyse([__DIR__ . '/data/partial-function-application.php'], $errors); + } + +} diff --git a/tests/PHPStan/Rules/Functions/data/partial-function-application-php-version-range.neon b/tests/PHPStan/Rules/Functions/data/partial-function-application-php-version-range.neon new file mode 100644 index 00000000000..80612c037a1 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/partial-function-application-php-version-range.neon @@ -0,0 +1,4 @@ +parameters: + phpVersion: + min: 80500 + max: 80699 diff --git a/tests/PHPStan/Rules/Functions/data/partial-function-application-php-versions.php b/tests/PHPStan/Rules/Functions/data/partial-function-application-php-versions.php new file mode 100644 index 00000000000..1a803bc7b9f --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/partial-function-application-php-versions.php @@ -0,0 +1,21 @@ += 8.6 + +namespace PartialFunctionApplicationPhpVersions; + +function doFoo(int $a): int +{ + return $a; +} + +function doBar(): void +{ + if (PHP_VERSION_ID >= 80600) { + $f = doFoo(?); + } + + if (PHP_VERSION_ID < 80600) { + $g = doFoo(?); + } + + $h = doFoo(?); +} diff --git a/tests/PHPStan/Rules/Functions/data/partial-function-application.php b/tests/PHPStan/Rules/Functions/data/partial-function-application.php new file mode 100644 index 00000000000..3620e63c791 --- /dev/null +++ b/tests/PHPStan/Rules/Functions/data/partial-function-application.php @@ -0,0 +1,39 @@ += 8.6 + +namespace PartialFunctionApplication; + +use Closure; + +class Foo +{ + + public function __construct( + public Closure $callback = strlen(?), + ) + { + } + + public function doFoo(int $a, string $b): string + { + return $b . $a; + } + + public static function doBar(int $a, int ...$rest): int + { + return $a; + } + +} + +function doFoo(Foo $foo, ?Foo $maybe): void +{ + $a = str_replace(' ', '-', ?); + $b = $foo->doFoo(1, ?); + $c = Foo::doBar(?, ...); + $d = $foo->doFoo(b: ?, a: 1); + $e = array_map(strtoupper(?), ['a']); + $f = strlen(...); + $g = $maybe?->doFoo(?, 'x'); + $h = $maybe?->doFoo(...); + $i = new Foo(?); +} diff --git a/turbo-ext/src/ArrowFunctionArgVisitor.cpp b/turbo-ext/src/ArrowFunctionArgVisitor.cpp index 3ee7e1e63ae..c9a4be7f053 100644 --- a/turbo-ext/src/ArrowFunctionArgVisitor.cpp +++ b/turbo-ext/src/ArrowFunctionArgVisitor.cpp @@ -39,7 +39,7 @@ class ArrowFunctionArgVisitor if (!visitors::isInstanceOf(node, PT_CLASS_FUNC_CALL)) return !EG(exception); zval *args = argsProp.of(node); - if (visitors::isFirstClassCallable(args)) return true; + if (visitors::isPartialFunctionApplication(args)) return true; zval *callee = calleeProp.of(node); if (callee == NULL || Z_TYPE_P(callee) != IS_OBJECT) return true; diff --git a/turbo-ext/src/AssignHandler.cpp b/turbo-ext/src/AssignHandler.cpp index 393a822d2b8..58b1b28affd 100644 --- a/turbo-ext/src/AssignHandler.cpp +++ b/turbo-ext/src/AssignHandler.cpp @@ -2757,9 +2757,9 @@ class AssignHandler [[nodiscard]] bool processInArrayForConditionalExpressionsAfterAssign(zval *nsr, zval *scope, zend_string *variableName, zv::Val &conditionalExpressions, zval *assignedExpr, zval *assignedType, zval *rhsImpurePoints) { if (!ahNameIs(AH_PROP(assignedExpr, name), PT_LC("in_array"))) return true; - bool firstClassCallable; - AH_OKB(pt_call_like_is_first_class_callable(Z_OBJ_P(assignedExpr), firstClassCallable)); - if (firstClassCallable) return true; + bool partialFunctionApplication; + AH_OKB(pt_call_like_is_partial_function_application(Z_OBJ_P(assignedExpr), partialFunctionApplication)); + if (partialFunctionApplication) return true; { zend_long isTrue = ahCallTri(assignedType, PT_LC("istrue"), 0, NULL); AH_OKB(isTrue >= 0); @@ -4269,9 +4269,9 @@ class AssignHandler call = AH_PROP(assignedExpr, left); } if (call == NULL) return ahEmptyArray(); - bool firstClassCallable; - AH_OK(pt_call_like_is_first_class_callable(Z_OBJ_P(call), firstClassCallable)); - if (firstClassCallable) return ahEmptyArray(); + bool partialFunctionApplication; + AH_OK(pt_call_like_is_partial_function_application(Z_OBJ_P(call), partialFunctionApplication)); + if (partialFunctionApplication) return ahEmptyArray(); zv::Val argResults = ahEmptyArray(); HashTable *args = ahArgs(call); @@ -4370,16 +4370,16 @@ class AssignHandler bool isFuncCall = ahIs(exprExpr, PT_CLASS_FUNC_CALL); zval *funcName = isFuncCall ? AH_PROP(exprExpr, name) : NULL; - bool firstClassCallable = false; + bool partialFunctionApplication = false; if (isFuncCall && ahIs(funcName, PT_CLASS_NAME)) { - AH_OK(pt_call_like_is_first_class_callable(Z_OBJ_P(exprExpr), firstClassCallable)); + AH_OK(pt_call_like_is_partial_function_application(Z_OBJ_P(exprExpr), partialFunctionApplication)); } // infer $arr[$key] after $key = array_key_first/last($arr) if ( isFuncCall && ahIs(funcName, PT_CLASS_NAME) - && !firstClassCallable + && !partialFunctionApplication && (ahNameIs(funcName, PT_LC("array_key_first")) || ahNameIs(funcName, PT_LC("array_key_last"))) && ahArgCount(exprExpr) >= 1 ) { @@ -4422,7 +4422,7 @@ class AssignHandler if ( isFuncCall && ahIs(funcName, PT_CLASS_NAME) - && !firstClassCallable + && !partialFunctionApplication && ahArgCount(exprExpr) >= 2 ) { zval *arrayArg = NULL; @@ -4494,7 +4494,7 @@ class AssignHandler if ( isFuncCall && ahIs(funcName, PT_CLASS_NAME) - && !firstClassCallable + && !partialFunctionApplication && ahNameIs(funcName, PT_LC("array_rand")) && ahArgCount(exprExpr) >= 1 ) { @@ -4531,14 +4531,14 @@ class AssignHandler if (ahIs(exprExpr, PT_CLASS_BINARY_OP_MINUS)) { zval *left = AH_PROP(exprExpr, left); zval *right = AH_PROP(exprExpr, right); - bool leftFirstClassCallable = false; + bool leftPartialFunctionApplication = false; bool leftIsCall = ahIs(left, PT_CLASS_FUNC_CALL) && ahIs(AH_PROP(left, name), PT_CLASS_NAME); if (leftIsCall) { - AH_OK(pt_call_like_is_first_class_callable(Z_OBJ_P(left), leftFirstClassCallable)); + AH_OK(pt_call_like_is_partial_function_application(Z_OBJ_P(left), leftPartialFunctionApplication)); } if ( leftIsCall - && !leftFirstClassCallable + && !leftPartialFunctionApplication && ahIs(right, PT_CLASS_SCALAR_INT) && isIntOne(right) && (ahNameIs(AH_PROP(left, name), PT_LC("count")) || ahNameIs(AH_PROP(left, name), PT_LC("sizeof"))) diff --git a/turbo-ext/src/BinaryOpHandler.cpp b/turbo-ext/src/BinaryOpHandler.cpp index 06f10c60b7d..8261df5863b 100644 --- a/turbo-ext/src/BinaryOpHandler.cpp +++ b/turbo-ext/src/BinaryOpHandler.cpp @@ -242,7 +242,7 @@ struct NamedCall }; /* $node instanceof FuncCall && $node->name instanceof Name (and, when - * requireCallable, && !$node->isFirstClassCallable()); 1 fills `out`, -1 = + * requireCallable, && !$node->isPartialFunctionApplication()); 1 fills `out`, -1 = * pending exception */ int namedCall(zval *node, bool requireCallable, NamedCall &out) { @@ -253,9 +253,9 @@ int namedCall(zval *node, bool requireCallable, NamedCall &out) int isName = ptoh::isInstance(name, PT_CLASS_NAME); if (isName <= 0) return isName; if (requireCallable) { - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(node), firstClassCallable))) return -1; - if (firstClassCallable) return 0; + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(node), partialFunctionApplication))) return -1; + if (partialFunctionApplication) return 0; } /* (string) $node->name: Name::__toString() is its $name */ zend_class_entry *nameCe = pt_class(PT_CLASS_NAME); @@ -746,15 +746,15 @@ class BinaryOpHandler return true; } - /* `!$call->isFirstClassCallable() && isset($call->getArgs()[0])` of a + /* `!$call->isPartialFunctionApplication() && isset($call->getArgs()[0])` of a * FuncCall: its first argument's value (the condition holding sets * `matched`); NULL with an exception pending, or when it does not hold */ static zval *callableFirstArgValue(zval *call, bool &matched, zv::Val &hold) { matched = false; - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(call), firstClassCallable))) return NULL; - if (firstClassCallable) return NULL; + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(call), partialFunctionApplication))) return NULL; + if (partialFunctionApplication) return NULL; zval *args = pt_call_like_args(Z_OBJ_P(call), hold); if (UNEXPECTED(args == NULL)) return NULL; zval *first = Z_TYPE_P(args) == IS_ARRAY ? zend_hash_index_find(Z_ARRVAL_P(args), 0) : NULL; diff --git a/turbo-ext/src/ClassStatementsGatherer.cpp b/turbo-ext/src/ClassStatementsGatherer.cpp index 2754ebdc2ef..8e26dcbd104 100644 --- a/turbo-ext/src/ClassStatementsGatherer.cpp +++ b/turbo-ext/src/ClassStatementsGatherer.cpp @@ -73,6 +73,7 @@ static NodeProp pt_csg_identifier_name = PT_IDENTIFIER_PROP; static NodeProp pt_csg_name_name = PT_NAME_PROP; static NodeProp pt_csg_method_callable_original = PT_NODE_PROP(PT_CLASS_METHOD_CALLABLE_NODE, "originalNode"); static NodeProp pt_csg_static_method_callable_original = PT_NODE_PROP(PT_CLASS_STATIC_METHOD_CALLABLE_NODE, "originalNode"); +static NodeProp pt_csg_partial_function_application_original = PT_NODE_PROP(PT_CLASS_PARTIAL_FUNCTION_APPLICATION_NODE, "originalNode"); static NodeProp pt_csg_function_callable_original = PT_NODE_PROP(PT_CLASS_FUNCTION_CALLABLE_NODE, "originalNode"); static NodeProp pt_csg_instantiation_callable_original = PT_NODE_PROP(PT_CLASS_INSTANTIATION_CALLABLE_NODE, "originalNode"); static NodeProp pt_csg_return_statements_class_method = PT_NODE_PROP(PT_CLASS_METHOD_RETURN_STATEMENTS_NODE, "classMethod"); @@ -293,6 +294,14 @@ class ClassStatementsGatherer callableOriginal = pt_csg_static_method_callable_original.of(node); } if (callableOriginal != NULL) return pushNodeScope(slots::methodCalls, PT_CLASS_GATHERED_METHOD_CALL, callableOriginal, scope); + if (isInstanceOf(node, PT_CLASS_PARTIAL_FUNCTION_APPLICATION_NODE)) { + zval *original = pt_csg_partial_function_application_original.of(node); + if (original != NULL && Z_TYPE_P(original) == IS_OBJECT + && (isInstanceOf(Z_OBJ_P(original), PT_CLASS_METHOD_CALL) || isInstanceOf(Z_OBJ_P(original), PT_CLASS_STATIC_CALL))) { + return pushNodeScope(slots::methodCalls, PT_CLASS_GATHERED_METHOD_CALL, original, scope); + } + return true; + } if (isInstanceOf(node, PT_CLASS_METHOD_RETURN_STATEMENTS_NODE)) return addReturnStatementsNode(node, &nodeZv); if (isInstanceOf(node, PT_CLASS_FUNC_CALL)) { zend_object *callee = pt_csg_func_call_name.objectOf(node, PT_CLASS_NAME); diff --git a/turbo-ext/src/ClosureArgVisitor.cpp b/turbo-ext/src/ClosureArgVisitor.cpp index 9aa3dc860d9..7b24c4a8773 100644 --- a/turbo-ext/src/ClosureArgVisitor.cpp +++ b/turbo-ext/src/ClosureArgVisitor.cpp @@ -39,7 +39,7 @@ class ClosureArgVisitor if (!visitors::isInstanceOf(node, PT_CLASS_FUNC_CALL)) return !EG(exception); zval *args = argsProp.of(node); - if (visitors::isFirstClassCallable(args)) return true; + if (visitors::isPartialFunctionApplication(args)) return true; zval *callee = calleeProp.of(node); if (callee == NULL || Z_TYPE_P(callee) != IS_OBJECT) return true; diff --git a/turbo-ext/src/ClosureBindArgVisitor.cpp b/turbo-ext/src/ClosureBindArgVisitor.cpp index ef46e41e96f..deae5016abe 100644 --- a/turbo-ext/src/ClosureBindArgVisitor.cpp +++ b/turbo-ext/src/ClosureBindArgVisitor.cpp @@ -49,7 +49,7 @@ class ClosureBindArgVisitor zend_string *methodName = visitors::nameString(method, identifierProp); if (methodName == NULL || !visitors::lowerEquals(methodName, "bind")) return true; zval *args = argsProp.of(node); - if (visitors::isFirstClassCallable(args)) return true; + if (visitors::isPartialFunctionApplication(args)) return true; if (args == NULL || Z_TYPE_P(args) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(args)) <= 1) return true; zend_object *arg = visitors::argAt(args, 0); diff --git a/turbo-ext/src/ClosureBindToVarVisitor.cpp b/turbo-ext/src/ClosureBindToVarVisitor.cpp index 24e580caf15..6d7a423d3d9 100644 --- a/turbo-ext/src/ClosureBindToVarVisitor.cpp +++ b/turbo-ext/src/ClosureBindToVarVisitor.cpp @@ -44,7 +44,7 @@ class ClosureBindToVarVisitor zend_string *methodName = visitors::nameString(method, identifierProp); if (methodName == NULL || !visitors::lowerEquals(methodName, "bindto")) return true; zval *args = argsProp.of(node); - if (visitors::isFirstClassCallable(args)) return true; + if (visitors::isPartialFunctionApplication(args)) return true; zend_object *arg = visitors::argAt(args, 0); zval *var = varProp.of(node); diff --git a/turbo-ext/src/DefaultNarrowingHelper.cpp b/turbo-ext/src/DefaultNarrowingHelper.cpp index 30963b7bae8..d3995d74b88 100644 --- a/turbo-ext/src/DefaultNarrowingHelper.cpp +++ b/turbo-ext/src/DefaultNarrowingHelper.cpp @@ -203,14 +203,14 @@ zend_string *identifierString(zval *identifier) return Z_TYPE_P(name) == IS_STRING ? Z_STR_P(name) : NULL; } -/* $call->getArgs(): the raw `args` of a call that is not a first-class - * callable (what getRawArgs() returns for every CallLike), the method — +/* $call->getArgs(): the raw `args` of a call that is not a partial function + * application (what getRawArgs() returns for every CallLike), the method — * whose assert() fires — for one that is; UNDEF = pending exception */ zv::Val callArgs(zval *call) { - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(call), firstClassCallable))) return zv::Val(); - if (UNEXPECTED(firstClassCallable)) return pt_type_call(Z_OBJ_P(call), PT_LC("getargs"), 0, NULL); + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(call), partialFunctionApplication))) return zv::Val(); + if (UNEXPECTED(partialFunctionApplication)) return pt_type_call(Z_OBJ_P(call), PT_LC("getargs"), 0, NULL); zval *args = nodeProp(pt_dnh_args_site, call, PT_LC("args")); if (UNEXPECTED(Z_TYPE_P(args) == IS_UNDEF)) return zv::Val::null(); return zv::Val::copyOf(zv::Ref(args)); @@ -376,9 +376,9 @@ class DefaultNarrowingHelper zv::Val specifyTypesForNode(zval *scope, zval *node, zval *context) const { if (isA(node, PT_CLASS_CALL_LIKE)) { - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(node), firstClassCallable))) return zv::Val(); - if (firstClassCallable) return emptyTypesRooted(node); + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(node), partialFunctionApplication))) return zv::Val(); + if (partialFunctionApplication) return emptyTypesRooted(node); } else if (UNEXPECTED(EG(exception))) { return zv::Val(); } diff --git a/turbo-ext/src/ExprHandlerRegistry.cpp b/turbo-ext/src/ExprHandlerRegistry.cpp index 07d14005851..0ea06336db0 100644 --- a/turbo-ext/src/ExprHandlerRegistry.cpp +++ b/turbo-ext/src/ExprHandlerRegistry.cpp @@ -5,7 +5,7 @@ * The memo lives where the twin keeps it: the class's private static * $exprHandlersByClass array, keyed by spl_object_id($container), then by * the twin's cache-key string (the Expr class name; `|1` or `|` for a - * CallLike's first-class-callable flag; `|class>` for a + * CallLike's partial-function-application flag; `|class>` for a * New_). The key is assembled in a stack buffer and looked up by its bytes, * so a hit allocates nothing; the string is created only for the insert. * A miss sweeps $container->getExtensionsCollection(ExprHandler::class) @@ -77,7 +77,7 @@ struct CacheKey } }; -/* $cacheKey = get_class($expr); CallLike: .= '|' . $expr->isFirstClassCallable(); +/* $cacheKey = get_class($expr); CallLike: .= '|' . $expr->isPartialFunctionApplication(); * New_: .= '|' . get_class($expr->class). false = pending exception */ [[nodiscard]] bool buildCacheKey(zend_object *expr, CacheKey &key) { @@ -91,8 +91,8 @@ struct CacheKey return true; } - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(expr, firstClassCallable))) return false; + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(expr, partialFunctionApplication))) return false; zend_string *newClassName = NULL; zend_class_entry *newCe = pt_class(PT_CLASS_NEW); if (UNEXPECTED(newCe == NULL)) return false; @@ -112,7 +112,7 @@ struct CacheKey newClassName = Z_OBJCE_P(value.raw())->name; } - size_t len = ZSTR_LEN(className) + 1 + (firstClassCallable ? 1 : 0) + (newClassName != NULL ? 1 + ZSTR_LEN(newClassName) : 0); + size_t len = ZSTR_LEN(className) + 1 + (partialFunctionApplication ? 1 : 0) + (newClassName != NULL ? 1 + ZSTR_LEN(newClassName) : 0); char *target; if (len <= PT_EHR_KEY_BUFFER_LIMIT) { target = key.buffer; @@ -125,7 +125,7 @@ struct CacheKey memcpy(target + at, ZSTR_VAL(className), ZSTR_LEN(className)); at += ZSTR_LEN(className); target[at++] = '|'; - if (firstClassCallable) { + if (partialFunctionApplication) { target[at++] = '1'; } if (newClassName != NULL) { diff --git a/turbo-ext/src/ExpressionResult.cpp b/turbo-ext/src/ExpressionResult.cpp index c72c7984242..6765d420339 100644 --- a/turbo-ext/src/ExpressionResult.cpp +++ b/turbo-ext/src/ExpressionResult.cpp @@ -991,17 +991,17 @@ class ExpressionResult if (instanceof_function(expr->ce, funcCallCe)) { zv::Ref name = zv::ObjRef(expr).prop(PT_LC("name")); if (name.raw() == NULL || !name.deref().instanceOf(nameCe)) return 0; - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(expr, firstClassCallable))) return -1; - return firstClassCallable ? 0 : 1; + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(expr, partialFunctionApplication))) return -1; + return partialFunctionApplication ? 0 : 1; } if (!instanceof_function(expr->ce, methodCallCe) && !instanceof_function(expr->ce, nullsafeMethodCallCe) && !instanceof_function(expr->ce, staticCallCe)) { return 0; } - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(expr, firstClassCallable))) return -1; - return firstClassCallable ? 0 : 1; + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(expr, partialFunctionApplication))) return -1; + return partialFunctionApplication ? 0 : 1; } /* Mirrors hasTrackedExpressionType(); -1 = pending exception, else 0/1. */ diff --git a/turbo-ext/src/ForHandler.cpp b/turbo-ext/src/ForHandler.cpp index f187def3240..9d2ed40d382 100644 --- a/turbo-ext/src/ForHandler.cpp +++ b/turbo-ext/src/ForHandler.cpp @@ -646,7 +646,7 @@ class ForHandler } /* $call instanceof FuncCall && $call->name instanceof Name && - * !$call->isFirstClassCallable() && in_array($call->name->toLowerString(), + * !$call->isPartialFunctionApplication() && in_array($call->name->toLowerString(), * ['count', 'sizeof'], true) && count($call->getArgs()) > 0 && * $call->getArgs()[0]->value instanceof Variable — the array argument * (borrowed from `hold`), NULL otherwise (and on a pending exception, @@ -660,12 +660,12 @@ class ForHandler return NULL; } if (!ptsh::isInstanceOf(name, PT_CLASS_NAME, error)) return NULL; - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(call), firstClassCallable))) { + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(call), partialFunctionApplication))) { error = true; return NULL; } - if (firstClassCallable) return NULL; + if (partialFunctionApplication) return NULL; zv::Val lower = ptlh::nameToLowerString(name); if (UNEXPECTED(lower.isUndef())) { error = true; diff --git a/turbo-ext/src/ForeachHandler.cpp b/turbo-ext/src/ForeachHandler.cpp index 93d6fd6990c..ba534717b40 100644 --- a/turbo-ext/src/ForeachHandler.cpp +++ b/turbo-ext/src/ForeachHandler.cpp @@ -1400,9 +1400,9 @@ class ForeachHandler if (UNEXPECTED(name == NULL)) return zv::Val(); bool error = false; if (!ptsh::isInstanceOf(name, PT_CLASS_NAME, error)) return error ? zv::Val() : std::move(scope); - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(call), firstClassCallable))) return zv::Val(); - if (firstClassCallable) return scope; + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(call), partialFunctionApplication))) return zv::Val(); + if (partialFunctionApplication) return scope; zv::Val nameHold = zv::Val::copyOf(zv::Ref(name)); zv::Val lower = ptlh::nameToLowerString(nameHold.raw()); if (UNEXPECTED(lower.isUndef())) return zv::Val(); diff --git a/turbo-ext/src/FuncCallHandler.cpp b/turbo-ext/src/FuncCallHandler.cpp index e47625e64ad..bfcfad8564e 100644 --- a/turbo-ext/src/FuncCallHandler.cpp +++ b/turbo-ext/src/FuncCallHandler.cpp @@ -309,6 +309,12 @@ zv::Val callArgs(zval *call) return pt_call_like_is_first_class_callable(Z_OBJ_P(call), out); } +/* $call->isPartialFunctionApplication(); false = pending exception */ +[[nodiscard]] inline bool isPartialFunctionApplication(zval *call, bool &out) +{ + return pt_call_like_is_partial_function_application(Z_OBJ_P(call), out); +} + inline uint32_t countOf(zval *array) { return Z_TYPE_P(array) == IS_ARRAY ? zend_hash_num_elements(Z_ARRVAL_P(array)) : 0; @@ -801,9 +807,9 @@ class FuncCallHandler { out = false; if (!isA(expr, PT_CLASS_FUNC_CALL)) return EG(exception) == NULL; - bool firstClassCallable; - if (UNEXPECTED(!isFirstClassCallable(expr, firstClassCallable))) return false; - out = !firstClassCallable; + bool partialFunctionApplication; + if (UNEXPECTED(!isPartialFunctionApplication(expr, partialFunctionApplication))) return false; + out = !partialFunctionApplication; return true; } @@ -2810,9 +2816,9 @@ class FuncCallHandler if (UNEXPECTED(EG(exception))) return zv::Val(); return types; } - bool firstClassCallable; - if (UNEXPECTED(!isFirstClassCallable(expr, firstClassCallable))) return zv::Val(); - if (firstClassCallable) return types; + bool partialFunctionApplication; + if (UNEXPECTED(!isPartialFunctionApplication(expr, partialFunctionApplication))) return zv::Val(); + if (partialFunctionApplication) return types; zv::Val normalizedArgs = callArgs(normalizedExpr); if (UNEXPECTED(normalizedArgs.isUndef())) return zv::Val(); if (!argIsset(normalizedArgs.raw(), 0)) return types; diff --git a/turbo-ext/src/IdenticalNarrowingHelper.cpp b/turbo-ext/src/IdenticalNarrowingHelper.cpp index 40d230d5598..524a1d861ec 100644 --- a/turbo-ext/src/IdenticalNarrowingHelper.cpp +++ b/turbo-ext/src/IdenticalNarrowingHelper.cpp @@ -171,8 +171,8 @@ ConstantLiteral constFetchLiteral(zval *expr) return LITERAL_NONE; } -/* the raw `args` of a call that is not a first-class callable (what - * getArgs() returns once isFirstClassCallable() said no); borrowed, NULL +/* the raw `args` of a call that is not a partial function application (what + * getArgs() returns once isPartialFunctionApplication() said no); borrowed, NULL * when unset */ HashTable *rawArgs(zval *call) { @@ -180,8 +180,8 @@ HashTable *rawArgs(zval *call) return Z_TYPE_P(args) == IS_ARRAY ? Z_ARRVAL_P(args) : NULL; } -/* $call->getArgs()[0]->value, the call known not to be a first-class - * callable and to have that argument (borrowed) */ +/* $call->getArgs()[0]->value, the call known not to be a partial function + * application and to have that argument (borrowed) */ zval *firstArgValue(zval *call) { HashTable *args = rawArgs(call); @@ -192,13 +192,13 @@ zval *firstArgValue(zval *call) return nodeProp(pt_inh_arg_value_site, arg, PT_LC("value")); } -/* !$call->isFirstClassCallable() && isset($call->getArgs()[0]); false = +/* !$call->isPartialFunctionApplication() && isset($call->getArgs()[0]); false = * pending exception */ [[nodiscard]] bool hasFirstArg(zval *call, bool &out) { - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(call), firstClassCallable))) return false; - if (firstClassCallable) { + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(call), partialFunctionApplication))) return false; + if (partialFunctionApplication) { out = false; return true; } @@ -1234,7 +1234,7 @@ class IdenticalNarrowingHelper } /* $call->name instanceof Name && in_array($call->name->toLowerString(), - * ['count', 'sizeof'], true) && !$call->isFirstClassCallable() && + * ['count', 'sizeof'], true) && !$call->isPartialFunctionApplication() && * isset($call->getArgs()[0]); false = pending exception */ [[nodiscard]] static bool isNormalCountCall(zval *call, bool &out) { diff --git a/turbo-ext/src/ImmediatelyInvokedClosureVisitor.cpp b/turbo-ext/src/ImmediatelyInvokedClosureVisitor.cpp index f98de880a79..7704ac4d170 100644 --- a/turbo-ext/src/ImmediatelyInvokedClosureVisitor.cpp +++ b/turbo-ext/src/ImmediatelyInvokedClosureVisitor.cpp @@ -44,7 +44,7 @@ class ImmediatelyInvokedClosureVisitor zend_object *closure = Z_OBJ_P(callee); if (!visitors::isInstanceOf(closure, PT_CLASS_CLOSURE_EXPR) && !visitors::isInstanceOf(closure, PT_CLASS_ARROW_FUNCTION)) return !EG(exception); zval *args = argsProp.of(node); - if (visitors::isFirstClassCallable(args)) return true; + if (visitors::isPartialFunctionApplication(args)) return true; visitors::setAttributeTrue(closure, pt_immediately_invoked_closure_attribute_str); if (args != NULL) { diff --git a/turbo-ext/src/InitializerExprTypeResolver.cpp b/turbo-ext/src/InitializerExprTypeResolver.cpp index a8dccb753ac..13ec6a7a772 100644 --- a/turbo-ext/src/InitializerExprTypeResolver.cpp +++ b/turbo-ext/src/InitializerExprTypeResolver.cpp @@ -3328,6 +3328,11 @@ class InitializerExprTypeResolver if (firstClassCallable) { return getFirstClassCallableType(expr, context, false); } + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(expr), partialFunctionApplication))) return zv::Val(); + if (partialFunctionApplication) { + return newObjectTypeLiteral(PT_LC("Closure")); + } break; } case ARM_CLOSURE: { diff --git a/turbo-ext/src/MethodCallHandler.cpp b/turbo-ext/src/MethodCallHandler.cpp index 7a32051e52c..65ad50a179e 100644 --- a/turbo-ext/src/MethodCallHandler.cpp +++ b/turbo-ext/src/MethodCallHandler.cpp @@ -76,7 +76,7 @@ pt_property_site pt_mch_arg_value_site; zval *exprVar(zval *expr) { return nodeProperty(pt_mch_var_site, expr, PT_LC("var")); } zval *exprName(zval *expr) { return nodeProperty(pt_mch_name_site, expr, PT_LC("name")); } -/* $expr->getArgs() of a MethodCall that is not a first-class callable (the +/* $expr->getArgs() of a MethodCall that is not a partial function application (the * handler never sees one): its $args */ zval *exprArgs(zval *expr) { return nodeProperty(pt_mch_args_site, expr, PT_LC("args")); } @@ -163,9 +163,9 @@ class MethodCallHandler out = false; return true; } - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(expr), firstClassCallable))) return false; - out = !firstClassCallable; + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(expr), partialFunctionApplication))) return false; + out = !partialFunctionApplication; return true; } diff --git a/turbo-ext/src/MutatingScope.cpp b/turbo-ext/src/MutatingScope.cpp index 1138b46ed9a..42d30cfd91a 100644 --- a/turbo-ext/src/MutatingScope.cpp +++ b/turbo-ext/src/MutatingScope.cpp @@ -2136,12 +2136,12 @@ class MutatingScope return isInstance(name.deref(), classIdx, out); } - /* !$call->isFirstClassCallable() && $call->getArgs() === []; false = pending exception */ + /* !$call->isPartialFunctionApplication() && $call->getArgs() === []; false = pending exception */ [[nodiscard]] static bool isArgumentLessPlainCall(zend_object *call, bool &out) { - bool fcc; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(call, fcc))) return false; - if (fcc) { + bool pfa; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(call, pfa))) return false; + if (pfa) { out = false; return true; } @@ -2518,17 +2518,18 @@ class MutatingScope if (UNEXPECTED(expressionType.isUndef())) return zv::Val(); if (!expressionType.isNull()) return expressionType; - /* NodeScopeResolver intercepts a first-class callable CallLike before - * the ExprHandler dispatch - no handler supports the original node, - * its closure type lives on the stored result's typeCallback */ + /* NodeScopeResolver intercepts a first-class callable or a partial + * function application before the ExprHandler dispatch - no handler + * supports the original node, its closure type lives on the stored + * result's typeCallback */ zval nodeZv; ZVAL_OBJ(&nodeZv, node); bool isCallLike; if (UNEXPECTED(!isInstance(zv::Ref(&nodeZv), PT_CLASS_CALL_LIKE, isCallLike))) return zv::Val(); if (isCallLike) { - bool fcc; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(node, fcc))) return zv::Val(); - if (fcc) return resolveTypeOfNewWorldHandlerNode(node); + bool pfa; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(node, pfa))) return zv::Val(); + if (pfa) return resolveTypeOfNewWorldHandlerNode(node); } zv::Ref container = service(PT_MSS_CONTAINER); @@ -3097,7 +3098,7 @@ class MutatingScope zval nodeZv; ZVAL_OBJ(&nodeZv, node); /* !Match_ && !Yield_ && !YieldFrom && ((!FuncCall && !MethodCall && - * !NullsafeMethodCall && !StaticCall) || isFirstClassCallable()) */ + * !NullsafeMethodCall && !StaticCall) || isPartialFunctionApplication()) */ bool plain = true; static const int valueClasses[] = { PT_CLASS_MATCH, PT_CLASS_YIELD, PT_CLASS_YIELD_FROM }; for (int classIdx : valueClasses) { @@ -3116,7 +3117,7 @@ class MutatingScope if (isCall) break; } if (isCall) { - if (UNEXPECTED(!pt_call_like_is_first_class_callable(node, plain))) return zv::Val(); + if (UNEXPECTED(!pt_call_like_is_partial_function_application(node, plain))) return zv::Val(); } } if (plain) return getScopeStateType(node); @@ -5456,9 +5457,9 @@ class MutatingScope if (UNEXPECTED(!isInstance(expr.ref(), PT_CLASS_FUNC_CALL, isFuncCall))) return false; if (!isFuncCall) return true; zend_object *call = expr.ref().asObject(); - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(call, firstClassCallable))) return false; - if (firstClassCallable) return true; + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(call, partialFunctionApplication))) return false; + if (partialFunctionApplication) return true; zv::Ref name = nodeProp(call, PT_LC("name")); if (UNEXPECTED(name.raw() == NULL)) return false; bool isFullyQualified; diff --git a/turbo-ext/src/NewHandler.cpp b/turbo-ext/src/NewHandler.cpp index 42e18fc70a4..c0c6e2f6fb3 100644 --- a/turbo-ext/src/NewHandler.cpp +++ b/turbo-ext/src/NewHandler.cpp @@ -206,7 +206,7 @@ pt_property_site pt_nh_static_call_args_site; pt_property_site pt_nh_name_name_site; zval *exprClass(zval *expr) { return nodeProperty(pt_nh_class_site, expr, PT_LC("class")); } -/* $expr->getArgs() of a New_ that is not a first-class callable (the handler +/* $expr->getArgs() of a New_ that is not a partial function application (the handler * never sees one), and $expr->args: its $args */ zval *exprArgs(zval *expr) { return nodeProperty(pt_nh_args_site, expr, PT_LC("args")); } /* $methodCall->getArgs() of the synthetic constructor StaticCall (its own @@ -344,9 +344,9 @@ class NewHandler out = false; return true; } - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(expr), firstClassCallable))) return false; - out = !firstClassCallable; + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(expr), partialFunctionApplication))) return false; + out = !partialFunctionApplication; return true; } diff --git a/turbo-ext/src/NodeScopeResolver.cpp b/turbo-ext/src/NodeScopeResolver.cpp index b2d0b6b4a62..ded78f1b94a 100644 --- a/turbo-ext/src/NodeScopeResolver.cpp +++ b/turbo-ext/src/NodeScopeResolver.cpp @@ -746,11 +746,11 @@ class NodeScopeResolver zv::Val handler = pt_expr_handler_registry_resolve(Z_OBJ_P(expr), container.raw()); if (UNEXPECTED(handler.isUndef())) return zv::Val(); if (handler.isNull()) { - bool firstClassCallable = false; + bool partialFunctionApplication = false; bool isCallLike; if (UNEXPECTED(!isInstance(Z_OBJ_P(expr), PT_CLASS_CALL_LIKE, isCallLike))) return zv::Val(); - if (isCallLike && UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(expr), firstClassCallable))) return zv::Val(); - if (!firstClassCallable) { + if (isCallLike && UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(expr), partialFunctionApplication))) return zv::Val(); + if (!partialFunctionApplication) { zv::Val mixed = pt_type_new_mixed_type(); if (UNEXPECTED(mixed.isUndef())) return zv::Val(); zv::Val specifyTypesCallback = pt_specified_types_empty_specify_callback(); @@ -1981,9 +1981,9 @@ class NodeScopeResolver bool isCallLike; if (UNEXPECTED(!isInstance(exprObject, PT_CLASS_CALL_LIKE, isCallLike))) return zv::Val(); if (isCallLike) { - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(exprObject, firstClassCallable))) return zv::Val(); - if (firstClassCallable) return processFirstClassCallable(stmt, expr, scope, storage, nodeCallback, context); + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(exprObject, partialFunctionApplication))) return zv::Val(); + if (partialFunctionApplication) return processPartialFunctionApplication(stmt, expr, scope, storage, nodeCallback, context); } zv::Ref container = slot(slots::container); @@ -2058,14 +2058,19 @@ class NodeScopeResolver return expressionResult; } - /* the first-class-callable branch of processExprNodeInternal() */ - zv::Val processFirstClassCallable(zval *stmt, zval *expr, zval *scope, zval *storage, zval *nodeCallback, zval *context) + /* the partial-function-application branch of processExprNodeInternal(), + * first-class callables included */ + zv::Val processPartialFunctionApplication(zval *stmt, zval *expr, zval *scope, zval *storage, zval *nodeCallback, zval *context) { zend_object *exprObject = Z_OBJ_P(expr); zv::Val newExpr; - bool is; - if (UNEXPECTED(!isInstance(exprObject, PT_CLASS_FUNC_CALL, is))) return zv::Val(); - if (is) { + bool firstClassCallable; + if (UNEXPECTED(!pt_call_like_is_first_class_callable(exprObject, firstClassCallable))) return zv::Val(); + bool is = false; + if (firstClassCallable && UNEXPECTED(!isInstance(exprObject, PT_CLASS_FUNC_CALL, is))) return zv::Val(); + if (!firstClassCallable) { + newExpr = pt_type_new(PT_CLASS_PARTIAL_FUNCTION_APPLICATION_NODE, 1, expr); + } else if (is) { zval *name = nodeProperty(pt_nsr_name_site, exprObject, PT_LC("name")); if (UNEXPECTED(name == NULL)) return zv::Val(); zv::Args argv{name, expr}; @@ -2098,11 +2103,12 @@ class NodeScopeResolver if (UNEXPECTED(!isInstanceValue(classNode, PT_CLASS_CLASS_STMT, anonymousClass))) return zv::Val(); } if (!is || anonymousClass) { - pt_throw_should_not_happen(); - return zv::Val(); + // $a?->b(...) and new class (...) {} do not compile + newExpr = pt_type_new(PT_CLASS_PARTIAL_FUNCTION_APPLICATION_NODE, 1, expr); + } else { + zv::Args argv{classNode, expr}; + newExpr = pt_type_new(PT_CLASS_INSTANTIATION_CALLABLE_NODE, 2, argv); } - zv::Args argv{classNode, expr}; - newExpr = pt_type_new(PT_CLASS_INSTANTIATION_CALLABLE_NODE, 2, argv); } } } diff --git a/turbo-ext/src/NullsafeMethodCallHandler.cpp b/turbo-ext/src/NullsafeMethodCallHandler.cpp index e1ec35da861..421577fe16a 100644 --- a/turbo-ext/src/NullsafeMethodCallHandler.cpp +++ b/turbo-ext/src/NullsafeMethodCallHandler.cpp @@ -153,7 +153,13 @@ class NullsafeMethodCallHandler { int is = isInstanceOf(expr, PT_CLASS_NULLSAFE_METHOD_CALL); if (UNEXPECTED(is < 0)) return false; - out = is == 1; + if (is != 1) { + out = false; + return true; + } + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(expr), partialFunctionApplication))) return false; + out = !partialFunctionApplication; return true; } diff --git a/turbo-ext/src/ParserVisitors.h b/turbo-ext/src/ParserVisitors.h index 0afa679b7c5..4f83b8edab2 100644 --- a/turbo-ext/src/ParserVisitors.h +++ b/turbo-ext/src/ParserVisitors.h @@ -10,7 +10,7 @@ * * The helpers here are the operations those visitor bodies share: reading a * subnode at a memoized property offset, php-parser's toLowerString() - * comparison, and CallLike::isFirstClassCallable()/getArgs(). + * comparison, and CallLike::isPartialFunctionApplication()/getArgs(). */ #ifndef PHPSTANTURBO_PARSER_VISITORS_H @@ -115,18 +115,17 @@ inline zend_string *nameString(zend_object *node, NodeProp &nameProp) } /* - * CallLike::isFirstClassCallable(): exactly one raw argument, and it is a - * VariadicPlaceholder. `rawArgs` is the call's own $args slot. + * CallLike::isPartialFunctionApplication(): some raw argument is a `?` or a + * `...` placeholder, i.e. not an Arg. `rawArgs` is the call's own $args slot. */ -inline bool isFirstClassCallable(zval *rawArgs) +inline bool isPartialFunctionApplication(zval *rawArgs) { - if (rawArgs == NULL || Z_TYPE_P(rawArgs) != IS_ARRAY || zend_hash_num_elements(Z_ARRVAL_P(rawArgs)) != 1) return false; - zend_class_entry *placeholder = pt_class(PT_CLASS_VARIADIC_PLACEHOLDER); - if (UNEXPECTED(placeholder == NULL)) return false; - /* current($rawArgs) — the first element of a freshly built list */ + if (rawArgs == NULL || Z_TYPE_P(rawArgs) != IS_ARRAY) return false; + zend_class_entry *arg = pt_class(PT_CLASS_ARG); + if (UNEXPECTED(arg == NULL)) return false; for (auto entry : zv::ArrRef(rawArgs)) { zv::Ref value = entry.value().deref(); - return value.instanceOf(placeholder); + if (!value.instanceOf(arg)) return true; } return false; } @@ -196,7 +195,7 @@ inline void popStack(zval *stack) /* * The shape every port of a "reacts to one FuncCall by name" visitor has: * `$node instanceof FuncCall && $node->name instanceof Name && - * !$node->isFirstClassCallable()`, then the lowercased function name. + * !$node->isPartialFunctionApplication()`, then the lowercased function name. * Returns NULL when the node is not such a call. */ struct FuncCallProps @@ -214,8 +213,8 @@ struct FuncCallProps /* * The plain-function-call name of `node` ($node->name->toLowerString()) when - * the node is a FuncCall with a Name callee that is not a first-class - * callable, plus its $args slot; NULL otherwise. + * the node is a FuncCall with a Name callee that is not a partial function + * application, plus its $args slot; NULL otherwise. */ inline zend_string *plainFuncCallName(zend_object *node, FuncCallProps &props, NodeProp &nameProp, zval **argsOut) { @@ -223,7 +222,7 @@ inline zend_string *plainFuncCallName(zend_object *node, FuncCallProps &props, N zend_object *callee = props.name.objectOf(node, PT_CLASS_NAME); if (callee == NULL) return NULL; zval *args = props.args.of(node); - if (isFirstClassCallable(args)) return NULL; + if (isPartialFunctionApplication(args)) return NULL; *argsOut = args; return nameString(callee, nameProp); } diff --git a/turbo-ext/src/StaticCallHandler.cpp b/turbo-ext/src/StaticCallHandler.cpp index b951a7df69d..829813b5b23 100644 --- a/turbo-ext/src/StaticCallHandler.cpp +++ b/turbo-ext/src/StaticCallHandler.cpp @@ -172,7 +172,7 @@ pt_property_site pt_sch_arg_value_site; zval *exprClass(zval *expr) { return nodeProperty(pt_sch_class_site, expr, PT_LC("class")); } zval *exprName(zval *expr) { return nodeProperty(pt_sch_name_site, expr, PT_LC("name")); } -/* $expr->getArgs() of a StaticCall that is not a first-class callable (the +/* $expr->getArgs() of a StaticCall that is not a partial function application (the * handler never sees one), and $expr->args: its $args */ zval *exprArgs(zval *expr) { return nodeProperty(pt_sch_args_site, expr, PT_LC("args")); } /* $identifier->name / ->toString() */ @@ -319,9 +319,9 @@ class StaticCallHandler out = false; return true; } - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(expr), firstClassCallable))) return false; - out = !firstClassCallable; + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(expr), partialFunctionApplication))) return false; + out = !partialFunctionApplication; return true; } diff --git a/turbo-ext/src/TypeSpecifier.cpp b/turbo-ext/src/TypeSpecifier.cpp index ea253087b22..a8f8b976cd5 100644 --- a/turbo-ext/src/TypeSpecifier.cpp +++ b/turbo-ext/src/TypeSpecifier.cpp @@ -201,9 +201,9 @@ class TypeSpecifier bool isCallLike; if (UNEXPECTED(!isA(expr, PT_CLASS_CALL_LIKE, isCallLike))) return zv::Val(); if (isCallLike) { - bool firstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(expr), firstClassCallable))) return zv::Val(); - if (firstClassCallable) return emptySpecifiedTypesWithRoot(expr); + bool partialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(expr), partialFunctionApplication))) return zv::Val(); + if (partialFunctionApplication) return emptySpecifiedTypesWithRoot(expr); } zval *container = slot(slots::container); diff --git a/turbo-ext/src/VariableFlowBuilder.cpp b/turbo-ext/src/VariableFlowBuilder.cpp index cbee929e037..22b27b3ad82 100644 --- a/turbo-ext/src/VariableFlowBuilder.cpp +++ b/turbo-ext/src/VariableFlowBuilder.cpp @@ -159,9 +159,9 @@ class VariableFlowBuilder * the identity set of those arguments, keyed by object handle */ zv::ScratchTable callbackArguments(0); if (instanceof_function(Z_OBJCE_P(expr), classes.callLike)) { - bool isFirstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(Z_OBJ_P(expr), isFirstClassCallable))) return zv::Val(); - if (!isFirstClassCallable) { + bool isPartialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(Z_OBJ_P(expr), isPartialFunctionApplication))) return zv::Val(); + if (!isPartialFunctionApplication) { zv::Val argsHold; zval *args = pt_call_like_args(Z_OBJ_P(expr), argsHold); if (UNEXPECTED(args == NULL)) return zv::Val(); diff --git a/turbo-ext/src/VolatileExpressionHelper.cpp b/turbo-ext/src/VolatileExpressionHelper.cpp index ebeb2809b1d..8661df29a77 100644 --- a/turbo-ext/src/VolatileExpressionHelper.cpp +++ b/turbo-ext/src/VolatileExpressionHelper.cpp @@ -201,9 +201,9 @@ class VolatileExpressionHelper zend_object *expr = zv::ObjRef(exprTypeHolder.asObject()).propAt(PT_ETH_PROP_EXPR).asObject(); if (!instanceof_function(expr->ce, funcCallCe)) continue; - bool isFirstClassCallable; - if (UNEXPECTED(!pt_call_like_is_first_class_callable(expr, isFirstClassCallable))) return zv::Val(); - if (isFirstClassCallable) continue; + bool isPartialFunctionApplication; + if (UNEXPECTED(!pt_call_like_is_partial_function_application(expr, isPartialFunctionApplication))) return zv::Val(); + if (isPartialFunctionApplication) continue; zv::Ref name = zv::ObjRef(expr).prop(PT_LC("name")); if (UNEXPECTED(name.raw() == NULL)) continue; name = name.deref(); diff --git a/turbo-ext/src/support.cpp b/turbo-ext/src/support.cpp index e34c9b87288..1f1001f58ce 100644 --- a/turbo-ext/src/support.cpp +++ b/turbo-ext/src/support.cpp @@ -206,6 +206,7 @@ static const pt_class_template pt_class_templates[PT_CLASS_COUNT] = { /* PT_CLASS_STATIC_METHOD_CALLABLE_NODE */ {"staticMethodCallableNode", "PHPStan\\Node\\StaticMethodCallableNode"}, /* PT_CLASS_FUNCTION_CALLABLE_NODE */ {"functionCallableNode", "PHPStan\\Node\\FunctionCallableNode"}, /* PT_CLASS_INSTANTIATION_CALLABLE_NODE */ {"instantiationCallableNode", "PHPStan\\Node\\InstantiationCallableNode"}, + /* PT_CLASS_PARTIAL_FUNCTION_APPLICATION_NODE */ {"partialFunctionApplicationNode", "PHPStan\\Node\\PartialFunctionApplicationNode"}, /* PT_CLASS_SET_OFFSET_VALUE_TYPE_EXPR */ {"setOffsetValueTypeExpr", "PHPStan\\Node\\Expr\\SetOffsetValueTypeExpr"}, /* PT_CLASS_GATHERED_METHOD_CALL */ {"gatheredMethodCall", "PHPStan\\Node\\Method\\MethodCall"}, /* PT_CLASS_PROPERTY_READ */ {"propertyRead", "PHPStan\\Node\\Property\\PropertyRead"}, @@ -1181,6 +1182,7 @@ int32_t pt_call_like_resolve(zend_class_entry *ce) if (info != NULL && (info->flags & ZEND_ACC_STATIC) == 0 && pt_call_like_declared_by(ce, PT_LC("getrawargs"), { PT_CLASS_FUNC_CALL, PT_CLASS_METHOD_CALL, PT_CLASS_NULLSAFE_METHOD_CALL, PT_CLASS_STATIC_CALL, PT_CLASS_NEW }) && pt_call_like_declared_by(ce, PT_LC("isfirstclasscallable"), { PT_CLASS_CALL_LIKE }) + && pt_call_like_declared_by(ce, PT_LC("ispartialfunctionapplication"), { PT_CLASS_CALL_LIKE }) && pt_call_like_declared_by(ce, PT_LC("getargs"), { PT_CLASS_CALL_LIKE })) { argsOffset = (int32_t) info->offset; } @@ -1217,6 +1219,24 @@ bool pt_call_like_raw_args_are_first_class_callable(zval *rawArgs, bool &out) return true; } +/* some raw argument is a VariadicPlaceholder or an ArgPlaceholder - a raw + * argument is one of those or an Arg; false = pending exception */ +bool pt_call_like_raw_args_are_partial_function_application(zval *rawArgs, bool &out) +{ + out = false; + if (Z_TYPE_P(rawArgs) != IS_ARRAY) return true; + zend_class_entry *argCe = pt_class(PT_CLASS_ARG); + if (UNEXPECTED(argCe == NULL)) return false; + for (auto entry : zv::ArrRef(rawArgs)) { + zv::Ref value = entry.value().deref(); + if (!value.instanceOf(argCe)) { + out = true; + return true; + } + } + return true; +} + /* $call->method() by name, kept in hold; NULL = pending exception */ zend_never_inline ZEND_COLD zval *pt_call_like_call(zend_object *call, const char *lcname, size_t len, zv::Val &hold) { @@ -1243,6 +1263,17 @@ bool pt_call_like_is_first_class_callable(zend_object *call, bool &out) return true; } +bool pt_call_like_is_partial_function_application(zend_object *call, bool &out) +{ + zval *args = pt_call_like_args_slot(call); + if (EXPECTED(args != NULL)) return pt_call_like_raw_args_are_partial_function_application(args, out); + zv::Val hold; + zval *result = pt_call_like_call(call, PT_LC("ispartialfunctionapplication"), hold); + if (UNEXPECTED(result == NULL)) return false; + out = zend_is_true(result); + return true; +} + zval *pt_call_like_args(zend_object *call, zv::Val &hold) { zval *args = pt_call_like_args_slot(call); diff --git a/turbo-ext/src/support.h b/turbo-ext/src/support.h index 6c0e26f9c3a..66fbd7a77e3 100644 --- a/turbo-ext/src/support.h +++ b/turbo-ext/src/support.h @@ -293,6 +293,7 @@ enum { PT_CLASS_STATIC_METHOD_CALLABLE_NODE, PT_CLASS_FUNCTION_CALLABLE_NODE, PT_CLASS_INSTANTIATION_CALLABLE_NODE, + PT_CLASS_PARTIAL_FUNCTION_APPLICATION_NODE, PT_CLASS_SET_OFFSET_VALUE_TYPE_EXPR, PT_CLASS_GATHERED_METHOD_CALL, PT_CLASS_PROPERTY_READ, @@ -1707,6 +1708,9 @@ zval *pt_call_like_raw_args(zend_object *call, zv::Val &hold); /* $call->isFirstClassCallable(): a single VariadicPlaceholder argument; * false = pending exception */ [[nodiscard]] bool pt_call_like_is_first_class_callable(zend_object *call, bool &out); +/* $call->isPartialFunctionApplication(): some argument is a `?` or a `...` + * placeholder, a first-class callable included; false = pending exception */ +[[nodiscard]] bool pt_call_like_is_partial_function_application(zend_object *call, bool &out); /* $call->getArgs(): the `args` slot of a call that is not a first-class * callable (borrowed, dereferenced); a first-class callable gets the method, * whose assert() throws under zend.assertions=1 and returns the raw