Completed
Push — master ( a86a81...89795a )
by Marco
19s
created

PhpStormStubsSourceStubber.php$0 ➔ clearNodes()   A

Complexity

Conditions 1

Size

Total Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 4
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 5
ccs 4
cts 4
cp 1
rs 10
c 0
b 0
f 0
cc 1
crap 1
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Roave\BetterReflection\SourceLocator\SourceStubber;
6
7
use PhpParser\Node;
8
use PhpParser\NodeTraverser;
9
use PhpParser\NodeVisitor\NameResolver;
10
use PhpParser\NodeVisitorAbstract;
11
use PhpParser\Parser;
12
use PhpParser\PrettyPrinter\Standard;
13
use Roave\BetterReflection\SourceLocator\FileChecker;
14
use Roave\BetterReflection\SourceLocator\SourceStubber\Exception\CouldNotFindPhpStormStubs;
15
use Traversable;
16
use function array_key_exists;
17
use function explode;
18
use function file_get_contents;
19
use function is_dir;
20
use function sprintf;
21
use function str_replace;
22
23
/**
24
 * @internal
25
 */
26
final class PhpStormStubsSourceStubber implements SourceStubber
27
{
28
    private const BUILDER_OPTIONS    = ['shortArraySyntax' => true];
29
    private const SEARCH_DIRECTORIES = [
30
        __DIR__ . '/../../../../../jetbrains/phpstorm-stubs',
31
        __DIR__ . '/../../../vendor/jetbrains/phpstorm-stubs',
32
    ];
33
34
    /** @var Parser */
35
    private $phpParser;
36
37
    /** @var Standard */
38
    private $prettyPrinter;
39
40
    /** @var NodeTraverser */
41
    private $nodeTraverser;
42
43
    /** @var string|null */
44
    private $stubsDirectory;
45
46
    /** @var NodeVisitorAbstract */
47
    private $cachingVisitor;
48
49
    /** @var array<string, Node\Stmt\ClassLike> */
50
    private $classNodes = [];
51
52
    /** @var array<string, Node\Stmt\Function_> */
53
    private $functionNodes = [];
54
55 693
    public function __construct(Parser $phpParser)
56
    {
57 693
        $this->phpParser     = $phpParser;
58 693
        $this->prettyPrinter = new Standard(self::BUILDER_OPTIONS);
59
60 693
        $this->cachingVisitor = $this->createCachingVisitor();
61
62 693
        $this->nodeTraverser = new NodeTraverser();
63 693
        $this->nodeTraverser->addVisitor(new NameResolver());
64 693
        $this->nodeTraverser->addVisitor($this->cachingVisitor);
65 693
    }
66
67
    /**
68
     * {@inheritDoc}
69
     */
70 79
    public function generateClassStub(string $className) : ?StubData
71
    {
72 79
        if (! array_key_exists($className, PhpStormStubsMap::CLASSES)) {
73 1
            return null;
74
        }
75
76 78
        $filePath = PhpStormStubsMap::CLASSES[$className];
77
78 78
        if (! array_key_exists($className, $this->classNodes)) {
79 78
            $this->parseFile($filePath);
80
        }
81
82 78
        $stub = $this->createStub($this->classNodes[$className]);
83
84 78
        if ($className === Traversable::class) {
85
            // See https://github.com/JetBrains/phpstorm-stubs/commit/0778a26992c47d7dbee4d0b0bfb7fad4344371b1#diff-575bacb45377d474336c71cbf53c1729
86 43
            $stub = str_replace(' extends \iterable', '', $stub);
87
        }
88
89 78
        return new StubData($stub, $this->getExtensionFromFilePath($filePath));
90
    }
91
92
    /**
93
     * {@inheritDoc}
94
     */
95 614
    public function generateFunctionStub(string $functionName) : ?StubData
96
    {
97 614
        if (! array_key_exists($functionName, PhpStormStubsMap::FUNCTIONS)) {
98 1
            return null;
99
        }
100
101 613
        $filePath = PhpStormStubsMap::FUNCTIONS[$functionName];
102
103 613
        if (! array_key_exists($functionName, $this->functionNodes)) {
104 613
            $this->parseFile($filePath);
105
        }
106
107 613
        return new StubData($this->createStub($this->functionNodes[$functionName]), $this->getExtensionFromFilePath($filePath));
108
    }
109
110 691
    private function parseFile(string $filePath) : void
111
    {
112 691
        $absoluteFilePath = $this->getAbsoluteFilePath($filePath);
113 691
        FileChecker::assertReadableFile($absoluteFilePath);
114
115 691
        $ast = $this->phpParser->parse(file_get_contents($absoluteFilePath));
116
117
        /** @psalm-suppress UndefinedMethod */
118 691
        $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...
119
120 691
        $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 115 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...
121
122
        /** @psalm-suppress UndefinedMethod */
123 691
        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...
124 115
            $this->classNodes[$className] = $classNode;
125
        }
126
127
        /** @psalm-suppress UndefinedMethod */
128 691
        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...
129 618
            $this->functionNodes[$functionName] = $functionNode;
130
        }
131 691
    }
132
133 691
    private function createStub(Node $node) : string
134
    {
135 691
        return "<?php\n\n" . $this->prettyPrinter->prettyPrint([$node]) . "\n";
136
    }
137
138
    private function createCachingVisitor() : NodeVisitorAbstract
139
    {
140
        return new class() extends NodeVisitorAbstract
141
        {
142
            /** @var array<string, Node\Stmt\ClassLike> */
143
            private $classNodes = [];
144
145
            /** @var array<string, Node\Stmt\Function_> */
146
            private $functionNodes = [];
147
148 691
            public function enterNode(Node $node) : ?int
149
            {
150 691
                if ($node instanceof Node\Stmt\ClassLike) {
151 115
                    $this->classNodes[$node->namespacedName->toString()] = $node;
152
153 115
                    return NodeTraverser::DONT_TRAVERSE_CHILDREN;
154
                }
155
156 660
                if ($node instanceof Node\Stmt\Function_) {
157
                    /** @psalm-suppress UndefinedPropertyFetch */
158 618
                    $this->functionNodes[$node->namespacedName->toString()] = $node;
159
160 618
                    return NodeTraverser::DONT_TRAVERSE_CHILDREN;
161
                }
162
163 180
                return null;
164
            }
165
166
            /**
167
             * @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...
168
             */
169 691
            public function getClassNodes() : array
170
            {
171 691
                return $this->classNodes;
172
            }
173
174
            /**
175
             * @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...
176
             */
177 691
            public function getFunctionNodes() : array
178
            {
179 691
                return $this->functionNodes;
180
            }
181
182 691
            public function clearNodes() : void
183
            {
184 691
                $this->classNodes    = [];
185 691
                $this->functionNodes = [];
186 691
            }
187
        };
188
    }
189
190 691
    private function getExtensionFromFilePath(string $filePath) : string
191
    {
192 691
        return explode('/', $filePath)[0];
193
    }
194
195 691
    private function getAbsoluteFilePath(string $filePath) : string
196
    {
197 691
        return sprintf('%s/%s', $this->getStubsDirectory(), $filePath);
198
    }
199
200 691
    private function getStubsDirectory() : string
201
    {
202 691
        if ($this->stubsDirectory !== null) {
203 54
            return $this->stubsDirectory;
204
        }
205
206 691
        foreach (self::SEARCH_DIRECTORIES as $directory) {
207 691
            if (is_dir($directory)) {
208 691
                return $this->stubsDirectory = $directory;
209
            }
210
        }
211
212
        throw CouldNotFindPhpStormStubs::create();
213
    }
214
}
215