| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437 |
- <?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 PhpParser;
- use PhpParser\Node;
- use PhpParser\NodeFinder;
- use PhpParser\ParserFactory;
- /**
- * Extracts information from PHP code.
- * @internal
- */
- final class Extractor
- {
- use Nette\SmartObject;
- private $code;
- private $statements;
- private $printer;
- public function __construct(string $code)
- {
- if (!class_exists(ParserFactory::class)) {
- throw new Nette\NotSupportedException("PHP-Parser is required to load method bodies, install package 'nikic/php-parser' 4.7 or newer.");
- }
- $this->printer = new PhpParser\PrettyPrinter\Standard;
- $this->parseCode($code);
- }
- private function parseCode(string $code): void
- {
- if (substr($code, 0, 5) !== '<?php') {
- throw new Nette\InvalidStateException('The input string is not a PHP code.');
- }
- $this->code = str_replace("\r\n", "\n", $code);
- $lexer = new PhpParser\Lexer\Emulative(['usedAttributes' => ['startFilePos', 'endFilePos', 'comments']]);
- $parser = (new ParserFactory)->create(ParserFactory::ONLY_PHP7, $lexer);
- $stmts = $parser->parse($this->code);
- $traverser = new PhpParser\NodeTraverser;
- $traverser->addVisitor(new PhpParser\NodeVisitor\ParentConnectingVisitor);
- $traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver(null, ['preserveOriginalNames' => true]));
- $this->statements = $traverser->traverse($stmts);
- }
- public function extractMethodBodies(string $className): array
- {
- $nodeFinder = new NodeFinder;
- $classNode = $nodeFinder->findFirst($this->statements, function (Node $node) use ($className) {
- return ($node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Trait_)
- && $node->namespacedName->toString() === $className;
- });
- $res = [];
- foreach ($nodeFinder->findInstanceOf($classNode, Node\Stmt\ClassMethod::class) as $methodNode) {
- /** @var Node\Stmt\ClassMethod $methodNode */
- if ($methodNode->stmts) {
- $res[$methodNode->name->toString()] = $this->getReformattedContents($methodNode->stmts, 2);
- }
- }
- return $res;
- }
- public function extractFunctionBody(string $name): ?string
- {
- /** @var Node\Stmt\Function_ $functionNode */
- $functionNode = (new NodeFinder)->findFirst($this->statements, function (Node $node) use ($name) {
- return $node instanceof Node\Stmt\Function_ && $node->namespacedName->toString() === $name;
- });
- return $this->getReformattedContents($functionNode->stmts, 1);
- }
- /** @param Node[] $statements */
- private function getReformattedContents(array $statements, int $level): string
- {
- $body = $this->getNodeContents(...$statements);
- $body = $this->performReplacements($body, $this->prepareReplacements($statements));
- return Helpers::unindent($body, $level);
- }
- private function prepareReplacements(array $statements): array
- {
- $start = $statements[0]->getStartFilePos();
- $replacements = [];
- (new NodeFinder)->find($statements, function (Node $node) use (&$replacements, $start) {
- if ($node instanceof Node\Name\FullyQualified) {
- if ($node->getAttribute('originalName') instanceof Node\Name) {
- $of = $node->getAttribute('parent') instanceof Node\Expr\ConstFetch
- ? PhpNamespace::NameConstant
- : ($node->getAttribute('parent') instanceof Node\Expr\FuncCall ? PhpNamespace::NameFunction : PhpNamespace::NameNormal);
- $replacements[] = [
- $node->getStartFilePos() - $start,
- $node->getEndFilePos() - $start,
- Helpers::tagName($node->toCodeString(), $of),
- ];
- }
- } elseif ($node instanceof Node\Scalar\String_ || $node instanceof Node\Scalar\EncapsedStringPart) {
- // multi-line strings => singleline
- $token = $this->getNodeContents($node);
- if (strpos($token, "\n") !== false) {
- $quote = $node instanceof Node\Scalar\String_ ? '"' : '';
- $replacements[] = [
- $node->getStartFilePos() - $start,
- $node->getEndFilePos() - $start,
- $quote . addcslashes($node->value, "\x00..\x1F") . $quote,
- ];
- }
- } elseif ($node instanceof Node\Scalar\Encapsed) {
- // HEREDOC => "string"
- if ($node->getAttribute('kind') === Node\Scalar\String_::KIND_HEREDOC) {
- $replacements[] = [
- $node->getStartFilePos() - $start,
- $node->parts[0]->getStartFilePos() - $start - 1,
- '"',
- ];
- $replacements[] = [
- end($node->parts)->getEndFilePos() - $start + 1,
- $node->getEndFilePos() - $start,
- '"',
- ];
- }
- }
- });
- return $replacements;
- }
- private function performReplacements(string $s, array $replacements): string
- {
- usort($replacements, function ($a, $b) { // sort by position in file
- return $b[0] <=> $a[0];
- });
- foreach ($replacements as [$start, $end, $replacement]) {
- $s = substr_replace($s, $replacement, $start, $end - $start + 1);
- }
- return $s;
- }
- public function extractAll(): PhpFile
- {
- $phpFile = new PhpFile;
- $namespace = '';
- $visitor = new class extends PhpParser\NodeVisitorAbstract {
- public $callback;
- public function enterNode(Node $node)
- {
- return ($this->callback)($node);
- }
- };
- $visitor->callback = function (Node $node) use (&$class, &$namespace, $phpFile) {
- if ($node instanceof Node\Stmt\DeclareDeclare && $node->key->name === 'strict_types') {
- $phpFile->setStrictTypes((bool) $node->value->value);
- } elseif ($node instanceof Node\Stmt\Namespace_) {
- $namespace = $node->name ? $node->name->toString() : '';
- } elseif ($node instanceof Node\Stmt\Use_) {
- $this->addUseToNamespace($node, $phpFile->addNamespace($namespace));
- } elseif ($node instanceof Node\Stmt\Class_) {
- if (!$node->name) {
- return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
- }
- $class = $this->addClassToFile($phpFile, $node);
- } elseif ($node instanceof Node\Stmt\Interface_) {
- $class = $this->addInterfaceToFile($phpFile, $node);
- } elseif ($node instanceof Node\Stmt\Trait_) {
- $class = $this->addTraitToFile($phpFile, $node);
- } elseif ($node instanceof Node\Stmt\Enum_) {
- $class = $this->addEnumToFile($phpFile, $node);
- } elseif ($node instanceof Node\Stmt\Function_) {
- $this->addFunctionToFile($phpFile, $node);
- } elseif ($node instanceof Node\Stmt\TraitUse) {
- $this->addTraitToClass($class, $node);
- } elseif ($node instanceof Node\Stmt\Property) {
- $this->addPropertyToClass($class, $node);
- } elseif ($node instanceof Node\Stmt\ClassMethod) {
- $this->addMethodToClass($class, $node);
- } elseif ($node instanceof Node\Stmt\ClassConst) {
- $this->addConstantToClass($class, $node);
- } elseif ($node instanceof Node\Stmt\EnumCase) {
- $this->addEnumCaseToClass($class, $node);
- }
- if ($node instanceof Node\FunctionLike) {
- return PhpParser\NodeTraverser::DONT_TRAVERSE_CHILDREN;
- }
- };
- if ($this->statements) {
- $this->addCommentAndAttributes($phpFile, $this->statements[0]);
- }
- $traverser = new PhpParser\NodeTraverser;
- $traverser->addVisitor($visitor);
- $traverser->traverse($this->statements);
- return $phpFile;
- }
- private function addUseToNamespace(Node\Stmt\Use_ $node, PhpNamespace $namespace): void
- {
- $of = [
- $node::TYPE_NORMAL => PhpNamespace::NameNormal,
- $node::TYPE_FUNCTION => PhpNamespace::NameFunction,
- $node::TYPE_CONSTANT => PhpNamespace::NameConstant,
- ][$node->type];
- foreach ($node->uses as $use) {
- $namespace->addUse($use->name->toString(), $use->alias ? $use->alias->toString() : null, $of);
- }
- }
- private function addClassToFile(PhpFile $phpFile, Node\Stmt\Class_ $node): ClassType
- {
- $class = $phpFile->addClass($node->namespacedName->toString());
- if ($node->extends) {
- $class->setExtends($node->extends->toString());
- }
- foreach ($node->implements as $item) {
- $class->addImplement($item->toString());
- }
- $class->setFinal($node->isFinal());
- $class->setAbstract($node->isAbstract());
- $this->addCommentAndAttributes($class, $node);
- return $class;
- }
- private function addInterfaceToFile(PhpFile $phpFile, Node\Stmt\Interface_ $node): ClassType
- {
- $class = $phpFile->addInterface($node->namespacedName->toString());
- foreach ($node->extends as $item) {
- $class->addExtend($item->toString());
- }
- $this->addCommentAndAttributes($class, $node);
- return $class;
- }
- private function addTraitToFile(PhpFile $phpFile, Node\Stmt\Trait_ $node): ClassType
- {
- $class = $phpFile->addTrait($node->namespacedName->toString());
- $this->addCommentAndAttributes($class, $node);
- return $class;
- }
- private function addEnumToFile(PhpFile $phpFile, Node\Stmt\Enum_ $node): ClassType
- {
- $class = $phpFile->addEnum($node->namespacedName->toString());
- foreach ($node->implements as $item) {
- $class->addImplement($item->toString());
- }
- $this->addCommentAndAttributes($class, $node);
- return $class;
- }
- private function addFunctionToFile(PhpFile $phpFile, Node\Stmt\Function_ $node): void
- {
- $function = $phpFile->addFunction($node->namespacedName->toString());
- $this->setupFunction($function, $node);
- }
- private function addTraitToClass(ClassType $class, Node\Stmt\TraitUse $node): void
- {
- foreach ($node->traits as $item) {
- $trait = $class->addTrait($item->toString(), true);
- }
- foreach ($node->adaptations as $item) {
- $trait->addResolution(trim($this->toPhp($item), ';'));
- }
- $this->addCommentAndAttributes($trait, $node);
- }
- private function addPropertyToClass(ClassType $class, Node\Stmt\Property $node): void
- {
- foreach ($node->props as $item) {
- $prop = $class->addProperty($item->name->toString());
- $prop->setStatic($node->isStatic());
- $prop->setVisibility($this->toVisibility($node->flags));
- $prop->setType($node->type ? $this->toPhp($node->type) : null);
- if ($item->default) {
- $prop->setValue(new Literal($this->getReformattedContents([$item->default], 1)));
- }
- $prop->setReadOnly(method_exists($node, 'isReadonly') && $node->isReadonly());
- $this->addCommentAndAttributes($prop, $node);
- }
- }
- private function addMethodToClass(ClassType $class, Node\Stmt\ClassMethod $node): void
- {
- $method = $class->addMethod($node->name->toString());
- $method->setAbstract($node->isAbstract());
- $method->setFinal($node->isFinal());
- $method->setStatic($node->isStatic());
- $method->setVisibility($this->toVisibility($node->flags));
- $this->setupFunction($method, $node);
- }
- private function addConstantToClass(ClassType $class, Node\Stmt\ClassConst $node): void
- {
- foreach ($node->consts as $item) {
- $value = $this->getReformattedContents([$item->value], 1);
- $const = $class->addConstant($item->name->toString(), new Literal($value));
- $const->setVisibility($this->toVisibility($node->flags));
- $const->setFinal(method_exists($node, 'isFinal') && $node->isFinal());
- $this->addCommentAndAttributes($const, $node);
- }
- }
- private function addEnumCaseToClass(ClassType $class, Node\Stmt\EnumCase $node)
- {
- $case = $class->addCase($node->name->toString(), $node->expr ? $node->expr->value : null);
- $this->addCommentAndAttributes($case, $node);
- }
- private function addCommentAndAttributes($element, Node $node): void
- {
- if ($node->getDocComment()) {
- $comment = $node->getDocComment()->getReformattedText();
- $comment = Helpers::unformatDocComment($comment);
- $element->setComment($comment);
- $node->setDocComment(new PhpParser\Comment\Doc(''));
- }
- foreach ($node->attrGroups ?? [] as $group) {
- foreach ($group->attrs as $attribute) {
- $args = [];
- foreach ($attribute->args as $arg) {
- $value = new Literal($this->getReformattedContents([$arg->value], 0));
- if ($arg->name) {
- $args[$arg->name->toString()] = $value;
- } else {
- $args[] = $value;
- }
- }
- $element->addAttribute($attribute->name->toString(), $args);
- }
- }
- }
- /**
- * @param GlobalFunction|Method $function
- */
- private function setupFunction($function, Node\FunctionLike $node): void
- {
- $function->setReturnReference($node->returnsByRef());
- $function->setReturnType($node->getReturnType() ? $this->toPhp($node->getReturnType()) : null);
- foreach ($node->getParams() as $item) {
- $visibility = $this->toVisibility($item->flags);
- $isReadonly = (bool) ($item->flags & Node\Stmt\Class_::MODIFIER_READONLY);
- $param = $visibility
- ? ($function->addPromotedParameter($item->var->name))->setVisibility($visibility)->setReadonly($isReadonly)
- : $function->addParameter($item->var->name);
- $param->setType($item->type ? $this->toPhp($item->type) : null);
- $param->setReference($item->byRef);
- $function->setVariadic($item->variadic);
- if ($item->default) {
- $param->setDefaultValue(new Literal($this->getReformattedContents([$item->default], 2)));
- }
- $this->addCommentAndAttributes($param, $item);
- }
- $this->addCommentAndAttributes($function, $node);
- if ($node->getStmts()) {
- $function->setBody($this->getReformattedContents($node->getStmts(), 2));
- }
- }
- private function toVisibility(int $flags): ?string
- {
- if ($flags & Node\Stmt\Class_::MODIFIER_PUBLIC) {
- return ClassType::VisibilityPublic;
- } elseif ($flags & Node\Stmt\Class_::MODIFIER_PROTECTED) {
- return ClassType::VisibilityProtected;
- } elseif ($flags & Node\Stmt\Class_::MODIFIER_PRIVATE) {
- return ClassType::VisibilityPrivate;
- }
- return null;
- }
- private function toPhp($value): string
- {
- return $this->printer->prettyPrint([$value]);
- }
- private function getNodeContents(Node ...$nodes): string
- {
- $start = $nodes[0]->getStartFilePos();
- return substr($this->code, $start, end($nodes)->getEndFilePos() - $start + 1);
- }
- }
|