Completed
Push — master ( 9c08c3...ca0298 )
by Marco
11:14
created

setDocCommentFromString()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 1
cts 1
cp 1
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Roave\BetterReflection\Reflection;
6
7
use Closure;
8
use Exception;
9
use phpDocumentor\Reflection\Type;
10
use PhpParser\Comment\Doc;
11
use PhpParser\Node;
12
use PhpParser\Node\Expr\Yield_ as YieldNode;
13
use PhpParser\Node\NullableType;
14
use PhpParser\Node\Param as ParamNode;
15
use PhpParser\Node\Stmt\Namespace_ as NamespaceNode;
16
use PhpParser\NodeTraverser;
17
use PhpParser\Parser;
18
use PhpParser\PrettyPrinter\Standard as StandardPrettyPrinter;
19
use PhpParser\PrettyPrinterAbstract;
20
use Reflector as CoreReflector;
21
use Roave\BetterReflection\BetterReflection;
22
use Roave\BetterReflection\Identifier\Exception\InvalidIdentifierName;
23
use Roave\BetterReflection\Identifier\Identifier;
24
use Roave\BetterReflection\Identifier\IdentifierType;
25
use Roave\BetterReflection\Reflection\Exception\InvalidAbstractFunctionNodeType;
26
use Roave\BetterReflection\Reflection\Exception\Uncloneable;
27
use Roave\BetterReflection\Reflector\Reflector;
28
use Roave\BetterReflection\SourceLocator\Ast\Exception\ParseToAstFailure;
29
use Roave\BetterReflection\SourceLocator\Located\LocatedSource;
30
use Roave\BetterReflection\SourceLocator\Type\ClosureSourceLocator;
31
use Roave\BetterReflection\TypesFinder\FindReturnType;
32
use Roave\BetterReflection\Util\CalculateReflectionColum;
33
use Roave\BetterReflection\Util\GetFirstDocComment;
34
use Roave\BetterReflection\Util\Visitor\ReturnNodeVisitor;
35
use function array_filter;
36
use function count;
37
use function implode;
38
use function is_array;
39
use function is_string;
40
use function strtolower;
41
42
abstract class ReflectionFunctionAbstract implements CoreReflector
43
{
44
    public const CLOSURE_NAME = '{closure}';
45
46
    /** @var NamespaceNode|null */
47
    private $declaringNamespace;
48
49
    /** @var LocatedSource */
50
    private $locatedSource;
51
52
    /** @var Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\Expr\Closure */
53
    private $node;
54
55
    /** @var Reflector */
56
    private $reflector;
57
58
    /** @var Parser */
59
    private static $parser;
60 69
61
    protected function __construct()
62 69
    {
63
    }
64 1
65
    public static function export() : void
66 1
    {
67
        throw new Exception('Unable to export statically');
68
    }
69
70
    /**
71
     * Populate the common elements of the function abstract.
72
     *
73
     * @param Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\Expr\Closure $node Node has to be processed by the PhpParser\NodeVisitor\NameResolver
74
     *
75
     * @throws InvalidAbstractFunctionNodeType
76 69
     */
77
    protected function populateFunctionAbstract(
78
        Reflector $reflector,
79
        Node\FunctionLike $node,
80
        LocatedSource $locatedSource,
81
        ?NamespaceNode $declaringNamespace = null
82 69
    ) : void {
83 69
        $this->reflector          = $reflector;
84 69
        $this->node               = $node;
0 ignored issues
show
Documentation Bug introduced by Jaroslav Hanslík
It seems like $node of type object<PhpParser\Node\FunctionLike> is incompatible with the declared type object<PhpParser\Node\St...rser\Node\Expr\Closure> of property $node.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
85 69
        $this->locatedSource      = $locatedSource;
86
        $this->declaringNamespace = $declaringNamespace;
87 69
88 69
        $this->setNodeOptionalFlag();
89
    }
90
91
    /**
92
     * Get the AST node from which this function was created
93
     *
94
     * @return Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\FunctionLike
95 1
     */
96
    protected function getNode() : Node\FunctionLike
97 1
    {
98
        return $this->node;
99
    }
100
101
    /**
102
     * We must determine if params are optional or not ahead of time, but we
103
     * must do it in reverse...
104 69
     */
105
    private function setNodeOptionalFlag() : void
106 69
    {
107 69
        $overallOptionalFlag = true;
108 69
        $lastParamIndex      = (count($this->node->params) - 1);
109 10
        for ($i = $lastParamIndex; $i >= 0; $i--) {
110
            $hasDefault = ($this->node->params[$i]->default !== null);
111
112
            // When we find the first parameter that does not have a default,
113
            // flip the flag as all params for this are no longer optional
114 10
            // EVEN if they have a default value
115 10
            if (! $hasDefault) {
116
                $overallOptionalFlag = false;
117
            }
118 10
119
            $this->node->params[$i]->isOptional = $overallOptionalFlag;
120 69
        }
121
    }
122
123
    /**
124
     * Get the "full" name of the function (e.g. for A\B\foo, this will return
125
     * "A\B\foo").
126 67
     */
127
    public function getName() : string
128 67
    {
129 64
        if (! $this->inNamespace()) {
130
            return $this->getShortName();
131
        }
132 3
133
        return $this->getNamespaceName() . '\\' . $this->getShortName();
134
    }
135
136
    /**
137
     * Get the "short" name of the function (e.g. for A\B\foo, this will return
138
     * "foo").
139 67
     */
140
    public function getShortName() : string
141 67
    {
142 1
        if ($this->node instanceof Node\Expr\Closure) {
143
            return self::CLOSURE_NAME;
144
        }
145 66
146
        return $this->node->name->name;
147
    }
148
149
    /**
150
     * Get the "namespace" name of the function (e.g. for A\B\foo, this will
151
     * return "A\B").
152
     *
153
     * @psalm-suppress PossiblyNullPropertyFetch
154 4
     */
155
    public function getNamespaceName() : string
156 4
    {
157 1
        if (! $this->inNamespace()) {
158
            return '';
159
        }
160 3
161
        return implode('\\', $this->declaringNamespace->name->parts);
162
    }
163
164
    /**
165
     * Decide if this function is part of a namespace. Returns false if the class
166
     * is in the global namespace or does not have a specified namespace.
167 67
     */
168
    public function inNamespace() : bool
169 67
    {
170 67
        return $this->declaringNamespace !== null
171
            && $this->declaringNamespace->name !== null;
172
    }
173
174
    /**
175
     * Get the number of parameters for this class.
176 1
     */
177
    public function getNumberOfParameters() : int
178 1
    {
179
        return count($this->getParameters());
180
    }
181
182
    /**
183
     * Get the number of required parameters for this method.
184 1
     */
185
    public function getNumberOfRequiredParameters() : int
186 1
    {
187 1
        return count(array_filter(
188
            $this->getParameters(),
189 1
            function (ReflectionParameter $p) : bool {
190 1
                return ! $p->isOptional();
191
            }
192
        ));
193
    }
194
195
    /**
196
     * Get an array list of the parameters for this method signature, as an
197
     * array of ReflectionParameter instances.
198
     *
199
     * @return ReflectionParameter[]
200 6
     */
201
    public function getParameters() : array
202 6
    {
203
        $parameters = [];
204 6
205 6
        foreach ($this->node->params as $paramIndex => $paramNode) {
206 6
            $parameters[] = ReflectionParameter::createFromNode(
207 6
                $this->reflector,
208 6
                $paramNode,
209 6
                $this->declaringNamespace,
210 6
                $this,
211
                $paramIndex
212
            );
213
        }
214 6
215
        return $parameters;
216
    }
217
218
    /**
219
     * Get a single parameter by name. Returns null if parameter not found for
220
     * the function.
221 2
     */
222
    public function getParameter(string $parameterName) : ?ReflectionParameter
223 2
    {
224 2
        foreach ($this->getParameters() as $parameter) {
225 2
            if ($parameter->getName() === $parameterName) {
226
                return $parameter;
227
            }
228 1
        }
229
        return null;
230
    }
231 3
232
    public function getDocComment() : string
233 3
    {
234
        return GetFirstDocComment::forNode($this->node);
235
    }
236 2
237
    public function setDocCommentFromString(string $string) : void
238 2
    {
239
        $this->getAst()->setDocComment(new Doc($string));
240
    }
241 1
242
    public function getFileName() : ?string
243 1
    {
244
        return $this->locatedSource->getFileName();
245
    }
246
247
    public function getLocatedSource() : LocatedSource
248
    {
249 2
        return $this->locatedSource;
250
    }
251 2
252
    /**
253
     * Is this function a closure?
254
     */
255
    public function isClosure() : bool
256
    {
257
        return $this->node instanceof Node\Expr\Closure;
258
    }
259
260
    /**
261
     * Is this function deprecated?
262
     *
263 1
     * Note - we cannot reflect on internal functions (as there is no PHP source
264
     * code we can access. This means, at present, we can only EVER return false
265 1
     * from this function.
266
     *
267
     * @see https://github.com/Roave/BetterReflection/issues/38
268
     */
269
    public function isDeprecated() : bool
270
    {
271
        return false;
272
    }
273
274
    /**
275
     * Is this an internal function?
276
     *
277 1
     * Note - we cannot reflect on internal functions (as there is no PHP source
278
     * code we can access. This means, at present, we can only EVER return false
279 1
     * from this function.
280
     *
281
     * @see https://github.com/Roave/BetterReflection/issues/38
282
     */
283
    public function isInternal() : bool
284
    {
285
        return false;
286 1
    }
287
288 1
    /**
289
     * Is this a user-defined function (will always return the opposite of
290
     * whatever isInternal returns).
291
     */
292
    public function isUserDefined() : bool
293
    {
294 3
        return ! $this->isInternal();
295
    }
296 3
297
    /**
298 3
     * Check if the function has a variadic parameter.
299 3
     */
300 3
    public function isVariadic() : bool
301
    {
302
        $parameters = $this->getParameters();
303
304 1
        foreach ($parameters as $parameter) {
305
            if ($parameter->isVariadic()) {
306
                return true;
307
            }
308
        }
309
310
        return false;
311 16
    }
312
313 16
    /**
314 15
     * Recursively search an array of statements (PhpParser nodes) to find if a
315
     * yield expression exists anywhere (thus indicating this is a generator).
316
     */
317 16
    private function nodeIsOrContainsYield(Node $node) : bool
318 16
    {
319
        if ($node instanceof YieldNode) {
320 16
            return true;
321 15
        }
322
323
        foreach ($node->getSubNodeNames() as $nodeName) {
324 16
            $nodeProperty = $node->$nodeName;
325 16
326
            if ($nodeProperty instanceof Node && $this->nodeIsOrContainsYield($nodeProperty)) {
327
                return true;
328 16
            }
329 16
330 16
            if (! is_array($nodeProperty)) {
331
                continue;
332
            }
333
334
            foreach ($nodeProperty as $nodePropertyArrayItem) {
335 16
                if ($nodePropertyArrayItem instanceof Node && $this->nodeIsOrContainsYield($nodePropertyArrayItem)) {
336
                    return true;
337
                }
338
            }
339
        }
340
341
        return false;
342 17
    }
343
344 17
    /**
345 1
     * Check if this function can be used as a generator (i.e. contains the
346
     * "yield" keyword).
347
     */
348 16
    public function isGenerator() : bool
349
    {
350
        if ($this->node === null) {
351
            return false;
352
        }
353
354 3
        return $this->nodeIsOrContainsYield($this->node);
355
    }
356 3
357
    /**
358
     * Get the line number that this function starts on.
359
     */
360
    public function getStartLine() : int
361
    {
362 3
        return $this->node->getStartLine();
363
    }
364 3
365
    /**
366
     * Get the line number that this function ends on.
367 3
     */
368
    public function getEndLine() : int
369 3
    {
370
        return $this->node->getEndLine();
371
    }
372 3
373
    public function getStartColumn() : int
374 3
    {
375
        return CalculateReflectionColum::getStartColumn($this->locatedSource->getSource(), $this->node);
376
    }
377
378
    public function getEndColumn() : int
379
    {
380 2
        return CalculateReflectionColum::getEndColumn($this->locatedSource->getSource(), $this->node);
381
    }
382 2
383
    /**
384
     * Is this function declared as a reference.
385
     */
386
    public function returnsReference() : bool
387
    {
388
        return $this->node->byRef;
389
    }
390
391
    /**
392
     * Get the return types defined in the DocBlocks. This returns an array because
393 1
     * the parameter may have multiple (compound) types specified (for example
394
     * when you type hint pipe-separated "string|null", in which case this
395 1
     * would return an array of Type objects, one for string, one for null.
396
     *
397
     * @return Type[]
398
     */
399
    public function getDocBlockReturnTypes() : array
400
    {
401 13
        return (new FindReturnType())->__invoke($this, $this->declaringNamespace);
402
    }
403 13
404
    /**
405 13
     * Get the return type declaration (only for PHP 7+ code)
406 3
     */
407
    public function getReturnType() : ?ReflectionType
408
    {
409 10
        $returnType = $this->node->getReturnType();
410 3
411
        if ($returnType === null) {
412
            return null;
413 7
        }
414
415
        if ($returnType instanceof NullableType) {
416
            return ReflectionType::createFromTypeAndReflector((string) $returnType->type, true, $this->reflector);
417
        }
418
419 2
        return ReflectionType::createFromTypeAndReflector((string) $returnType, false, $this->reflector);
420
    }
421 2
422
    /**
423
     * Do we have a return type declaration (only for PHP 7+ code)
424
     */
425
    public function hasReturnType() : bool
426
    {
427 1
        return $this->getReturnType() !== null;
428
    }
429 1
430 1
    /**
431
     * Set the return type declaration.
432
     */
433
    public function setReturnType(string $newReturnType) : void
434
    {
435 1
        $this->node->returnType = new Node\Name($newReturnType);
436
    }
437 1
438 1
    /**
439
     * Remove the return type declaration completely.
440
     */
441
    public function removeReturnType() : void
442
    {
443 1
        $this->node->returnType = null;
444
    }
445 1
446
    /**
447
     * @throws Uncloneable
448
     */
449
    public function __clone()
450
    {
451
        throw Uncloneable::fromClass(__CLASS__);
452
    }
453 5
454
    /**
455 5
     * Retrieves the body of this function as AST nodes
456
     *
457
     * @return Node[]
458
     */
459
    public function getBodyAst() : array
460
    {
461
        return $this->node->stmts ?: [];
462
    }
463
464
    /**
465
     * Retrieves the body of this function as code.
466
     *
467
     * If a PrettyPrinter is provided as a parameter, it will be used, otherwise
468 4
     * a default will be used.
469
     *
470 4
     * Note that the formatting of the code may not be the same as the original
471 4
     * function. If specific formatting is required, you should provide your
472
     * own implementation of a PrettyPrinter to unparse the AST.
473
     */
474 4
    public function getBodyCode(?PrettyPrinterAbstract $printer = null) : string
475
    {
476
        if ($printer === null) {
477
            $printer = new StandardPrettyPrinter();
478
        }
479
480
        return $printer->prettyPrint($this->getBodyAst());
481
    }
482 5
483
    /**
484 5
     * Fetch the AST for this method or function.
485
     *
486
     * @return Node\Stmt\ClassMethod|Node\Stmt\Function_|Node\FunctionLike
487
     */
488
    public function getAst() : Node\FunctionLike
489
    {
490
        return $this->node;
491
    }
492
493
    /**
494
     * Override the method or function's body of statements with an entirely new
495
     * body of statements within the reflection.
496
     *
497
     * @example
498 1
     * $reflectionFunction->setBodyFromClosure(function () { return true; });
499
     *
500
     *
501 1
     * @throws ParseToAstFailure
502 1
     * @throws InvalidIdentifierName
503 1
     */
504
    public function setBodyFromClosure(Closure $newBody) : void
505
    {
506
        /** @var self $closureReflection */
507 1
        $closureReflection = (new ClosureSourceLocator($newBody, $this->loadStaticParser()))->locateIdentifier(
508
            $this->reflector,
509 1
            new Identifier(self::CLOSURE_NAME, new IdentifierType(IdentifierType::IDENTIFIER_FUNCTION))
510 1
        );
511
512
        /** @var Node\Stmt\Function_ $functionNode */
513
        $functionNode = $closureReflection->getNode();
514
515
        $this->node->stmts = $functionNode->stmts;
516
    }
517
518
    /**
519 1
     * Override the method or function's body of statements with an entirely new
520
     * body of statements within the reflection.
521 1
     *
522 1
     * @example
523
     * $reflectionFunction->setBodyFromString('return true;');
524
     */
525
    public function setBodyFromString(string $newBody) : void
526
    {
527
        $this->node->stmts = $this->loadStaticParser()->parse('<?php ' . $newBody);
528
    }
529
530
    /**
531
     * Override the method or function's body of statements with an entirely new
532
     * body of statements within the reflection.
533
     *
534 2
     * @example
535
     * // $ast should be an array of Nodes
536
     * $reflectionFunction->setBodyFromAst($ast);
537
     *
538
     * @param Node[] $nodes
539 1
     */
540 2
    public function setBodyFromAst(array $nodes) : void
541 2
    {
542 1
        // This slightly confusing code simply type-checks the $sourceLocators
543
        // array by unpacking them and splatting them in the closure.
544
        $validator         = function (Node ...$node) : array {
545
            return $node;
546
        };
547 1
        $this->node->stmts = $validator(...$nodes);
548
    }
549 1
550 1
    /**
551
     * Add a new parameter to the method/function.
552
     */
553
    public function addParameter(string $parameterName) : void
554
    {
555 1
        $this->node->params[] = new ParamNode(new Node\Expr\Variable($parameterName));
556
    }
557 1
558
    /**
559 1
     * Remove a parameter from the method/function.
560 1
     */
561
    public function removeParameter(string $parameterName) : void
562
    {
563
        $lowerName = strtolower($parameterName);
564 1
565 1
        foreach ($this->node->params as $key => $paramNode) {
566
            if ($paramNode->var instanceof Node\Expr\Error) {
567
                throw new \LogicException('PhpParser left an "Error" node in the parameters AST, this should NOT happen');
568 1
            }
569
570 1
            if (! is_string($paramNode->var->name) || strtolower($paramNode->var->name) !== $lowerName) {
571
                continue;
572
            }
573
574
            unset($this->node->params[$key]);
575
        }
576
    }
577
578
    /**
579
     * Fetch an array of all return statements found within this function.
580 2
     *
581
     * Note that return statements within smaller scopes contained (e.g. anonymous classes, closures) are not returned
582 2
     * here as they are not within the immediate scope.
583
     *
584 2
     * @return Node\Stmt\Return_[]
585 2
     */
586
    public function getReturnStatementsAst() : array
587 2
    {
588
        $visitor = new ReturnNodeVisitor();
589 2
590
        $traverser = new NodeTraverser();
591
        $traverser->addVisitor($visitor);
592 2
593
        $traverser->traverse($this->node->getStmts());
594 2
595
        return $visitor->getReturnNodes();
596
    }
597
598
    private function loadStaticParser() : Parser
599
    {
600
        return self::$parser ?? self::$parser = (new BetterReflection())->phpParser();
601
    }
602
}
603