locateIdentifiersByType()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 2
crap 2
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Roave\BetterReflection\SourceLocator\Type;
6
7
use InvalidArgumentException;
8
use PhpParser\Node;
9
use PhpParser\Node\Stmt\Class_;
10
use PhpParser\NodeTraverser;
11
use PhpParser\NodeVisitorAbstract;
12
use PhpParser\Parser;
13
use ReflectionClass as CoreReflectionClass;
14
use ReflectionException;
15
use Roave\BetterReflection\Identifier\Identifier;
16
use Roave\BetterReflection\Identifier\IdentifierType;
17
use Roave\BetterReflection\Reflection\Reflection;
18
use Roave\BetterReflection\Reflection\ReflectionClass;
19
use Roave\BetterReflection\Reflector\Reflector;
20
use Roave\BetterReflection\SourceLocator\Ast\Exception\ParseToAstFailure;
21
use Roave\BetterReflection\SourceLocator\Ast\Strategy\NodeToReflection;
22
use Roave\BetterReflection\SourceLocator\Exception\EvaledAnonymousClassCannotBeLocated;
23
use Roave\BetterReflection\SourceLocator\Exception\TwoAnonymousClassesOnSameLine;
24
use Roave\BetterReflection\SourceLocator\FileChecker;
25
use Roave\BetterReflection\SourceLocator\Located\LocatedSource;
26
use Roave\BetterReflection\Util\FileHelper;
27
use function array_filter;
28
use function array_values;
29
use function assert;
30
use function file_get_contents;
31
use function gettype;
32
use function is_object;
33
use function sprintf;
34
use function strpos;
35
36
/**
37
 * @internal
38
 */
39
final class AnonymousClassObjectSourceLocator implements SourceLocator
40
{
41
    /** @var CoreReflectionClass */
42
    private $coreClassReflection;
43
44
    /** @var Parser */
45
    private $parser;
46
47
    /**
48
     * @param object $anonymousClassObject
49
     *
50
     * @throws InvalidArgumentException
51
     * @throws ReflectionException
52
     *
53
     * @psalm-suppress DocblockTypeContradiction
54
     */
55
    public function __construct($anonymousClassObject, Parser $parser)
56
    {
57
        if (! is_object($anonymousClassObject)) {
58
            throw new InvalidArgumentException(sprintf(
59
                'Can only create from an instance of an object, "%s" given',
60
                gettype($anonymousClassObject)
61
            ));
62
        }
63
64
        $this->coreClassReflection = new CoreReflectionClass($anonymousClassObject);
65
        $this->parser              = $parser;
66
    }
67
68
    /**
69
     * {@inheritDoc}
70
     *
71
     * @throws ParseToAstFailure
72
     */
73
    public function locateIdentifier(Reflector $reflector, Identifier $identifier) : ?Reflection
74
    {
75
        return $this->getReflectionClass($reflector, $identifier->getType());
76
    }
77
78
    /**
79
     * {@inheritDoc}
80
     *
81
     * @throws ParseToAstFailure
82
     */
83
    public function locateIdentifiersByType(Reflector $reflector, IdentifierType $identifierType) : array
84
    {
85
        return array_filter([$this->getReflectionClass($reflector, $identifierType)]);
86
    }
87
88 12
    private function getReflectionClass(Reflector $reflector, IdentifierType $identifierType) : ?ReflectionClass
89
    {
90
        if (! $identifierType->isClass()) {
91
            return null;
92
        }
93
94
        $fileName = $this->coreClassReflection->getFileName();
95
96
        if (strpos($fileName, 'eval()\'d code') !== false) {
97
            throw EvaledAnonymousClassCannotBeLocated::create();
98
        }
99
100
        FileChecker::assertReadableFile($fileName);
101
102
        $fileName = FileHelper::normalizeWindowsPath($fileName);
103
104
        $nodeVisitor = new class($fileName, $this->coreClassReflection->getStartLine()) extends NodeVisitorAbstract
105
        {
106
            /** @var string */
107
            private $fileName;
108
109
            /** @var int */
110
            private $startLine;
111
112
            /** @var Class_[] */
113
            private $anonymousClassNodes = [];
114
115
            public function __construct(string $fileName, int $startLine)
116
            {
117 12
                $this->fileName  = $fileName;
118 12
                $this->startLine = $startLine;
119 12
            }
120
121
            /**
122
             * {@inheritDoc}
123
             */
124 12
            public function enterNode(Node $node)
125
            {
126 12
                if (! ($node instanceof Node\Stmt\Class_) || $node->name !== null) {
127 12
                    return null;
128
                }
129
130 12
                $this->anonymousClassNodes[] = $node;
131
132 12
                return null;
133
            }
134
135 12
            public function getAnonymousClassNode() : ?Class_
136
            {
137
                /** @var Class_[] $anonymousClassNodesOnSameLine */
138
                $anonymousClassNodesOnSameLine = array_values(array_filter($this->anonymousClassNodes, function (Class_ $node) : bool {
139 12
                    return $node->getLine() === $this->startLine;
140 12
                }));
141
142 12
                if (! $anonymousClassNodesOnSameLine) {
0 ignored issues
show
Bug Best Practice introduced by Jaroslav Hanslík
The expression $anonymousClassNodesOnSameLine of type PhpParser\Node\Stmt\Class_[] is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
143
                    return null;
144
                }
145
146 12
                if (isset($anonymousClassNodesOnSameLine[1])) {
147 2
                    throw TwoAnonymousClassesOnSameLine::create($this->fileName, $this->startLine);
148
                }
149
150 10
                return $anonymousClassNodesOnSameLine[0];
151
            }
152
        };
153
154
        $fileContents = file_get_contents($fileName);
155
        $ast          = $this->parser->parse($fileContents);
156
157
        $nodeTraverser = new NodeTraverser();
158
        $nodeTraverser->addVisitor($nodeVisitor);
159
        $nodeTraverser->traverse($ast);
0 ignored issues
show
Bug introduced by Jaroslav Hanslík
It seems like $ast defined by $this->parser->parse($fileContents) on line 155 can also be of type null; however, PhpParser\NodeTraverser::traverse() does only seem to accept array<integer,object<PhpParser\Node>>, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
160
161
        $reflectionClass = (new NodeToReflection())->__invoke(
162
            $reflector,
163
            $nodeVisitor->getAnonymousClassNode(),
164
            new LocatedSource($fileContents, $fileName),
165
            null
166
        );
167
        assert($reflectionClass instanceof ReflectionClass || $reflectionClass === null);
168
169
        return $reflectionClass;
170
    }
171
}
172