annotate vendor/psy/psysh/src/CodeCleaner/ValidClassNamePass.php @ 19:fa3358dc1485 tip

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