From cd23c46c8549b52890e990aed5187f3e2cf44eb5 Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Tue, 3 Nov 2026 23:20:11 +0100 Subject: [PATCH 1/3] Analyse partial function applications as closures A call with a ? placeholder (PHP 8.6) passed the guards that only skipped first-class callables and crashed in getArgs(). NodeScopeResolver now replaces it with PartialFunctionApplicationNode, which processes the callee and the bound arguments and has the type Closure. The guards in front of getArgs() check isPartialFunctionApplication(). PartialFunctionApplicationRule reports partial function application below PHP 8.6, and the nullsafe and new forms that PHP does not compile. ClassStatementsGatherer counts a partially applied method as a call. Co-Authored-By: Claude Opus 5.5 --- .../Build/RequiredPhpVersionVisitor.php | 6 +- src/Analyser/ExprHandler/AssignHandler.php | 12 +- src/Analyser/ExprHandler/BinaryOpHandler.php | 14 +- src/Analyser/ExprHandler/FuncCallHandler.php | 4 +- .../Helper/DefaultNarrowingHelper.php | 2 +- .../Helper/IdenticalNarrowingHelper.php | 10 +- .../ExprHandler/MethodCallHandler.php | 2 +- src/Analyser/ExprHandler/NewHandler.php | 2 +- .../ExprHandler/NullsafeMethodCallHandler.php | 2 +- .../ExprHandler/StaticCallHandler.php | 2 +- .../PartialFunctionApplicationNodeHandler.php | 126 ++++++++++++++++++ src/Analyser/ExprHandlerRegistry.php | 2 +- src/Analyser/ExpressionResult.php | 4 +- src/Analyser/MutatingScope.php | 22 +-- src/Analyser/NodeScopeResolver.php | 15 ++- src/Analyser/StmtHandler/ForHandler.php | 4 +- src/Analyser/StmtHandler/ForeachHandler.php | 2 +- src/Analyser/TypeSpecifier.php | 2 +- src/Analyser/VariableFlowBuilder.php | 2 +- src/Analyser/VolatileExpressionHelper.php | 2 +- src/Node/ClassStatementsGatherer.php | 7 + src/Node/PartialFunctionApplicationNode.php | 48 +++++++ src/Node/Printer/Printer.php | 6 + src/Parser/ArrayFilterArgVisitor.php | 2 +- src/Parser/ArrayFindArgVisitor.php | 2 +- src/Parser/ArrayMapArgVisitor.php | 2 +- src/Parser/ArrayWalkArgVisitor.php | 2 +- src/Parser/ArrowFunctionArgVisitor.php | 2 +- src/Parser/ClosureArgVisitor.php | 2 +- src/Parser/ClosureBindArgVisitor.php | 2 +- src/Parser/ClosureBindToVarVisitor.php | 2 +- src/Parser/CurlSetOptArgVisitor.php | 2 +- src/Parser/CurlSetOptArrayArgVisitor.php | 2 +- .../ImmediatelyInvokedClosureVisitor.php | 2 +- src/Parser/ImplodeArgVisitor.php | 2 +- src/Parser/ReversePipeTransformerVisitor.php | 6 +- src/Php/PhpVersion.php | 5 + src/Php/PhpVersions.php | 5 + .../SourceLocator/CachingVisitor.php | 2 +- .../InitializerExprTypeResolver.php | 3 + .../NonexistentOffsetInArrayDimFetchRule.php | 2 +- .../FunctionCallConstantConditionHelper.php | 2 +- .../Comparison/ImpossibleCheckTypeHelper.php | 4 +- ...ssiblyPureCallTransitivePurityResolver.php | 6 +- .../PossiblyPureFuncCallCollector.php | 4 +- .../PossiblyPureMethodCallCollector.php | 4 +- .../PossiblyPureStaticCallCollector.php | 4 +- ...llToFunctionStatementWithNoDiscardRule.php | 4 +- .../PartialFunctionApplicationRule.php | 57 ++++++++ ...CallToMethodStatementWithNoDiscardRule.php | 4 +- ...StaticMethodStatementWithNoDiscardRule.php | 4 +- src/Type/Regex/RegexExpressionHelper.php | 2 +- .../nsrt/partial-function-application.php | 44 ++++++ .../Build/RequiredPhpVersionCommentTest.php | 2 + .../DeadCode/UnusedPrivateMethodRuleTest.php | 5 + ...tion-application-unused-private-method.php | 28 ++++ .../PartialFunctionApplicationRuleTest.php | 48 +++++++ .../data/partial-function-application.php | 39 ++++++ turbo-ext/src/ArrowFunctionArgVisitor.cpp | 2 +- turbo-ext/src/AssignHandler.cpp | 28 ++-- turbo-ext/src/BinaryOpHandler.cpp | 16 +-- turbo-ext/src/ClassStatementsGatherer.cpp | 9 ++ turbo-ext/src/ClosureArgVisitor.cpp | 2 +- turbo-ext/src/ClosureBindArgVisitor.cpp | 2 +- turbo-ext/src/ClosureBindToVarVisitor.cpp | 2 +- turbo-ext/src/DefaultNarrowingHelper.cpp | 16 +-- turbo-ext/src/ExprHandlerRegistry.cpp | 12 +- turbo-ext/src/ExpressionResult.cpp | 12 +- turbo-ext/src/ForHandler.cpp | 8 +- turbo-ext/src/ForeachHandler.cpp | 6 +- turbo-ext/src/FuncCallHandler.cpp | 18 ++- turbo-ext/src/IdenticalNarrowingHelper.cpp | 18 +-- .../src/ImmediatelyInvokedClosureVisitor.cpp | 2 +- turbo-ext/src/InitializerExprTypeResolver.cpp | 5 + turbo-ext/src/MethodCallHandler.cpp | 8 +- turbo-ext/src/MutatingScope.cpp | 31 ++--- turbo-ext/src/NewHandler.cpp | 8 +- turbo-ext/src/NodeScopeResolver.cpp | 36 ++--- turbo-ext/src/NullsafeMethodCallHandler.cpp | 8 +- turbo-ext/src/ParserVisitors.h | 25 ++-- turbo-ext/src/StaticCallHandler.cpp | 8 +- turbo-ext/src/TypeSpecifier.cpp | 6 +- turbo-ext/src/VariableFlowBuilder.cpp | 6 +- turbo-ext/src/VolatileExpressionHelper.cpp | 6 +- turbo-ext/src/support.cpp | 31 +++++ turbo-ext/src/support.h | 4 + 86 files changed, 720 insertions(+), 221 deletions(-) create mode 100644 src/Analyser/ExprHandler/Virtual/PartialFunctionApplicationNodeHandler.php create mode 100644 src/Node/PartialFunctionApplicationNode.php create mode 100644 src/Rules/Functions/PartialFunctionApplicationRule.php create mode 100644 tests/PHPStan/Analyser/nsrt/partial-function-application.php create mode 100644 tests/PHPStan/Rules/DeadCode/data/partial-function-application-unused-private-method.php create mode 100644 tests/PHPStan/Rules/Functions/PartialFunctionApplicationRuleTest.php create mode 100644 tests/PHPStan/Rules/Functions/data/partial-function-application.php 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/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/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 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 From c30faedea143047e7e34dc3afb220de6b5eb192f Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Tue, 3 Nov 2026 23:25:36 +0100 Subject: [PATCH 2/3] Exclude the partial function application fixtures from the name collision check The detector parses with the tokenizer of PHP 8.5, which cannot read the ? placeholder. Co-Authored-By: Claude Opus 5.5 --- build/collision-detector.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/build/collision-detector.json b/build/collision-detector.json index fbdee3556b9..9e102455119 100644 --- a/build/collision-detector.json +++ b/build/collision-detector.json @@ -18,6 +18,9 @@ "../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" ] } From bdd66a3ff80eb1f1d29f5a5ac04de8392f19584d Mon Sep 17 00:00:00 2001 From: Sander Muller Date: Wed, 4 Nov 2026 00:03:32 +0100 Subject: [PATCH 3/3] Test the partial function application rule with a PHP version range Co-Authored-By: Claude Opus 5.5 --- build/collision-detector.json | 3 +- ...alFunctionApplicationRuleConfigPhpTest.php | 40 +++++++++++++++++++ ...unction-application-php-version-range.neon | 4 ++ ...tial-function-application-php-versions.php | 21 ++++++++++ 4 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 tests/PHPStan/Rules/Functions/PartialFunctionApplicationRuleConfigPhpTest.php create mode 100644 tests/PHPStan/Rules/Functions/data/partial-function-application-php-version-range.neon create mode 100644 tests/PHPStan/Rules/Functions/data/partial-function-application-php-versions.php diff --git a/build/collision-detector.json b/build/collision-detector.json index 9e102455119..d920bf8d16b 100644 --- a/build/collision-detector.json +++ b/build/collision-detector.json @@ -21,6 +21,7 @@ "../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", + "../tests/PHPStan/Rules/Functions/data/partial-function-application-php-versions.php" ] } 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/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(?); +}