4 * This file is part of Psy Shell.
6 * (c) 2012-2018 Justin Hileman
8 * For the full copyright and license information, please view the LICENSE
9 * file that was distributed with this source code.
12 namespace Psy\CodeCleaner;
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;
30 * Validate that classes exist.
32 * This pass throws a FatalErrorException rather than letting PHP run
33 * headfirst into a real fatal error and die.
35 class ValidClassNamePass extends NamespaceAwarePass
37 const CLASS_TYPE = 'class';
38 const INTERFACE_TYPE = 'interface';
39 const TRAIT_TYPE = 'trait';
41 protected $checkTraits;
42 private $conditionalScopes = 0;
43 private $atLeastPhp55;
45 public function __construct()
47 $this->checkTraits = function_exists('trait_exists');
48 $this->atLeastPhp55 = version_compare(PHP_VERSION, '5.5', '>=');
52 * Validate class, interface and trait definitions.
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
60 public function enterNode(Node $node)
62 parent::enterNode($node);
64 if (self::isConditional($node)) {
65 $this->conditionalScopes++;
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);
82 * Validate `new` expressions, class constant fetches, and static calls.
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
92 public function leaveNode(Node $node)
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);
105 private static function isConditional(Node $node)
107 return $node instanceof If_ ||
108 $node instanceof While_ ||
109 $node instanceof Do_ ||
110 $node instanceof Switch_;
114 * Validate a class definition statement.
116 * @param Class_ $stmt
118 protected function validateClassStatement(Class_ $stmt)
120 $this->ensureCanDefine($stmt, self::CLASS_TYPE);
121 if (isset($stmt->extends)) {
122 $this->ensureClassExists($this->getFullyQualifiedName($stmt->extends), $stmt);
124 $this->ensureInterfacesExist($stmt->implements, $stmt);
128 * Validate an interface definition statement.
130 * @param Interface_ $stmt
132 protected function validateInterfaceStatement(Interface_ $stmt)
134 $this->ensureCanDefine($stmt, self::INTERFACE_TYPE);
135 $this->ensureInterfacesExist($stmt->extends, $stmt);
139 * Validate a trait definition statement.
141 * @param Trait_ $stmt
143 protected function validateTraitStatement(Trait_ $stmt)
145 $this->ensureCanDefine($stmt, self::TRAIT_TYPE);
149 * Validate a `new` expression.
153 protected function validateNewExpression(New_ $stmt)
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);
162 * Validate a class constant fetch expression's class.
164 * @param ClassConstFetch $stmt
166 protected function validateClassConstFetchExpression(ClassConstFetch $stmt)
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) {
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);
180 * Validate a class constant fetch expression's class.
182 * @param StaticCall $stmt
184 protected function validateStaticCallExpression(StaticCall $stmt)
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);
193 * Ensure that no class, interface or trait name collides with a new definition.
195 * @throws FatalErrorException
198 * @param string $scopeType
200 protected function ensureCanDefine(Stmt $stmt, $scopeType = self::CLASS_TYPE)
202 $name = $this->getFullyQualifiedName($stmt->name);
204 // check for name collisions
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;
214 if ($errorType !== null) {
215 throw $this->createError(sprintf('%s named %s already exists', ucfirst($errorType), $name), $stmt);
218 // Store creation for the rest of this code snippet so we can find local
220 $this->currentScope[strtolower($name)] = $scopeType;
224 * Ensure that a referenced class exists.
226 * @throws FatalErrorException
228 * @param string $name
231 protected function ensureClassExists($name, $stmt)
233 if (!$this->classExists($name)) {
234 throw $this->createError(sprintf('Class \'%s\' not found', $name), $stmt);
239 * Ensure that a referenced class _or interface_ exists.
241 * @throws FatalErrorException
243 * @param string $name
246 protected function ensureClassOrInterfaceExists($name, $stmt)
248 if (!$this->classExists($name) && !$this->interfaceExists($name)) {
249 throw $this->createError(sprintf('Class \'%s\' not found', $name), $stmt);
254 * Ensure that a statically called method exists.
256 * @throws FatalErrorException
258 * @param string $class
259 * @param string $name
262 protected function ensureMethodExists($class, $name, $stmt)
264 $this->ensureClassExists($class, $stmt);
266 // let's pretend all calls to self, parent and static are valid
267 if (in_array(strtolower($class), ['self', 'parent', 'static'])) {
271 // ... and all calls to classes defined right now
272 if ($this->findInScope($class) === self::CLASS_TYPE) {
276 // if method name is an expression, give it a pass for now
277 if ($name instanceof Expr) {
281 if (!method_exists($class, $name) && !method_exists($class, '__callStatic')) {
282 throw $this->createError(sprintf('Call to undefined method %s::%s()', $class, $name), $stmt);
287 * Ensure that a referenced interface exists.
289 * @throws FatalErrorException
291 * @param Interface_[] $interfaces
294 protected function ensureInterfacesExist($interfaces, $stmt)
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);
306 * Get a symbol type key for storing in the scope name cache.
308 * @deprecated No longer used. Scope type should be passed into ensureCanDefine directly.
309 * @codeCoverageIgnore
315 protected function getScopeType(Stmt $stmt)
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;
327 * Check whether a class exists, or has been defined in the current code snippet.
329 * Gives `self`, `static` and `parent` a free pass.
331 * @param string $name
335 protected function classExists($name)
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'])) {
344 return class_exists($name) || $this->findInScope($name) === self::CLASS_TYPE;
348 * Check whether an interface exists, or has been defined in the current code snippet.
350 * @param string $name
354 protected function interfaceExists($name)
356 return interface_exists($name) || $this->findInScope($name) === self::INTERFACE_TYPE;
360 * Check whether a trait exists, or has been defined in the current code snippet.
362 * @param string $name
366 protected function traitExists($name)
368 return $this->checkTraits && (trait_exists($name) || $this->findInScope($name) === self::TRAIT_TYPE);
372 * Find a symbol in the current code snippet scope.
374 * @param string $name
376 * @return string|null
378 protected function findInScope($name)
380 $name = strtolower($name);
381 if (isset($this->currentScope[$name])) {
382 return $this->currentScope[$name];
387 * Error creation factory.
392 * @return FatalErrorException
394 protected function createError($msg, $stmt)
396 return new FatalErrorException($msg, 0, E_ERROR, null, $stmt->getLine());