• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nette / php-generator / 6544644916

17 Oct 2023 08:36AM UTC coverage: 93.484% (-0.004%) from 93.488%
6544644916

push

github

dg
Extractor: extracts native PHP values

30 of 30 new or added lines in 1 file covered. (100.0%)

1492 of 1596 relevant lines covered (93.48%)

0.93 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.84
/src/PhpGenerator/Extractor.php
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
        private string $code;
26

27
        /** @var Node[] */
28
        private array $statements;
29
        private PhpParser\PrettyPrinterAbstract $printer;
30

31

32
        public function __construct(string $code)
1✔
33
        {
34
                if (!class_exists(ParserFactory::class)) {
1✔
35
                        throw new Nette\NotSupportedException("PHP-Parser is required to load method bodies, install package 'nikic/php-parser' 4.7 or newer.");
×
36
                }
37

38
                $this->printer = new PhpParser\PrettyPrinter\Standard;
1✔
39
                $this->parseCode($code);
1✔
40
        }
1✔
41

42

43
        private function parseCode(string $code): void
1✔
44
        {
45
                if (!str_starts_with($code, '<?php')) {
1✔
46
                        throw new Nette\InvalidStateException('The input string is not a PHP code.');
1✔
47
                }
48

49
                $this->code = Nette\Utils\Strings::normalizeNewlines($code);
1✔
50
                $lexer = new PhpParser\Lexer\Emulative(['usedAttributes' => ['startFilePos', 'endFilePos', 'comments']]);
1✔
51
                $parser = (new ParserFactory)->create(ParserFactory::ONLY_PHP7, $lexer);
1✔
52
                $stmts = $parser->parse($this->code);
1✔
53

54
                $traverser = new PhpParser\NodeTraverser;
1✔
55
                $traverser->addVisitor(new PhpParser\NodeVisitor\ParentConnectingVisitor);
1✔
56
                $traverser->addVisitor(new PhpParser\NodeVisitor\NameResolver(null, ['preserveOriginalNames' => true]));
1✔
57
                $this->statements = $traverser->traverse($stmts);
1✔
58
        }
1✔
59

60

61
        /** @return array<string, string> */
62
        public function extractMethodBodies(string $className): array
1✔
63
        {
64
                $nodeFinder = new NodeFinder;
1✔
65
                $classNode = $nodeFinder->findFirst(
1✔
66
                        $this->statements,
1✔
67
                        fn(Node $node) => $node instanceof Node\Stmt\ClassLike && $node->namespacedName->toString() === $className,
1✔
68
                );
69

70
                $res = [];
1✔
71
                foreach ($nodeFinder->findInstanceOf($classNode, Node\Stmt\ClassMethod::class) as $methodNode) {
1✔
72
                        assert($methodNode instanceof Node\Stmt\ClassMethod);
73
                        if ($methodNode->stmts) {
1✔
74
                                $res[$methodNode->name->toString()] = $this->getReformattedContents($methodNode->stmts, 2);
1✔
75
                        }
76
                }
77

78
                return $res;
1✔
79
        }
80

81

82
        public function extractFunctionBody(string $name): ?string
1✔
83
        {
84
                $functionNode = (new NodeFinder)->findFirst(
1✔
85
                        $this->statements,
1✔
86
                        fn(Node $node) => $node instanceof Node\Stmt\Function_ && $node->namespacedName->toString() === $name,
1✔
87
                );
88
                assert($functionNode instanceof Node\Stmt\Function_);
89

90
                return $this->getReformattedContents($functionNode->stmts, 1);
1✔
91
        }
92

93

94
        /** @param  Node[]  $nodes */
95
        private function getReformattedContents(array $nodes, int $level): string
1✔
96
        {
97
                $body = $this->getNodeContents(...$nodes);
1✔
98
                $body = $this->performReplacements($body, $this->prepareReplacements($nodes));
1✔
99
                return Helpers::unindent($body, $level);
1✔
100
        }
101

102

103
        /**
104
         * @param  Node[]  $nodes
105
         * @return array<array{int, int, string}>
106
         */
107
        private function prepareReplacements(array $nodes): array
