5de75d5f9154853cffb5b033b0dc960b46e72f70
[yaffs-website] / vendor / psy / psysh / src / CodeCleaner / ValidClassNamePass.php
1 <?php
2
3 /*
4  * This file is part of Psy Shell.
5  *
6  * (c) 2012-2018 Justin Hileman
7  *
8  * For the full copyright and license information, please view the LICENSE
9  * file that was distributed with this source code.
10  */
11
12 namespace Psy\CodeCleaner;
13
14 use PhpParser\Node;
15 use PhpParser\Node\Expr;
16 use PhpParser\Node\Expr\ClassConstFetch;
17 use PhpParser\Node\Expr\New_;
18 use PhpParser\Node\Expr\StaticCall;
19 use PhpParser\Node\Stmt;
20 use PhpParser\Node\Stmt\Class_;
21 use PhpParser\Node\Stmt\Do_;
22 use PhpParser\Node\Stmt\If_;
23 use PhpParser\Node\Stmt\Interface_;
24 use PhpParser\Node\Stmt\Switch_;
25 use PhpParser\Node\Stmt\Trait_;
26 use PhpParser\Node\Stmt\While_;
27 use Psy\Exception\FatalErrorException;
28
29 /**
30  * Validate that classes exist.
31  *
32  * This pass throws a FatalErrorException rather than letting PHP run
33  * headfirst into a real fatal error and die.
34  */
35 class ValidClassNamePass extends NamespaceAwarePass
36 {
37     const CLASS_TYPE     = 'class';
38     const INTERFACE_TYPE = 'interface';
39     const TRAIT_TYPE     = 'trait';
40
41     protected $checkTraits;
42     private $conditionalScopes = 0;
43     private $atLeastPhp55;
44
45     public function __construct()
46     {
47         $this->checkTraits = function_exists('trait_exists');
48         $this->atLeastPhp55 = version_compare(PHP_VERSION, '5.5', '>=');
49     }
50
51     /**
52      * Validate class, interface and trait definitions.
53      *
54      * Validate them upon entering the node, so that we know about their
55      * presence and can validate constant fetches and static calls in class or
56      * trait methods.
57      *
58      * @param Node $node
59      */
60     public function enterNode(Node $node)
61     {
62         parent::enterNode($node);
63
64         if (self::isConditional($node)) {
65             $this->conditionalScopes++;
66         } else {
67             // @todo add an "else" here which adds a runtime check for instances where we can't tell
68             // whether a class is being redefined by static analysis alone.
69             if ($this->conditionalScopes === 0) {
70                 if ($node instanceof Class_) {
71                     $this->validateClassStatement($node);
72                 } elseif ($node instanceof Interface_) {
73                     $this->validateInterfaceStatement($node);
74                 } elseif ($node instanceof Trait_) {
75                     $this->validateTraitStatement($node);
76                 }
77             }
78         }
79     }
80
81     /**
82      * Validate `new` expressions, class constant fetches, and static calls.
83      *
84      * @throws FatalErrorException if a class, interface or trait is referenced which does not exist
85      * @throws FatalErrorException if a class extends something that is not a class
86      * @throws FatalErrorException if a class implements something that is not an interface
87      * @throws FatalErrorException if an interface extends something that is not an interface
88      * @throws FatalErrorException if a class, interface or trait redefines an existing class, interface or trait name
89      *
90      * @param Node $node
91      */
92     public function leaveNode(Node $node)
93     {
94         if (self::isConditional($node)) {
95             $this->conditionalScopes--;
96         } elseif ($node instanceof New_) {
97             $this->validateNewExpression($node);
98         } elseif ($node instanceof ClassConstFetch) {
99             $this->validateClassConstFetchExpression($node);
100         } elseif ($node instanceof StaticCall) {
101             $this->validateStaticCallExpression($node);
102         }
103     }
104
105     private static function isConditional(Node $node)
106     {
107         return $node instanceof If_ ||
108             $node instanceof While_ ||
109             $node instanceof Do_ ||
110             $node instanceof Switch_;
111     }
112
113     /**
114      * Validate a class definition statement.
115      *
116      * @param Class_ $stmt
117      */
118     protected function validateClassStatement(Class_ $stmt)
119     {
120         $this->ensureCanDefine($stmt, self::CLASS_TYPE);
121         if (isset($stmt->extends)) {
122             $this->ensureClassExists($this->getFullyQualifiedName($stmt->extends), $stmt);
123         }
124         $this->ensureInterfacesExist($stmt->implements, $stmt);
125     }
126
127     /**
128      * Validate an interface definition statement.
129      *
130      * @param Interface_ $stmt
131      */
132     protected function validateInterfaceStatement(Interface_ $stmt)
133     {
134         $this->ensureCanDefine($stmt, self::INTERFACE_TYPE);
135         $this->ensureInterfacesExist($stmt->extends, $stmt);
136     }
137
138     /**
139      * Validate a trait definition statement.
140      *
141      * @param Trait_ $stmt
142      */
143     protected function validateTraitStatement(Trait_ $stmt)
144     {
145         $this->ensureCanDefine($stmt, self::TRAIT_TYPE);
146     }
147
148     /**
149      * Validate a `new` expression.
150      *
151      * @param New_ $stmt
152      */
153     protected function validateNewExpression(New_ $stmt)
154     {
155         // if class name is an expression or an anonymous class, give it a pass for now
156         if (!$stmt->class instanceof Expr && !$stmt->class instanceof Class_) {
157             $this->ensureClassExists($this->getFullyQualifiedName($stmt->class), $stmt);
158         }
159     }
160
161     /**
162      * Validate a class constant fetch expression's class.
163      *
164      * @param ClassConstFetch $stmt
165      */
166     protected function validateClassConstFetchExpression(ClassConstFetch $stmt)
167     {
168         // there is no need to check exists for ::class const for php 5.5 or newer
169         if (strtolower($stmt->name) === 'class' && $this->atLeastPhp55) {
170             return;
171         }
172
173         // if class name is an expression, give it a pass for now
174         if (!$stmt->class instanceof Expr) {
175             $this->ensureClassOrInterfaceExists($this->getFullyQualifiedName($stmt->class), $stmt);
176         }
177     }
178
179     /**
180      * Validate a class constant fetch expression's class.
181      *
182      * @param StaticCall $stmt
183      */
184     protected function validateStaticCallExpression(StaticCall $stmt)
185     {
186         // if class name is an expression, give it a pass for now
187         if (!$stmt->class instanceof Expr) {
188             $this->ensureMethodExists($this->getFullyQualifiedName($stmt->class), $stmt->name, $stmt);
189         }
190     }
191
192     /**
193      * Ensure that no class, interface or trait name collides with a new definition.
194      *
195      * @throws FatalErrorException
196      *
197      * @param Stmt   $stmt
198      * @param string $scopeType
199      */
200     protected function ensureCanDefine(Stmt $stmt, $scopeType = self::CLASS_TYPE)
201     {
202         $name = $this->getFullyQualifiedName($stmt->name);
203
204         // check for name collisions
205         $errorType = null;
206         if ($this->classExists($name)) {
207             $errorType = self::CLASS_TYPE;
208         } elseif ($this->interfaceExists($name)) {
209             $errorType = self::INTERFACE_TYPE;
210         } elseif ($this->traitExists($name)) {
211             $errorType = self::TRAIT_TYPE;
212         }
213
214         if ($errorType !== null) {
215             throw $this->createError(sprintf('%s named %s already exists', ucfirst($errorType), $name), $stmt);
216         }
217
218         // Store creation for the rest of this code snippet so we can find local
219         // issue too
220         $this->currentScope[strtolower($name)] = $scopeType;
221     }
222
223     /**
224      * Ensure that a referenced class exists.
225      *
226      * @throws FatalErrorException
227      *
228      * @param string $name
229      * @param Stmt   $stmt
230      */
231     protected function ensureClassExists($name, $stmt)
232     {
233         if (!$this->classExists($name)) {
234             throw $this->createError(sprintf('Class \'%s\' not found', $name), $stmt);
235         }
236     }
237
238     /**
239      * Ensure that a referenced class _or interface_ exists.
240      *
241      * @throws FatalErrorException
242      *
243      * @param string $name
244      * @param Stmt   $stmt
245      */
246     protected function ensureClassOrInterfaceExists($name, $stmt)
247     {
248         if (!$this->classExists($name) && !$this->interfaceExists($name)) {
249             throw $this->createError(sprintf('Class \'%s\' not found', $name), $stmt);
250         }
251     }
252
253     /**
254      * Ensure that a statically called method exists.
255      *
256      * @throws FatalErrorException
257      *
258      * @param string $class
259      * @param string $name
260      * @param Stmt   $stmt
261      */
262     protected function ensureMethodExists($class, $name, $stmt)
263     {
264         $this->ensureClassExists($class, $stmt);
265
266         // let's pretend all calls to self, parent and static are valid
267         if (in_array(strtolower($class), ['self', 'parent', 'static'])) {
268             return;
269         }
270
271         // ... and all calls to classes defined right now
272         if ($this->findInScope($class) === self::CLASS_TYPE) {
273             return;
274         }
275
276         // if method name is an expression, give it a pass for now
277         if ($name instanceof Expr) {
278             return;
279         }
280
281         if (!method_exists($class, $name) && !method_exists($class, '__callStatic')) {
282             throw $this->createError(sprintf('Call to undefined method %s::%s()', $class, $name), $stmt);
283         }
284     }
285
286     /**
287      * Ensure that a referenced interface exists.
288      *
289      * @throws FatalErrorException
290      *
291      * @param Interface_[] $interfaces
292      * @param Stmt         $stmt
293      */
294     protected function ensureInterfacesExist($interfaces, $stmt)
295     {
296         foreach ($interfaces as $interface) {
297             /** @var string $name */
298             $name = $this->getFullyQualifiedName($interface);
299             if (!$this->interfaceExists($name)) {
300                 throw $this->createError(sprintf('Interface \'%s\' not found', $name), $stmt);
301             }
302         }
303     }
304
305     /**
306      * Get a symbol type key for storing in the scope name cache.
307      *
308      * @deprecated No longer used. Scope type should be passed into ensureCanDefine directly.
309      * @codeCoverageIgnore
310      *
311      * @param Stmt $stmt
312      *
313      * @return string
314      */
315     protected function getScopeType(Stmt $stmt)
316     {
317         if ($stmt instanceof Class_) {
318             return self::CLASS_TYPE;
319         } elseif ($stmt instanceof Interface_) {
320             return self::INTERFACE_TYPE;
321         } elseif ($stmt instanceof Trait_) {
322             return self::TRAIT_TYPE;
323         }
324     }
325
326     /**
327      * Check whether a class exists, or has been defined in the current code snippet.
328      *
329      * Gives `self`, `static` and `parent` a free pass.
330      *
331      * @param string $name
332      *
333      * @return bool
334      */
335     protected function classExists($name)
336     {
337         // Give `self`, `static` and `parent` a pass. This will actually let
338         // some errors through, since we're not checking whether the keyword is
339         // being used in a class scope.
340         if (in_array(strtolower($name), ['self', 'static', 'parent'])) {
341             return true;
342         }
343
344         return class_exists($name) || $this->findInScope($name) === self::CLASS_TYPE;
345     }
346
347     /**
348      * Check whether an interface exists, or has been defined in the current code snippet.
349      *
350      * @param string $name
351      *
352      * @return bool
353      */
354     protected function interfaceExists($name)
355     {
356         return interface_exists($name) || $this->findInScope($name) === self::INTERFACE_TYPE;
357     }
358
359     /**
360      * Check whether a trait exists, or has been defined in the current code snippet.
361      *
362      * @param string $name
363      *
364      * @return bool
365      */
366     protected function traitExists($name)
367     {
368         return $this->checkTraits && (trait_exists($name) || $this->findInScope($name) === self::TRAIT_TYPE);
369     }
370
371     /**
372      * Find a symbol in the current code snippet scope.
373      *
374      * @param string $name
375      *
376      * @return string|null
377      */
378     protected function findInScope($name)
379     {
380         $name = strtolower($name);
381         if (isset($this->currentScope[$name])) {
382             return $this->currentScope[$name];
383         }
384     }
385
386     /**
387      * Error creation factory.
388      *
389      * @param string $msg
390      * @param Stmt   $stmt
391      *
392      * @return FatalErrorException
393      */
394     protected function createError($msg, $stmt)
395     {
396         return new FatalErrorException($msg, 0, E_ERROR, null, $stmt->getLine());
397     }
398 }