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

PhpStormStubsSourceStubber.php$0 ➔ enterNode()   C

Complexity

Conditions 11

Size

Total Lines 55

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 10
CRAP Score 11.0908

Importance

Changes 0
Metric Value
dl 0
loc 55
rs 6.8351
c 0
b 0
f 0
ccs 10
cts 11
cp 0.9091
cc 11
crap 11.0908

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
declare(strict_types=1);
4
5
namespace Roave\BetterReflection\SourceLocator\SourceStubber;
6
7
use PhpParser\BuilderHelpers;
8
use PhpParser\Node;
9
use PhpParser\NodeTraverser;
10
use PhpParser\NodeVisitor\NameResolver;
11
use PhpParser\NodeVisitorAbstract;
12
use PhpParser\Parser;
13
use PhpParser\PrettyPrinter\Standard;
14
use Roave\BetterReflection\Reflection\Exception\InvalidConstantNode;
15
use Roave\BetterReflection\SourceLocator\FileChecker;
16
use Roave\BetterReflection\SourceLocator\SourceStubber\Exception\CouldNotFindPhpStormStubs;
17
use Roave\BetterReflection\Util\ConstantNodeChecker;
18
use Traversable;
19
use function array_key_exists;
20
use function constant;
21
use function count;
22
use function defined;
23
use function explode;
24
use function file_get_contents;
25
use function in_array;
26
use function is_dir;
27
use function sprintf;
28
use function str_replace;
29
use function strtolower;
30
31
/**
32
 * @internal
33
 */