1✔
108
        {
109
                $start = $this->getNodeStartPos($nodes[0]);
1✔
110
                $replacements = [];
1✔
111
                (new NodeFinder)->find($nodes, function (Node $node) use (&$replacements, $start) {
1✔
112
                        if ($node instanceof Node\Name\FullyQualified) {
1✔
113
                                if ($node->getAttribute('originalName') instanceof Node\Name) {
1✔
114
                                        $of = match (true) {
1✔
115
                                                $node->getAttribute('parent') instanceof Node\Expr\ConstFetch => PhpNamespace::NameConstant,
1✔
116
                                                $node->getAttribute('parent') instanceof Node\Expr\FuncCall => PhpNamespace::NameFunction,
1✔
117
                                                default => PhpNamespace::NameNormal,
1✔
118
                                        };
119
                                        $replacements[] = [
1✔
120
                                                $node->getStartFilePos() - $start,
1✔
121
                                                $node->getEndFilePos() - $start,
1✔
122
                                                Helpers::tagName($node->toCodeString(), $of),
1✔
123
                                        ];
124
                                }
125
                        } elseif ($node instanceof Node\Scalar\String_ || $node instanceof Node\Scalar\EncapsedStringPart) {
1✔
126
                                // multi-line strings => singleline
127
                                $token = $this->getNodeContents($node);
1✔
128
                                if (str_contains($token, "\n")) {
1✔
129
                                        $quote = $node instanceof Node\Scalar\String_ ? '"' : '';
1✔
130
                                        $replacements[] = [
1✔
131
                                                $node->getStartFilePos() - $start,
1✔
132
                                                $node->getEndFilePos() - $start,
1✔
133
                                                $quote . addcslashes($node->value, "\x00..\x1F") . $quote,
1✔
134
                                        ];
135
                                }
136
                        } elseif ($node instanceof Node\Scalar\Encapsed) {
1✔
137
                                // HEREDOC => "string"
138
                                if ($node->getAttribute('kind') === Node\Scalar\String_::KIND_HEREDOC) {
1✔
139
                                        $replacements[] = [
1✔
140
                                                $node->getStartFilePos() - $start,
1✔
141
                                                $node->parts[0]->getStartFilePos() - $start - 1,
1✔
142
                                                '"',
1✔
143
                                        ];
144
                                        $replacements[] = [
1✔
145
                                                end($node->parts)->getEndFilePos() - $start + 1,
1✔
146
                                                $node->getEndFilePos() - $start,
1✔
147
                                                '"',
1✔
148
                                        ];
149
                                }
150
                        }
151
                });
1✔
152
                return $replacements;
1✔
153
        }
154

155

156
        /** @param  array<array{int, int, string}>  $replacements */
157
        private function performReplacements(string $s, array $replacements): string
1✔
158
        {
159
                usort($replacements, fn($a, $b) => $b[0] <=> $a[0]);
1✔
160

161
                foreach ($replacements as [$start, $end, $replacement]) {
1✔
162
                        $s = substr_replace($s, $replacement, $start, $end - $start + 1);
1✔
163
                }
164

165
                return $s;
1✔
166
        }
167

168

169
        public function extractAll(): PhpFile
170
        {
171
                $phpFile = new PhpFile;
1✔
172

173
                if (
174
                        $this->statements
1✔
175
                        && !$this->statements[0] instanceof Node\Stmt\ClassLike
1✔
176
                        && !$this->statements[0] instanceof Node\Stmt\Function_
1✔
177
                ) {
178
                        $this->addCommentAndAttributes($phpFile, $this->statements[0]);
1✔
179
                }
180

181
                $namespaces = ['' => $this->statements];
1✔
182
                foreach ($this->statements as $node) {
1✔
183
                        if ($node instanceof Node\Stmt\Declare_
1✔
184
                                && $node->declares[0] instanceof Node\Stmt\DeclareDeclare
1✔
185
                                && $node->declares[0]->key->name === 'strict_types'
1✔
186
                                && $node->declares[0]->value instanceof Node\Scalar\LNumber
1✔
187
                        ) {
188
                                $phpFile->setStrictTypes((bool) $node->declares[0]->value->value);
1✔
189

190
                        } elseif ($node instanceof Node\Stmt\Namespace_) {
1✔
191
                                $namespaces[$node->name->toString()] = $node->stmts;
1✔
192
                        }
193
                }
194

195
                foreach ($namespaces as $name => $nodes) {
1✔
196
                        foreach ($nodes as $node) {
1✔
197
                                match (true) {
198
                                        $node instanceof Node\Stmt\Use_ => $this->addUseToNamespace($phpFile->addNamespace($name), $node),
1✔
199
                                        $node instanceof Node\Stmt\ClassLike => $this->addClassLikeToFile($phpFile, $node),
1✔
200
                                        $node instanceof Node\Stmt\Function_ => $this->addFunctionToFile($phpFile, $node),
1✔
201
                                        default => null,
1✔
202
                                };
203
                        }
204
                }
205

206
                return $phpFile;
1✔
207
        }
