diff --git a/docs/INCOMPATIBLE_PHP_FEATURES.md b/docs/INCOMPATIBLE_PHP_FEATURES.md index cf4ff0ad..c8fd4153 100644 --- a/docs/INCOMPATIBLE_PHP_FEATURES.md +++ b/docs/INCOMPATIBLE_PHP_FEATURES.md @@ -34,6 +34,7 @@ ## 调用与引用 - TypePHP 使用严格参数数量规则:非 variadic 函数不接受声明范围之外的额外参数;`func_get_args()` 不会隐式放宽签名。 +- 已知签名的普通函数、普通方法和 native 直调支持引用参数及写回;不要把编译器内部跨 Trait 动态分派的限制误写成“TypePHP 不支持引用参数”。 - 闭包和箭头函数不支持引用参数。 - 引用赋值不支持从复杂静态属性表达式建立引用。 - 动态调用、闭包调用等编译期无法确定参数签名的调用,不能自动转换引用参数;需要显式使用 `refval()` 或等价关键词方法 `toRef()`。 @@ -66,3 +67,14 @@ - `Closure::bind()` 绑定静态闭包访问私有成员时,当前行为与标准 PHP 不完全一致。 - first-class callable 存入 typed nullable `Closure` 属性后,当前存在运行时稳定性限制。 - 所有源文件必须是 `UTF-8` 编码。 + +## 编译器自举与内部重构约束 + +本节描述编译器自身使用 TypePHP 编译时的约束,不是面向用户代码新增的 PHP 语义差异。 + +- 重构前,同一核心类内可静态解析的 `$this->method()` 会生成 native C++ 直调。引用参数会直接映射为 `php::Ref` 或 C++ 引用,写回语义正常。 +- 将调用方和被调用方拆到不同 Trait 后,单独编译 Trait 本体时无法从 Trait 的 `$this` 确定最终宿主类。当前方法解析器可能将跨 Trait 调用降级为 Zend method call,例如生成 `this_.call(..., php::ArgList{value})`。 +- 动态 method call 的 `ArgList` 不会仅凭被调 wrapper 的 arginfo 自动把普通实参升级为引用。若被调方法声明 `&$value`,wrapper 会通过 `getCallArgByRef()` 取参;调用方传入的却是普通值,结果是 `must be passed by reference` 警告,并且被调方修改无法写回调用方。 +- 因此,编译器内部跨 Trait API 禁止使用引用输出参数和“修改传入标量/数组后由调用方读取”的协议。应返回结果值、元组数组或 DTO,例如用 `[$type, $class] = resolveTypeDecl(...)` 代替 `parseTypeDecl(..., &$class)`。 +- 对字符串累加、数组排序、解析结果输出等内部 helper,优先设计为纯返回值:`$code .= format(...)`、`$files = sort(...)`。只有确认调用会保持 native 直调时,才允许依赖引用写回。 +- 每次移动方法到 Trait、父类或独立组件后,必须使用自举产物重新编译至少一个覆盖该调用的测试;仅使用 `bin/tpc.php` 运行测试不能发现“源编译器正常、自举编译器退化”的问题。 diff --git a/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md b/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md index 5174ee61..ffd858af 100644 --- a/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md +++ b/docs/PHP_INCOMPATIBILITY_CLASSIFICATION.md @@ -112,6 +112,34 @@ These items should be documented with the exact boundary. | Native typed properties | Partial / Intentional Rule | Fast native paths may not preserve every PHP dynamic state transition. Unknown or incompatible values can fall back to `setProperty()`. | | Reflection metadata | Partial | Runtime declarations exist, but some AOT-specific metadata such as promoted-property flags may be incomplete. | +## Self-hosting Compatibility Notes + +The compiler itself is a TypePHP program, so an internal refactor can change +how its own calls are lowered even when the PHP source-level API is unchanged. +This is an implementation compatibility boundary, not a new user-facing rule +that reference parameters are unsupported. + +| Internal pattern | Status | Boundary and required design | +|---|---|---| +| Statically resolved function or method with by-reference parameters | Supported | Native direct calls preserve reference slots and write-back semantics. This was the path used before the core methods were split into traits. | +| Cross-trait `$this->method()` with a by-reference output parameter | Self-hosting Partial | While compiling a trait body, the final consuming class may be unknown. The call can fall back to `this_.call()` with ordinary `ArgList` values, while the callee wrapper expects `getCallArgByRef()`. This produces a by-reference warning and loses write-back. | +| Cross-trait helper returning a value, tuple array or DTO | Supported / Required internally | Return data explicitly and assign it at the call site. Do not use by-reference output parameters for compiler services that may cross trait boundaries. | +| Moving an existing method into a trait | Requires bootstrap verification | Test both the PHP-source compiler and the newly bootstrapped `tpc`; source-compiler tests alone do not exercise the changed lowering path. | + +The observed regression after refactoring followed this exact sequence: + +1. Before extraction, calls such as file sorting, captured-statement appending and + type declaration parsing were statically resolved inside the core class. +2. After extraction, their callers and implementations lived in different + traits mixed into `Translator`, `CompilerBase` or `Preprocessor`. +3. The self-hosted compiler emitted Zend dynamic method calls for those + cross-trait edges and passed ordinary values. +4. Callee wrappers still correctly advertised and parsed reference parameters, + but they could not retroactively turn the caller's value argument into the + caller's reference slot. +5. The fixes replaced internal output-parameter protocols with explicit return + values. User-level statically known reference calls remain supported. + ## Documentation Rule When documenting a compatibility difference, use one of these labels: diff --git a/phpunit/src/PreprocessorTest.php b/phpunit/src/PreprocessorTest.php index cf25e4c7..140b6bfb 100644 --- a/phpunit/src/PreprocessorTest.php +++ b/phpunit/src/PreprocessorTest.php @@ -239,25 +239,24 @@ class PreprocessorTest extends TestCase } // ======================================================================== - // sortFiles + // getSortedFiles // ======================================================================== public function testSortFilesPreservesOrderForUnrelatedFiles(): void { $files = ['/a/file1.php', '/a/file2.php', '/a/file3.php']; - $this->compiler->sortFiles($files); + $files = $this->invokeMethod('getSortedFiles', $files); // All original files must still be present $this->assertContains('/a/file1.php', $files); $this->assertContains('/a/file2.php', $files); $this->assertContains('/a/file3.php', $files); - // Original files are preserved (sortFiles may append, not remove) + // Original files are preserved (sorting may append, not remove) $this->assertGreaterThanOrEqual(3, count($files)); } public function testSortFilesEmpty(): void { - $files = []; - $this->compiler->sortFiles($files); + $files = $this->invokeMethod('getSortedFiles', []); // Empty array stays empty or nearly empty $this->assertIsArray($files); } @@ -334,7 +333,7 @@ class PreprocessorTest extends TestCase $this->compiler->prepareFile($traitFile); $files = [$classFile, $interfaceFile, $traitUserFile, $traitFile]; - $this->compiler->sortFiles($files); + $files = $this->invokeMethod('getSortedFiles', $files); $this->assertLessThan(array_search($classFile, $files, true), array_search($interfaceFile, $files, true)); $this->assertLessThan(array_search($traitUserFile, $files, true), array_search($traitFile, $files, true)); diff --git a/src/Build/SourcePipelineTrait.php b/src/Build/SourcePipelineTrait.php index eedeb34b..20f5dac5 100644 --- a/src/Build/SourcePipelineTrait.php +++ b/src/Build/SourcePipelineTrait.php @@ -99,7 +99,7 @@ trait SourcePipelineTrait } } } - $this->sortFiles($files); + $files = $this->getSortedFiles($files); return $files; } diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 4d3df31f..75b21849 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -1434,13 +1434,6 @@ class CompilerBase implements PropertyAccessContext throw new \LogicException('Parsed expression must be stringable'); } - protected function appendCapturedStmtLines(string &$code, array $stmts): void - { - if ($stmts) { - $code .= $this->formatCapturedStmtLines($stmts); - } - } - protected function formatCapturedStmtLines(array $stmts): string { if (!$stmts) { @@ -1454,11 +1447,11 @@ class CompilerBase implements PropertyAccessContext $this->assertExprCanBeUsedAsCondition($cond); [$condExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($cond); $code = ''; - $this->appendCapturedStmtLines($code, $beforeStmts); + $code .= $this->formatCapturedStmtLines($beforeStmts); if ($afterStmts) { $tmpVar = $this->addTmpVar(Type::VAR); $code .= $this->getIndent() . $tmpVar . ' = ' . $condExpr . ';' . PHP_EOL; - $this->appendCapturedStmtLines($code, $afterStmts); + $code .= $this->formatCapturedStmtLines($afterStmts); $condExpr = $tmpVar; } @@ -3380,7 +3373,7 @@ class CompilerBase implements PropertyAccessContext } } - private function createPropertyAccessResolver(): PropertyAccessResolver + protected function createPropertyAccessResolver(): PropertyAccessResolver { $this->assertCompilerPhase(self::PHASE_CONVERT, 'PropertyAccessResolver'); return new PropertyAccessResolver($this); @@ -3413,19 +3406,19 @@ class CompilerBase implements PropertyAccessContext || $this->isSameOrSubclassOf($declaringClass, $scope); } - private function resolveNativeInstanceProperty(NodeAbstract $expr, string $property, string $class): ?PropertyAccessResult + protected function resolveNativeInstanceProperty(NodeAbstract $expr, string $property, string $class): ?PropertyAccessResult { $scope = $this->class ? $this->getFullClassName() : ''; return $this->createPropertyAccessResolver()->resolveNativeInstanceProperty($expr, $property, $class, $scope); } - private function resolveNativeStaticProperty(NodeAbstract $expr, string $property, string $class): ?PropertyAccessResult + protected function resolveNativeStaticProperty(NodeAbstract $expr, string $property, string $class): ?PropertyAccessResult { $scope = $this->class ? $this->getFullClassName() : ''; return $this->createPropertyAccessResolver()->resolveNativeStaticProperty($expr, $property, $class, $scope); } - private function applyNativePropertyAccessResult(NodeAbstract $expr, PropertyAccessResult $result): string + protected function applyNativePropertyAccessResult(NodeAbstract $expr, PropertyAccessResult $result): string { $offset = $this->getPropertyOffset($result->classDef->getNamespacedName(false), $result->property); $expr->setAttribute('nativePropertyAccess', new NativePropertyAccess($offset, $result)); diff --git a/src/Generator/FiberGenerator.php b/src/Generator/FiberGenerator.php index 7250b289..213ccc66 100644 --- a/src/Generator/FiberGenerator.php +++ b/src/Generator/FiberGenerator.php @@ -110,8 +110,7 @@ trait FiberGenerator return true; } - $class = ''; - $this->parseTypeDecl($type, self::DECL_TYPE_OF_RETURN, $class); + [, $class] = $this->resolveTypeDecl($type, self::DECL_TYPE_OF_RETURN); $class = strtolower(ltrim($class, '\\')); return in_array($class, ['iterator', 'traversable', 'typephp\\fibergenerator'], true); } diff --git a/src/Parser/LoopControlTrait.php b/src/Parser/LoopControlTrait.php index 3a29ebbd..de32676e 100644 --- a/src/Parser/LoopControlTrait.php +++ b/src/Parser/LoopControlTrait.php @@ -25,7 +25,7 @@ trait LoopControlTrait foreach ($init as $expr) { [$initExpr, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($expr); $initExpr = $this->stringifyParsedExpr($initExpr); - $this->appendCapturedStmtLines($code, $beforeStmts); + $code .= $this->formatCapturedStmtLines($beforeStmts); $list_expr[] = $initExpr; if ($afterStmts) { $list_expr[] = implode(";\n" . $this->getIndent(), $afterStmts); @@ -56,11 +56,11 @@ trait LoopControlTrait $condResult = $this->genTmpVarName(); $condCode .= $this->getIndent() . 'bool ' . $condResult . ' = true;' . PHP_EOL; foreach ($list_cond as [$condExpr, $beforeStmts, $afterStmts]) { - $this->appendCapturedStmtLines($condCode, $beforeStmts); + $condCode .= $this->formatCapturedStmtLines($beforeStmts); if ($afterStmts) { $tmpVar = $this->addTmpVar(Type::VAR); $condCode .= $this->getIndent() . $tmpVar . ' = ' . $condExpr . ';' . PHP_EOL; - $this->appendCapturedStmtLines($condCode, $afterStmts); + $condCode .= $this->formatCapturedStmtLines($afterStmts); $condExpr = $tmpVar; } $condCode .= $this->getIndent() . $condResult . ' = ' . $this->convertBoolExpr($condExpr) . ';' . PHP_EOL; @@ -80,9 +80,9 @@ trait LoopControlTrait $loopExpr = $this->stringifyParsedExpr($loopExpr); if ($beforeStmts || $afterStmts) { $loopCode = '[&]() {'; - $this->appendCapturedStmtLines($loopCode, $beforeStmts); + $loopCode .= $this->formatCapturedStmtLines($beforeStmts); $loopCode .= $this->getIndent() . $loopExpr . ';' . PHP_EOL; - $this->appendCapturedStmtLines($loopCode, $afterStmts); + $loopCode .= $this->formatCapturedStmtLines($afterStmts); $loopCode .= $this->getIndent() . '}()'; $list_loop[] = $loopCode; } else { @@ -115,11 +115,11 @@ trait LoopControlTrait $code = $this->parseBeforeStmtLines() . PHP_EOL; if ($beforeStmts || $afterStmts) { $code .= 'while (true) {' . PHP_EOL; - $this->appendCapturedStmtLines($code, $beforeStmts); + $code .= $this->formatCapturedStmtLines($beforeStmts); if ($afterStmts) { $tmpVar = $this->addTmpVar(Type::VAR); $code .= $this->getIndent() . $tmpVar . ' = ' . $cond . ';' . PHP_EOL; - $this->appendCapturedStmtLines($code, $afterStmts); + $code .= $this->formatCapturedStmtLines($afterStmts); $cond = $tmpVar; } $code .= $this->getIndent() . 'if (!(' . $cond . ')) { break; }' . PHP_EOL; @@ -141,11 +141,11 @@ trait LoopControlTrait [$cond, $beforeStmts, $afterStmts] = $this->parseExprWithCapturedStmts($v->cond); if ($beforeStmts || $afterStmts) { $condCode = '[&]() -> bool {'; - $this->appendCapturedStmtLines($condCode, $beforeStmts); + $condCode .= $this->formatCapturedStmtLines($beforeStmts); if ($afterStmts) { $tmpVar = $this->addTmpVar(Type::VAR); $condCode .= $this->getIndent() . $tmpVar . ' = ' . $cond . ';' . PHP_EOL; - $this->appendCapturedStmtLines($condCode, $afterStmts); + $condCode .= $this->formatCapturedStmtLines($afterStmts); $cond = $tmpVar; } $condCode .= $this->getIndent() . 'return ' . $this->convertBoolExpr($cond) . ';'; @@ -216,4 +216,3 @@ trait LoopControlTrait } } - diff --git a/src/Parser/NullsafeAccessTrait.php b/src/Parser/NullsafeAccessTrait.php index 95e20567..3c9d287a 100644 --- a/src/Parser/NullsafeAccessTrait.php +++ b/src/Parser/NullsafeAccessTrait.php @@ -109,7 +109,7 @@ trait NullsafeAccessTrait return "{$tmpFn}()"; } - private function containsNullsafeChain(NodeAbstract $expr): bool + protected function containsNullsafeChain(NodeAbstract $expr): bool { while ($expr instanceof Expr\PropertyFetch || $expr instanceof Expr\MethodCall @@ -161,4 +161,3 @@ trait NullsafeAccessTrait } } - diff --git a/src/Parser/PropertyAccessTrait.php b/src/Parser/PropertyAccessTrait.php index 11036c10..6bb55ef9 100644 --- a/src/Parser/PropertyAccessTrait.php +++ b/src/Parser/PropertyAccessTrait.php @@ -839,7 +839,19 @@ trait PropertyAccessTrait protected function isPropertyHookBackingAccess(NodeAbstract $expr): bool { - return $expr->getAttribute(PropertyHookLowering::BACKING_ACCESS_ATTRIBUTE, false) === true; + if ($expr->getAttribute(PropertyHookLowering::BACKING_ACCESS_ATTRIBUTE, false) === true) { + return true; + } + if (!$expr instanceof Expr\PropertyFetch + || !$expr->var instanceof Expr\Variable + || $expr->var->name !== 'this' + || !$expr->name instanceof Node\Identifier) { + return false; + } + + $property = $expr->name->toString(); + return $this->method === PropertyHookLowering::getterName($property) + || $this->method === PropertyHookLowering::setterName($property); } protected function getPropertyHookGetter(NodeAbstract $expr): ?string diff --git a/src/Parser/SwitchTrait.php b/src/Parser/SwitchTrait.php index ca770119..33d2b6a1 100644 --- a/src/Parser/SwitchTrait.php +++ b/src/Parser/SwitchTrait.php @@ -24,9 +24,9 @@ trait SwitchTrait } [$condExpr, $condBeforeStmts, $condAfterStmts] = $this->parseExprWithCapturedStmts($cond); $var_def = ''; - $this->appendCapturedStmtLines($var_def, $condBeforeStmts); + $var_def .= $this->formatCapturedStmtLines($condBeforeStmts); $var_def .= $type . ' ' . $tmp_var . ' = ' . $condExpr . ';' . PHP_EOL; - $this->appendCapturedStmtLines($var_def, $condAfterStmts); + $var_def .= $this->formatCapturedStmtLines($condAfterStmts); // 保存作用域,switch 可能会解析失败,在这个过程中会增加变量,需重置 $localVars = $this->context->localVars; @@ -118,11 +118,11 @@ trait SwitchTrait $this->context->afterStmtLines = array_slice($this->context->afterStmtLines, 0, $caseAfterStmtCount); $code .= $this->getIndent() . 'if (!' . $switchMatched . ' && !' . $groupMatched . ') {' . PHP_EOL; - $this->appendCapturedStmtLines($code, $caseBeforeStmts); + $code .= $this->formatCapturedStmtLines($caseBeforeStmts); if ($caseAfterStmts) { $caseTmpVar = $this->addTmpVar(Type::VAR); $code .= $this->getIndent() . $caseTmpVar . ' = ' . $caseCondExpr . ';' . PHP_EOL; - $this->appendCapturedStmtLines($code, $caseAfterStmts); + $code .= $this->formatCapturedStmtLines($caseAfterStmts); $caseCondExpr = $caseTmpVar; } $code .= $this->getIndent() . $groupMatched . ' = php::equals(' . $tmp_var . ', ' . $caseCondExpr . ');' . PHP_EOL; @@ -163,4 +163,3 @@ trait SwitchTrait } } - diff --git a/src/Preprocessor.php b/src/Preprocessor.php index 6dd940c3..4b6b4759 100644 --- a/src/Preprocessor.php +++ b/src/Preprocessor.php @@ -31,7 +31,7 @@ use PhpParser\NodeTraverser; class Preprocessor extends CompilerBase { - public function sortFiles(array &$list): void + protected function getSortedFiles(array $list): array { $sorter = new StringSort(); $fileDeps = []; @@ -61,9 +61,8 @@ class Preprocessor extends CompilerBase } } - $list = $sortedFiles; - - $this->climate->lightBlue('prepare completed: ' . count($list) . ' source files in total'); + $this->climate->lightBlue('prepare completed: ' . count($sortedFiles) . ' source files in total'); + return $sortedFiles; } protected function genArgumentDeclaration(ArgInfo $argInfo): string @@ -269,8 +268,7 @@ class Preprocessor extends CompilerBase if ($param->byRef) { return Type::REF; } - $class = ''; - $type = $this->parseTypeDecl($param->type, self::DECL_TYPE_OF_PARAM, $class); + [$type, $class] = $this->resolveTypeDecl($param->type, self::DECL_TYPE_OF_PARAM); $argInfo->undeclared = $param->type === null; if ( $param->type !== null @@ -434,8 +432,7 @@ class Preprocessor extends CompilerBase } $fnName = $this->parseIdentifier($v->name); - $class = ''; - $returnType = $this->parseTypeDecl($v->returnType, self::DECL_TYPE_OF_RETURN, $class); + [$returnType, $class] = $this->resolveTypeDecl($v->returnType, self::DECL_TYPE_OF_RETURN); // 构造、析构、克隆方法不能有返回值 if ($this->method and in_array($this->method, ['__construct', '__destruct', '__clone'])) { $returnType = Type::VOID; @@ -587,13 +584,18 @@ class Preprocessor extends CompilerBase } } - // 将 trait 方法参数中的类名升级为 FullyQualified,避免 gen_stub 时上下文丢失 + // Trait members are later injected into the consuming class for stub + // generation. Fully qualify every declared class type while the + // trait's own namespace/import context is still active. if ($class instanceof Node\Stmt\Trait_) { foreach ($class->stmts as $v) { if ($v instanceof Node\Stmt\ClassMethod) { + $v->returnType = $this->upgradeToFullyQualifiedName($v->returnType); foreach ($v->params as $param) { $param->type = $this->upgradeToFullyQualifiedName($param->type); } + } elseif ($v instanceof Node\Stmt\Property || $v instanceof Node\Stmt\ClassConst) { + $v->type = $this->upgradeToFullyQualifiedName($v->type); } } } @@ -702,9 +704,9 @@ class Preprocessor extends CompilerBase { $this->resetFunction(); $flags = $this->parseModifiers($v->flags); - $class = ''; - - $declaredType = $v->type ? $this->parseTypeDecl($v->type, self::DECL_TYPE_OF_CONST, $class) : null; + [$declaredType, $class] = $v->type + ? $this->resolveTypeDecl($v->type, self::DECL_TYPE_OF_CONST) + : [null, '']; foreach ($v->consts as $const) { $type = $declaredType; @@ -752,8 +754,7 @@ class Preprocessor extends CompilerBase protected function addClassProperty(string $name, int $flags, ?NodeAbstract $typeNode, $defaultNode, bool $nullable, NodeAbstract $errorNode, bool $promoted = false): PropertyDef { $flags = $this->parseModifiers($flags); - $class = ''; - $type = $this->parseTypeDecl($typeNode, self::DECL_TYPE_OF_PROPERTY, $class); + [$type, $class] = $this->resolveTypeDecl($typeNode, self::DECL_TYPE_OF_PROPERTY); $default = null; $arrayInitPlan = null; @@ -927,14 +928,16 @@ class Preprocessor extends CompilerBase if ($this->interfaceDef->hasConstant($constName)) { $this->fatalError($stmt, "Duplicate constant `{$constName}`"); } - $class = ''; - $type = $stmt->type - ? $this->parseTypeDecl($stmt->type, self::DECL_TYPE_OF_CONST, $class) - : match ($const->value->getType()) { + if ($stmt->type) { + [$type, $class] = $this->resolveTypeDecl($stmt->type, self::DECL_TYPE_OF_CONST); + } else { + $class = ''; + $type = match ($const->value->getType()) { 'Expr_Array' => Type::ARRAY, 'Scalar_String' => Type::STR, default => Type::VAR, }; + } $constInfo = $this->parseClassLikeConstant($const, $this->parseModifiers($stmt->flags), $type, $class); $this->interfaceDef->constants[$constName] = $constInfo; } diff --git a/src/Resolver/NameResolutionTrait.php b/src/Resolver/NameResolutionTrait.php index 53473da9..307117e5 100644 --- a/src/Resolver/NameResolutionTrait.php +++ b/src/Resolver/NameResolutionTrait.php @@ -103,10 +103,21 @@ trait NameResolutionTrait if (isset($this->zendTypeMap[strtolower($typeName)]) || in_array(strtolower($typeName), ['self', 'static', 'parent'], true)) { return $type; } - if ($type->isQualified()) { - return new Node\Name\FullyQualified($typeName, $type->getAttributes()); + $resolved = $typeName; + $firstSegment = explode('\\', $typeName, 2)[0]; + $hasImportedPrefix = isset($this->useAliases[$firstSegment]); + if (!$hasImportedPrefix) { + foreach ($this->useNamespaces as $useNamespace) { + $segments = explode('\\', trim($useNamespace, '\\')); + if (strcasecmp($segments[array_key_last($segments)], $firstSegment) === 0) { + $hasImportedPrefix = true; + break; + } + } + } + if (!$type->isQualified() || $hasImportedPrefix) { + $resolved = $this->getNamespacedClassName($typeName); } - $resolved = $this->getNamespacedClassName($typeName); return new Node\Name\FullyQualified($resolved, $type->getAttributes()); } return $type; @@ -129,6 +140,13 @@ trait NameResolutionTrait /** * @param string $class 一定是带有命名空间的完整类名 */ + protected function resolveTypeDecl(?NodeAbstract $type, int $what): array + { + $class = ''; + $declaredType = $this->parseTypeDecl($type, $what, $class); + return [$declaredType, $class]; + } + protected function parseTypeDecl(?NodeAbstract $type, int $what, string &$class): string { // 未定义类型视为 var (mixed, any) diff --git a/src/gen_stub.php b/src/gen_stub.php index ccc56d82..f266157f 100755 --- a/src/gen_stub.php +++ b/src/gen_stub.php @@ -245,7 +245,12 @@ class SimpleType { return new SimpleType('mixed', true); } - $class = $node->isFullyQualified() ? $node->toString() : getTranslator()->getNamespacedClassName($node->toString()); + $resolvedName = $node->getAttribute('resolvedName'); + if ($resolvedName instanceof Node\Name) { + $class = $resolvedName->toString(); + } else { + $class = $node->isFullyQualified() ? $node->toString() : getTranslator()->getNamespacedClassName($node->toString()); + } return new SimpleType($class, false); } diff --git a/tests/compiler/basic/extends-redis.phpt b/tests/compiler/basic/extends-redis.phpt index e74e0b0a..e065eb5c 100644 --- a/tests/compiler/basic/extends-redis.phpt +++ b/tests/compiler/basic/extends-redis.phpt @@ -1,5 +1,20 @@ --TEST-- extends redis +--SKIPIF-- +connect('127.0.0.1', 6379, 0.2)) { + die('skip redis server is not available'); + } + $redis->close(); +} catch (Throwable) { + die('skip redis server is not available'); +} +?> --FILE--