Completed
Push — master ( 454e04...38876e )
by Marco
21s queued 11s
created

AutoloadSourceLocator.php$0 ➔ enterNode()   B

Complexity

Conditions 8

Size

Total Lines 38

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 72

Importance

Changes 0
Metric Value
dl 0
loc 38
rs 8.0675
c 0
b 0
f 0
ccs 0
cts 0
cp 0
cc 8
crap 72
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\NodeTraverser;
10
use PhpParser\NodeVisitor\NameResolver;
11
use PhpParser\NodeVisitorAbstract;
12
use PhpParser\Parser;
13
use ReflectionClass;
14
use ReflectionException;
15
use ReflectionFunction;
16
use Roave\BetterReflection\BetterReflection;
17
use Roave\BetterReflection\Identifier\Identifier;
18
use Roave\BetterReflection\Reflection\Exception\InvalidConstantNode;
19
use Roave\BetterReflection\SourceLocator\Ast\Locator as AstLocator;
20
use Roave\BetterReflection\SourceLocator\Exception\InvalidFileLocation;
21
use Roave\BetterReflection\SourceLocator\Located\LocatedSource;
22
use Roave\BetterReflection\Util\ConstantNodeChecker;
23
use const STREAM_URL_STAT_QUIET;
24
use function array_key_exists;
25
use function class_exists;
26
use function defined;
27
use function file_exists;
28
use function file_get_contents;
29
use function function_exists;
30
use function get_defined_constants;
31
use function get_included_files;
32
use function interface_exists;
33
use function is_string;
34
use function restore_error_handler;
35
use function set_error_handler;
36
use function stat;
37
use function stream_wrapper_register;
38
use function stream_wrapper_restore;
39
use function stream_wrapper_unregister;
40
use function trait_exists;
41
42
/**
43
 * Use PHP's built in autoloader to locate a class, without actually loading.
44
 *
45
 * There are some prerequisites...
46 13
 *   - we expect the autoloader to load classes from a file (i.e. using require/include)
47
 */
