fix object throws and internal parent lookup

韩天峰 8 hours ago
parent b493ac79c5
commit 0da0109a2d
  1. 71
      phpunit/src/InternalParentExtensionCodegenTest.php
  2. 9
      src/Parser/ExceptionControlFlowTrait.php
  3. 21
      src/Translator.php
  4. 64
      tests/compiler/exception/throw-method-object-result.phpt
  5. 32
      tests/compiler/keyword_method/fluent-object-to-string.phpt

@ -0,0 +1,71 @@
<?php
use TypePhp\CompilerBase;
use TypePhp\CompilerTest;
final class InternalParentExtensionCodegenTest extends BaseTest
{
private string $projectDir;
protected function setUp(): void
{
parent::setUp();
$this->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'
<?php
class InternalParentChild extends ArrayObject
{
}
PHP);
global $translator;
$compiler = CompilerTest::create($this->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);
}
}

@ -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 . ')';

@ -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_class_entry *>(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<uint32_t>(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_')) . '"',
];
}

@ -0,0 +1,64 @@
--TEST--
throw accepts object-valued method call results
--FILE--
<?php
class ExceptionFactory
{
private function typedException(): LogicException
{
return new LogicException('typed');
}
private function objectException(): object
{
return new RuntimeException('object');
}
private function nonThrowable(): stdClass
{
return new stdClass();
}
public function throwTyped(): void
{
throw $this->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

@ -0,0 +1,32 @@
--TEST--
toString keyword converts fluent object results through __toString
--FILE--
<?php
final class FluentText
{
public function __construct(private string $value)
{
}
public function append(string ...$values): self
{
$this->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
Loading…
Cancel
Save