diff --git a/phpunit/src/SsaAnalysisTest.php b/phpunit/src/SsaAnalysisTest.php index 6fd61b86..1903e2bc 100644 --- a/phpunit/src/SsaAnalysisTest.php +++ b/phpunit/src/SsaAnalysisTest.php @@ -1345,6 +1345,18 @@ public function testDetectTypeOfExplicitStdNativeCalls(): void } } + public function testDetectTypeOfFirstClassCallableBeforeBuiltinReturnTypeOptimizations(): void + { + foreach (['round', 'fopen'] as $function) { + $call = new Expr\FuncCall( + new Node\Name($function), + [new Node\VariadicPlaceholder()] + ); + + $this->assertSame(Type::OBJECT, $this->invoke('detectTypeOfExpr', $call)); + } + } + public function testDetectTypeOfConcatExpressions(): void { $concat = new Expr\BinaryOp\Concat(new Scalar\LNumber(1), new Scalar\String_('')); diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 3ef2a3a8..f7e370ce 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -3139,11 +3139,15 @@ protected function detectTypeOfExpr($expr): string } break; case 'Expr_FuncCall': + if ($expr->isFirstClassCallable()) { + return Type::OBJECT; + } if ($this->isNameExpr($expr->name)) { $name = $this->parseIdentifier($expr->name); $globalName = ltrim($name, '\\'); // Math function optimization: propagate Big* return types - if (in_array($name, ['abs', 'pow', 'sqrt', 'floor', 'ceil', 'round'], true) && !empty($expr->args)) { + if (in_array($name, ['abs', 'pow', 'sqrt', 'floor', 'ceil', 'round'], true) + && !empty($expr->args)) { $argType = $this->detectTypeOfExpr($expr->args[0]->value); if ( $argType === Type::BIGINT @@ -3167,9 +3171,6 @@ protected function detectTypeOfExpr($expr): string if (in_array($name, self::STREAM_FUNCTIONS)) { return Type::STREAM; } - if (count($expr->args) === 1 and $this->isPlaceholderExpr($expr->args[0])) { - return Type::OBJECT; - } if ($this->hasFunction($name)) { return $this->getFunction($name)->returnType; } @@ -3458,6 +3459,12 @@ protected function checkInternalFunctionArgCount(string $funcName, Node\Expr\Fun if ($this->hasUnpackCallArg($expr->args)) { return; } + // `foo(...)` is PHP 8.1 first-class callable syntax: it creates a + // Closure instead of calling foo, so its single VariadicPlaceholder + // must not be counted/validated against foo's real signature. + if ($expr->isFirstClassCallable()) { + return; + } $actualArgCount = count($expr->args); $config = $this->getFuncCallConfig()[ltrim($funcName, '\\')] ?? null; $allowedArgCounts = is_array($config) ? ($config['argCounts'] ?? null) : null; diff --git a/tests/compiler/place-holder/builtin-multi-arg-callable.phpt b/tests/compiler/place-holder/builtin-multi-arg-callable.phpt new file mode 100644 index 00000000..6d8e0156 --- /dev/null +++ b/tests/compiler/place-holder/builtin-multi-arg-callable.phpt @@ -0,0 +1,19 @@ +--TEST-- +First-class callable of builtins requiring multiple arguments +--FILE-- + +--EXPECT-- +ababab +2,3 diff --git a/tests/compiler/place-holder/math-function-callable.phpt b/tests/compiler/place-holder/math-function-callable.phpt new file mode 100644 index 00000000..8e2a0830 --- /dev/null +++ b/tests/compiler/place-holder/math-function-callable.phpt @@ -0,0 +1,16 @@ +--TEST-- +First-class callable of a math function (round) +--FILE-- +value, so the optimization must be skipped and the expression must + // resolve to a Closure instead of crashing. + $values = [1.4, 2.6, 3.5]; + $rounded = array_map(round(...), $values); + echo implode(',', $rounded), "\n"; +} +?> +--EXPECT-- +1,3,4 diff --git a/tests/compiler/place-holder/stream-function-callable.phpt b/tests/compiler/place-holder/stream-function-callable.phpt new file mode 100644 index 00000000..da2ec95d --- /dev/null +++ b/tests/compiler/place-holder/stream-function-callable.phpt @@ -0,0 +1,14 @@ +--TEST-- +First-class callable of a stream-returning function +--FILE-- + +--EXPECT-- +callable