48 13
class AutoloadSourceLocator extends AbstractSourceLocator
49
{
50 13
    /** @var AstLocator */
51
    private $astLocator;
0 ignored issues
show
Comprehensibility introduced by Marco Pivetta
Consider using a different property name as you override a private property of the parent class.
Loading history...
52 13
53 13
    /** @var Parser */
54
    private $phpParser;
55
56
    /** @var NodeTraverser */
57
    private $nodeTraverser;
58
59
    /** @var NodeVisitorAbstract */
60
    private $constantVisitor;
61
62
    /**
63
     * Note: the constructor has been made a 0-argument constructor because `\stream_wrapper_register`
64
     *       is a piece of trash, and doesn't accept instances, just class names.
65
     */
66
    public function __construct(?AstLocator $astLocator = null, ?Parser $phpParser = null)
67
    {
68
        $betterReflection = new BetterReflection();
69
70
        $validLocator = $astLocator ?? self::$currentAstLocator ?? $betterReflection->astLocator();
71
72 13
        parent::__construct($validLocator);
73
74 13
        $this->astLocator      = $validLocator;
75
        $this->phpParser       = $phpParser ?? $betterReflection->phpParser();
76 13
        $this->constantVisitor = $this->createConstantVisitor();
77 5
78
        $this->nodeTraverser = new NodeTraverser();
79
        $this->nodeTraverser->addVisitor(new NameResolver());
80 8
        $this->nodeTraverser->addVisitor($this->constantVisitor);
81 8
    }
82 8
83
    /**
84
     * Primarily used by the non-loading-autoloader magic trickery to determine
85
     * the filename used during autoloading.
86
     *
87
     * @var string|null
88
     */
89
    private static $autoloadLocatedFile;
90
91 13
    /** @var AstLocator */
92
    private static $currentAstLocator;
93 13
94 9
    /**
95
     * {@inheritDoc}
96
     *
97 4
     * @throws InvalidArgumentException
98 3
     * @throws InvalidFileLocation
99
     */
100
    protected function createLocatedSource(Identifier $identifier) : ?LocatedSource
101 1
    {
102
        $potentiallyLocatedFile = $this->attemptAutoloadForIdentifier($identifier);
103
104
        if (! ($potentiallyLocatedFile && file_exists($potentiallyLocatedFile))) {
105
            return null;
106
        }
107
108
        return new LocatedSource(
109
            file_get_contents($potentiallyLocatedFile),
110
            $potentiallyLocatedFile
111
        );
112
    }
113
114
    /**
115
     * Attempts to locate the specified identifier.
116
     *
117
     * @throws ReflectionException
118
     */
119 9
    private function attemptAutoloadForIdentifier(Identifier $identifier) : ?string
120
    {
121 9
        if ($identifier->isClass()) {
122 4
            return $this->locateClassByName($identifier->getName());
123
        }
124 4
125
        if ($identifier->isFunction()) {
126
            return $this->locateFunctionByName($identifier->getName());
127
        }
128 4
129
        if ($identifier->isConstant()) {
130
            return $this->locateConstantByName($identifier->getName());
131 5
        }
132 5
133
        return null;
134 5
    }
135 5
136 5
    /**
137 5
     * Attempt to locate a class by name.
138 5
     *
139 5
     * If class already exists, simply use internal reflection API to get the
140
     * filename and store it.
141 5
     *
142
     * If class does not exist, we make an assumption that whatever autoloaders
143
     * that are registered will be loading a file. We then override the file://
144
     * protocol stream wrapper to "capture" the filename we expect the class to
145
     * be in, and then restore it. Note that class_exists will cause an error
146
     * that it cannot find the file, so we squelch the errors by overriding the
147
     * error handler temporarily.
148
     *
149
     * @throws ReflectionException
150
     */
151
    private function locateClassByName(string $className) : ?string
152 3
    {
153
        if (class_exists($className, false) || interface_exists($className, false) || trait_exists($className, false)) {
154 3
            $filename = (new ReflectionClass($className))->getFileName();
155 1
156
            if (! is_string($filename)) {
157
                return null;
158 2
            }
159 2
160
            return $filename;
161 2
        }
162 1
163
        self::$autoloadLocatedFile = null;
164
        self::$currentAstLocator   = $this->astLocator; // passing the locator on to the implicitly instantiated `self`
165 1
        $previousErrorHandler      = set_error_handler(static function () : void {
166
        });
167
        stream_wrapper_unregister('file');
168
        stream_wrapper_register('file', self::class);
169
        class_exists($className);
170
        stream_wrapper_restore('file');
171
        set_error_handler($previousErrorHandler);
172
173
        return self::$autoloadLocatedFile;
174
    }
175
176
    /**
177
     * We can only load functions if they already exist, because PHP does not
178
     * have function autoloading. Therefore if it exists, we simply use the
179
     * internal reflection API to find the filename. If it doesn't we can do
180
     * nothing so throw an exception.
181
     *
182 4
     * @throws ReflectionException
183
     */
184 4
    private function locateFunctionByName(string $functionName) : ?string
185
    {
186 4
        if (! function_exists($functionName)) {
187
            return null;
188
        }
189
190
        $reflection         = new ReflectionFunction($functionName);
191
        $reflectionFileName = $reflection->getFileName();
192
193
        if (! is_string($reflectionFileName)) {
194
            return null;
195
        }
196
197
        return $reflectionFileName;
198
    }
199
200
    /**
201
     * We can only load constants if they already exist, because PHP does not
202
     * have constant autoloading. Therefore if it exists, we simply use brute force
203
     * to search throught all included files to find the right filename.
204 4
     */
205
    private function locateConstantByName(string $constantName) : ?string
206 4
    {
207
        if (! defined($constantName)) {
208 4
            return null;
209
        }
210
211 1
        if (! array_key_exists($constantName, get_defined_constants(true)['user'])) {
212 4
            return null;
213 4
        }
214 4
215
        /** @psalm-suppress UndefinedMethod */
216
        $this->constantVisitor->setConstantName($constantName);
0 ignored issues
show
Bug introduced by Jaroslav Hanslík
It seems like you code against a specific sub-type and not the parent class PhpParser\NodeVisitorAbstract as the method setConstantName() does only exist in the following sub-classes of PhpParser\NodeVisitorAbstract: Roave\BetterReflection\S...loadSourceLocator.php$0. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
217
218
        $constantFileName = null;
219 4
220 4
        foreach (get_included_files() as $includedFileName) {
221
            $ast = $this->phpParser->parse(file_get_contents($includedFileName));
222 4
223
            $this->nodeTraverser->traverse($ast);
0 ignored issues
show
Bug introduced by Jaroslav Hanslík
It seems like $ast defined by $this->phpParser->parse(...nts($includedFileName)) on line 221 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...
224
225
            /** @psalm-suppress UndefinedMethod */
226
            if ($this->constantVisitor->getNode() !== null) {
0 ignored issues
show
Bug introduced by Jaroslav Hanslík
It seems like you code against a specific sub-type and not the parent class PhpParser\NodeVisitorAbstract as the method getNode() does only exist in the following sub-classes of PhpParser\NodeVisitorAbstract: Roave\BetterReflection\S...loadSourceLocator.php$0. Maybe you want to instanceof check for one of these explicitly?

Let’s take a look at an example:

abstract class User
{
    /** @return string */
    abstract public function getPassword();
}

class MyUser extends User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different sub-classes of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the parent class:

    abstract class User
    {
        /** @return string */
        abstract public function getPassword();
    
        /** @return string */
        abstract public function getDisplayName();
    }
    
Loading history...
227
                $constantFileName = $includedFileName;
228
                break;
229
            }
230
        }
231
232
        return $constantFileName;
233
    }
234
235
    /**
236
     * Our wrapper simply records which file we tried to load and returns
237
     * boolean false indicating failure.
238
     *
239
     * @see https://php.net/manual/en/class.streamwrapper.php
240
     * @see https://php.net/manual/en/streamwrapper.stream-open.php
241
     *
242
     * @param string $path
243
     * @param string $mode
244
     * @param int    $options
245
     * @param string $opened_path
246
     *
247
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
248
     */
249
    public function stream_open($path, $mode, $options, &$opened_path) : bool
0 ignored issues
show
Unused Code introduced by James Titcumb
The parameter $mode is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by James Titcumb
The parameter $options is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by James Titcumb
The parameter $opened_path is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
250
    {
251
        self::$autoloadLocatedFile = $path;
252
253
        return false;
254
    }
255
256
    /**
257
     * url_stat is triggered by calls like "file_exists". The call to "file_exists" must not be overloaded.
258
     * This function restores the original "file" stream, issues a call to "stat" to get the real results,
259
     * and then re-registers the AutoloadSourceLocator stream wrapper.
260
     *
261
     * @see https://php.net/manual/en/class.streamwrapper.php
262
     * @see https://php.net/manual/en/streamwrapper.url-stat.php
263
     *
264
     * @param string $path
265
     * @param int    $flags
266
     *
267
     * @return mixed[]|bool
268
     *
269
     * @phpcsSuppress SlevomatCodingStandard.TypeHints.TypeHintDeclaration.MissingParameterTypeHint
270
     */
271
    public function url_stat($path, $flags)
272
    {
273
        stream_wrapper_restore('file');
274
275
        if ($flags & STREAM_URL_STAT_QUIET) {
276
            set_error_handler(static function () {
277
                // Use native error handler
278
                return false;
279
            });
280
            $result = @stat($path);
281
            restore_error_handler();
282
        } else {
283
            $result = stat($path);
284
        }
285
286
        stream_wrapper_unregister('file');
287
        stream_wrapper_register('file', self::class);
288
289
        return $result;
290
    }
291
292
    private function createConstantVisitor() : NodeVisitorAbstract
293
    {
294
        return new class() extends NodeVisitorAbstract
295
        {
296
            /** @var string|null */
297
            private $constantName;
298
299
            /** @var Node\Stmt\Const_|Node\Expr\FuncCall|null */
300
            private $node;
301
302
            public function enterNode(Node $node) : ?int
303
            {
304
                if ($node instanceof Node\Stmt\Const_) {
305
                    foreach ($node->consts as $constNode) {
306
                        /** @psalm-suppress UndefinedPropertyFetch */
307
                        if ($constNode->namespacedName->toString() === $this->constantName) {
308
                            $this->node = $node;
309
310
                            return NodeTraverser::STOP_TRAVERSAL;
311
                        }
312
                    }
313
314
                    return NodeTraverser::DONT_TRAVERSE_CHILDREN;
315
                }
316
317
                if ($node instanceof Node\Expr\FuncCall) {
318
                    try {
319
                        ConstantNodeChecker::assertValidDefineFunctionCall($node);
320
                    } catch (InvalidConstantNode $e) {
321
                        return null;
322
                    }
323
324
                    /** @var Node\Scalar\String_ $nameNode */
325
                    $nameNode = $node->args[0]->value;
326
327
                    if ($nameNode->value === $this->constantName) {
328
                        $this->node = $node;
329
330
                        return NodeTraverser::STOP_TRAVERSAL;
331
                    }
332
                }
333
334
                if ($node instanceof Node\Stmt\Class_) {
335
                    return NodeTraverser::DONT_TRAVERSE_CHILDREN;
336
                }
337
338
                return null;
339
            }
340
341
            public function setConstantName(string $constantName) : void
342
            {
343
                $this->constantName = $constantName;
344
            }
345
346
            /**
347
             * @return Node\Stmt\Const_|Node\Expr\FuncCall|null
348
             */
349
            public function getNode() : ?Node
350
            {
351
                return $this->node;
352
            }
353
        };
354
    }
355
}
356