Skip to content

Commit 249f307

Browse files
committed
Factory: refactoring, moved parser to Extractor
1 parent b35f406 commit 249f307

File tree

5 files changed

+253
-142
lines changed

5 files changed

+253
-142
lines changed

src/PhpGenerator/Extractor.php

Lines changed: 160 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,160 @@
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+
8+
declare(strict_types=1);
9+
10+
namespace Nette\PhpGenerator;
11+
12+
use Nette;
13+
use PhpParser;
14+
use PhpParser\Node;
15+
use PhpParser\NodeFinder;
16+
use PhpParser\ParserFactory;
17+
18+
19+
/**
20+
* Extracts information from PHP code.
21+
* @internal
22+
*/
23+
final class Extractor
24+
{
25+
use Nette\SmartObject;
26+
27+
private $code;
28+
private $statements;
29+
30+
31+
public function __construct(string $code)
32+
{
33+
if (!class_exists(ParserFactory::class)) {
34+
throw new Nette\NotSupportedException("PHP-Parser is required to load method bodies, install package 'nikic/php-parser'.");
35+
}
36+
$this->parseCode($code);
37+
}
38+
39+
40+
private function parseCode(string $code): void
41+
{
42+
$this->code = str_replace("\r\n", "\n", $code);
43+
$lexer = new PhpParser\Lexer(['usedAttributes' => ['startFilePos', 'endFilePos']]);
44+
$parser = (new ParserFactory)->create(ParserFactory::ONLY_PHP7, $lexer);
45+
$stmts = $parser->parse($this->code);
46+
47+
$traverser = new PhpParser\NodeTraverser;
48+
$traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver(null, ['replaceNodes' => false]));
49+
$this->statements = $traverser->traverse($stmts);
50+
}
51+
52+
53+
public function extractMethodBodies(string $className): array
54+
{
55+
$nodeFinder = new NodeFinder;
56+
$classNode = $nodeFinder->findFirst($this->statements, function (Node $node) use ($className) {
57+
return ($node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Trait_)
58+
&& $node->namespacedName->toString() === $className;
59+
});
60+
61+
$res = [];
62+
foreach ($nodeFinder->findInstanceOf($classNode, Node\Stmt\ClassMethod::class) as $methodNode) {
63+
/** @var Node\Stmt\ClassMethod $methodNode */
64+
if ($methodNode->stmts) {
65+
$body = $this->extractBody($nodeFinder, $methodNode->stmts);
66+
$res[$methodNode->name->toString()] = Helpers::unindent($body, 2);
67+
}
68+
}
69+
return $res;
70+
}
71+
72+
73+
public function extractFunctionBody(string $name): ?string
74+
{
75+
$nodeFinder = new NodeFinder;
76+
/** @var Node\Stmt\Function_ $functionNode */
77+
$functionNode = $nodeFinder->findFirst($this->statements, function (Node $node) use ($name) {
78+
return $node instanceof Node\Stmt\Function_ && $node->namespacedName->toString() === $name;
79+
});
80+
81+
$body = $this->extractBody($nodeFinder, $functionNode->stmts);
82+
return Helpers::unindent($body, 1);
83+
}
84+
85+
86+
/**
87+
* @param Node[] $statements
88+
*/
89+
private function extractBody(NodeFinder $nodeFinder, array $statements): string
90+
{
91+
$start = $statements[0]->getAttribute('startFilePos');
92+
$body = substr($this->code, $start, end($statements)->getAttribute('endFilePos') - $start + 1);
93+
94+
$replacements = [];
95+
// name-nodes => resolved fully-qualified name
96+
foreach ($nodeFinder->findInstanceOf($statements, Node\Name::class) as $node) {
97+
if ($node->hasAttribute('resolvedName')
98+
&& $node->getAttribute('resolvedName') instanceof Node\Name\FullyQualified
99+
) {
100+
$replacements[] = [
101+
$node->getStartFilePos(),
102+
$node->getEndFilePos(),
103+
$node->getAttribute('resolvedName')->toCodeString(),
104+
];
105+
}
106+
}
107+
108+
// multi-line strings => singleline
109+
foreach (array_merge(
110+
$nodeFinder->findInstanceOf($statements, Node\Scalar\String_::class),
111+
$nodeFinder->findInstanceOf($statements, Node\Scalar\EncapsedStringPart::class)
112+
) as $node) {
113+
/** @var Node\Scalar\String_|Node\Scalar\EncapsedStringPart $node */
114+
$token = substr($body, $node->getStartFilePos() - $start, $node->getEndFilePos() - $node->getStartFilePos() + 1);
115+
if (strpos($token, "\n") !== false) {
116+
$quote = $node instanceof Node\Scalar\String_ ? '"' : '';
117+
$replacements[] = [
118+
$node->getStartFilePos(),
119+
$node->getEndFilePos(),
120+
$quote . addcslashes($node->value, "\x00..\x1F") . $quote,
121+
];
122+
}
123+
}
124+
125+
// HEREDOC => "string"
126+
foreach ($nodeFinder->findInstanceOf($statements, Node\Scalar\Encapsed::class) as $node) {
127+
/** @var Node\Scalar\Encapsed $node */
128+
if ($node->getAttribute('kind') === Node\Scalar\String_::KIND_HEREDOC) {
129+
$replacements[] = [
130+
$node->getStartFilePos(),
131+
$node->parts[0]->getStartFilePos() - 1,
132+
'"',
133+
];
134+
$replacements[] = [
135+
end($node->parts)->getEndFilePos() + 1,
136+
$node->getEndFilePos(),
137+
'"',
138+
];
139+
}
140+
}
141+
142+
//sort collected resolved names by position in file
143+
usort($replacements, function ($a, $b) {
144+
return $a[0] <=> $b[0];
145+
});
146+
$correctiveOffset = -$start;
147+
//replace changes body length so we need correct offset
148+
foreach ($replacements as [$startPos, $endPos, $replacement]) {
149+
$replacingStringLength = $endPos - $startPos + 1;
150+
$body = substr_replace(
151+
$body,
152+
$replacement,
153+
$correctiveOffset + $startPos,
154+
$replacingStringLength
155+
);
156+
$correctiveOffset += strlen($replacement) - $replacingStringLength;
157+
}
158+
return $body;
159+
}
160+
}

