fix: compose trait declarations before conversion (fix gh-80)

master
韩天峰 2 days ago
parent d73291e11c
commit d1a9397f6c
  1. 13
      phpunit/code/trait-cross-file-call/a-caller.php
  2. 18
      phpunit/code/trait-cross-file-call/z-classes.php
  3. 11
      phpunit/code/trait-cross-file-call/zz-trait.php
  4. 66
      phpunit/src/TraitCrossFileCallTest.php
  5. 5
      src/Build/SourcePipelineTrait.php
  6. 2
      src/CompilerBase.php
  7. 2
      src/Entity/ConstantDef.php
  8. 92
      src/Preprocessor.php
  9. 107
      src/Translator.php
  10. 33
      tests/compiler/trait/trait-method-magic-fallback.phpt

@ -0,0 +1,13 @@
<?php
use CrossFileTrait\ClassB;
use CrossFileTrait\ClassC;
function callCrossFileTraitMethods(): void
{
$withMagic = new ClassB();
$withMagic->getAttribute('with-magic');
$withoutMagic = new ClassC();
$withoutMagic->getAttribute('without-magic');
}

@ -0,0 +1,18 @@
<?php
namespace CrossFileTrait;
class ClassB
{
use AttributeLookup;
public function __call(string $name, array $arguments): mixed
{
return 'magic:' . $name;
}
}
class ClassC
{
use AttributeLookup;
}

@ -0,0 +1,11 @@
<?php
namespace CrossFileTrait;
trait AttributeLookup
{
public function getAttribute(string $class = 'default'): string
{
return 'attribute:' . $class;
}
}

@ -0,0 +1,66 @@
<?php
/**
* This file is part of TypePHP(AOT).
*
* @link https://www.swoole.com/aot/
* @contact service@swoole.com
*/
use TypePhp\CompilerTest;
/**
* @internal
* @coversNothing
*/
final class TraitCrossFileCallTest extends BaseTest
{
public function testTraitMethodsAreResolvedBeforeTheConsumingClassFileIsConverted(): void
{
$compiler = CompilerTest::create(TYPEPHP_ROOT_PATH);
$directory = TYPEPHP_ROOT_PATH . '/phpunit/code/trait-cross-file-call/';
$caller = $directory . 'a-caller.php';
$classes = $directory . 'z-classes.php';
$trait = $directory . 'zz-trait.php';
// The caller is deliberately prepared and converted before the class
// file. Native method resolution must depend on the complete
// declaration graph, not source filenames or conversion order.
$files = [$caller, $classes, $trait];
$compiler->addFiles($files);
foreach ($files as $file) {
$compiler->prepareFile($file);
}
$classDef = $compiler->getClassDef('CrossFileTrait\\ClassB');
self::assertNotNull($classDef);
self::assertFalse($classDef->hasMethod('getAttribute'));
// Trait expansion is an explicit declaration phase after all files
// have been prepared and before any body is converted.
$compiler->composeTraitDeclarations($files);
self::assertTrue($classDef->hasMethod('getAttribute'));
$argument = $classDef->getMethod('getAttribute')->functionDef->argInfoList[0];
self::assertTrue($argument->hasDefaultValue());
self::assertSame('', $argument->default);
$generated = $compiler->convertFile($caller);
self::assertNotNull($generated);
$code = file_get_contents($generated);
self::assertIsString($code);
self::assertStringContainsString(
'php_crossfiletrait__classb__getattribute(',
$code,
);
self::assertStringContainsString(
'php_crossfiletrait__classc__getattribute(',
$code,
);
self::assertStringNotContainsString(
'php_crossfiletrait__classb____call(',
$code,
);
self::assertStringNotContainsString('.call(', $code);
self::assertNotSame('', $argument->default);
}
}

