Extractor.php 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  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 PhpParser;
  10. use PhpParser\Node;
  11. use PhpParser\NodeFinder;
  12. use PhpParser\ParserFactory;
  13. /**
  14. * Extracts information from PHP code.
  15. * @internal
  16. */
  17. final class Extractor
  18. {
  19. use Nette\SmartObject;
  20. private $code;
  21. private $statements;
  22. private $printer;
  23. public function __construct(string $code)
  24. {
  25. if (!class_exists(ParserFactory::class)) {
  26. throw new Nette\NotSupportedException("PHP-Parser is required to load method bodies, install package 'nikic/php-parser' 4.7 or newer.");
  27. }
  28. $this->printer = new PhpParser\PrettyPrinter\Standard;
  29. $this->parseCode($code);
  30. }
  31. private function parseCode(string $code): void
  32. {
  33. if (substr($code, 0, 5) !== '<?php') {
  34. throw new Nette\InvalidStateException('The input string is not a PHP code.');
  35. }
  36. $this->code = str_replace("\r\n", "\n", $code);
  37. $lexer = new PhpParser\Lexer\Emulative(['usedAttributes' => ['startFilePos', 'endFilePos', 'comments']]);
  38. $parser = (new ParserFactory)->create(ParserFactory::ONLY_PHP7, $lexer);
  39. $stmts = $parser->parse($this->code);
  40. $traverser = new PhpParser\NodeTraverser;
  41. $traverser->addVisitor(new PhpParser\NodeVisitor\ParentConnectingVisitor);
  42. $traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver(null, ['preserveOriginalNames' => true]));
  43. $this->statements = $traverser->traverse($stmts);
  44. }
  45. public function extractMethodBodies(string $className): array
  46. {
  47. $nodeFinder = new NodeFinder;
  48. $classNode = $nodeFinder->findFirst($this->statements, function (Node $node) use ($className) {
  49. return ($node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Trait_)
  50. && $node->namespacedName->toString() === $className;
  51. });
  52. $res = [];
  53. foreach ($nodeFinder->findInstanceOf($classNode, Node\Stmt\ClassMethod::class) as $methodNode) {
  54. /** @var Node\Stmt\ClassMethod $methodNode */
  55. if ($methodNode->stmts) {
  56. $res[$methodNode->name->toString()] = $this->getReformattedContents($methodNode->stmts, 2);
  57. }
  58. }
  59. return $res;
  60. }
  61. public function extractFunctionBody(string $name): ?string
  62. {
  63. /** @var Node\Stmt\Function_ $functionNode */
  64. $functionNode = (new NodeFinder)->findFirst($this->statements, function (Node $node) use ($name) {
  65. return $node instanceof Node\Stmt\Function_ && $node->namespacedName->toString() === $name;
  66. });
  67. return $this->getReformattedContents($functionNode->stmts, 1);
  68. }
  69. /** @param Node[] $statements */
  70. private function getReformattedContents(array $statements, int $level): string
  71. {
  72. $body = $this->getNodeContents(...$statements);
  73. $body = $this->performReplacements($body, $this->prepareReplacements($statements));
  74. return Helpers::unindent($body, $level);
  75. }
  76. private function prepareReplacements(array $statements): array
  77. {
  78. $start = $statements[0]->getStartFilePos();
  79. $replacements = [];
  80. (new NodeFinder)->find($statements, function (Node $node) use (&$replacements, $start) {
  81. if ($node instanceof Node\Name\FullyQualified) {
  82. if ($node->getAttribute('originalName') instanceof Node\Name) {
  83. $of = $node->getAttribute('parent') instanceof Node\Expr\ConstFetch
  84. ? PhpNamespace::NameConstant
  85. : ($node->getAttribute('parent') instanceof Node\Expr\FuncCall ? PhpNamespace::NameFunction : PhpNamespace::NameNormal);
  86. $replacements[] = [
  87. $node->getStartFilePos() - $start,
  88. $node->getEndFilePos() - $start,
  89. Helpers::tagName($node->toCodeString(), $of),
  90. ];
  91. }
  92. } elseif ($node instanceof Node\Scalar\String_ || $node instanceof Node\Scalar\EncapsedStringPart) {
  93. // multi-line strings => singleline
  94. $token = $this->getNodeContents($node);
  95. if (strpos($token, "\n") !== false) {
  96. $quote = $node instanceof Node\Scalar\String_ ? '"' : '';
  97. $replacements[] = [
  98. $node->getStartFilePos() - $start,
  99. $node->getEndFilePos() - $start,
  100. $quote . addcslashes($node->value, "\x00..\x1F") . $quote,
  101. ];
  102. }
  103. } elseif ($node instanceof Node\Scalar\Encapsed) {
  104. // HEREDOC => "string"
  105. if ($node->getAttribute('kind') === Node\Scalar\String_::KIND_HEREDOC) {
  106. $replacements[] = [
  107. $node->getStartFilePos() - $start,
  108. $node->parts[0]->getStartFilePos() - $start - 1,
  109. '"',
  110. ];
  111. $replacements[] = [
  112. end($node->parts)->getEndFilePos() - $start + 1,
  113. $node->getEndFilePos() - $start,
  114. '"',
  115. ];
  116. }
  117. }
  118. });
  119. return $replacements;
  120. }
  121. private function performReplacements(string $s, array $replacements): string
  122. {
  123. usort($replacements, function ($a, $b) { // sort by position in file
  124. return $b[0] <=> $a[0];
  125. });
  126. foreach ($replacements as [$start, $end, $replacement]) {
  127. $s = substr_replace($s, $replacement, $start, $end - $start + 1);
  128. }
  129. return $s;
  130. }
  131. public function extractAll(): PhpFile
  132. {
  133. $phpFile = new PhpFile;
  134. $namespace = '';
  135. $visitor = new class extends PhpParser\NodeVisitorAbstract {
  136. public $callback;
  137. public function enterNode(Node $node)
  138. {
  139. return ($this->callback)($node);
  140. }
  141. };
  142. $visitor->callback = function (Node $node) use (&$class, &$namespace, $phpFile) {
  143. if ($node instanceof Node\Stmt\DeclareDeclare && $node->key->name === 'strict_types') {
  144. $phpFile->setStrictTypes((bool) $node->value->value);
  145. } elseif ($node instanceof Node\Stmt\Namespace_) {
  146. $namespace = $node->name ? $node->name->toString() : '';
  147. } elseif ($node instanceof Node\Stmt\Use_) {
  148. $this->addUseToNamespace($node, $phpFile->addNamespace($namespace));
  149. } elseif ($node instanceof Node\Stmt\Class_) {
  150. if (!$node->name) {
  151. return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
  152. }
  153. $class = $this->addClassToFile($phpFile, $node);
  154. } elseif ($node instanceof Node\Stmt\Interface_) {
  155. $class = $this->addInterfaceToFile($phpFile, $node);
  156. } elseif ($node instanceof Node\Stmt\Trait_) {
  157. $class = $this->addTraitToFile($phpFile, $node);
  158. } elseif ($node instanceof Node\Stmt\Enum_) {
  159. $class = $this->addEnumToFile($phpFile, $node);
  160. } elseif ($node instanceof Node\Stmt\Function_) {
  161. $this->addFunctionToFile($phpFile, $node);
  162. } elseif ($node instanceof Node\Stmt\TraitUse) {
  163. $this->addTraitToClass($class, $node);
  164. } elseif ($node instanceof Node\Stmt\Property) {
  165. $this->addPropertyToClass($class, $node);
  166. } elseif ($node instanceof Node\Stmt\ClassMethod) {
  167. $this->addMethodToClass($class, $node);
  168. } elseif ($node instanceof Node\Stmt\ClassConst) {
  169. $this->addConstantToClass($class, $node);
  170. } elseif ($node instanceof Node\Stmt\EnumCase) {
  171. $this->addEnumCaseToClass($class, $node);
  172. }
  173. if ($node instanceof Node\FunctionLike) {
  174. return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
  175. }
  176. };
  177. if ($this->statements) {
  178. $this->addCommentAndAttributes($phpFile, $this->statements[0]);
  179. }
  180. $traverser = new PhpParser\NodeTraverser;
  181. $traverser->addVisitor($visitor);
  182. $traverser->traverse($this->statements);
  183. return $phpFile;
  184. }
  185. private function addUseToNamespace(Node\Stmt\Use_ $node, PhpNamespace $namespace): void
  186. {
  187. $of = [
  188. $node::TYPE_NORMAL => PhpNamespace::NameNormal,
  189. $node::TYPE_FUNCTION => PhpNamespace::NameFunction,
  190. $node::TYPE_CONSTANT => PhpNamespace::NameConstant,
  191. ][$node->type];
  192. foreach ($node->uses as $use) {
  193. $namespace->addUse($use->name->toString(), $use->alias ? $use->alias->toString() : null, $of);
  194. }
  195. }
  196. private function addClassToFile(PhpFile $phpFile, Node\Stmt\Class_ $node): ClassType
  197. {
  198. $class = $phpFile->addClass($node->namespacedName->toString());
  199. if ($node->extends) {
  200. $class->setExtends($node->extends->toString());
  201. }
  202. foreach ($node->implements as $item) {
  203. $class->addImplement($item->toString());
  204. }
  205. $class->setFinal($node->isFinal());
  206. $class->setAbstract($node->isAbstract());
  207. $this->addCommentAndAttributes($class, $node);
  208. return $class;
  209. }
  210. private function addInterfaceToFile(PhpFile $phpFile, Node\Stmt\Interface_ $node): ClassType
  211. {
  212. $class = $phpFile->addInterface($node->namespacedName->toString());
  213. foreach ($node->extends as $item) {
  214. $class->addExtend($item->toString());
  215. }
  216. $this->addCommentAndAttributes($class, $node);
  217. return $class;
  218. }
  219. private function addTraitToFile(PhpFile $phpFile, Node\Stmt\Trait_ $node): ClassType
  220. {
  221. $class = $phpFile->addTrait($node->namespacedName->toString());
  222. $this->addCommentAndAttributes($class, $node);
  223. return $class;
  224. }
  225. private function addEnumToFile(PhpFile $phpFile, Node\Stmt\Enum_ $node): ClassType
  226. {
  227. $class = $phpFile->addEnum($node->namespacedName->toString());
  228. foreach ($node->implements as $item) {
  229. $class->addImplement($item->toString());
  230. }
  231. $this->addCommentAndAttributes($class, $node);
  232. return $class;
  233. }
  234. private function addFunctionToFile(PhpFile $phpFile, Node\Stmt\Function_ $node): void
  235. {
  236. $function = $phpFile->addFunction($node->namespacedName->toString());
  237. $this->setupFunction($function, $node);
  238. }
  239. private function addTraitToClass(ClassType $class, Node\Stmt\TraitUse $node): void
  240. {
  241. foreach ($node->traits as $item) {
  242. $trait = $class->addTrait($item->toString(), true);
  243. }
  244. foreach ($node->adaptations as $item) {
  245. $trait->addResolution(trim($this->toPhp($item), ';'));
  246. }
  247. $this->addCommentAndAttributes($trait, $node);
  248. }
  249. private function addPropertyToClass(ClassType $class, Node\Stmt\Property $node): void
  250. {
  251. foreach ($node->props as $item) {
  252. $prop = $class->addProperty($item->name->toString());
  253. $prop->setStatic($node->isStatic());
  254. $prop->setVisibility($this->toVisibility($node->flags));
  255. $prop->setType($node->type ? $this->toPhp($node->type) : null);
  256. if ($item->default) {
  257. $prop->setValue(new Literal($this->getReformattedContents([$item->default], 1)));
  258. }
  259. $prop->setReadOnly(method_exists($node, 'isReadonly') && $node->isReadonly());
  260. $this->addCommentAndAttributes($prop, $node);
  261. }
  262. }
  263. private function addMethodToClass(ClassType $class, Node\Stmt\ClassMethod $node): void
  264. {
  265. $method = $class->addMethod($node->name->toString());
  266. $method->setAbstract($node->isAbstract());
  267. $method->setFinal($node->isFinal());
  268. $method->setStatic($node->isStatic());
  269. $method->setVisibility($this->toVisibility($node->flags));
  270. $this->setupFunction($method, $node);
  271. }
  272. private function addConstantToClass(ClassType $class, Node\Stmt\ClassConst $node): void
  273. {
  274. foreach ($node->consts as $item) {
  275. $value = $this->getReformattedContents([$item->value], 1);
  276. $const = $class->addConstant($item->name->toString(), new Literal($value));
  277. $const->setVisibility($this->toVisibility($node->flags));
  278. $const->setFinal(method_exists($node, 'isFinal') && $node->isFinal());
  279. $this->addCommentAndAttributes($const, $node);
  280. }
  281. }
  282. private function addEnumCaseToClass(ClassType $class, Node\Stmt\EnumCase $node)
  283. {
  284. $case = $class->addCase($node->name->toString(), $node->expr ? $node->expr->value : null);
  285. $this->addCommentAndAttributes($case, $node);
  286. }
  287. private function addCommentAndAttributes($element, Node $node): void
  288. {
  289. if ($node->getDocComment()) {
  290. $comment = $node->getDocComment()->getReformattedText();
  291. $comment = Helpers::unformatDocComment($comment);
  292. $element->setComment($comment);
  293. $node->setDocComment(new PhpParser\Comment\Doc(''));
  294. }
  295. foreach ($node->attrGroups ?? [] as $group) {
  296. foreach ($group->attrs as $attribute) {
  297. $args = [];
  298. foreach ($attribute->args as $arg) {
  299. $value = new Literal($this->getReformattedContents([$arg->value], 0));
  300. if ($arg->name) {
  301. $args[$arg->name->toString()] = $value;
  302. } else {
  303. $args[] = $value;
  304. }
  305. }
  306. $element->addAttribute($attribute->name->toString(), $args);
  307. }
  308. }
  309. }
  310. /**
  311. * @param GlobalFunction|Method $function
  312. */
  313. private function setupFunction($function, Node\FunctionLike $node): void
  314. {
  315. $function->setReturnReference($node->returnsByRef());
  316. $function->setReturnType($node->getReturnType() ? $this->toPhp($node->getReturnType()) : null);
  317. foreach ($node->getParams() as $item) {
  318. $visibility = $this->toVisibility($item->flags);
  319. $isReadonly = (bool) ($item->flags & Node\Stmt\Class_::MODIFIER_READONLY);
  320. $param = $visibility
  321. ? ($function->addPromotedParameter($item->var->name))->setVisibility($visibility)->setReadonly($isReadonly)
  322. : $function->addParameter($item->var->name);
  323. $param->setType($item->type ? $this->toPhp($item->type) : null);
  324. $param->setReference($item->byRef);
  325. $function->setVariadic($item->variadic);
  326. if ($item->default) {
  327. $param->setDefaultValue(new Literal($this->getReformattedContents([$item->default], 2)));
  328. }
  329. $this->addCommentAndAttributes($param, $item);
  330. }
  331. $this->addCommentAndAttributes($function, $node);
  332. if ($node->getStmts()) {
  333. $function->setBody($this->getReformattedContents($node->getStmts(), 2));
  334. }
  335. }
  336. private function toVisibility(int $flags): ?string
  337. {
  338. if ($flags & Node\Stmt\Class_::MODIFIER_PUBLIC) {
  339. return ClassType::VisibilityPublic;
  340. } elseif ($flags & Node\Stmt\Class_::MODIFIER_PROTECTED) {
  341. return ClassType::VisibilityProtected;
  342. } elseif ($flags & Node\Stmt\Class_::MODIFIER_PRIVATE) {
  343. return ClassType::VisibilityPrivate;
  344. }
  345. return null;
  346. }
  347. private function toPhp($value): string
  348. {
  349. return $this->printer->prettyPrint([$value]);
  350. }
  351. private function getNodeContents(Node ...$nodes): string
  352. {
  353. $start = $nodes[0]->getStartFilePos();
  354. return substr($this->code, $start, end($nodes)->getEndFilePos() - $start + 1);
  355. }
  356. }