fix(preprocessor): recompute method override flags after all files prepared - #42
Conversation
|
Thank you for the detailed analysis and the real-world reproduction. The root cause is correct, and I verified that this patch fixes the normal project/CLI pipeline. There is still one compatibility gap that needs to be addressed before merging: The public
|
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 dynamically
d7e207b to
ae3d936
Compare
|
Thanks for the thorough review — all three points are addressed in the updated commit: Shared finalization path
It follows the existing dirty/finalized pattern: AlgorithmReplaced the descendant scan with your suggested upward walk: for every declared method key, walk its complete parent chain and mark each existing ancestor method of the same name as overridden. Order-independent, ~ method count × inheritance depth. Tests
Verified: on master both PHPUnit sandwich assertions and the sandwich PHPT fail (the generated |
Problem
Preprocessor::prepareClassMethod()registers method-override flags in two directions, but neither covers the "sandwich" preprocessing order (ancestor → leaf → intermediate class):symbols->parent()and marks already-registered ancestor methods as overridden — but theisset()guard only reaches already-prepared ancestors, and an unregistered link in the parent chain terminates the loop early.isMethodOverriddenInSubClasses()): when an ancestor is prepared, it scans subclasses — but only sees already-prepared subclasses.With source order
Base → Leaf → Mid:Baseprepared (1st): downward lookup,Leafnot processed yet → flag staysfalseLeafprepared (2nd): walks up,symbols->parent('app\mid')not registered yet (Midis 3rd) → chain breaks after one step, never reachesBaseMidprepared (3rd): only marks upward for its own declared methods;performis not among them →Base's flag staysfalseforeverLater,
MethodCallTrait::findNativeMethod()queriesisOverrideMethod('app\base::perform'), getsfalse, and devirtualizes the call into a direct native call — subclass overrides never execute.Minimal reproduction
Four files; the
sourcesorder is part of the trigger (Leaf before Mid):Actual: prints
base—Leaf::perform()is ignored. In the generated C++, theperform()call insideBase::delete()is devirtualized to a direct native call (php_app__base__perform(this_)).Expected (Zend parity): prints
leaf.Control experiment (both verified): swapping only the order of b/c flips the result —
a → b → c❌ printsbase,a → c → b✅ printsleaf.Real-world impact
Hit in a real Hyperf project, and it is a data-correctness bug:
Hyperf\Database\Model\SoftDeletes::performDeleteOnModel()overrides the same-named baseModelmethod to implement soft deletes. After the faulty devirtualization,delete()calls the base implementation directly, so soft-deletable models are physically deleted (delete from ... where id = ?instead ofupdate deleted_at). The same pattern exists inHyperf\ModelCache\Cacheable(overridesquery()/newQuery()/increment()etc., silently disabling the cache) andHyperf\DbConnection\Traits\HasContainer(overridesgetConnection()).In the real project the trigger order came from directory alphabetical order:
app/Model/Branch.php(B) is prepared beforeapp/Model/Model.php(M), while the vendor ancestor classes precede the wholeapp/tree — exactly the sandwich.Fix
After all files are prepared and before conversion starts, recompute the override flags once against the complete inheritance graph (idempotent, +29 lines). With the patch, the reproduction prints
leaf, and the full Hyperf project passes end-to-end smoke tests (soft delete writesdeleted_atcorrectly; login/list endpoints all OK).Environment