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