540 lines
17 KiB
PHP
540 lines
17 KiB
PHP
<?php
|
|
|
|
/**
|
|
* This file is part of the Nette Framework (https://nette.org)
|
|
* Copyright (c) 2004 David Grudl (https://davidgrudl.com)
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace Nette\PhpGenerator;
|
|
|
|
use Nette;
|
|
use Nette\Utils\Strings;
|
|
|
|
|
|
/**
|
|
* Generates PHP code.
|
|
*/
|
|
class Printer
|
|
{
|
|
public int $wrapLength = 120;
|
|
public string $indentation = "\t";
|
|
public int $linesBetweenProperties = 0;
|
|
public int $linesBetweenMethods = 2;
|
|
public int $linesBetweenUseTypes = 0;
|
|
public string $returnTypeColon = ': ';
|
|
public bool $bracesOnNextLine = true;
|
|
public bool $singleParameterOnOneLine = false;
|
|
public bool $omitEmptyNamespaces = true;
|
|
protected ?PhpNamespace $namespace = null;
|
|
protected ?Dumper $dumper;
|
|
private bool $resolveTypes = true;
|
|
|
|
|
|
public function __construct()
|
|
{
|
|
$this->dumper = new Dumper;
|
|
}
|
|
|
|
|
|
public function printFunction(GlobalFunction $function, ?PhpNamespace $namespace = null): string
|
|
{
|
|
$this->namespace = $this->resolveTypes ? $namespace : null;
|
|
$line = 'function '
|
|
. ($function->getReturnReference() ? '&' : '')
|
|
. $function->getName();
|
|
$returnType = $this->printReturnType($function);
|
|
$params = $this->printParameters($function, strlen($line) + strlen($returnType) + 2); // 2 = parentheses
|
|
$body = $this->printFunctionBody($function);
|
|
$braceOnNextLine = $this->isBraceOnNextLine(str_contains($params, "\n"), (bool) $returnType);
|
|
|
|
return $this->printDocComment($function)
|
|
. $this->printAttributes($function->getAttributes())
|
|
. $line
|
|
. $params
|
|
. $returnType
|
|
. ($braceOnNextLine ? "\n" : ' ')
|
|
. "{\n" . $this->indent($body) . "}\n";
|
|
}
|
|
|
|
|
|
public function printClosure(Closure $closure, ?PhpNamespace $namespace = null): string
|
|
{
|
|
$this->namespace = $this->resolveTypes ? $namespace : null;
|
|
$uses = [];
|
|
foreach ($closure->getUses() as $param) {
|
|
$uses[] = ($param->isReference() ? '&' : '') . '$' . $param->getName();
|
|
}
|
|
|
|
$useStr = strlen($tmp = implode(', ', $uses)) > $this->wrapLength && count($uses) > 1
|
|
? "\n" . $this->indentation . implode(",\n" . $this->indentation, $uses) . ",\n"
|
|
: $tmp;
|
|
$body = $this->printFunctionBody($closure);
|
|
|
|
return $this->printAttributes($closure->getAttributes(), inline: true)
|
|
. 'function '
|
|
. ($closure->getReturnReference() ? '&' : '')
|
|
. $this->printParameters($closure)
|
|
. ($uses ? " use ($useStr)" : '')
|
|
. $this->printReturnType($closure)
|
|
. " {\n" . $this->indent($body) . '}';
|
|
}
|
|
|
|
|
|
public function printArrowFunction(Closure $closure, ?PhpNamespace $namespace = null): string
|
|
{
|
|
$this->namespace = $this->resolveTypes ? $namespace : null;
|
|
foreach ($closure->getUses() as $use) {
|
|
if ($use->isReference()) {
|
|
throw new Nette\InvalidArgumentException('Arrow function cannot bind variables by-reference.');
|
|
}
|
|
}
|
|
|
|
$body = $this->printFunctionBody($closure);
|
|
|
|
return $this->printAttributes($closure->getAttributes())
|
|
. 'fn'
|
|
. ($closure->getReturnReference() ? '&' : '')
|
|
. $this->printParameters($closure)
|
|
. $this->printReturnType($closure)
|
|
. ' => ' . rtrim($body, "\n") . ';';
|
|
}
|
|
|
|
|
|
public function printMethod(Method $method, ?PhpNamespace $namespace = null, bool $isInterface = false): string
|
|
{
|
|
$this->namespace = $this->resolveTypes ? $namespace : null;
|
|
$method->validate();
|
|
$line = ($method->isAbstract() && !$isInterface ? 'abstract ' : '')
|
|
. ($method->isFinal() ? 'final ' : '')
|
|
. ($method->getVisibility() ? $method->getVisibility() . ' ' : '')
|
|
. ($method->isStatic() ? 'static ' : '')
|
|
. 'function '
|
|
. ($method->getReturnReference() ? '&' : '')
|
|
. $method->getName();
|
|
$returnType = $this->printReturnType($method);
|
|
$params = $this->printParameters($method, strlen($line) + strlen($returnType) + strlen($this->indentation) + 2);
|
|
$body = $this->printFunctionBody($method);
|
|
$braceOnNextLine = $this->isBraceOnNextLine(str_contains($params, "\n"), (bool) $returnType);
|
|
|
|
return $this->printDocComment($method)
|
|
. $this->printAttributes($method->getAttributes())
|
|
. $line
|
|
. $params
|
|
. $returnType
|
|
. ($method->isAbstract() || $isInterface
|
|
? ";\n"
|
|
: ($braceOnNextLine ? "\n" : ' ') . "{\n" . $this->indent($body) . "}\n");
|
|
}
|
|
|
|
|
|
private function printFunctionBody(Closure|GlobalFunction|Method|PropertyHook $function): string
|
|
{
|
|
$code = Helpers::simplifyTaggedNames($function->getBody(), $this->namespace);
|
|
$code = Strings::normalize($code);
|
|
return ltrim(rtrim($code) . "\n");
|
|
}
|
|
|
|
|
|
public function printClass(
|
|
ClassType|InterfaceType|TraitType|EnumType $class,
|
|
?PhpNamespace $namespace = null,
|
|
): string
|
|
{
|
|
$this->namespace = $this->resolveTypes ? $namespace : null;
|
|
$class->validate();
|
|
$resolver = $this->namespace
|
|
? [$namespace, 'simplifyType']
|
|
: fn($s) => $s;
|
|
|
|
$traits = [];
|
|
if ($class instanceof ClassType || $class instanceof TraitType || $class instanceof EnumType) {
|
|
foreach ($class->getTraits() as $trait) {
|
|
$resolutions = implode(";\n", $trait->getResolutions());
|
|
$resolutions = Helpers::simplifyTaggedNames($resolutions, $this->namespace);
|
|
$traits[] = $this->printDocComment($trait)
|
|
. 'use ' . $resolver($trait->getName())
|
|
. ($resolutions
|
|
? " {\n" . $this->indent($resolutions) . ";\n}\n"
|
|
: ";\n");
|
|
}
|
|
}
|
|
|
|
$cases = [];
|
|
$enumType = null;
|
|
if ($class instanceof EnumType) {
|
|
$enumType = $class->getType();
|
|
foreach ($class->getCases() as $case) {
|
|
$enumType ??= is_scalar($case->getValue()) ? get_debug_type($case->getValue()) : null;
|
|
$cases[] = $this->printDocComment($case)
|
|
. $this->printAttributes($case->getAttributes())
|
|
. 'case ' . $case->getName()
|
|
. ($case->getValue() === null ? '' : ' = ' . $this->dump($case->getValue()))
|
|
. ";\n";
|
|
}
|
|
}
|
|
|
|
$readOnlyClass = $class instanceof ClassType && $class->isReadOnly();
|
|
$consts = [];
|
|
$methods = [];
|
|
if (
|
|
$class instanceof ClassType
|
|
|| $class instanceof InterfaceType
|
|
|| $class instanceof TraitType
|
|
|| $class instanceof EnumType
|
|
) {
|
|
foreach ($class->getConstants() as $const) {
|
|
$consts[] = $this->printConstant($const);
|
|
}
|
|
|
|
foreach ($class->getMethods() as $method) {
|
|
if ($readOnlyClass && $method->getName() === Method::Constructor) {
|
|
$method = clone $method;
|
|
array_map(fn($param) => $param instanceof PromotedParameter ? $param->setReadOnly(false) : null, $method->getParameters());
|
|
}
|
|
$methods[] = $this->printMethod($method, $namespace, $class->isInterface());
|
|
}
|
|
}
|
|
|
|
$properties = [];
|
|
if ($class instanceof ClassType || $class instanceof TraitType || $class instanceof InterfaceType) {
|
|
foreach ($class->getProperties() as $property) {
|
|
$properties[] = $this->printProperty($property, $readOnlyClass, $class instanceof InterfaceType);
|
|
}
|
|
}
|
|
|
|
$members = array_filter([
|
|
implode('', $traits),
|
|
$this->joinProperties($consts),
|
|
$this->joinProperties($cases),
|
|
$this->joinProperties($properties),
|
|
($methods && $properties ? str_repeat("\n", $this->linesBetweenMethods - 1) : '')
|
|
. implode(str_repeat("\n", $this->linesBetweenMethods), $methods),
|
|
]);
|
|
|
|
if ($class instanceof ClassType) {
|
|
$line[] = $class->isAbstract() ? 'abstract' : null;
|
|
$line[] = $class->isFinal() ? 'final' : null;
|
|
$line[] = $class->isReadOnly() ? 'readonly' : null;
|
|
}
|
|
|
|
$line[] = match (true) {
|
|
$class instanceof ClassType => $class->getName() ? 'class ' . $class->getName() : null,
|
|
$class instanceof InterfaceType => 'interface ' . $class->getName(),
|
|
$class instanceof TraitType => 'trait ' . $class->getName(),
|
|
$class instanceof EnumType => 'enum ' . $class->getName() . ($enumType ? $this->returnTypeColon . $enumType : ''),
|
|
};
|
|
$line[] = ($class instanceof ClassType || $class instanceof InterfaceType) && $class->getExtends()
|
|
? 'extends ' . implode(', ', array_map($resolver, (array) $class->getExtends()))
|
|
: null;
|
|
$line[] = ($class instanceof ClassType || $class instanceof EnumType) && $class->getImplements()
|
|
? 'implements ' . implode(', ', array_map($resolver, $class->getImplements()))
|
|
: null;
|
|
$line[] = $class->getName() ? null : '{';
|
|
|
|
return $this->printDocComment($class)
|
|
. $this->printAttributes($class->getAttributes())
|
|
. implode(' ', array_filter($line))
|
|
. ($class->getName() ? "\n{\n" : "\n")
|
|
. ($members ? $this->indent(implode("\n", $members)) : '')
|
|
. '}'
|
|
. ($class->getName() ? "\n" : '');
|
|
}
|
|
|
|
|
|
public function printNamespace(PhpNamespace $namespace): string
|
|
{
|
|
$this->namespace = $this->resolveTypes ? $namespace : null;
|
|
$name = $namespace->getName();
|
|
$uses = [
|
|
$this->printUses($namespace),
|
|
$this->printUses($namespace, PhpNamespace::NameFunction),
|
|
$this->printUses($namespace, PhpNamespace::NameConstant),
|
|
];
|
|
$uses = implode(str_repeat("\n", $this->linesBetweenUseTypes), array_filter($uses));
|
|
|
|
$items = [];
|
|
foreach ($namespace->getClasses() as $class) {
|
|
$items[] = $this->printClass($class, $namespace);
|
|
}
|
|
|
|
foreach ($namespace->getFunctions() as $function) {
|
|
$items[] = $this->printFunction($function, $namespace);
|
|
}
|
|
|
|
$body = ($uses ? $uses . "\n" : '')
|
|
. implode("\n", $items);
|
|
|
|
if ($namespace->hasBracketedSyntax()) {
|
|
return 'namespace' . ($name ? " $name" : '') . "\n{\n"
|
|
. $this->indent($body)
|
|
. "}\n";
|
|
|
|
} else {
|
|
return ($name ? "namespace $name;\n\n" : '')
|
|
. $body;
|
|
}
|
|
}
|
|
|
|
|
|
public function printFile(PhpFile $file): string
|
|
{
|
|
$namespaces = [];
|
|
foreach ($file->getNamespaces() as $namespace) {
|
|
if (!$this->omitEmptyNamespaces || $namespace->getClasses() || $namespace->getFunctions()) {
|
|
$namespaces[] = $this->printNamespace($namespace);
|
|
}
|
|
}
|
|
|
|
return "<?php\n"
|
|
. ($file->getComment() ? "\n" . $this->printDocComment($file) : '')
|
|
. "\n"
|
|
. ($file->hasStrictTypes() ? "declare(strict_types=1);\n\n" : '')
|
|
. implode("\n\n", $namespaces);
|
|
}
|
|
|
|
|
|
protected function printUses(PhpNamespace $namespace, string $of = PhpNamespace::NameNormal): string
|
|
{
|
|
$prefix = [
|
|
PhpNamespace::NameNormal => '',
|
|
PhpNamespace::NameFunction => 'function ',
|
|
PhpNamespace::NameConstant => 'const ',
|
|
][$of];
|
|
$uses = [];
|
|
foreach ($namespace->getUses($of) as $alias => $original) {
|
|
$uses[] = Helpers::extractShortName($original) === $alias
|
|
? "use $prefix$original;\n"
|
|
: "use $prefix$original as $alias;\n";
|
|
}
|
|
|
|
return implode('', $uses);
|
|
}
|
|
|
|
|
|
protected function printParameters(Closure|GlobalFunction|Method|PropertyHook $function, int $column = 0): string
|
|
{
|
|
$special = false;
|
|
foreach ($function->getParameters() as $param) {
|
|
$param->validate();
|
|
$special = $special || $param instanceof PromotedParameter || $param->getAttributes() || $param->getComment();
|
|
}
|
|
|
|
if (!$special || ($this->singleParameterOnOneLine && count($function->getParameters()) === 1)) {
|
|
$line = $this->formatParameters($function, multiline: false);
|
|
if (!str_contains($line, "\n") && strlen($line) + $column <= $this->wrapLength) {
|
|
return $line;
|
|
}
|
|
}
|
|
|
|
return $this->formatParameters($function, multiline: true);
|
|
}
|
|
|
|
|
|
private function formatParameters(Closure|GlobalFunction|Method|PropertyHook $function, bool $multiline): string
|
|
{
|
|
$params = $function->getParameters();
|
|
$res = '';
|
|
|
|
foreach ($params as $param) {
|
|
$variadic = !$function instanceof PropertyHook && $function->isVariadic() && $param === end($params);
|
|
$attrs = $this->printAttributes($param->getAttributes(), inline: true);
|
|
$res .=
|
|
$this->printDocComment($param)
|
|
. ($attrs ? ($multiline ? substr($attrs, 0, -1) . "\n" : $attrs) : '')
|
|
. ($param instanceof PromotedParameter
|
|
? $this->printPropertyVisibility($param) . ($param->isReadOnly() && $param->getType() ? ' readonly' : '') . ' '
|
|
: '')
|
|
. ltrim($this->printType($param->getType(), $param->isNullable()) . ' ')
|
|
. ($param->isReference() ? '&' : '')
|
|
. ($variadic ? '...' : '')
|
|
. '$' . $param->getName()
|
|
. ($param->hasDefaultValue() && !$variadic ? ' = ' . $this->dump($param->getDefaultValue()) : '')
|
|
. ($param instanceof PromotedParameter ? $this->printHooks($param) : '')
|
|
. ($multiline ? ",\n" : ', ');
|
|
}
|
|
|
|
return $multiline
|
|
? "(\n" . $this->indent($res) . ')'
|
|
: '(' . substr($res, 0, -2) . ')';
|
|
}
|
|
|
|
|
|
private function printConstant(Constant $const): string
|
|
{
|
|
$def = ($const->isFinal() ? 'final ' : '')
|
|
. ($const->getVisibility() ? $const->getVisibility() . ' ' : '')
|
|
. 'const '
|
|
. ltrim($this->printType($const->getType(), nullable: false) . ' ')
|
|
. $const->getName() . ' = ';
|
|
|
|
return $this->printDocComment($const)
|
|
. $this->printAttributes($const->getAttributes())
|
|
. $def
|
|
. $this->dump($const->getValue(), strlen($def)) . ";\n";
|
|
}
|
|
|
|
|
|
private function printProperty(Property $property, bool $readOnlyClass = false, bool $isInterface = false): string
|
|
{
|
|
$property->validate();
|
|
$type = $property->getType();
|
|
$def = ($property->isAbstract() && !$isInterface ? 'abstract ' : '')
|
|
. ($property->isFinal() ? 'final ' : '')
|
|
. $this->printPropertyVisibility($property)
|
|
. ($property->isStatic() ? ' static' : '')
|
|
. (!$readOnlyClass && $property->isReadOnly() && $type ? ' readonly' : '')
|
|
. ' '
|
|
. ltrim($this->printType($type, $property->isNullable()) . ' ')
|
|
. '$' . $property->getName();
|
|
|
|
$defaultValue = $property->getValue() === null && !$property->isInitialized()
|
|
? ''
|
|
: ' = ' . $this->dump($property->getValue(), strlen($def) + 3); // 3 = ' = '
|
|
|
|
return $this->printDocComment($property)
|
|
. $this->printAttributes($property->getAttributes())
|
|
. $def
|
|
. $defaultValue
|
|
. ($this->printHooks($property, $isInterface) ?: ';')
|
|
. "\n";
|
|
}
|
|
|
|
|
|
private function printPropertyVisibility(Property|PromotedParameter $param): string
|
|
{
|
|
$get = $param->getVisibility(PropertyAccessMode::Get);
|
|
$set = $param->getVisibility(PropertyAccessMode::Set);
|
|
return $set
|
|
? ($get ? "$get $set(set)" : "$set(set)")
|
|
: $get ?? 'public';
|
|
}
|
|
|
|
|
|
protected function printType(?string $type, bool $nullable): string
|
|
{
|
|
if ($type === null) {
|
|
return '';
|
|
}
|
|
|
|
if ($this->namespace) {
|
|
$type = $this->namespace->simplifyType($type);
|
|
}
|
|
|
|
return $nullable
|
|
? Type::nullable($type)
|
|
: $type;
|
|
}
|
|
|
|
|
|
protected function printDocComment(/*Traits\CommentAware*/ $commentable): string
|
|
{
|
|
$multiLine = $commentable instanceof GlobalFunction
|
|
|| $commentable instanceof Method
|
|
|| $commentable instanceof ClassLike
|
|
|| $commentable instanceof PhpFile;
|
|
return Helpers::formatDocComment((string) $commentable->getComment(), $multiLine);
|
|
}
|
|
|
|
|
|
protected function printReturnType(Closure|GlobalFunction|Method $function): string
|
|
{
|
|
return ($tmp = $this->printType($function->getReturnType(), $function->isReturnNullable()))
|
|
? $this->returnTypeColon . $tmp
|
|
: '';
|
|
}
|
|
|
|
|
|
/** @param Attribute[] $attrs */
|
|
protected function printAttributes(array $attrs, bool $inline = false): string
|
|
{
|
|
if (!$attrs) {
|
|
return '';
|
|
}
|
|
|
|
$this->dumper->indentation = $this->indentation;
|
|
$items = [];
|
|
foreach ($attrs as $attr) {
|
|
$args = $this->dumper->format('...?:', $attr->getArguments());
|
|
$args = Helpers::simplifyTaggedNames($args, $this->namespace);
|
|
$items[] = $this->printType($attr->getName(), nullable: false) . ($args === '' ? '' : "($args)");
|
|
$inline = $inline && !str_contains($args, "\n");
|
|
}
|
|
|
|
return $inline
|
|
? '#[' . implode(', ', $items) . '] '
|
|
: '#[' . implode("]\n#[", $items) . "]\n";
|
|
}
|
|
|
|
|
|
private function printHooks(Property|PromotedParameter $property, bool $isInterface = false): string
|
|
{
|
|
$hooks = $property->getHooks();
|
|
if (!$hooks) {
|
|
return '';
|
|
}
|
|
|
|
$simple = true;
|
|
foreach ($property->getHooks() as $type => $hook) {
|
|
$simple = $simple && ($hook->isAbstract() || $isInterface);
|
|
$hooks[$type] = $this->printDocComment($hook)
|
|
. $this->printAttributes($hook->getAttributes())
|
|
. ($hook->isAbstract() || $isInterface
|
|
? ($hook->getReturnReference() ? '&' : '')
|
|
. $type . ';'
|
|
: ($hook->isFinal() ? 'final ' : '')
|
|
. ($hook->getReturnReference() ? '&' : '')
|
|
. $type
|
|
. ($hook->getParameters() ? $this->printParameters($hook) : '')
|
|
. ' '
|
|
. ($hook->isShort()
|
|
? '=> ' . $hook->getBody() . ';'
|
|
: "{\n" . $this->indent($this->printFunctionBody($hook)) . '}'));
|
|
}
|
|
|
|
return $simple
|
|
? ' { ' . implode(' ', $hooks) . ' }'
|
|
: " {\n" . $this->indent(implode("\n", $hooks)) . "\n}";
|
|
}
|
|
|
|
|
|
public function setTypeResolving(bool $state = true): static
|
|
{
|
|
$this->resolveTypes = $state;
|
|
return $this;
|
|
}
|
|
|
|
|
|
protected function indent(string $s): string
|
|
{
|
|
$s = str_replace("\t", $this->indentation, $s);
|
|
return Strings::indent($s, 1, $this->indentation);
|
|
}
|
|
|
|
|
|
protected function dump(mixed $var, int $column = 0): string
|
|
{
|
|
$this->dumper->indentation = $this->indentation;
|
|
$this->dumper->wrapLength = $this->wrapLength;
|
|
$s = $this->dumper->dump($var, $column);
|
|
$s = Helpers::simplifyTaggedNames($s, $this->namespace);
|
|
return $s;
|
|
}
|
|
|
|
|
|
/** @param string[] $props */
|
|
private function joinProperties(array $props): string
|
|
{
|
|
return $this->linesBetweenProperties
|
|
? implode(str_repeat("\n", $this->linesBetweenProperties), $props)
|
|
: preg_replace('#^(\w.*\n)\n(?=\w.*;)#m', '$1', implode("\n", $props));
|
|
}
|
|
|
|
|
|
protected function isBraceOnNextLine(bool $multiLine, bool $hasReturnType): bool
|
|
{
|
|
return $this->bracesOnNextLine && (!$multiLine || $hasReturnType);
|
|
}
|
|
}
|