anonClassIndex++; } /** * Resolve all relative type names in an anonymous class to fully qualified names. * The generated eval code runs without use imports, so all type references must be FQN. */ protected function resolveAnonClassTypeNames(Class_ $classDef): void { foreach ($classDef->stmts as $stmt) { if ($stmt instanceof ClassMethod) { foreach ($stmt->params as $param) { if ($param->type !== null) { $param->type = $this->resolveTypeNode($param->type); } } if ($stmt->returnType !== null) { $stmt->returnType = $this->resolveTypeNode($stmt->returnType); } } elseif ($stmt instanceof Property) { if ($stmt->type !== null) { $stmt->type = $this->resolveTypeNode($stmt->type); } } elseif ($stmt instanceof ClassConst) { if ($stmt->type !== null) { $stmt->type = $this->resolveTypeNode($stmt->type); } } } } /** * Resolve a single type node, converting relative Name to FullyQualified. */ protected function resolveTypeNode(Node $type): NodeAbstract { if ($type instanceof Name) { if ($type->isFullyQualified()) { return $type; } $resolved = $this->getNamespacedClassName($type->toString()); return new Name\FullyQualified($resolved); } if ($type instanceof NullableType) { $type->type = $this->resolveTypeNode($type->type); return $type; } if ($type instanceof UnionType) { foreach ($type->types as $i => $subType) { $type->types[$i] = $this->resolveTypeNode($subType); } return $type; } if ($type instanceof IntersectionType) { foreach ($type->types as $i => $subType) { $type->types[$i] = $this->resolveTypeNode($subType); } return $type; } return $type; } protected function genEmbeddedCode(NodeAbstract $stmt): string { if ($stmt instanceof Class_) { $stmt = clone $stmt; $traverser = new NodeTraverser(); $traverser->addVisitor(new class($this) extends NodeVisitorAbstract { /** @var list */ private array $classStack = []; public function __construct(private object $compiler) { } public function enterNode(Node $node) { if ($node instanceof Class_) { $this->classStack[] = $node; return null; } if ($node instanceof ClassMethod && $node->returnType === null) { $class = $this->classStack[count($this->classStack) - 1] ?? null; if ($class !== null && $this->compiler->shouldAddMixedReturnToEmbeddedClassMethod($class, $node)) { $node->returnType = new Identifier('mixed'); } } return null; } public function leaveNode(Node $node) { if ($node instanceof Class_) { array_pop($this->classStack); } return null; } }); $stmt = $traverser->traverse([$stmt])[0]; } return $this->printer->prettyPrint([$stmt]); } public function shouldAddMixedReturnToEmbeddedClassMethod(Class_ $class, ClassMethod $method): bool { $methodName = strtolower($method->name->toString()); if ($this->isEmbeddedMagicMethodReturnSensitive($methodName)) { return false; } if (!empty($class->implements)) { return true; } return $class->extends !== null && $this->ancestorMethodMayRequireMixedReturn($class->extends, $methodName); } protected function isEmbeddedMagicMethodReturnSensitive(string $methodName): bool { return in_array($methodName, [ '__construct', '__destruct', '__clone', '__debuginfo', '__isset', '__serialize', '__set', '__set_state', '__sleep', '__tostring', '__unserialize', '__unset', '__wakeup', ], true); } protected function ancestorMethodMayRequireMixedReturn(Name $extends, string $methodName): bool { $className = ltrim($extends->toString(), '\\'); while ($className !== '') { if ($this->hasClass($className)) { $classDef = $this->getClass($className); if ($classDef->hasMethod($methodName)) { $functionDef = $classDef->getMethod($methodName)->functionDef; return $functionDef !== null && ($functionDef->returnTypeUndeclared || $functionDef->returnType === Type::VAR); } $className = $classDef->extends; continue; } if ($this->isInternalClass($className) || $this->isInternalInterface($className)) { if (!Reflection::hasMethod($className, $methodName)) { return false; } $returnType = Reflection::getMethodReturnType($className, $methodName); return $returnType === null || strtolower($returnType) === 'mixed'; } return true; } return false; } }