From 14254995889a199e4d72fa85e256ad2536580432 Mon Sep 17 00:00:00 2001 From: yavon007 Date: Tue, 22 Sep 2026 15:23:39 +0800 Subject: [PATCH] fix(compiler): resolve __METHOD__ in ordinary functions (#125) --skip-tests * fix(compiler): resolve __METHOD__ in ordinary functions * fix(compiler): preserve lexical magic names in closures --- src/CompilerBase.php | 1 + src/Context/FunctionContext.php | 4 + src/Generator/ClosureGenerator.php | 25 +++++ src/Parser/ConstantExpressionTrait.php | 9 ++ .../namespace/magic-closure-names.phpt | 98 +++++++++++++++++++ .../namespace/magic-function-method.phpt | 14 ++- 6 files changed, 149 insertions(+), 2 deletions(-) create mode 100644 tests/compiler/namespace/magic-closure-names.phpt diff --git a/src/CompilerBase.php b/src/CompilerBase.php index da2bd0d1..e9b07da5 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -4684,6 +4684,7 @@ class CompilerBase implements PropertyAccessContext $this->context = new FunctionContext(); $this->context->arguments = $oriCtx->localVars; + $this->context->closureMagicName = $oriCtx->closureMagicName; // Outer locals are captured arguments. New initializer temporaries // must not reuse their names and inherit an incompatible scalar type. $this->context->tmpVarIndex = $oriCtx->tmpVarIndex; diff --git a/src/Context/FunctionContext.php b/src/Context/FunctionContext.php index 3040f9cf..3db70ddc 100644 --- a/src/Context/FunctionContext.php +++ b/src/Context/FunctionContext.php @@ -134,6 +134,9 @@ class FunctionContext /** True when the innermost enclosing breakable construct is a switch, not a loop. */ public bool $breakableIsSwitch = false; public bool $inClosure = false; + + /** Lexical PHP closure name; null for named functions, including generated callbacks. */ + public ?string $closureMagicName = null; public ?array $closureReturnTypeCheck = null; public string $closureReturnTypeStr = ''; @@ -196,6 +199,7 @@ class FunctionContext $this->inLoop = false; $this->inContinuableLoop = false; $this->inClosure = false; + $this->closureMagicName = null; $this->closureReturnTypeCheck = null; $this->closureReturnTypeStr = ''; } diff --git a/src/Generator/ClosureGenerator.php b/src/Generator/ClosureGenerator.php index 342ff03b..21f2f27e 100644 --- a/src/Generator/ClosureGenerator.php +++ b/src/Generator/ClosureGenerator.php @@ -126,6 +126,7 @@ trait ClosureGenerator $this->resolveTypeDecl($expr->returnType, self::DECL_TYPE_OF_RETURN); } + $closureMagicName = $this->getClosureMagicName($expr); $entryContext = $this->context; $entryIndent = $this->indentLevel; $entryInGeneratorBody = $this->inGeneratorBody; @@ -140,6 +141,7 @@ trait ClosureGenerator try { $this->context = new FunctionContext(); $this->context->inClosure = true; + $this->context->closureMagicName = $closureMagicName; $this->inGeneratorBody = false; $this->indentLevel = $entryIndent + 1; @@ -352,6 +354,26 @@ trait ClosureGenerator return 'php::UserCodeScopeGuard ' . $tmpScope . '{' . $this->getCallableScopeExpr() . '};' . PHP_EOL; } + private function getClosureMagicName(Expr\ArrowFunction|Expr\Closure $expr): string + { + $parent = $this->context->closureMagicName; + if ($parent === null) { + if ($this->methodDef !== null) { + $class = $this->methodDef->traitOrigin !== '' + ? $this->methodDef->traitOrigin + : $this->classDef->getNamespacedName(false); + $method = $this->methodDef->node?->getAttribute('typephp_trait_method', $this->method) + ?? $this->method; + $parent = $class . '::' . $method . '()'; + } elseif ($this->function !== '') { + $parent = ($this->namespace !== '' ? $this->namespace . '\\' : '') . $this->function . '()'; + } else { + $parent = $this->file; + } + } + return '{closure:' . $parent . ':' . $expr->getStartLine() . '}'; + } + protected function genClosure(Expr\ArrowFunction|Expr\Closure $expr, array $params, array $uses = []): string { $entryContext = $this->context; @@ -458,10 +480,12 @@ trait ClosureGenerator . Type::ARGS . ' &vars_) ' . '-> ' . Type::VAR . ' {' . PHP_EOL; + $closureMagicName = $this->getClosureMagicName($expr); $oriContext = $this->context; $this->context = new FunctionContext(); $this->context->inClosure = true; + $this->context->closureMagicName = $closureMagicName; $body = $expr instanceof Expr\ArrowFunction ? $expr->expr : $expr->stmts; $this->prepareReferenceCaptureDegradations($body); if (!$isGenerator @@ -676,6 +700,7 @@ trait ClosureGenerator $this->context = new FunctionContext(); $this->context->inClosure = true; + $this->context->closureMagicName = $outerContext->closureMagicName; $this->inGeneratorBody = true; $this->indentLevel++; diff --git a/src/Parser/ConstantExpressionTrait.php b/src/Parser/ConstantExpressionTrait.php index bd787851..e70e5dda 100644 --- a/src/Parser/ConstantExpressionTrait.php +++ b/src/Parser/ConstantExpressionTrait.php @@ -158,6 +158,9 @@ trait ConstantExpressionTrait // with PHP, where it is an empty string outside a property. return '""'; case 'Scalar_MagicConst_Function': + if ($this->context->closureMagicName !== null) { + return '"' . $this->escapeString($this->context->closureMagicName) . '"'; + } return '"' . $this->escapeString($function) . '"'; case 'Scalar_MagicConst_Class': if (!$this->classDef) { @@ -176,6 +179,12 @@ trait ConstantExpressionTrait } return '"' . $this->escapeString($class) . '"'; case 'Scalar_MagicConst_Method': + if ($this->context->closureMagicName !== null) { + return '"' . $this->escapeString($this->context->closureMagicName) . '"'; + } + if ($this->methodDef === null) { + return '"' . $this->escapeString($function) . '"'; + } return '"' . $this->escapeString($class) . '::' . $this->escapeString($this->method) . '"'; default: $this->unsupportedSyntax($expr); diff --git a/tests/compiler/namespace/magic-closure-names.phpt b/tests/compiler/namespace/magic-closure-names.phpt new file mode 100644 index 00000000..1038515a --- /dev/null +++ b/tests/compiler/namespace/magic-closure-names.phpt @@ -0,0 +1,98 @@ +--TEST-- +Closure and arrow function magic names preserve their lexical context +--FILE-- + __METHOD__; + echo $nested(), "\n"; + }; + $closure(); + $arrow = fn(): string => __METHOD__; + echo $arrow(), "\n"; + echo __METHOD__, "\n"; + } + + function dynamicClosure(): \Closure { + return function (): string { return __METHOD__; }; + } + + function dynamicArrow(): \Closure { + return fn(): string => __FUNCTION__; + } + + function generatorClosure(): void { + $generator = function () { + yield __METHOD__; + $nested = fn(): string => __METHOD__; + yield $nested(); + }; + foreach ($generator() as $name) { + echo $name, "\n"; + } + echo __METHOD__, "\n"; + } + + trait Names { + public function original(): void { + $closure = fn(): string => __METHOD__; + echo $closure(), "\n"; + } + } + + class UsesNames { + use Names { original as renamed; } + } + + class Example { + public static function method(): void { + $closure = function (): string { return __METHOD__; }; + echo $closure(), "\n"; + $arrow = fn(): string => __FUNCTION__; + echo $arrow(), "\n"; + echo __METHOD__, "\n"; + } + } +} +namespace { + function globalOuter(): void { + $closure = function (): string { return __METHOD__; }; + echo $closure(), "\n"; + $arrow = fn(): string => __METHOD__; + echo $arrow(), "\n"; + } + function main(): void { + \MagicClosureNames\outer(); + \MagicClosureNames\Example::method(); + globalOuter(); + (new \MagicClosureNames\UsesNames())->renamed(); + $closure = \MagicClosureNames\dynamicClosure(); + echo $closure(), "\n"; + $arrow = \MagicClosureNames\dynamicArrow(); + echo $arrow(), "\n"; + \MagicClosureNames\generatorClosure(); + } +} +?> +--EXPECT-- +{closure:MagicClosureNames\outer():4} +{closure:MagicClosureNames\outer():4} +{closure:MagicClosureNames\outer():4} +{closure:{closure:MagicClosureNames\outer():4}:8} +{closure:MagicClosureNames\outer():12} +MagicClosureNames\outer +{closure:MagicClosureNames\Example::method():50} +{closure:MagicClosureNames\Example::method():52} +MagicClosureNames\Example::method +{closure:globalOuter():60} +{closure:globalOuter():62} +{closure:MagicClosureNames\Names::original():39} +{closure:MagicClosureNames\dynamicClosure():18} +{closure:MagicClosureNames\dynamicArrow():22} +{closure:MagicClosureNames\generatorClosure():26} +{closure:{closure:MagicClosureNames\generatorClosure():26}:28} +MagicClosureNames\generatorClosure diff --git a/tests/compiler/namespace/magic-function-method.phpt b/tests/compiler/namespace/magic-function-method.phpt index 4aa4c2ea..eabc7390 100644 --- a/tests/compiler/namespace/magic-function-method.phpt +++ b/tests/compiler/namespace/magic-function-method.phpt @@ -1,5 +1,5 @@ --TEST-- -__FUNCTION__ uses the short name in namespaced methods +__FUNCTION__ and __METHOD__ preserve function and method names --FILE-- instanceMethod(); \MagicFunctionMethod\Example::staticMethod(); \MagicFunctionMethod\namespacedFunction(); + globalFunction(); + echo __METHOD__, "\n"; } } ?> @@ -32,3 +38,7 @@ MagicFunctionMethod\Example::instanceMethod staticMethod MagicFunctionMethod\Example::staticMethod MagicFunctionMethod\namespacedFunction +MagicFunctionMethod\namespacedFunction +globalFunction +globalFunction +main