@ -154,6 +154,10 @@ trait SourcePipelineTrait
}
}
}
// Trait declarations can only be flattened after the complete source
// set has been prepared: a consuming class may precede its Trait file.
// Complete the declaration graph before any body is converted.
$this->composeTraitDeclarations(array_values($files));
// Global slots are shared by every translation unit. Fix any Native
// pointer ABI now, after declarations are known and before the first
// per-file C++ body is generated.
@ -253,6 +257,7 @@ trait SourcePipelineTrait
public function convert(array $files): array
{
$this->composeTraitDeclarations($files);
$previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT);
try {
// All declarations are now known. Lower declaration constant

@ -273,6 +273,7 @@ class CompilerBase implements PropertyAccessContext
public const string ENTRY_FUNCTION = 'main';
protected const string PHASE_IDLE = 'idle';
protected const string PHASE_PREPARE = 'prepare';
protected const string PHASE_COMPOSE = 'compose';
protected const string PHASE_CONVERT = 'convert';
protected string $lang = 'PHP';
@ -339,6 +340,7 @@ class CompilerBase implements PropertyAccessContext
protected int $propertyAccessCacheIndex = 0;
/** @var array<string, array<Node\Stmt>> Prepared declaration ASTs keyed by real path. */
protected array $preparedFileAsts = [];
protected bool $traitDeclarationsComposed = false;
protected bool $declarationExpressionsFinalized = false;
protected bool $methodOverrideFlagsFinalized = false;
protected const array PHP_RUNTIME_TYPE_MAP = [

@ -18,6 +18,8 @@ class ConstantDef
public string $value;
public string $arrayExpr = '';
public string $class = '';
/** Trait whose lexical namespace/import context owns this declaration. */
public string $traitOrigin = '';
public ?NodeAbstract $valueExpr = null;
/** True after the declaration AST has been lowered to C++ in convert. */
public bool $codegenFinalized = false;

@ -316,6 +316,7 @@ class Preprocessor extends CompilerBase
// constants are validated here, but their C++ expressions are not
// generated until the complete symbol table is available.
$this->preparedFileAsts[$this->file] = $stmts;
$this->traitDeclarationsComposed = false;
$this->declarationExpressionsFinalized = false;
// The prepared class graph changed; override flags must be
// re-finalized before the next conversion.
@ -351,6 +352,89 @@ class Preprocessor extends CompilerBase
}
}
/**
* Compose Trait declarations after every source file has been prepared.
*
* A Trait may be declared after the class that uses it, so composition
* cannot be performed safely by prepareFile(). This intermediate phase
* runs against the complete declaration graph and makes the composed
* method/property/constant signatures visible before any function body is
* converted. Expression lowering remains a convert-phase responsibility.
*
* @param list<string> $files
*/
public function composeTraitDeclarations(array $files): void
{
if ($this->traitDeclarationsComposed) {
return;
}
$previousPhase = $this->enterCompilerPhase(self::PHASE_COMPOSE);
try {
foreach ($files as $file) {
$path = realpath($file);
if ($path === false || !isset($this->preparedFileAsts[$path])) {
continue;
}
$this->loadFile($path);
$this->resetFile();
$this->resetFunction();
$this->resetMethod();
$this->resetClass();
$this->resetNamespace();
$this->composeTraitDeclarationStatementList($this->preparedFileAsts[$path]);
}
$this->traitDeclarationsComposed = true;
// Trait methods are real methods of their consuming classes and
// therefore change virtual-dispatch analysis.
$this->methodOverrideFlagsFinalized = false;
} finally {
$this->restoreCompilerPhase($previousPhase);
}
}
/** @param array<Node\Stmt> $statements */
private function composeTraitDeclarationStatementList(array $statements): void
{
foreach ($statements as $statement) {
if ($statement instanceof Node\Stmt\Namespace_) {
$this->resetClass();
$this->resetMethod();
$this->resetFunction();
$this->resetNamespace();
$this->namespace = $statement->name ? $this->parseIdentifier($statement->name) : '';
$this->composeTraitDeclarationStatementList($statement->stmts);
continue;
}
if ($statement instanceof Node\Stmt\Use_) {
$this->parseUse($statement);
continue;
}
if ($statement instanceof Node\Stmt\GroupUse) {
$this->parseGroupUse($statement);
continue;
}
if ($statement instanceof Node\Stmt\Class_
|| $statement instanceof Node\Stmt\Trait_
|| $statement instanceof Node\Stmt\Enum_
) {
$this->resetClass();
$this->class = $this->parseIdentifier($statement->name);
$this->classDef = $this->getClass($this->getFullClassName());
$this->composePreparedTraitDeclarations($statement);
}
}
}
/**
* Translator supplies Trait AST composition; the declaration collector
* owns the phase and source/namespace traversal.
*/
protected function composePreparedTraitDeclarations(
Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class,
): void {
}
protected function fatalPhpParserError(\PhpParser\Error $error): never
{
$location = $this->file;
@ -451,7 +535,7 @@ class Preprocessor extends CompilerBase
}
}
private function finalizeClassDeclarationExpressions(
protected function finalizeClassDeclarationExpressions(
Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class,
): void {
$this->resetClass();
@ -528,7 +612,7 @@ class Preprocessor extends CompilerBase
$this->interfaceDef = null;
}
private function finalizePreparedFunctionDefaults(
protected function finalizePreparedFunctionDefaults(
Node\Stmt\Function_|Node\Stmt\ClassMethod $function,
FunctionDef $functionDef,
): void {
@ -546,7 +630,7 @@ class Preprocessor extends CompilerBase
}
}
private function finalizePreparedProperty(PropertyDef $property, Node\Expr $expression): void
protected function finalizePreparedProperty(PropertyDef $property, Node\Expr $expression): void
{
$this->resetFunction();
$property->arrayInitPlan = null;
@ -558,7 +642,7 @@ class Preprocessor extends CompilerBase
}
}
private function finalizePreparedConstant(ConstantDef $constant, Node\Expr $expression): void
protected function finalizePreparedConstant(ConstantDef $constant, Node\Expr $expression): void
{
$this->resetFunction();
$constant->arrayExpr = '';

@ -580,6 +580,10 @@ class Translator extends Preprocessor
public function convertFile(string $file): ?string
{
// Public embedding/test callers may prepare files directly instead of
// using SourcePipelineTrait::prepare(). Preserve the same explicit
// prepare -> Trait composition -> convert ordering for that API.
$this->composeTraitDeclarations(array_keys($this->preparedFileAsts));
$previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT);
try {
if (!$this->declarationExpressionsFinalized) {
@ -3437,6 +3441,9 @@ CODE;
$traitMethods[$methodName] = [$traitFullName, $traitStmt];
}
if ($traitStmt instanceof Node\Stmt\ClassConst) {
if ($traitStmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE) === null) {
$traitStmt->setAttribute(self::TRAIT_ORIGIN_ATTRIBUTE, $traitFullName);
}
foreach ($traitStmt->consts as $k2 => $const) {
$constName = strtolower($const->name->toString());
if (isset($constants[$constName])) {
@ -4344,6 +4351,98 @@ CODE;
return $code;
}
/**
* Inject declarations received through Trait composition during the
* explicit phase between prepare and convert. At this point every Trait
* AST is known, while expression lowering and method-body generation are
* still forbidden.
*/
protected function composePreparedTraitDeclarations(
Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class,
): void {
if ($class instanceof Node\Stmt\Trait_ || $this->classDef->usedTraits === []) {
return;
}
/** @var Node\Stmt\Class_|Node\Stmt\Enum_ $composedClass */
$composedClass = $this->cloneAstNode($class);
$this->composeTraitAst(
$composedClass,
new Node\Name($this->classDef->getNamespacedName(false)),
);
$this->installComposedTraitDataMembers($composedClass);
foreach ($composedClass->stmts as $statement) {
if (!$statement instanceof Node\Stmt\ClassMethod
|| !is_string($statement->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE))) {
continue;
}
$origin = (string) $statement->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE);
$this->withTraitNameContext($origin, function () use ($statement): void {
$this->installComposedTraitMethod($statement);
});
}
$this->resetMethod();
$this->resetFunction();
}
/**
* Lower only the declaration expressions belonging to members injected in
* the preceding Trait-composition phase. Their signatures already exist in
* ClassDef; this convert-phase pass does not add declarations or bodies.
*/
protected function finalizeClassDeclarationExpressions(
Node\Stmt\Class_|Node\Stmt\Trait_|Node\Stmt\Enum_ $class,
): void {
parent::finalizeClassDeclarationExpressions($class);
if ($class instanceof Node\Stmt\Trait_ || $this->classDef->usedTraits === []) {
return;
}
foreach ($this->classDef->constants as $constant) {
if ($constant->codegenFinalized
|| $constant->traitOrigin === ''
|| !$constant->valueExpr instanceof Node\Expr
) {
continue;
}
$this->withTraitNameContext($constant->traitOrigin, function () use ($constant): void {
$this->finalizePreparedConstant($constant, $constant->valueExpr);
});
}
foreach ($this->classDef->properties as $property) {
$origin = $property->node?->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE);
if (!is_string($origin) || $origin === '' || !$property->defaultExpr instanceof Node\Expr) {
continue;
}
$this->withTraitNameContext($origin, function () use ($property): void {
$this->finalizePreparedProperty($property, $property->defaultExpr);
});
}
foreach ([$this->classDef->methods, $this->classDef->abstractMethodDefs] as $methods) {
foreach ($methods as $method) {
if ($method->traitOrigin === ''
|| !$method->node instanceof Node\Stmt\ClassMethod
|| $method->functionDef === null
) {
continue;
}
$this->method = $method->name;
$this->methodDef = $method;
$this->withTraitNameContext($method->traitOrigin, function () use ($method): void {
$this->finalizePreparedFunctionDefaults($method->node, $method->functionDef);
});
}
}
$this->resetMethod();
$this->resetFunction();
}
protected function genNativeMethod(array $methodCodes): string
{
$code = '';
@ -6522,6 +6621,12 @@ CODE;
foreach ($stmt->consts as $const) {
if (!$this->classDef->hasConstant($const->name->toString())) {
$this->parseClassConstDef($stmt);
$origin = $stmt->getAttribute(self::TRAIT_ORIGIN_ATTRIBUTE);
if (is_string($origin)) {
foreach ($stmt->consts as $installedConst) {
$this->classDef->getConstant($installedConst->name->toString())->traitOrigin = $origin;
}
}
break;
}
}
@ -6547,7 +6652,7 @@ CODE;
{
$name = $methodStmt->name->toString();
$this->assertNativeMagicMethodSupported($methodStmt, $name);
if ($this->classDef->hasMethod($name)) {
if ($this->classDef->hasMethod($name) || $this->classDef->hasAbstractMethod($name)) {
return;
}

@ -0,0 +1,33 @@
--TEST--
Trait methods take precedence over __call magic fallback
--FILE--
<?php
trait AttributeLookup
{
public function getAttribute(string $class): string
{
return 'attribute:' . $class;
}
}
class TraitMethodConsumer
{
use AttributeLookup;
public function __call(string $name, array $arguments): mixed
{
return 'magic:' . $name;
}
}
function main(): void
{
$object = new TraitMethodConsumer();
var_dump($object->getAttribute('SomeClass'));
var_dump($object->missingMethod());
}
?>
--EXPECT--
string(19) "attribute:SomeClass"
string(19) "magic:missingMethod"
Loading…
Cancel
Save