src/PhpGenerator/Factory.php

Lines changed: 15 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,6 @@
1010
namespace Nette\PhpGenerator;
1111

1212
use Nette;
13-
use PhpParser;
14-
use PhpParser\Node;
15-
use PhpParser\ParserFactory;
1613

1714

1815
/**
@@ -24,6 +21,10 @@ final class Factory
2421

2522
public function fromClassReflection(\ReflectionClass $from, bool $withBodies = false): ClassType
2623
{
24+
if ($withBodies && $from->isAnonymous()) {
25+
throw new Nette\NotSupportedException('The $withBodies parameter cannot be used for anonymous functions.');
26+
}
27+
2728
$class = $from->isAnonymous()
2829
? new ClassType
2930
: new ClassType($from->getShortName(), new PhpNamespace($from->getNamespaceName()));
@@ -80,8 +81,8 @@ public function fromClassReflection(\ReflectionClass $from, bool $withBodies = f
8081
$methods[] = $m = $this->fromMethodReflection($method);
8182
if ($withBodies) {
8283
$srcMethod = Nette\Utils\Reflection::getMethodDeclaringMethod($method);
83-
$srcClass = $srcMethod->getDeclaringClass()->name;
84-
$b = $bodies[$srcClass] = $bodies[$srcClass] ?? $this->loadMethodBodies($srcMethod->getDeclaringClass());
84+
$srcClass = $srcMethod->getDeclaringClass();
85+
$b = $bodies[$srcClass->name] = $bodies[$srcClass->name] ?? $this->getExtractor($srcClass)->extractMethodBodies($srcClass->name);
8586
if (isset($b[$srcMethod->name])) {
8687
$m->setBody($b[$srcMethod->name]);
8788
}
@@ -152,7 +153,12 @@ public function fromFunctionReflection(\ReflectionFunction $from, bool $withBody
152153
) {
153154
$function->setReturnType((string) $from->getReturnType());
154155
}
155-
$function->setBody($withBody ? $this->loadFunctionBody($from) : '');
156+
if ($withBody) {
157+
if ($from->isClosure()) {
158+
throw new Nette\NotSupportedException('The $withBody parameter cannot be used for closures.');
159+
}
160+
$function->setBody($this->getExtractor($from)->extractFunctionBody($from->name));
161+
}
156162
return $function;
157163
}
158164

@@ -262,144 +268,12 @@ private function getVisibility($from): string
262268
}
263269

264270

265-
private function loadMethodBodies(\ReflectionClass $from): array
266-
{
267-
if ($from->isAnonymous()) {
268-
throw new Nette\NotSupportedException('Anonymous classes are not supported.');
269-
}
270-
271-
[$code, $stmts] = $this->parse($from);
272-
$nodeFinder = new PhpParser\NodeFinder;
273-
$class = $nodeFinder->findFirst($stmts, function (Node $node) use ($from) {
274-
return ($node instanceof Node\Stmt\Class_ || $node instanceof Node\Stmt\Trait_) && $node->namespacedName->toString() === $from->name;
275-
});
276-
277-
$bodies = [];
278-
foreach ($nodeFinder->findInstanceOf($class, Node\Stmt\ClassMethod::class) as $method) {
279-
/** @var Node\Stmt\ClassMethod $method */
280-
if ($method->stmts) {
281-
$body = $this->extractBody($nodeFinder, $code, $method->stmts);
282-
$bodies[$method->name->toString()] = Helpers::unindent($body, 2);
283-
}
284-
}
285-
return $bodies;
286-
}
287-
288-
289-
private function loadFunctionBody(\ReflectionFunction $from): string
290-
{
291-
if ($from->isClosure()) {
292-
throw new Nette\NotSupportedException('Closures are not supported.');
293-
}
294-
295-
[$code, $stmts] = $this->parse($from);
296-
297-
$nodeFinder = new PhpParser\NodeFinder;
298-
/** @var Node\Stmt\Function_ $function */
299-
$function = $nodeFinder->findFirst($stmts, function (Node $node) use ($from) {
300-
return $node instanceof Node\Stmt\Function_ && $node->namespacedName->toString() === $from->name;
301-
});
302-
303-
$body = $this->extractBody($nodeFinder, $code, $function->stmts);
304-
return Helpers::unindent($body, 1);
305-
}
306-
307-
308-
/**
309-
* @param Node[] $statements
310-
*/
311-
private function extractBody(PhpParser\NodeFinder $nodeFinder, string $originalCode, array $statements): string
312-
{
313-
$start = $statements[0]->getAttribute('startFilePos');
314-
$body = substr($originalCode, $start, end($statements)->getAttribute('endFilePos') - $start + 1);
315-
316-
$replacements = [];
317-
// name-nodes => resolved fully-qualified name
318-
foreach ($nodeFinder->findInstanceOf($statements, Node\Name::class) as $node) {
319-
if ($node->hasAttribute('resolvedName')
320-
&& $node->getAttribute('resolvedName') instanceof Node\Name\FullyQualified
321-
) {
322-
$replacements[] = [
323-
$node->getStartFilePos(),
324-
$node->getEndFilePos(),
325-
$node->getAttribute('resolvedName')->toCodeString(),
326-
];
327-
}
328-
}
329-
330-
// multi-line strings => singleline
331-
foreach (array_merge(
332-
$nodeFinder->findInstanceOf($statements, Node\Scalar\String_::class),
333-
$nodeFinder->findInstanceOf($statements, Node\Scalar\EncapsedStringPart::class)
334-
) as $node) {
335-
/** @var Node\Scalar\String_|Node\Scalar\EncapsedStringPart $node */
336-
$token = substr($body, $node->getStartFilePos() - $start, $node->getEndFilePos() - $node->getStartFilePos() + 1);
337-
if (strpos($token, "\n") !== false) {
338-
$quote = $node instanceof Node\Scalar\String_ ? '"' : '';
339-
$replacements[] = [
340-
$node->getStartFilePos(),
341-
$node->getEndFilePos(),
342-
$quote . addcslashes($node->value, "\x00..\x1F") . $quote,
343-
];
344-
}
345-
}
346-
347-
// HEREDOC => "string"
348-
foreach ($nodeFinder->findInstanceOf($statements, Node\Scalar\Encapsed::class) as $node) {
349-
/** @var Node\Scalar\Encapsed $node */
350-
if ($node->getAttribute('kind') === Node\Scalar\String_::KIND_HEREDOC) {
351-
$replacements[] = [
352-
$node->getStartFilePos(),
353-
$node->parts[0]->getStartFilePos() - 1,
354-
'"',
355-
];
356-
$replacements[] = [
357-
end($node->parts)->getEndFilePos() + 1,
358-
$node->getEndFilePos(),
359-
'"',
360-
];
361-
}
362-
}
363-
364-
//sort collected resolved names by position in file
365-
usort($replacements, function ($a, $b) {
366-
return $a[0] <=> $b[0];
367-
});
368-
$correctiveOffset = -$start;
369-
//replace changes body length so we need correct offset
370-
foreach ($replacements as [$startPos, $endPos, $replacement]) {
371-
$replacingStringLength = $endPos - $startPos + 1;
372-
$body = substr_replace(
373-
$body,
374-
$replacement,
375-
$correctiveOffset + $startPos,
376-
$replacingStringLength
377-
);
378-
$correctiveOffset += strlen($replacement) - $replacingStringLength;
379-
}
380-
return $body;
381-
}
382-
383-
384-
private function parse($from): array
271+
private function getExtractor($from): Extractor
385272
{
386273
$file = $from->getFileName();
387-
if (!class_exists(ParserFactory::class)) {
388-
throw new Nette\NotSupportedException("PHP-Parser is required to load method bodies, install package 'nikic/php-parser'.");
389-
} elseif (!$file) {
274+
if (!$file) {
390275
throw new Nette\InvalidStateException("Source code of $from->name not found.");
391276
}
392-
393-
$lexer = new PhpParser\Lexer(['usedAttributes' => ['startFilePos', 'endFilePos']]);
394-
$parser = (new ParserFactory)->create(ParserFactory::ONLY_PHP7, $lexer);
395-
$code = file_get_contents($file);
396-
$code = str_replace("\r\n", "\n", $code);
397-
$stmts = $parser->parse($code);
398-
399-
$traverser = new PhpParser\NodeTraverser;
400-
$traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver(null, ['replaceNodes' => false]));
401-
$stmts = $traverser->traverse($stmts);
402-
403-
return [$code, $stmts];
277+
return new Extractor(file_get_contents($file));
404278
}
405279
}

tests/PhpGenerator/ClassType.from.bodies.phpt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ Assert::exception(function () {
2121
{
2222
}
2323
});
24-
}, Nette\NotSupportedException::class, 'Anonymous classes are not supported.');
24+
}, Nette\NotSupportedException::class, 'The $withBodies parameter cannot be used for anonymous functions.');
2525

2626

2727
$res = ClassType::withBodiesFrom(Abc\Class7::class);
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
use Nette\PhpGenerator\Extractor;
6+
use Tester\Assert;
7+
8+
9+
require __DIR__ . '/../bootstrap.php';
10+
11+
12+
$extractor = new Extractor('<?php
13+
namespace NS;
14+
15+
function bar1()
16+
{
17+
$a = 10;
18+
echo 123;
19+
}
20+
21+
function bar2()
22+
{
23+
echo "hello";
24+
}
25+
');
26+
27+
Assert::match(
28+
"\$a = 10;\necho 123;",
29+
$extractor->extractFunctionBody('NS\bar1')
30+
);

0 commit comments

Comments
 (0)