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
pull/48/head
韩天峰 2 weeks ago
parent 5fb3fdf490
commit a79610fb2c
  1. 2
      composer.json
  2. 8
      phpunit/code/unsupported-function-extract-qualified.php
  3. 6
      phpunit/code/unsupported-function-extract.php
  4. 14
      phpunit/src/UnsupportedFunctionTest.php
  5. 17
      src/CompilerBase.php
  6. 28
      src/Generator/AnonClassGenerator.php
  7. 4
      src/Parser/FunctionCallTrait.php
  8. 49
      tests/compiler/anon_class/005.phpt
  9. 28
      tests/compiler/exception/destructor-wrapper-boundary.phpt
  10. 16
      tests/compiler/exception/dynamic-callback-frame-restored.phpt
  11. 38
      tests/compiler/exception/shutdown-exception-handler-include.phpt
  12. 28
      tests/compiler/include_require/include-error-handler-unwind.phpt

@ -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": {

@ -0,0 +1,8 @@
<?php
namespace App;
function importVariables(array $values): void
{
\extract($values);
}

@ -0,0 +1,6 @@
<?php
function importVariables(array $values): void
{
extract($values);
}

@ -0,0 +1,14 @@
<?php
class UnsupportedFunctionTest extends BaseTest
{
public function testExtractIsRejectedAtCompileTime(): void
{
$this->exec('Unsupported function: `extract`', 'unsupported-function-extract.php');
}
public function testFullyQualifiedExtractIsRejectedAtCompileTime(): void
{
$this->exec('Unsupported function: `extract`', 'unsupported-function-extract-qualified.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)";
}

@ -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) {

@ -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) {

@ -0,0 +1,49 @@
--TEST--
Anonymous class method bodies preserve namespace imports
--FILE--
<?php
namespace AnonymousClassSupport {
const FLAG = 'imported';
class Subject {}
class Marker {
public const string VALUE = 'resolved';
}
function accepts(Subject $value): bool {
return true;
}
}
namespace AnonymousClassConsumer {
use AnonymousClassSupport\Marker as ImportedMarker;
use AnonymousClassSupport\Subject as ImportedSubject;
use const AnonymousClassSupport\FLAG as IMPORTED_FLAG;
use function AnonymousClassSupport\accepts as imported_accepts;
function main(): void {
$visitor = new class('ready') {
public function __construct(private readonly string $state) {}
public function accepts(object $value): bool {
return $value instanceof ImportedSubject
&& ImportedMarker::VALUE === 'resolved'
&& IMPORTED_FLAG === 'imported'
&& imported_accepts($value)
&& $this->state === 'ready';
}
};
var_dump($visitor->accepts(new ImportedSubject()));
}
}
namespace {
function main(): void {
AnonymousClassConsumer\main();
}
}
?>
--EXPECT--
bool(true)

@ -0,0 +1,28 @@
--TEST--
TypePHP destructor exceptions remain inside the Zend wrapper boundary
--FILE--
<?php
final class ThrowingDestructor
{
public function __destruct()
{
throw new RuntimeException('destructor');
}
}
function main(): void
{
try {
$value = new ThrowingDestructor();
unset($value);
} catch (RuntimeException $exception) {
echo $exception->getMessage(), "\n";
}
echo "continued\n";
}
?>
--EXPECT--
destructor
continued

@ -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

@ -0,0 +1,38 @@
--TEST--
An exception handler may include a PHP file during shutdown
--FILE--
<?php
function handleShutdownException(Throwable $exception): void
{
global $shutdownState;
echo 'state:', $shutdownState, "\n";
echo 'handled:', $exception->getMessage(), "\n";
$file = tempnam(sys_get_temp_dir(), 'typephp-shutdown-');
file_put_contents($file, '<?php echo "included during shutdown\\n";');
include $file;
unlink($file);
}
function throwDuringShutdown(): void
{
throw new RuntimeException('shutdown failure');
}
function main(): void
{
global $shutdownState;
$shutdownState = 'request alive';
set_exception_handler('handleShutdownException');
register_shutdown_function('throwDuringShutdown');
echo "main completed\n";
}
?>
--EXPECT--
main completed
state:request alive
handled:shutdown failure
included during shutdown

@ -0,0 +1,28 @@
--TEST--
An exception from an error handler unwinds an included PHP frame safely
--FILE--
<?php
function throwIncludeWarning(int $severity, string $message, string $file, int $line): never
{
throw new ErrorException($message, 0, $severity, $file, $line);
}
function main(): void
{
$file = tempnam(sys_get_temp_dir(), 'typephp-include-unwind-');
file_put_contents($file, '<?php echo $undefinedIncludeVariable;');
set_error_handler('throwIncludeWarning');
try {
include $file;
} catch (ErrorException $exception) {
echo str_contains($exception->getMessage(), 'undefinedIncludeVariable') ? "caught\n" : "wrong exception\n";
} finally {
restore_error_handler();
unlink($file);
}
}
?>
--EXPECT--
caught
Loading…
Cancel
Save