208

209

210
        private function addUseToNamespace(PhpNamespace $namespace, Node\Stmt\Use_ $node): void
1✔
211
        {
212
                $of = [
1✔
213
                        $node::TYPE_NORMAL => PhpNamespace::NameNormal,
1✔
214
                        $node::TYPE_FUNCTION => PhpNamespace::NameFunction,
1✔
215
                        $node::TYPE_CONSTANT => PhpNamespace::NameConstant,
1✔
216
                ][$node->type];
1✔
217
                foreach ($node->uses as $use) {
1✔
218
                        $namespace->addUse($use->name->toString(), $use->alias?->toString(), $of);
1✔
219
                }
220
        }
1✔
221

222

223
        private function addClassLikeToFile(PhpFile $phpFile, Node\Stmt\ClassLike $node): ClassLike
1✔
224
        {
225
                if ($node instanceof Node\Stmt\Class_) {
1✔
226
                        $class = $phpFile->addClass($node->namespacedName->toString());
1✔
227
                        $class->setFinal($node->isFinal());
1✔
228
                        $class->setAbstract($node->isAbstract());
1✔
229
                        $class->setReadOnly(method_exists($node, 'isReadonly') && $node->isReadonly());
1✔
230
                        if ($node->extends) {
1✔
231
                                $class->setExtends($node->extends->toString());
1✔
232
                        }
233
                        foreach ($node->implements as $item) {
1✔
234
                                $class->addImplement($item->toString());
1✔
235
                        }
236
                } elseif ($node instanceof Node\Stmt\Interface_) {
1✔
237
                        $class = $phpFile->addInterface($node->namespacedName->toString());
1✔
238
                        foreach ($node->extends as $item) {
1✔
239
                                $class->addExtend($item->toString());
1✔
240
                        }
241
                } elseif ($node instanceof Node\Stmt\Trait_) {
1✔
242
                        $class = $phpFile->addTrait($node->namespacedName->toString());
1✔
243

244
                } elseif ($node instanceof Node\Stmt\Enum_) {
1✔
245
                        $class = $phpFile->addEnum($node->namespacedName->toString());
1✔
246
                        $class->setType($node->scalarType?->toString());
1✔
247
                        foreach ($node->implements as $item) {
1✔
248
                                $class->addImplement($item->toString());
1✔
249
                        }
250
                }
251

252
                $this->addCommentAndAttributes($class, $node);
1✔
253
                $this->addClassMembers($class, $node);
1✔
254
                return $class;
1✔
255
        }
256

257

258
        private function addClassMembers(ClassLike $class, Node\Stmt\ClassLike $node): void
1✔
259
        {
260
                foreach ($node->stmts as $stmt) {
1✔
261
                        match (true) {
262
                                $stmt instanceof Node\Stmt\TraitUse => $this->addTraitToClass($class, $stmt),
1✔
263
                                $stmt instanceof Node\Stmt\Property => $this->addPropertyToClass($class, $stmt),
1✔
264
                                $stmt instanceof Node\Stmt\ClassMethod => $this->addMethodToClass($class, $stmt),
1✔
265
                                $stmt instanceof Node\Stmt\ClassConst => $this->addConstantToClass($class, $stmt),
1✔
266
                                $stmt instanceof Node\Stmt\EnumCase => $this->addEnumCaseToClass($class, $stmt),
1✔
267
                                default => null,
×
268
                        };
269
                }
270
        }
1✔
271

272

273
        private function addTraitToClass(ClassLike $class, Node\Stmt\TraitUse $node): void
1✔
274
        {
275
                foreach ($node->traits as $item) {
1✔
276
                        $trait = $class->addTrait($item->toString());
1✔
277
                }
278

279
                foreach ($node->adaptations as $item) {
1✔
280
                        $trait->addResolution(rtrim($this->getReformattedContents([$item], 0), ';'));
1✔
281
                }
282

283
                $this->addCommentAndAttributes($trait, $node);
1✔
284
        }
