fix(preprocessor): finalize method override flags before conversion (#42)
The classMethodOverride registration in prepareClassMethod() depends on file preprocessing order. With a "sandwich" order (ancestor first, leaf second, intermediate class last), the ancestor method's override flag is missed: the upward marking cannot cross the not-yet-registered intermediate class, and the downward subclass lookup ran before the leaf was prepared. findNativeMethod() then devirtualizes the late-bound call into a direct native call, silently ignoring the override. In Hyperf this turns SoftDeletes::delete() into a physical DELETE. Finalize the flags once the complete class graph is known: finalizeMethodOverrideFlags() walks every declared method's complete parent chain and marks each existing ancestor method of the same name as overridden (method count x inheritance depth). It runs from Translator::convertFile() next to finalizeDeclarationExpressions(), so both the project pipeline and the public prepareFile()/convertFile() API share the same pre-conversion finalization, guarded by a dirty/finalized flag reset in prepareFile(). Tests: - tests/compiler/devirtualize/override-order-sandwich.phpt (fails on master with "base", passes with "leaf") - tests/compiler/devirtualize/override-order-normal.phpt (control) - phpunit/src/DevirtualizeOrderTest.php driving the public prepareFile()/convertFile() API in both orders, asserting the generated Base::delete() body dispatches dynamicallymaster
parent
d66f3d7e10
commit
8c0d52d330
9 changed files with 209 additions and 0 deletions
@ -0,0 +1,16 @@ |
||||
<?php |
||||
|
||||
namespace OrderTest; |
||||
|
||||
class Base |
||||
{ |
||||
protected function perform(): string |
||||
{ |
||||
return 'base'; |
||||
} |
||||
|
||||
public function delete(): string |
||||
{ |
||||
return $this->perform(); |
||||
} |
||||
} |
||||
@ -0,0 +1,11 @@ |
||||
<?php |
||||
|
||||
namespace OrderTest; |
||||
|
||||
class Leaf extends Mid |
||||
{ |
||||
protected function perform(): string |
||||
{ |
||||
return 'leaf'; |
||||
} |
||||
} |
||||
@ -0,0 +1,7 @@ |
||||
<?php |
||||
|
||||
namespace OrderTest; |
||||
|
||||
class Mid extends Base |
||||
{ |
||||
} |
||||
@ -0,0 +1,65 @@ |
||||
<?php |
||||
/** |
||||
* This file is part of TypePHP(AOT). |
||||
* |
||||
* @link https://www.swoole.com/aot/ |
||||
* @contact service@swoole.com |
||||
*/ |
||||
|
||||
namespace TypePhp\Tests; |
||||
|
||||
use PHPUnit\Framework\TestCase; |
||||
use TypePhp\CompilerTest; |
||||
|
||||
/** |
||||
* @internal |
||||
* @coversNothing |
||||
*/ |
||||
class DevirtualizeOrderTest extends TestCase |
||||
{ |
||||
public function testSandwichDeclarationOrderKeepsOverrideDynamic(): void |
||||
{ |
||||
// Ancestor first, leaf second, intermediate class last: the ancestor |
||||
// method's override flag used to be missed, and the late-bound call |
||||
// in Base::delete() was wrongly devirtualized to a direct native call. |
||||
$cpp = $this->compileBaseInOrder(['base.php', 'leaf.php', 'mid.php']); |
||||
$this->assertDeleteDispatchesDynamically($cpp); |
||||
} |
||||
|
||||
public function testNormalDeclarationOrderKeepsOverrideDynamic(): void |
||||
{ |
||||
// Control: ancestor, intermediate, leaf. |
||||
$cpp = $this->compileBaseInOrder(['base.php', 'mid.php', 'leaf.php']); |
||||
$this->assertDeleteDispatchesDynamically($cpp); |
||||
} |
||||
|
||||
/** @param list<string> $order */ |
||||
private function compileBaseInOrder(array $order): string |
||||
{ |
||||
global $translator; |
||||
|
||||
$dir = TYPEPHP_ROOT_PATH . '/phpunit/code/devirtualize-order'; |
||||
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH); |
||||
$translator = $compiler; |
||||
foreach ($order as $file) { |
||||
$compiler->addFiles([$dir . '/' . $file]); |
||||
$compiler->prepareFile($dir . '/' . $file); |
||||
} |
||||
|
||||
return file_get_contents($compiler->convertFile($dir . '/base.php')); |
||||
} |
||||
|
||||
private function assertDeleteDispatchesDynamically(string $cpp): void |
||||
{ |
||||
$matched = preg_match( |
||||
'/php::\w+ php_ordertest__base__delete\(php::Object &this_\) \{(?<body>.*?)\n\}/s', |
||||
$cpp, |
||||
$m, |
||||
); |
||||
self::assertSame(1, $matched, 'generated body of OrderTest\\Base::delete() not found'); |
||||
// A direct native call to the base implementation means the override |
||||
// was wrongly devirtualized; the call must go through dynamic dispatch. |
||||
self::assertStringNotContainsString('php_ordertest__base__perform', $m['body']); |
||||
self::assertStringContainsString('callScoped', $m['body']); |
||||
} |
||||
} |
||||
@ -0,0 +1,31 @@ |
||||
--TEST-- |
||||
Devirtualize: override flag with normal declaration order (ancestor, intermediate, leaf) - control |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class OrderedBase { |
||||
protected function perform(): string { |
||||
return "base"; |
||||
} |
||||
|
||||
public function delete(): string { |
||||
return $this->perform(); |
||||
} |
||||
} |
||||
|
||||
class OrderedMid extends OrderedBase { |
||||
} |
||||
|
||||
class OrderedLeaf extends OrderedMid { |
||||
protected function perform(): string { |
||||
return "leaf"; |
||||
} |
||||
} |
||||
|
||||
function main() { |
||||
var_dump((new OrderedLeaf())->delete()); |
||||
} |
||||
|
||||
?> |
||||
--EXPECT-- |
||||
string(4) "leaf" |
||||
@ -0,0 +1,34 @@ |
||||
--TEST-- |
||||
Devirtualize: override flag survives sandwich declaration order (ancestor, leaf, intermediate) |
||||
--FILE-- |
||||
<?php |
||||
|
||||
class SandwichBase { |
||||
protected function perform(): string { |
||||
return "base"; |
||||
} |
||||
|
||||
public function delete(): string { |
||||
return $this->perform(); |
||||
} |
||||
} |
||||
|
||||
// Leaf is declared before its parent SandwichMid: the override flag of |
||||
// SandwichBase::perform() must still be registered, so the late-bound call |
||||
// in delete() stays dynamic. |
||||
class SandwichLeaf extends SandwichMid { |
||||
protected function perform(): string { |
||||
return "leaf"; |
||||
} |
||||
} |
||||
|
||||
class SandwichMid extends SandwichBase { |
||||
} |
||||
|
||||
function main() { |
||||
var_dump((new SandwichLeaf())->delete()); |
||||
} |
||||
|
||||
?> |
||||
--EXPECT-- |
||||
string(4) "leaf" |
||||
Loading…
Reference in new issue