diff --git a/composer.json b/composer.json index fe3d7dab..30f44ead 100644 --- a/composer.json +++ b/composer.json @@ -16,7 +16,6 @@ ], "require": { "php": ">=7.1", - "ext-tokenizer": "*", "nette/utils": "^2.4.2 || ^3.0" }, "require-dev": { diff --git a/src/PhpGenerator/Dumper.php b/src/PhpGenerator/Dumper.php index bb4c24c7..6faa9560 100644 --- a/src/PhpGenerator/Dumper.php +++ b/src/PhpGenerator/Dumper.php @@ -38,7 +38,7 @@ public function dump($var, int $column = 0): string private function dumpVar(&$var, array $parents = [], int $level = 0, int $column = 0): string { if ($var instanceof Literal) { - return ltrim(Helpers::indentPhp(trim((string) $var), $level), "\t"); + return ltrim(Nette\Utils\Strings::indent(trim((string) $var), $level), "\t"); } elseif ($var === null) { return 'null'; diff --git a/src/PhpGenerator/Factory.php b/src/PhpGenerator/Factory.php index c7f9f91b..5a8f1a44 100644 --- a/src/PhpGenerator/Factory.php +++ b/src/PhpGenerator/Factory.php @@ -192,9 +192,8 @@ private function loadMethodBodies(\ReflectionClass $from): array foreach ($nodeFinder->findInstanceOf($class, Node\Stmt\ClassMethod::class) as $method) { /** @var Node\Stmt\ClassMethod $method */ if ($method->stmts) { - $start = $method->stmts[0]->getAttribute('startFilePos'); - $body = substr($code, $start, end($method->stmts)->getAttribute('endFilePos') - $start + 1); - $bodies[$method->name->toString()] = Helpers::indentPhp($body, -2); + $body = $this->extractBody($nodeFinder, $code, $method->stmts); + $bodies[$method->name->toString()] = Helpers::unindent($body, 2); } } return $bodies; @@ -208,14 +207,89 @@ private function loadFunctionBody(\ReflectionFunction $from): string } [$code, $stmts] = $this->parse($from); + + $nodeFinder = new PhpParser\NodeFinder; /** @var Node\Stmt\Function_ $function */ - $function = (new PhpParser\NodeFinder)->findFirst($stmts, function (Node $node) use ($from) { + $function = $nodeFinder->findFirst($stmts, function (Node $node) use ($from) { return $node instanceof Node\Stmt\Function_ && $node->namespacedName->toString() === $from->name; }); - $start = $function->stmts[0]->getAttribute('startFilePos'); - $body = substr($code, $start, end($function->stmts)->getAttribute('endFilePos') - $start + 1); - return Helpers::indentPhp($body, -1); + $body = $this->extractBody($nodeFinder, $code, $function->stmts); + return Helpers::unindent($body, 1); + } + + + /** + * @param Node[] $statements + */ + private function extractBody(PhpParser\NodeFinder $nodeFinder, string $originalCode, array $statements): string + { + $start = $statements[0]->getAttribute('startFilePos'); + $body = substr($originalCode, $start, end($statements)->getAttribute('endFilePos') - $start + 1); + + $replacements = []; + // name-nodes => resolved fully-qualified name + foreach ($nodeFinder->findInstanceOf($statements, Node\Name::class) as $node) { + if ($node->hasAttribute('resolvedName') + && $node->getAttribute('resolvedName') instanceof Node\Name\FullyQualified + ) { + $replacements[] = [ + $node->getStartFilePos(), + $node->getEndFilePos(), + $node->getAttribute('resolvedName')->toCodeString(), + ]; + } + } + + // multi-line strings => singleline + foreach (array_merge( + $nodeFinder->findInstanceOf($statements, Node\Scalar\String_::class), + $nodeFinder->findInstanceOf($statements, Node\Scalar\EncapsedStringPart::class) + ) as $node) { + $token = substr($body, $node->getStartFilePos() - $start, $node->getEndFilePos() - $node->getStartFilePos() + 1); + if (strpos($token, "\n") !== false) { + $quote = $node instanceof Node\Scalar\String_ ? '"' : ''; + $replacements[] = [ + $node->getStartFilePos(), + $node->getEndFilePos(), + $quote . addcslashes($node->value, "\x00..\x1F") . $quote, + ]; + } + } + + // HEREDOC => "string" + foreach ($nodeFinder->findInstanceOf($statements, Node\Scalar\Encapsed::class) as $node) { + if ($node->getAttribute('kind') === Node\Scalar\String_::KIND_HEREDOC) { + $replacements[] = [ + $node->getStartFilePos(), + $node->parts[0]->getStartFilePos() - 1, + '"', + ]; + $replacements[] = [ + end($node->parts)->getEndFilePos() + 1, + $node->getEndFilePos(), + '"', + ]; + } + } + + //sort collected resolved names by position in file + usort($replacements, function ($a, $b) { + return $a[0] <=> $b[0]; + }); + $correctiveOffset = -$start; + //replace changes body length so we need correct offset + foreach ($replacements as [$startPos, $endPos, $replacement]) { + $replacingStringLength = $endPos - $startPos + 1; + $body = substr_replace( + $body, + $replacement, + $correctiveOffset + $startPos, + $replacingStringLength + ); + $correctiveOffset += strlen($replacement) - $replacingStringLength; + } + return $body; } @@ -235,7 +309,7 @@ private function parse($from): array $stmts = $parser->parse($code); $traverser = new PhpParser\NodeTraverser; - $traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver); + $traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver(null, ['replaceNodes' => false])); $stmts = $traverser->traverse($stmts); return [$code, $stmts]; diff --git a/src/PhpGenerator/Helpers.php b/src/PhpGenerator/Helpers.php index 898f633f..24a8f444 100644 --- a/src/PhpGenerator/Helpers.php +++ b/src/PhpGenerator/Helpers.php @@ -61,32 +61,9 @@ public static function unformatDocComment(string $comment): string } - public static function indentPhp(string $s, int $level = 1, string $chars = "\t"): string + public static function unindent(string $s, int $level = 1): string { - $tbl = []; - $s = str_replace("\r\n", "\n", $s); - - if ($level && strpos($s, "\n") !== false && preg_match('#\?>|<<<|"|\'#', $s)) { - static $save = [T_CONSTANT_ENCAPSED_STRING => 1, T_ENCAPSED_AND_WHITESPACE => 1, T_INLINE_HTML => 1, T_START_HEREDOC => 1, T_CLOSE_TAG => 1]; - $tokens = token_get_all(" 0) { - $s = Nette\Utils\Strings::indent($s, $level, $chars); - } elseif ($level < 0) { - $s = preg_replace('#^(\t|\ \ \ \ ){1,' . (-$level) . '}#m', '', $s); - } - return strtr($s, $tbl); + return preg_replace('#^(\t|\ \ \ \ ){1,' . $level . '}#m', '', $s); } diff --git a/src/PhpGenerator/PhpNamespace.php b/src/PhpGenerator/PhpNamespace.php index 8276b260..1447f6f5 100644 --- a/src/PhpGenerator/PhpNamespace.php +++ b/src/PhpGenerator/PhpNamespace.php @@ -28,7 +28,8 @@ final class PhpNamespace private const KEYWORDS = [ 'string' => 1, 'int' => 1, 'float' => 1, 'bool' => 1, 'array' => 1, 'object' => 1, - 'callable' => 1, 'iterable' => 1, 'void' => 1, 'self' => 1, 'parent' => 1, + 'callable' => 1, 'iterable' => 1, 'void' => 1, 'self' => 1, 'parent' => 1, 'static' => 1, + 'mixed' => 1, 'null' => 1, 'false' => 1, ]; /** @var string */ diff --git a/src/PhpGenerator/Printer.php b/src/PhpGenerator/Printer.php index 618017cd..23dc76ca 100644 --- a/src/PhpGenerator/Printer.php +++ b/src/PhpGenerator/Printer.php @@ -23,9 +23,15 @@ class Printer /** @var string */ protected $indentation = "\t"; + /** @var int */ + protected $linesBetweenProperties = 0; + /** @var int */ protected $linesBetweenMethods = 2; + /** @var string */ + protected $returnTypeColon = ': '; + /** @var bool */ private $resolveTypes = true; @@ -138,8 +144,8 @@ public function printClass(ClassType $class, PhpNamespace $namespace = null): st $members = array_filter([ implode('', $traits), - preg_replace('#^(\w.*\n)\n(?=\w.*;)#m', '$1', implode("\n", $consts)), - preg_replace('#^(\w.*\n)\n(?=\w.*;)#m', '$1', implode("\n", $properties)), + $this->joinProperties($consts), + $this->joinProperties($properties), ($methods && $properties ? str_repeat("\n", $this->linesBetweenMethods - 1) : '') . implode(str_repeat("\n", $this->linesBetweenMethods), $methods), ]); @@ -211,7 +217,7 @@ public function setTypeResolving(bool $state = true): self protected function indent(string $s): string { $s = str_replace("\t", $this->indentation, $s); - return Helpers::indentPhp($s, 1, $this->indentation); + return Strings::indent($s, 1, $this->indentation); } @@ -275,7 +281,15 @@ public function printType(?string $type, bool $nullable = false, PhpNamespace $n private function printReturnType($function, ?PhpNamespace $namespace): string { return ($tmp = $this->printType($function->getReturnType(), $function->isReturnNullable(), $namespace)) - ? ': ' . $tmp + ? $this->returnTypeColon . $tmp : ''; } + + + private function joinProperties(array $props) + { + return $this->linesBetweenProperties + ? implode(str_repeat("\n", $this->linesBetweenProperties), $props) + : preg_replace('#^(\w.*\n)\n(?=\w.*;)#m', '$1', implode("\n", $props)); + } } diff --git a/src/PhpGenerator/Type.php b/src/PhpGenerator/Type.php index e82c7e28..d320bf3e 100644 --- a/src/PhpGenerator/Type.php +++ b/src/PhpGenerator/Type.php @@ -25,9 +25,12 @@ class Type CALLABLE = 'callable', ITERABLE = 'iterable', VOID = 'void', + MIXED = 'mixed', + FALSE = 'false', NULL = 'null', SELF = 'self', - PARENT = 'parent'; + PARENT = 'parent', + STATIC = 'static'; public static function nullable(string $type, bool $state = true): string diff --git a/tests/PhpGenerator/GlobalFunction.phpt b/tests/PhpGenerator/GlobalFunction.phpt index a8105ef0..47657522 100644 --- a/tests/PhpGenerator/GlobalFunction.phpt +++ b/tests/PhpGenerator/GlobalFunction.phpt @@ -11,7 +11,7 @@ require __DIR__ . '/../bootstrap.php'; /** global */ function func(stdClass $a, $b = null) { - echo 'hello'; + echo sprintf('hello, %s', 'world'); return 1; } @@ -28,13 +28,15 @@ function func(stdClass $a, $b = null) $function = GlobalFunction::withBodyFrom('func'); -same( -'/** +same(<<<'XX' +/** * global */ function func(stdClass $a, $b = null) { - echo \'hello\'; + echo \sprintf('hello, %s', 'world'); return 1; } -', (string) $function); + +XX +, (string) $function); diff --git a/tests/PhpGenerator/Helpers.indentPhp.phpt b/tests/PhpGenerator/Helpers.indentPhp.phpt deleted file mode 100644 index 77be559d..00000000 --- a/tests/PhpGenerator/Helpers.indentPhp.phpt +++ /dev/null @@ -1,117 +0,0 @@ - -a - b - -a - b - -a - b - -a - b - printClass($class sameFile(__DIR__ . '/expected/Printer.method.expect', $printer->printMethod($class->getMethod('first'))); +Assert::with($printer, function () { + $this->linesBetweenProperties = 1; + $this->linesBetweenMethods = 3; +}); +sameFile(__DIR__ . '/expected/Printer.class-alt.expect', $printer->printClass($class)); + + + $function = new Nette\PhpGenerator\GlobalFunction('func'); $function ->setReturnType('stdClass') diff --git a/tests/PhpGenerator/expected/ClassType.from.bodies.expect b/tests/PhpGenerator/expected/ClassType.from.bodies.expect index 3697fadd..729902bc 100644 --- a/tests/PhpGenerator/expected/ClassType.from.bodies.expect +++ b/tests/PhpGenerator/expected/ClassType.from.bodies.expect @@ -27,13 +27,13 @@ abstract class Class7 public function long() { - if ($member instanceof Method) { + if ($member instanceof \Abc\Method) { $s = [1, 2, 3]; } /* $this->methods[$member->getName()] = $member; */ - throw new Nette\InvalidArgumentException('Argument must be Method|Property|Constant.'); + throw new \Nette\InvalidArgumentException('Argument must be Method|Property|Constant.'); } @@ -56,37 +56,22 @@ abstract class Class7 /** multi line comment */ + // Alias Method will not be resolved in comment + if ($member instanceof \Abc\Method) { + $s1 = "\na\n\tb\n\t\tc\n"; + $s2 = "\na\n\t{$b}\n\t\t$c\n"; - if ($member instanceof Method) { - $s1 = ' -a - b - c -'; - $s2 = " -a - {$b} - $c -"; - - $s3 = << -a - b + a + b c 1, + 'bbbbbbbb' => 2, + 'cccccccc' => 3, + 'dddddddd' => 4, + 'eeeeeeee' => 5, + 'ffffffff' => 6, + ]; + + const SHORT = ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5, 'ffffffff' => 6]; + + /** @var resource orignal file handle */ + private $handle; + + public $order = RecursiveIteratorIterator::SELF_FIRST; + + public $multilineLong = [ + 'aaaaaaaa' => 1, + 'bbbbbbbb' => 2, + 'cccccccc' => 3, + 'dddddddd' => 4, + 'eeeeeeee' => 5, + 'ffffffff' => 6, + ]; + + public $short = ['aaaaaaaa' => 1, 'bbbbbbbb' => 2, 'cccccccc' => 3, 'dddddddd' => 4, 'eeeeeeee' => 5, 'ffffffff' => 6]; + + + + /** + * @return resource + */ + final public function first(stdClass $var): stdClass + { + func(); + return [ + 'aaaaaaaaaaaa' => 1, + 'bbbbbbbbbbb' => 2, + 'cccccccccccccc' => 3, + 'dddddddddddd' => 4, + 'eeeeeeeeeeee' => 5, + 'ffffffff' => 6, + ]; + } + + + + public function second() + { + } +} diff --git a/tests/PhpGenerator/fixtures/class-body.phpf b/tests/PhpGenerator/fixtures/class-body.phpf index 0f5dcea5..5b710d56 100644 --- a/tests/PhpGenerator/fixtures/class-body.phpf +++ b/tests/PhpGenerator/fixtures/class-body.phpf @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Abc; +use Nette; + abstract class Class7 { abstract function abstractFun(); @@ -51,7 +53,7 @@ abstract class Class7 /** multi line comment */ - + // Alias Method will not be resolved in comment if ($member instanceof Method) { $s1 = ' a @@ -76,6 +78,7 @@ a c DOC ; + // inline HTML is not supported ?> a b