1✔
285

286

287
        private function addPropertyToClass(ClassLike $class, Node\Stmt\Property $node): void
1✔
288
        {
289
                foreach ($node->props as $item) {
1✔
290
                        $prop = $class->addProperty($item->name->toString());
1✔
291
                        $prop->setStatic($node->isStatic());
1✔
292
                        $prop->setVisibility($this->toVisibility($node->flags));
1✔
293
                        $prop->setType($node->type ? $this->toPhp($node->type) : null);
1✔
294
                        if ($item->default) {
1✔
295
                                $prop->setValue($this->toValue($item->default));
1✔
296
                        }
297

298
                        $prop->setReadOnly(method_exists($node, 'isReadonly') && $node->isReadonly());
1✔
299
                        $this->addCommentAndAttributes($prop, $node);
1✔
300
                }
301
        }
1✔
302

303

304
        private function addMethodToClass(ClassLike $class, Node\Stmt\ClassMethod $node): void
1✔
305
        {
306
                $method = $class->addMethod($node->name->toString());
1✔
307
                $method->setAbstract($node->isAbstract());
1✔
308
                $method->setFinal($node->isFinal());
1✔
309
                $method->setStatic($node->isStatic());
1✔
310
                $method->setVisibility($this->toVisibility($node->flags));
1✔
311
                $this->setupFunction($method, $node);
1✔
312
        }
1✔
313

314

315
        private function addConstantToClass(ClassLike $class, Node\Stmt\ClassConst $node): void
1✔
316
        {
317
                foreach ($node->consts as $item) {
1✔
318
                        $const = $class->addConstant($item->name->toString(), $this->toValue($item->value));
1✔
319
                        $const->setVisibility($this->toVisibility($node->flags));
1✔
320
                        $const->setFinal(method_exists($node, 'isFinal') && $node->isFinal());
1✔
321
                        $this->addCommentAndAttributes($const, $node);
1✔
322
                }
323
        }
1✔
324

325

326
        private function addEnumCaseToClass(EnumType $class, Node\Stmt\EnumCase $node): void
1✔
327
        {
328
                $value = match (true) {
1✔
329
                        $node->expr === null => null,
1✔
330
                        $node->expr instanceof Node\Scalar\LNumber, $node->expr instanceof Node\Scalar\String_ => $node->expr->value,
1✔
331
                        default => $this->toValue($node->expr),
1✔
332
                };
333
                $case = $class->addCase($node->name->toString(), $value);
1✔
334
                $this->addCommentAndAttributes($case, $node);
1✔
335
        }
1✔
336

337

338
        private function addFunctionToFile(PhpFile $phpFile, Node\Stmt\Function_ $node): void
1✔
339
        {
340
                $function = $phpFile->addFunction($node->namespacedName->toString());
1✔
341
                $this->setupFunction($function, $node);
1✔
342
        }
1✔
343

344

345
        private function addCommentAndAttributes(
1✔
346
                PhpFile|ClassLike|Constant|Property|GlobalFunction|Method|Parameter|EnumCase|TraitUse $element,
347
                Node $node,
348
        ): void
349
        {
350
                if ($node->getDocComment()) {
1✔
351
                        $comment = $node->getDocComment()->getReformattedText();
1✔
352
                        $comment = Helpers::unformatDocComment($comment);
1✔
353
                        $element->setComment($comment);
1✔
354
                        $node->setDocComment(new PhpParser\Comment\Doc(''));
1✔
355
                }
356

357
                foreach ($node->attrGroups ?? [] as $group) {
1✔
358
                        foreach ($group->attrs as $attribute) {
1✔
359
                                $args = [];
1✔
360
                                foreach ($attribute->args as $arg) {
1✔
361
                                        if ($arg->name) {
1✔
362
                                                $args[$arg->name->toString()] = $this->toValue($arg->value);
1✔
363
                                        } else {
364
                                                $args[] = $this->toValue($arg->value);
1✔
365
                                        }
366
                                }
367

368
                                $element->addAttribute($attribute->name->toString(), $args);
1✔
369
                        }
370
                }
371
        }
1✔
372

373

374
        private function setupFunction(GlobalFunction|Method $function, Node\FunctionLike $node): void
