PluginProbe
Packeta / 2.1
Packeta v2.1
2.3.2 2.3.1 trunk 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3.0 1.3.1 1.3.2 1.4 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.2 1.5.3 1.5.4 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 All 56 releases
packeta / deps / nette / php-generator / src / PhpGenerator / Extractor.php

Extractor.php in Packeta 2.1, at deps/nette/php-generator/src/PhpGenerator/Extractor.php

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