From 0da0109a2d7d2bb5bdaed6b87ade8a119402bd68 Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Sun, 30 Aug 2026 11:10:34 +0800 Subject: [PATCH] fix object throws and internal parent lookup --- .../InternalParentExtensionCodegenTest.php | 71 +++++++++++++++++++ src/Parser/ExceptionControlFlowTrait.php | 9 ++- src/Translator.php | 21 +++++- .../exception/throw-method-object-result.phpt | 64 +++++++++++++++++ .../fluent-object-to-string.phpt | 32 +++++++++ 5 files changed, 194 insertions(+), 3 deletions(-) create mode 100644 phpunit/src/InternalParentExtensionCodegenTest.php create mode 100644 tests/compiler/exception/throw-method-object-result.phpt create mode 100644 tests/compiler/keyword_method/fluent-object-to-string.phpt diff --git a/phpunit/src/InternalParentExtensionCodegenTest.php b/phpunit/src/InternalParentExtensionCodegenTest.php new file mode 100644 index 00000000..25d9a1e5 --- /dev/null +++ b/phpunit/src/InternalParentExtensionCodegenTest.php @@ -0,0 +1,71 @@ +projectDir = sys_get_temp_dir() . '/typephp_internal_parent_' . bin2hex(random_bytes(6)); + mkdir($this->projectDir, 0777, true); + } + + protected function tearDown(): void + { + $this->removeDirectory($this->projectDir); + parent::tearDown(); + } + + public function testExtensionResolvesInternalParentFromCompilerClassTable(): void + { + $source = $this->projectDir . '/internal-parent.php'; + file_put_contents($source, <<<'PHP' +projectDir); + $translator = $compiler; + $compiler->setBuildMode(CompilerBase::BUILD_MODE_EXT); + $compiler->setTargetName('internal_parent'); + $compiler->addFiles([$source]); + $compiler->prepareFile($source); + $compiler->convertFile($source); + + $extension = file_get_contents($compiler->genExtension()); + + self::assertStringContainsString( + 'php_class_entry_ArrayObject = get_internal_class("ArrayObject");', + $extension, + ); + self::assertStringNotContainsString( + 'php_class_entry_ArrayObject = php::getClassEntrySafe("ArrayObject");', + $extension, + ); + } + + private function removeDirectory(string $directory): void + { + if (!is_dir($directory)) { + return; + } + + foreach (array_diff(scandir($directory), ['.', '..']) as $entry) { + $path = $directory . '/' . $entry; + if (is_dir($path)) { + $this->removeDirectory($path); + } else { + unlink($path); + } + } + rmdir($directory); + } +} diff --git a/src/Parser/ExceptionControlFlowTrait.php b/src/Parser/ExceptionControlFlowTrait.php index ec938a72..b947c716 100644 --- a/src/Parser/ExceptionControlFlowTrait.php +++ b/src/Parser/ExceptionControlFlowTrait.php @@ -22,7 +22,8 @@ trait ExceptionControlFlowTrait if ($this->method === '__destruct') { $this->warning($expr, "Throwing exception in {$this->getFullClassName()}::__destruct() may cause memory leak"); } - if ($this->isNativeObjectClass($this->detectClassOfExpr($expr->expr))) { + $class = $this->detectDeclaredClassOfExpr($expr->expr); + if ($this->isNativeObjectClass($class)) { $this->fatalError($expr, 'Native objects cannot be thrown as Zend exceptions'); } $type = $this->detectTypeOfExpr($expr->expr); @@ -37,7 +38,11 @@ trait ExceptionControlFlowTrait } else { $ex = $this->parseExpr($expr->expr); } - if ($type != Type::VAR) { + // A method call with a class return declaration is represented by a + // php::Variant on the dynamic path, but it is still statically known + // to be an object. Let throwValue() preserve that runtime value and + // perform Zend's ordinary Throwable validation. + if ($type !== Type::VAR && $type !== Type::OBJECT && $class === '') { $this->fatalError($expr, 'Can only throw objects'); } return 'php::throwValue(' . $ex . ')'; diff --git a/src/Translator.php b/src/Translator.php index 57fb2b8a..e1ee8dfe 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -914,6 +914,21 @@ zend_class_entry *get_class(RequestClassId class_id, const php::Str &class_name) return php_class_map[index]; } +zend_class_entry *get_internal_class(const php::Str &class_name) { + // MINIT-only lookup. Internal classes and classes supplied by extension + // dependencies already live in CG(class_table), while EG(class_table) is + // not initialized yet on PHP 8.4. Never use this path for a PHP-script + // class: those classes are loaded at call time and belong in the + // RequestClassId cache, which is cleared at request shutdown. + zend_string *lcname = zend_string_tolower_ex(class_name.str(), true); + auto *ce = static_cast(zend_hash_find_ptr(CG(class_table), lcname)); + zend_string_release_ex(lcname, true); + if (UNEXPECTED(ce == nullptr)) { + php::throwError("class '%s' is undefined", class_name.data()); + } + return ce; +} + zend_function *get_func(RequestFuncId func_id, const php::Str &func_name) { const auto index = static_cast(func_id); if (UNEXPECTED(php_func_map[index] == nullptr)) { @@ -2792,8 +2807,12 @@ CODE; protected function getInternalCeInfo(string $ce): array { + // This metadata is consumed only by genClassPropertyInit() in MINIT to + // register compiled classes against internal parents/interfaces. It is + // deliberately separate from both persistentClassMap (module-lifetime + // lazy call-site cache) and classMap (request-lifetime dynamic cache). return [ - 'func' => Symbol::getClassEntrySafe(), + 'func' => 'get_internal_class', 'args' => '"' . substr($ce, strlen(self::PREFIX . 'class_entry_')) . '"', ]; } diff --git a/tests/compiler/exception/throw-method-object-result.phpt b/tests/compiler/exception/throw-method-object-result.phpt new file mode 100644 index 00000000..c74296b2 --- /dev/null +++ b/tests/compiler/exception/throw-method-object-result.phpt @@ -0,0 +1,64 @@ +--TEST-- +throw accepts object-valued method call results +--FILE-- +typedException(); + } + + public function throwObject(): void + { + throw $this->objectException(); + } + + public function throwNonThrowable(): void + { + throw $this->nonThrowable(); + } +} + +function main(): void +{ + $factory = new ExceptionFactory(); + try { + $factory->throwTyped(); + } catch (Throwable $e) { + echo get_class($e), ':', $e->getMessage(), "\n"; + } + + try { + $factory->throwObject(); + } catch (Throwable $e) { + echo get_class($e), ':', $e->getMessage(), "\n"; + } + + try { + $factory->throwNonThrowable(); + } catch (Error $e) { + echo $e->getMessage(), "\n"; + } +} +?> +--EXPECT-- +LogicException:typed +RuntimeException:object +Cannot throw objects that do not implement Throwable diff --git a/tests/compiler/keyword_method/fluent-object-to-string.phpt b/tests/compiler/keyword_method/fluent-object-to-string.phpt new file mode 100644 index 00000000..dbcbdd08 --- /dev/null +++ b/tests/compiler/keyword_method/fluent-object-to-string.phpt @@ -0,0 +1,32 @@ +--TEST-- +toString keyword converts fluent object results through __toString +--FILE-- +value .= implode('', $values); + return $this; + } + + public function __toString(): string + { + return $this->value; + } +} + +function main(): void +{ + $start = new FluentText('start'); + $end = new FluentText('end'); + echo $start->append(':', $end->toString())->toString(), "\n"; +} +?> +--EXPECT-- +start:end