Printer.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424
  1. <?php
  2. /**
  3. * This file is part of the Nette Framework (https://nette.org)
  4. * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
  5. */
  6. declare(strict_types=1);
  7. namespace Nette\PhpGenerator;
  8. use Nette;
  9. use Nette\Utils\Strings;
  10. /**
  11. * Generates PHP code.
  12. */
  13. class Printer
  14. {
  15. use Nette\SmartObject;
  16. /** @var int */
  17. public $wrapLength = 120;
  18. /** @var string */
  19. protected $indentation = "\t";
  20. /** @var int */
  21. protected $linesBetweenProperties = 0;
  22. /** @var int */
  23. protected $linesBetweenMethods = 2;
  24. /** @var string */
  25. protected $returnTypeColon = ': ';
  26. /** @var ?PhpNamespace */
  27. protected $namespace;
  28. /** @var ?Dumper */
  29. protected $dumper;
  30. /** @var bool */
  31. private $resolveTypes = true;
  32. public function __construct()
  33. {
  34. $this->dumper = new Dumper;
  35. }
  36. public function printFunction(GlobalFunction $function, ?PhpNamespace $namespace = null): string
  37. {
  38. $this->namespace = $this->resolveTypes ? $namespace : null;
  39. $line = 'function '
  40. . ($function->getReturnReference() ? '&' : '')
  41. . $function->getName();
  42. $returnType = $this->printReturnType($function);
  43. $body = Helpers::simplifyTaggedNames($function->getBody(), $this->namespace);
  44. return Helpers::formatDocComment($function->getComment() . "\n")
  45. . self::printAttributes($function->getAttributes())
  46. . $line
  47. . $this->printParameters($function, strlen($line) + strlen($returnType) + 2) // 2 = parentheses
  48. . $returnType
  49. . "\n{\n" . $this->indent(ltrim(rtrim($body) . "\n")) . "}\n";
  50. }
  51. public function printClosure(Closure $closure, ?PhpNamespace $namespace = null): string
  52. {
  53. $this->namespace = $this->resolveTypes ? $namespace : null;
  54. $uses = [];
  55. foreach ($closure->getUses() as $param) {
  56. $uses[] = ($param->isReference() ? '&' : '') . '$' . $param->getName();
  57. }
  58. $useStr = strlen($tmp = implode(', ', $uses)) > $this->wrapLength && count($uses) > 1
  59. ? "\n" . $this->indentation . implode(",\n" . $this->indentation, $uses) . "\n"
  60. : $tmp;
  61. $body = Helpers::simplifyTaggedNames($closure->getBody(), $this->namespace);
  62. return self::printAttributes($closure->getAttributes(), true)
  63. . 'function '
  64. . ($closure->getReturnReference() ? '&' : '')
  65. . $this->printParameters($closure)
  66. . ($uses ? " use ($useStr)" : '')
  67. . $this->printReturnType($closure)
  68. . " {\n" . $this->indent(ltrim(rtrim($body) . "\n")) . '}';
  69. }
  70. public function printArrowFunction(Closure $closure, ?PhpNamespace $namespace = null): string
  71. {
  72. $this->namespace = $this->resolveTypes ? $namespace : null;
  73. foreach ($closure->getUses() as $use) {
  74. if ($use->isReference()) {
  75. throw new Nette\InvalidArgumentException('Arrow function cannot bind variables by-reference.');
  76. }
  77. }
  78. $body = Helpers::simplifyTaggedNames($closure->getBody(), $this->namespace);
  79. return self::printAttributes($closure->getAttributes())
  80. . 'fn'
  81. . ($closure->getReturnReference() ? '&' : '')
  82. . $this->printParameters($closure)
  83. . $this->printReturnType($closure)
  84. . ' => ' . trim($body) . ';';
  85. }
  86. public function printMethod(Method $method, ?PhpNamespace $namespace = null): string
  87. {
  88. $this->namespace = $this->resolveTypes ? $namespace : null;
  89. $method->validate();
  90. $line = ($method->isAbstract() ? 'abstract ' : '')
  91. . ($method->isFinal() ? 'final ' : '')
  92. . ($method->getVisibility() ? $method->getVisibility() . ' ' : '')
  93. . ($method->isStatic() ? 'static ' : '')
  94. . 'function '
  95. . ($method->getReturnReference() ? '&' : '')
  96. . $method->getName();
  97. $returnType = $this->printReturnType($method);
  98. $params = $this->printParameters($method, strlen($line) + strlen($returnType) + strlen($this->indentation) + 2);
  99. $body = Helpers::simplifyTaggedNames((string) $method->getBody(), $this->namespace);
  100. return Helpers::formatDocComment($method->getComment() . "\n")
  101. . self::printAttributes($method->getAttributes())
  102. . $line
  103. . $params
  104. . $returnType
  105. . ($method->isAbstract() || $method->getBody() === null
  106. ? ";\n"
  107. : (strpos($params, "\n") === false ? "\n" : ' ')
  108. . "{\n"
  109. . $this->indent(ltrim(rtrim($body) . "\n"))
  110. . "}\n");
  111. }
  112. public function printClass(ClassType $class, ?PhpNamespace $namespace = null): string
  113. {
  114. $this->namespace = $this->resolveTypes ? $namespace : null;
  115. $class->validate();
  116. $resolver = $this->namespace
  117. ? [$namespace, 'simplifyType']
  118. : function ($s) { return $s; };
  119. $traits = [];
  120. foreach ($class->getTraitResolutions() as $trait) {
  121. $resolutions = $trait->getResolutions();
  122. $traits[] = Helpers::formatDocComment((string) $trait->getComment())
  123. . 'use ' . $resolver($trait->getName())
  124. . ($resolutions
  125. ? " {\n" . $this->indentation . implode(";\n" . $this->indentation, $resolutions) . ";\n}\n"
  126. : ";\n");
  127. }
  128. $cases = [];
  129. foreach ($class->getCases() as $case) {
  130. $cases[] = Helpers::formatDocComment((string) $case->getComment())
  131. . self::printAttributes($case->getAttributes())
  132. . 'case ' . $case->getName()
  133. . ($case->getValue() === null ? '' : ' = ' . $this->dump($case->getValue()))
  134. . ";\n";
  135. }
  136. $enumType = isset($case) && $case->getValue() !== null
  137. ? $this->returnTypeColon . Type::getType($case->getValue())
  138. : '';
  139. $consts = [];
  140. foreach ($class->getConstants() as $const) {
  141. $def = ($const->isFinal() ? 'final ' : '')
  142. . ($const->getVisibility() ? $const->getVisibility() . ' ' : '')
  143. . 'const ' . $const->getName() . ' = ';
  144. $consts[] = Helpers::formatDocComment((string) $const->getComment())
  145. . self::printAttributes($const->getAttributes())
  146. . $def
  147. . $this->dump($const->getValue(), strlen($def)) . ";\n";
  148. }
  149. $properties = [];
  150. foreach ($class->getProperties() as $property) {
  151. $property->validate();
  152. $type = $property->getType();
  153. $def = (($property->getVisibility() ?: 'public')
  154. . ($property->isStatic() ? ' static' : '')
  155. . ($property->isReadOnly() && $type ? ' readonly' : '')
  156. . ' '
  157. . ltrim($this->printType($type, $property->isNullable()) . ' ')
  158. . '$' . $property->getName());
  159. $properties[] = Helpers::formatDocComment((string) $property->getComment())
  160. . self::printAttributes($property->getAttributes())
  161. . $def
  162. . ($property->getValue() === null && !$property->isInitialized()
  163. ? ''
  164. : ' = ' . $this->dump($property->getValue(), strlen($def) + 3)) // 3 = ' = '
  165. . ";\n";
  166. }
  167. $methods = [];
  168. foreach ($class->getMethods() as $method) {
  169. $methods[] = $this->printMethod($method, $namespace);
  170. }
  171. $members = array_filter([
  172. implode('', $traits),
  173. $this->joinProperties($cases),
  174. $this->joinProperties($consts),
  175. $this->joinProperties($properties),
  176. ($methods && $properties ? str_repeat("\n", $this->linesBetweenMethods - 1) : '')
  177. . implode(str_repeat("\n", $this->linesBetweenMethods), $methods),
  178. ]);
  179. return Strings::normalize(
  180. Helpers::formatDocComment($class->getComment() . "\n")
  181. . self::printAttributes($class->getAttributes())
  182. . ($class->isAbstract() ? 'abstract ' : '')
  183. . ($class->isFinal() ? 'final ' : '')
  184. . ($class->getName() ? $class->getType() . ' ' . $class->getName() . $enumType . ' ' : '')
  185. . ($class->getExtends() ? 'extends ' . implode(', ', array_map($resolver, (array) $class->getExtends())) . ' ' : '')
  186. . ($class->getImplements() ? 'implements ' . implode(', ', array_map($resolver, $class->getImplements())) . ' ' : '')
  187. . ($class->getName() ? "\n" : '') . "{\n"
  188. . ($members ? $this->indent(implode("\n", $members)) : '')
  189. . '}'
  190. ) . ($class->getName() ? "\n" : '');
  191. }
  192. public function printNamespace(PhpNamespace $namespace): string
  193. {
  194. $this->namespace = $this->resolveTypes ? $namespace : null;
  195. $name = $namespace->getName();
  196. $uses = $this->printUses($namespace)
  197. . $this->printUses($namespace, PhpNamespace::NAME_FUNCTION)
  198. . $this->printUses($namespace, PhpNamespace::NAME_CONSTANT);
  199. $items = [];
  200. foreach ($namespace->getClasses() as $class) {
  201. $items[] = $this->printClass($class, $namespace);
  202. }
  203. foreach ($namespace->getFunctions() as $function) {
  204. $items[] = $this->printFunction($function, $namespace);
  205. }
  206. $body = ($uses ? $uses . "\n" : '')
  207. . implode("\n", $items);
  208. if ($namespace->hasBracketedSyntax()) {
  209. return 'namespace' . ($name ? " $name" : '') . "\n{\n"
  210. . $this->indent($body)
  211. . "}\n";
  212. } else {
  213. return ($name ? "namespace $name;\n\n" : '')
  214. . $body;
  215. }
  216. }
  217. public function printFile(PhpFile $file): string
  218. {
  219. $namespaces = [];
  220. foreach ($file->getNamespaces() as $namespace) {
  221. $namespaces[] = $this->printNamespace($namespace);
  222. }
  223. return Strings::normalize(
  224. "<?php\n"
  225. . ($file->getComment() ? "\n" . Helpers::formatDocComment($file->getComment() . "\n") : '')
  226. . "\n"
  227. . ($file->hasStrictTypes() ? "declare(strict_types=1);\n\n" : '')
  228. . implode("\n\n", $namespaces)
  229. ) . "\n";
  230. }
  231. protected function printUses(PhpNamespace $namespace, string $of = PhpNamespace::NAME_NORMAL): string
  232. {
  233. $prefix = [
  234. PhpNamespace::NAME_NORMAL => '',
  235. PhpNamespace::NAME_FUNCTION => 'function ',
  236. PhpNamespace::NAME_CONSTANT => 'const ',
  237. ][$of];
  238. $name = $namespace->getName();
  239. $uses = [];
  240. foreach ($namespace->getUses($of) as $alias => $original) {
  241. $uses[] = Helpers::extractShortName($original) === $alias
  242. ? "use $prefix$original;\n"
  243. : "use $prefix$original as $alias;\n";
  244. }
  245. return implode('', $uses);
  246. }
  247. /**
  248. * @param Closure|GlobalFunction|Method $function
  249. */
  250. protected function printParameters($function, int $column = 0): string
  251. {
  252. $params = [];
  253. $list = $function->getParameters();
  254. $special = false;
  255. foreach ($list as $param) {
  256. $param->validate();
  257. $variadic = $function->isVariadic() && $param === end($list);
  258. $type = $param->getType();
  259. $promoted = $param instanceof PromotedParameter ? $param : null;
  260. $params[] =
  261. ($promoted ? Helpers::formatDocComment((string) $promoted->getComment()) : '')
  262. . ($attrs = self::printAttributes($param->getAttributes(), true))
  263. . ($promoted ?
  264. ($promoted->getVisibility() ?: 'public')
  265. . ($promoted->isReadOnly() && $type ? ' readonly' : '')
  266. . ' ' : '')
  267. . ltrim($this->printType($type, $param->isNullable()) . ' ')
  268. . ($param->isReference() ? '&' : '')
  269. . ($variadic ? '...' : '')
  270. . '$' . $param->getName()
  271. . ($param->hasDefaultValue() && !$variadic ? ' = ' . $this->dump($param->getDefaultValue()) : '');
  272. $special = $special || $promoted || $attrs;
  273. }
  274. $line = implode(', ', $params);
  275. return count($params) > 1 && ($special || strlen($line) + $column > $this->wrapLength)
  276. ? "(\n" . $this->indent(implode(",\n", $params)) . ($special ? ',' : '') . "\n)"
  277. : "($line)";
  278. }
  279. protected function printType(?string $type, bool $nullable): string
  280. {
  281. if ($type === null) {
  282. return '';
  283. }
  284. if ($this->namespace) {
  285. $type = $this->namespace->simplifyType($type);
  286. }
  287. if ($nullable && strcasecmp($type, 'mixed')) {
  288. $type = strpos($type, '|') === false
  289. ? '?' . $type
  290. : $type . '|null';
  291. }
  292. return $type;
  293. }
  294. /**
  295. * @param Closure|GlobalFunction|Method $function
  296. */
  297. private function printReturnType($function): string
  298. {
  299. return ($tmp = $this->printType($function->getReturnType(), $function->isReturnNullable()))
  300. ? $this->returnTypeColon . $tmp
  301. : '';
  302. }
  303. private function printAttributes(array $attrs, bool $inline = false): string
  304. {
  305. if (!$attrs) {
  306. return '';
  307. }
  308. $this->dumper->indentation = $this->indentation;
  309. $items = [];
  310. foreach ($attrs as $attr) {
  311. $args = $this->dumper->format('...?:', $attr->getArguments());
  312. $args = Helpers::simplifyTaggedNames($args, $this->namespace);
  313. $items[] = $this->printType($attr->getName(), false) . ($args ? "($args)" : '');
  314. }
  315. return $inline
  316. ? '#[' . implode(', ', $items) . '] '
  317. : '#[' . implode("]\n#[", $items) . "]\n";
  318. }
  319. /** @return static */
  320. public function setTypeResolving(bool $state = true): self
  321. {
  322. $this->resolveTypes = $state;
  323. return $this;
  324. }
  325. protected function indent(string $s): string
  326. {
  327. $s = str_replace("\t", $this->indentation, $s);
  328. return Strings::indent($s, 1, $this->indentation);
  329. }
  330. protected function dump($var, int $column = 0): string
  331. {
  332. $this->dumper->indentation = $this->indentation;
  333. $this->dumper->wrapLength = $this->wrapLength;
  334. $s = $this->dumper->dump($var, $column);
  335. $s = Helpers::simplifyTaggedNames($s, $this->namespace);
  336. return $s;
  337. }
  338. private function joinProperties(array $props): string
  339. {
  340. return $this->linesBetweenProperties
  341. ? implode(str_repeat("\n", $this->linesBetweenProperties), $props)
  342. : preg_replace('#^(\w.*\n)\n(?=\w.*;)#m', '$1', implode("\n", $props));
  343. }
  344. }