34
final class PhpStormStubsSourceStubber implements SourceStubber
35
{
36
    private const BUILDER_OPTIONS    = ['shortArraySyntax' => true];
37
    private const SEARCH_DIRECTORIES = [
38
        __DIR__ . '/../../../../../jetbrains/phpstorm-stubs',
39
        __DIR__ . '/../../../vendor/jetbrains/phpstorm-stubs',
40
    ];
41
42
    /** @var Parser */
43
    private $phpParser;
44
45
    /** @var Standard */
46
    private $prettyPrinter;
47
48
    /** @var NodeTraverser */
49
    private $nodeTraverser;
50
51
    /** @var string|null */
52
    private $stubsDirectory;
53
54
    /** @var NodeVisitorAbstract */
55 693
    private $cachingVisitor;
56
57 693
    /** @var array<string, Node\Stmt\ClassLike> */
58 693
    private $classNodes = [];
59
60 693
    /** @var array<string, Node\Stmt\Function_> */
61
    private $functionNodes = [];
62 693
63 693
    /** @var array<string, Node\Const_|Node\Expr\FuncCall> */
64 693
    private $constantNodes = [];
65 693
66
    public function __construct(Parser $phpParser)
67
    {
68
        $this->phpParser     = $phpParser;
69
        $this->prettyPrinter = new Standard(self::BUILDER_OPTIONS);
70 79
71
        $this->cachingVisitor = $this->createCachingVisitor();
72 79
73 1
        $this->nodeTraverser = new NodeTraverser();
74
        $this->nodeTraverser->addVisitor(new NameResolver());
75
        $this->nodeTraverser->addVisitor($this->cachingVisitor);
76 78
    }
77
78 78
    /**
79 78
     * {@inheritDoc}
80
     */
81
    public function generateClassStub(string $className) : ?StubData
82 78
    {
83
        if (! array_key_exists($className, PhpStormStubsMap::CLASSES)) {
84 78
            return null;
85
        }
86 43
87
        $filePath = PhpStormStubsMap::CLASSES[$className];
88
89 78
        if (! array_key_exists($className, $this->classNodes)) {
90
            $this->parseFile($filePath);
91
        }
92
93
        $stub = $this->createStub($this->classNodes[$className]);
94
95 614
        if ($className === Traversable::class) {
96
            // See https://github.com/JetBrains/phpstorm-stubs/commit/0778a26992c47d7dbee4d0b0bfb7fad4344371b1#diff-575bacb45377d474336c71cbf53c1729
97 614
            $stub = str_replace(' extends \iterable', '', $stub);
98 1
        }
99
100
        return new StubData($stub, $this->getExtensionFromFilePath($filePath));
101 613
    }
102
103 613
    /**
104 613
     * {@inheritDoc}
105
     */
106
    public function generateFunctionStub(string $functionName) : ?StubData
107 613
    {
108
        if (! array_key_exists($functionName, PhpStormStubsMap::FUNCTIONS)) {
109
            return null;
110 691
        }
111
112 691
        $filePath = PhpStormStubsMap::FUNCTIONS[$functionName];
113 691
114
        if (! array_key_exists($functionName, $this->functionNodes)) {
115 691
            $this->parseFile($filePath);
116
        }
117
118 691
        return new StubData($this->createStub($this->functionNodes[$functionName]), $this->getExtensionFromFilePath($filePath));
119
    }
120 691
121
    /**
122
     * {@inheritDoc}
123 691
     */
124 115
    public function generateConstantStub(string $constantName) : ?StubData
125
    {
126
        // https://github.com/JetBrains/phpstorm-stubs/pull/591
127
        if (in_array($constantName, ['TRUE', 'FALSE', 'NULL'], true)) {
128 691
            $constantName = strtolower($constantName);
129 618
        }
130
131 691
        if (! array_key_exists($constantName, PhpStormStubsMap::CONSTANTS)) {
132
            return null;
133 691
        }
134
135 691
        $filePath = PhpStormStubsMap::CONSTANTS[$constantName];
136
137
        if (! array_key_exists($constantName, $this->constantNodes)) {
138
            $this->parseFile($filePath);
139
        }
140
141
        return new StubData($this->createStub($this->constantNodes[$constantName]), $this->getExtensionFromFilePath($filePath));
142
    }
143
144
    private function parseFile(string $filePath) : void
145
    {
146
        $absoluteFilePath = $this->getAbsoluteFilePath($filePath);
147
        FileChecker::assertReadableFile($absoluteFilePath);
148 691
149
        $ast = $this->phpParser->parse(file_get_contents($absoluteFilePath));
150 691
151 115
        /** @psalm-suppress UndefinedMethod */
152
        $this->cachingVisitor->clearNodes();
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 clearNodes() does only exist in the following sub-classes of PhpParser\NodeVisitorAbstract: Roave\BetterReflection\S...tubsSourceStubber.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...
153 115
154
        $this->nodeTraverser->traverse($ast);
0 ignored issues
show
Bug introduced by Jaroslav Hanslík
It seems like $ast defined by $this->phpParser->parse(...nts($absoluteFilePath)) on line 149 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...
155
156 660
        /** @psalm-suppress UndefinedMethod */
157
        foreach ($this->cachingVisitor->getClassNodes() as $className => $classNode) {
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 getClassNodes() does only exist in the following sub-classes of PhpParser\NodeVisitorAbstract: Roave\BetterReflection\S...tubsSourceStubber.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...
158 618
            $this->classNodes[$className] = $classNode;
159
        }
160 618
161
        /** @psalm-suppress UndefinedMethod */
162
        foreach ($this->cachingVisitor->getFunctionNodes() as $functionName => $functionNode) {
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 getFunctionNodes() does only exist in the following sub-classes of PhpParser\NodeVisitorAbstract: Roave\BetterReflection\S...tubsSourceStubber.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...
163 180
            $this->functionNodes[$functionName] = $functionNode;
164
        }
165
166
        /** @psalm-suppress UndefinedMethod */
167
        foreach ($this->cachingVisitor->getConstantNodes() as $constantName => $constantNode) {
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 getConstantNodes() does only exist in the following sub-classes of PhpParser\NodeVisitorAbstract: Roave\BetterReflection\S...tubsSourceStubber.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...
168
            $this->constantNodes[$constantName] = $constantNode;
169 691
        }
170
    }
171 691
172
    private function createStub(Node $node) : string
173
    {
174
        return "<?php\n\n" . $this->prettyPrinter->prettyPrint([$node]) . ($node instanceof Node\Expr\FuncCall ? ';' : '') . "\n";
175
    }
176
177 691
    private function createCachingVisitor() : NodeVisitorAbstract
178
    {
179 691
        return new class() extends NodeVisitorAbstract
180
        {
181
            /** @var array<string, Node\Stmt\ClassLike> */
182 691
            private $classNodes = [];
183
184 691
            /** @var array<string, Node\Stmt\Function_> */
185 691
            private $functionNodes = [];
186 691
187
            /** @var array<string, Node\Stmt\Const_|Node\Expr\FuncCall> */
188
            private $constantNodes = [];
189
190 691
            public function enterNode(Node $node) : ?int
191
            {
192 691
                if ($node instanceof Node\Stmt\ClassLike) {
193
                    $this->classNodes[$node->namespacedName->toString()] = $node;
194
195 691
                    return NodeTraverser::DONT_TRAVERSE_CHILDREN;
196
                }
197 691
198
                if ($node instanceof Node\Stmt\Function_) {
199
                    /** @psalm-suppress UndefinedPropertyFetch */
200 691
                    $this->functionNodes[$node->namespacedName->toString()] = $node;
201
202 691
                    return NodeTraverser::DONT_TRAVERSE_CHILDREN;
203 54
                }
204
205
                if ($node instanceof Node\Stmt\Const_) {
206 691
                    foreach ($node->consts as $constNode) {
207 691
                        /** @psalm-suppress UndefinedPropertyFetch */
208 691
                        $this->constantNodes[$constNode->namespacedName->toString()] = $node;
209
                    }
210
211
                    return NodeTraverser::DONT_TRAVERSE_CHILDREN;
212
                }
213
214
                if ($node instanceof Node\Expr\FuncCall) {
215
                    try {
216
                        ConstantNodeChecker::assertValidDefineFunctionCall($node);
217
                    } catch (InvalidConstantNode $e) {
218
                        return null;
219
                    }
220
221
                    /** @var Node\Scalar\String_ $nameNode */
222
                    $nameNode     = $node->args[0]->value;
223
                    $constantName = $nameNode->value;
224
225
                    // Some constants has different values on different systems, some are not actual in stubs
226
                    if (defined($constantName)) {
227
                        $constantValue        = constant($constantName);
228
                        $node->args[1]->value = BuilderHelpers::normalizeValue($constantValue);
229
                    }
230
231
                    $this->constantNodes[$constantName] = $node;
232
233
                    if (count($node->args) === 3
234
                        && $node->args[2]->value instanceof Node\Expr\ConstFetch
235
                        && $node->args[2]->value->name->toLowerString() === 'true'
236
                    ) {
237
                        $this->constantNodes[strtolower($constantName)] = $node;
238
                    }
239
240
                    return NodeTraverser::DONT_TRAVERSE_CHILDREN;
241
                }
242
243
                return null;
244
            }
245
246
            /**
247
             * @return array<string, Node\Stmt\ClassLike>
0 ignored issues
show
Documentation introduced by Jaroslav Hanslík
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
248
             */
249
            public function getClassNodes() : array
250
            {
251
                return $this->classNodes;
252
            }
253
254
            /**
255
             * @return array<string, Node\Stmt\Function_>
0 ignored issues
show
Documentation introduced by Jaroslav Hanslík
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
256
             */
257
            public function getFunctionNodes() : array
258
            {
259
                return $this->functionNodes;
260
            }
261
262
            /**
263
             * @return array<string, Node\Stmt\Const_|Node\Expr\FuncCall>
0 ignored issues
show
Documentation introduced by Jaroslav Hanslík
The doc-type array<string, could not be parsed: Expected ">" at position 5, but found "end of type". (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
264
             */
265
            public function getConstantNodes() : array
266
            {
267
                return $this->constantNodes;
268
            }
269
270
            public function clearNodes() : void
271
            {
272
                $this->classNodes    = [];
273
                $this->functionNodes = [];
274
                $this->constantNodes = [];
275
            }
276
        };
277
    }
278
279
    private function getExtensionFromFilePath(string $filePath) : string
280
    {
281
        return explode('/', $filePath)[0];
282
    }
283
284
    private function getAbsoluteFilePath(string $filePath) : string
285
    {
286
        return sprintf('%s/%s', $this->getStubsDirectory(), $filePath);
287
    }
288
289
    private function getStubsDirectory() : string
290
    {
291
        if ($this->stubsDirectory !== null) {
292
            return $this->stubsDirectory;
293
        }
294
295
        foreach (self::SEARCH_DIRECTORIES as $directory) {
296
            if (is_dir($directory)) {
297
                return $this->stubsDirectory = $directory;
298
            }
299
        }
300
301
        throw CouldNotFindPhpStormStubs::create();
302
    }
303
}
304