1✔
375
        {
376
                $function->setReturnReference($node->returnsByRef());
1✔
377
                $function->setReturnType($node->getReturnType() ? $this->toPhp($node->getReturnType()) : null);
1✔
378
                foreach ($node->getParams() as $item) {
1✔
379
                        $visibility = $this->toVisibility($item->flags);
1✔
380
                        $isReadonly = (bool) ($item->flags & Node\Stmt\Class_::MODIFIER_READONLY);
1✔
381
                        $param = $visibility
1✔
382
                                ? ($function->addPromotedParameter($item->var->name))->setVisibility($visibility)->setReadonly($isReadonly)
1✔
383
                                : $function->addParameter($item->var->name);
1✔
384
                        $param->setType($item->type ? $this->toPhp($item->type) : null);
1✔
385
                        $param->setReference($item->byRef);
1✔
386
                        $function->setVariadic($item->variadic);
1✔
387
                        if ($item->default) {
1✔
388
                                $param->setDefaultValue($this->toValue($item->default));
1✔
389
                        }
390

391
                        $this->addCommentAndAttributes($param, $item);
1✔
392
                }
393

394
                $this->addCommentAndAttributes($function, $node);
1✔
395
                if ($node->getStmts()) {
1✔
396
                        $indent = $function instanceof GlobalFunction ? 1 : 2;
1✔
397
                        $function->setBody($this->getReformattedContents($node->getStmts(), $indent));
1✔
398
                }
399
        }
1✔
400

401

402
        private function toValue(Node\Expr $node): mixed
1✔
403
        {
404
                if ($node instanceof Node\Expr\ConstFetch) {
1✔
405
                        return match ($node->name->toLowerString()) {
1✔
406
                                'null' => null,
1✔
407
                                'true' => true,
1✔
408
                                'false' => false,
1✔
409
                                default => new Literal($this->getReformattedContents([$node], 0)),
1✔
410
                        };
411
                } elseif ($node instanceof Node\Scalar\LNumber
1✔
412
                        || $node instanceof Node\Scalar\DNumber
1✔
413
                        || $node instanceof Node\Scalar\String_
1✔
414
                ) {
415
                        return $node->value;
1✔
416

417
                } elseif ($node instanceof Node\Expr\Array_) {
1✔
418
                        $res = [];
1✔
419
                        foreach ($node->items as $item) {
1✔
420
                                if ($item->unpack) {
1✔
421
                                        $res[] = new Literal($this->getReformattedContents([$item], 0));
1✔
422

423
                                } elseif ($item->key) {
1✔
424
                                        $key = $item->key instanceof Node\Identifier
1✔
425
                                                ? $item->key->name
×
426
                                                : $this->toValue($item->key);
1✔
427
                                        $res[$key] = $this->toValue($item->value);
1✔
428

429
                                } else {
430
                                        $res[] = $this->toValue($item->value);
1✔
431
                                }
432
                        }
433
                        return $res;
1✔
434

435
                } else {
436
                        return new Literal($this->getReformattedContents([$node], 0));
1✔
437
                }
438
        }
439

440

441
        private function toVisibility(int $flags): ?string
1✔
442
        {
443
                return match (true) {
444
                        (bool) ($flags & Node\Stmt\Class_::MODIFIER_PUBLIC) => ClassType::VisibilityPublic,
1✔
445
                        (bool) ($flags & Node\Stmt\Class_::MODIFIER_PROTECTED) => ClassType::VisibilityProtected,
1✔
446
                        (bool) ($flags & Node\Stmt\Class_::MODIFIER_PRIVATE) => ClassType::VisibilityPrivate,
1✔
447
                        default => null,
1✔
448
                };
449
        }
450

451

452
        private function toPhp(Node $value): string
1✔
453
        {
454
                $dolly = clone $value;
1✔
455
                $dolly->setAttribute('comments', []);
1✔
456
                return $this->printer->prettyPrint([$dolly]);
1✔
457
        }
458

459

460
        private function getNodeContents(Node ...$nodes): string
1✔
461
        {
462
                $start = $this->getNodeStartPos($nodes[0]);
1✔
463
                return substr($this->code, $start, end($nodes)->getEndFilePos() - $start + 1);
1✔
464
        }
465

466

467
        private function getNodeStartPos(Node $node): int
1✔
468
        {
469
                return ($comments = $node->getComments())
1✔
470
                        ? $comments[0]->getStartFilePos()
1✔
471
                        : $node->getStartFilePos();
1✔
472
        }
473
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc