From a79610fb2c9ba3097de1d3b46f1bad25c7d69a4f Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Tue, 11 Aug 2026 22:45:25 +0800 Subject: [PATCH] fix(compiler): resolve anonymous class names with proper namespace imports - Updated resolveAnonClassTypeNames to resolveAnonClassNames with improved name resolution - Added proper handling of imported names in anonymous class method bodies - Ensured fully qualified names are embedded when anonymous classes are evaluated - Added test case for anonymous class method bodies preserving namespace imports - Fixed function call trait to check global name instead of local name for unsupported functions - Added tests for destructor exception handling boundaries - Improved include error handling and shutdown exception handler behavior - Added unsupported function detection for extract function with proper error messages - Updated swoole/phpx dependency from ~2.5.3 to ~2.5.5 --- composer.json | 2 +- ...unsupported-function-extract-qualified.php | 8 +++ phpunit/code/unsupported-function-extract.php | 6 +++ phpunit/src/UnsupportedFunctionTest.php | 14 ++++++ src/CompilerBase.php | 17 ++++++- src/Generator/AnonClassGenerator.php | 28 +++++++++-- src/Parser/FunctionCallTrait.php | 4 +- tests/compiler/anon_class/005.phpt | 49 +++++++++++++++++++ .../destructor-wrapper-boundary.phpt | 28 +++++++++++ .../dynamic-callback-frame-restored.phpt | 16 ++++++ .../shutdown-exception-handler-include.phpt | 38 ++++++++++++++ .../include-error-handler-unwind.phpt | 28 +++++++++++ 12 files changed, 228 insertions(+), 10 deletions(-) create mode 100644 phpunit/code/unsupported-function-extract-qualified.php create mode 100644 phpunit/code/unsupported-function-extract.php create mode 100644 phpunit/src/UnsupportedFunctionTest.php create mode 100644 tests/compiler/anon_class/005.phpt create mode 100644 tests/compiler/exception/destructor-wrapper-boundary.phpt create mode 100644 tests/compiler/exception/shutdown-exception-handler-include.phpt create mode 100644 tests/compiler/include_require/include-error-handler-unwind.phpt diff --git a/composer.json b/composer.json index bd7ae056..738b2dce 100644 --- a/composer.json +++ b/composer.json @@ -12,7 +12,7 @@ "marcj/topsort": "^2.0", "symfony/var-dumper": "^8.0", "symfony/yaml": "^8.0", - "swoole/phpx": "~2.5.3", + "swoole/phpx": "~2.5.5", "ajaxray/ansikit": "^0.3.1" }, "require-dev": { diff --git a/phpunit/code/unsupported-function-extract-qualified.php b/phpunit/code/unsupported-function-extract-qualified.php new file mode 100644 index 00000000..85f5b318 --- /dev/null +++ b/phpunit/code/unsupported-function-extract-qualified.php @@ -0,0 +1,8 @@ +exec('Unsupported function: `extract`', 'unsupported-function-extract.php'); + } + + public function testFullyQualifiedExtractIsRejectedAtCompileTime(): void + { + $this->exec('Unsupported function: `extract`', 'unsupported-function-extract-qualified.php'); + } +} diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 92b78c24..6e332ee3 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -3533,8 +3533,8 @@ class CompilerBase implements PropertyAccessContext } } $this->flattenEmbeddedClassTraits($classDef); - // 将匿名类内部的类型引用(方法参数、返回值、属性等)转为全限定名称 - $this->resolveAnonClassTypeNames($classDef); + // 匿名类由根命名空间中的 eval 定义,内部导入的符号必须转为全限定名称。 + $this->resolveAnonClassNames($classDef); $this->context->beforeStmtLines[] = 'static THREAD_LOCAL bool ' . $className . '_defined = false;'; $classCode = $this->genEmbeddedCode($classDef); $this->addConstData($className . '_code', $classCode); @@ -3777,6 +3777,19 @@ class CompilerBase implements PropertyAccessContext $fileName = $this->parseIdentifier($expr->expr); + $scope = []; + foreach ($this->context->localVars as $name => $_type) { + if ($name === 'this_' || str_starts_with($name, 'tmp_var_')) { + continue; + } + $phpName = $this->unescapeVarName($name); + $scope[] = '{ ' . $this->getLiteralString($phpName) . '.str(), php::Var(' . $name . ') }'; + } + + if ($scope) { + return "php::include(php::Var($fileName), $type, php::Array{" . implode(', ', $scope) . '})'; + } + return "php::include(php::Var($fileName), $type)"; } diff --git a/src/Generator/AnonClassGenerator.php b/src/Generator/AnonClassGenerator.php index ac89c901..6152bc86 100644 --- a/src/Generator/AnonClassGenerator.php +++ b/src/Generator/AnonClassGenerator.php @@ -76,12 +76,30 @@ trait AnonClassGenerator array_push($class->stmts, ...$injected); } - /** - * Resolve all relative type names in an anonymous class to fully qualified names. - * The generated eval code runs without use imports, so all type references must be FQN. - */ - protected function resolveAnonClassTypeNames(Class_ $classDef): void + /** Resolve imported names in an anonymous class before evaluating it in the root namespace. */ + protected function resolveAnonClassNames(Class_ $classDef): void { + // Anonymous classes are emitted through eval() in the root namespace. Names + // resolved from the declaring file's namespace and imports must therefore be + // embedded as fully-qualified names, including names used inside method bodies. + $traverser = new NodeTraverser(); + $traverser->addVisitor(new class extends NodeVisitorAbstract { + public function enterNode(Node $node): ?Node + { + if (!$node instanceof Name || $node->isSpecialClassName()) { + return null; + } + $resolvedName = $node->getAttribute('resolvedName'); + if (!$resolvedName instanceof Name) { + return null; + } + return new Name\FullyQualified($resolvedName->toString(), $node->getAttributes()); + } + }); + $traverser->traverse([$classDef]); + + // Lowering may synthesize type nodes after name resolution, so retain the + // explicit signature pass for nodes which do not carry resolvedName metadata. foreach ($classDef->stmts as $stmt) { if ($stmt instanceof ClassMethod) { foreach ($stmt->params as $param) { diff --git a/src/Parser/FunctionCallTrait.php b/src/Parser/FunctionCallTrait.php index a3955ad3..0d22826e 100644 --- a/src/Parser/FunctionCallTrait.php +++ b/src/Parser/FunctionCallTrait.php @@ -87,8 +87,8 @@ trait FunctionCallTrait $this->assertWasiFunctionSupported($expr, $globalName); $this->markInternalFunctionCallbackCall($globalName, $expr->args); } - if (in_array($name, Constants::UNSUPPORTED_FUNCTIONS)) { - $this->fatalError($expr, 'Unsupported function: `' . $name . '`'); + if (in_array($globalName, Constants::UNSUPPORTED_FUNCTIONS, true)) { + $this->fatalError($expr, 'Unsupported function: `' . $globalName . '`'); } if ($name === 'any') { if (count($expr->args) !== 1 || $expr->args[0]->unpack) { diff --git a/tests/compiler/anon_class/005.phpt b/tests/compiler/anon_class/005.phpt new file mode 100644 index 00000000..c3650797 --- /dev/null +++ b/tests/compiler/anon_class/005.phpt @@ -0,0 +1,49 @@ +--TEST-- +Anonymous class method bodies preserve namespace imports +--FILE-- +state === 'ready'; + } + }; + + var_dump($visitor->accepts(new ImportedSubject())); + } +} + +namespace { + function main(): void { + AnonymousClassConsumer\main(); + } +} +?> +--EXPECT-- +bool(true) diff --git a/tests/compiler/exception/destructor-wrapper-boundary.phpt b/tests/compiler/exception/destructor-wrapper-boundary.phpt new file mode 100644 index 00000000..e3f42d45 --- /dev/null +++ b/tests/compiler/exception/destructor-wrapper-boundary.phpt @@ -0,0 +1,28 @@ +--TEST-- +TypePHP destructor exceptions remain inside the Zend wrapper boundary +--FILE-- +getMessage(), "\n"; + } + + echo "continued\n"; +} +?> +--EXPECT-- +destructor +continued diff --git a/tests/compiler/exception/dynamic-callback-frame-restored.phpt b/tests/compiler/exception/dynamic-callback-frame-restored.phpt index 96819d59..2b79b3ee 100644 --- a/tests/compiler/exception/dynamic-callback-frame-restored.phpt +++ b/tests/compiler/exception/dynamic-callback-frame-restored.phpt @@ -16,6 +16,11 @@ final class CallbackFrameProbe } } +function fail_from_dynamic_function_callback(): void +{ + throw new DomainException('function'); +} + function callback_frame_is_stale(Throwable $exception, string $function): bool { foreach ($exception->getTrace() as $frame) { @@ -28,6 +33,16 @@ function callback_frame_is_stale(Throwable $exception, string $function): bool function main(): void { + try { + call_user_func('fail_from_dynamic_function_callback'); + } catch (DomainException $exception) { + } + try { + throw new RuntimeException('after function'); + } catch (RuntimeException $exception) { + echo 'function=', callback_frame_is_stale($exception, 'fail_from_dynamic_function_callback') ? 'stale' : 'clean', "\n"; + } + try { $copy = clone new CallbackFrameProbe(); } catch (DomainException $exception) { @@ -62,6 +77,7 @@ function main(): void } ?> --EXPECT-- +function=clean clone=clean array_map=clean reflection=clean diff --git a/tests/compiler/exception/shutdown-exception-handler-include.phpt b/tests/compiler/exception/shutdown-exception-handler-include.phpt new file mode 100644 index 00000000..18b96b31 --- /dev/null +++ b/tests/compiler/exception/shutdown-exception-handler-include.phpt @@ -0,0 +1,38 @@ +--TEST-- +An exception handler may include a PHP file during shutdown +--FILE-- +getMessage(), "\n"; + + $file = tempnam(sys_get_temp_dir(), 'typephp-shutdown-'); + file_put_contents($file, ' +--EXPECT-- +main completed +state:request alive +handled:shutdown failure +included during shutdown diff --git a/tests/compiler/include_require/include-error-handler-unwind.phpt b/tests/compiler/include_require/include-error-handler-unwind.phpt new file mode 100644 index 00000000..f02d605f --- /dev/null +++ b/tests/compiler/include_require/include-error-handler-unwind.phpt @@ -0,0 +1,28 @@ +--TEST-- +An exception from an error handler unwinds an included PHP frame safely +--FILE-- +getMessage(), 'undefinedIncludeVariable') ? "caught\n" : "wrong exception\n"; + } finally { + restore_error_handler(); + unlink($file); + } +} +?> +--EXPECT-- +caught