Completed
Push — master ( 5a5570...b1ed96 )
by Marco
10s
created

ReflectionFunctionAbstract::removeParameter()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 16
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 8
CRAP Score 5.0342

Importance

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