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