From 32b4853c7b47384ea35d3ad0de7cc0fb62a8d971 Mon Sep 17 00:00:00 2001 From: Matthew Brown Date: Fri, 29 Jan 2016 18:48:09 -0500 Subject: [PATCH] Add support for checking existing properties --- lib/ClassChecker.php | 28 ++++++++++++ lib/FileChecker.php | 11 +++-- lib/FunctionChecker.php | 9 +++- lib/NamespaceChecker.php | 11 +++-- lib/StatementsChecker.php | 89 +++++++++++++++++++++++++++++++-------- lib/StatementsSource.php | 2 + 6 files changed, 126 insertions(+), 24 deletions(-) diff --git a/lib/ClassChecker.php b/lib/ClassChecker.php index c26f9cbe5..5926d1250 100644 --- a/lib/ClassChecker.php +++ b/lib/ClassChecker.php @@ -13,6 +13,8 @@ class ClassChecker implements StatementsSource protected $_namespace; protected $_aliased_classes; protected $_absolute_class; + protected $_class_properties = []; + protected $_has_custom_get = false; protected static $_existing_classes = []; @@ -35,12 +37,23 @@ class ClassChecker implements StatementsSource $leftover_stmts = []; + try { + new \ReflectionMethod($this->_absolute_class . '::__get'); + $this->_has_custom_get = true; + + } catch (\ReflectionException $e) {} + foreach ($this->_class->stmts as $stmt) { if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod) { $method_checker = new ClassMethodChecker($stmt, $this); $method_checker->check(); } else { + if ($stmt instanceof PhpParser\Node\Stmt\Property) { + foreach ($stmt->props as $property) { + $this->_class_properties[] = $property->name; + } + } $leftover_stmts[] = $stmt; } } @@ -140,8 +153,23 @@ class ClassChecker implements StatementsSource return $this->_file_name; } + public function getClassChecker() + { + return $this; + } + public function isStatic() { return false; } + + public function hasCustomGet() + { + return $this->_has_custom_get; + } + + public function getPropertyNames() + { + return $this->_class_properties; + } } diff --git a/lib/FileChecker.php b/lib/FileChecker.php index ab2b6b145..cb1d3b6cd 100644 --- a/lib/FileChecker.php +++ b/lib/FileChecker.php @@ -51,7 +51,7 @@ class FileChecker implements StatementsSource } elseif ($stmt instanceof PhpParser\Node\Stmt\Trait_) { // @todo check trait - + } elseif ($stmt instanceof PhpParser\Node\Stmt\Namespace_) { $namespace_name = implode('\\', $stmt->name->parts); @@ -90,7 +90,7 @@ class FileChecker implements StatementsSource { if (isset(self::$_file_checkers[$file_name])) { $aliased_classes = self::$_file_checkers[$file_name]->getAliasedClasses($namespace); - + } else { $file_checker = new FileChecker($file_name); $file_checker->check(false); @@ -171,7 +171,7 @@ class FileChecker implements StatementsSource if ($namespace_name && isset(self::$_namespace_aliased_classes[$namespace_name])) { return self::$_namespace_aliased_classes[$namespace_name]; } - + return $this->_aliased_classes; } @@ -185,6 +185,11 @@ class FileChecker implements StatementsSource return $this->_class_name; } + public function getClassChecker() + { + return null; + } + public function getClassExtends() { return null; diff --git a/lib/FunctionChecker.php b/lib/FunctionChecker.php index 85aad7fa4..3fe36e49c 100644 --- a/lib/FunctionChecker.php +++ b/lib/FunctionChecker.php @@ -15,7 +15,8 @@ class FunctionChecker implements StatementsSource protected $_is_static = false; protected $_absolute_class; protected $_statements_checker; - + protected $_source; + protected $_function_params = []; protected $_function_return_types = []; @@ -28,6 +29,7 @@ class FunctionChecker implements StatementsSource $this->_class_extends = $source->getClassExtends(); $this->_file_name = $source->getFileName(); $this->_absolute_class = $source->getAbsoluteClass(); + $this->_source = $source; $this->_statements_checker = new StatementsChecker($this, substr($this->_file_name, -4) === '.php'); @@ -88,6 +90,11 @@ class FunctionChecker implements StatementsSource return $this->_class_name; } + public function getClassChecker() + { + return $this->_source->getClassChecker(); + } + public function getClassExtends() { return $this->_class_extends; diff --git a/lib/NamespaceChecker.php b/lib/NamespaceChecker.php index 613694f9c..e83323490 100644 --- a/lib/NamespaceChecker.php +++ b/lib/NamespaceChecker.php @@ -11,7 +11,7 @@ class NamespaceChecker implements StatementsSource protected $_contained_classes = []; protected $_aliased_classes = []; protected $_file_name; - + public function __construct(\PhpParser\Node\Stmt\Namespace_ $namespace, StatementsSource $source) { $this->_namespace = $namespace; @@ -33,10 +33,10 @@ class NamespaceChecker implements StatementsSource } } elseif ($stmt instanceof PhpParser\Node\Stmt\Interface_) { // @todo check interface - + } elseif ($stmt instanceof PhpParser\Node\Stmt\Trait_) { // @todo check trait - + } elseif ($stmt instanceof PhpParser\Node\Stmt\Use_) { foreach ($stmt->uses as $use) { $this->_aliased_classes[$use->alias] = implode('\\', $use->name->parts); @@ -81,6 +81,11 @@ class NamespaceChecker implements StatementsSource return null; } + public function getClassChecker() + { + return null; + } + public function getClassExtends() { return null; diff --git a/lib/StatementsChecker.php b/lib/StatementsChecker.php index 352d75959..fd8773b33 100644 --- a/lib/StatementsChecker.php +++ b/lib/StatementsChecker.php @@ -35,6 +35,7 @@ class StatementsChecker protected static $_static_methods = []; protected static $_declaring_classes = []; protected static $_existing_static_vars = []; + protected static $_existing_properties = []; public function __construct(StatementsSource $source = null, $check_variables = true) { @@ -571,7 +572,23 @@ class StatementsChecker { if ($stmt->var instanceof PhpParser\Node\Expr\Variable) { if ($stmt->var->name === 'this') { + $class_checker = $this->_source->getClassChecker(); + if ($class_checker) { + if ($class_checker->hasCustomGet()) { + // do nothing + } elseif (is_string($stmt->name)) { + $property_names = $class_checker->getPropertyNames(); + if (!in_array($stmt->name, $property_names)) { + if (!self::_propertyExists($this->_absolute_class . '::' . $stmt->name)) { + throw new CodeException('$this->' . $stmt->name . ' is not defined', $this->_file_name, $stmt->getLine()); + } + } + } + + } else { + throw new CodeException('Cannot use $this when not inside class', $this->_file_name, $stmt->getLine()); + } } else { $this->_checkVariable($stmt->var, $vars_in_scope, $vars_possibly_in_scope); } @@ -705,23 +722,6 @@ class StatementsChecker $vars_possibly_in_scope[$stmt->var->name] = true; $this->registerVariable($stmt->var->name, $stmt->var->getLine()); - } elseif ($stmt->var instanceof PhpParser\Node\Expr\List_) { - foreach ($stmt->var->vars as $var) { - if ($var) { - $vars_in_scope[$var->name] = true; - $vars_possibly_in_scope[$var->name] = true; - $this->registerVariable($var->name, $var->getLine()); - } - } - - } else if ($stmt->var instanceof PhpParser\Node\Expr\ArrayDimFetch && $stmt->var->var instanceof PhpParser\Node\Expr\Variable) { - // if it's an array assignment - $vars_in_scope[$stmt->var->var->name] = true; - $vars_possibly_in_scope[$stmt->var->var->name] = true; - $this->registerVariable($stmt->var->var->name, $stmt->var->var->getLine()); - } - - if ($stmt->var instanceof PhpParser\Node\Expr\Variable && is_string($stmt->var->name)) { $comments = []; $doc_comment = $stmt->getDocComment(); @@ -763,6 +763,26 @@ class StatementsChecker $vars_in_scope[$stmt->var->name] = $stmt->expr->returnType; } } + + } elseif ($stmt->var instanceof PhpParser\Node\Expr\List_) { + foreach ($stmt->var->vars as $var) { + if ($var) { + $vars_in_scope[$var->name] = true; + $vars_possibly_in_scope[$var->name] = true; + $this->registerVariable($var->name, $var->getLine()); + } + } + + } else if ($stmt->var instanceof PhpParser\Node\Expr\ArrayDimFetch && $stmt->var->var instanceof PhpParser\Node\Expr\Variable) { + // if it's an array assignment + $vars_in_scope[$stmt->var->var->name] = true; + $vars_possibly_in_scope[$stmt->var->var->name] = true; + $this->registerVariable($stmt->var->var->name, $stmt->var->var->getLine()); + } else if ($stmt->var instanceof PhpParser\Node\Expr\PropertyFetch) { + if ($stmt->var->var->name === 'this') { + self::$_existing_properties[$this->_absolute_class . '::' . $stmt->var->name] = 1; + } + } } @@ -1117,6 +1137,41 @@ class StatementsChecker } } + public static function _getClassProperties(\ReflectionClass $reflection_class, $absolute_class_name) + { + $properties = $reflection_class->getProperties(); + $props_arr = []; + + foreach ($properties as $reflection_property){ + if ($reflection_property->isPrivate() || $reflection_property->isStatic()) { + continue; + } + + self::$_existing_properties[$absolute_class_name . '::' . $reflection_property->getName()] = 1; + } + + $parent_reflection_class = $reflection_class->getParentClass(); + + if ($parent_reflection_class){ + self::_getClassProperties($parent_reflection_class, $absolute_class_name); + } + } + + protected static function _propertyExists($property_id) + { + if (isset(self::$_existing_properties[$property_id])) { + return true; + } + + $absolute_class = explode('::', $property_id)[0]; + + $reflection_class = new \ReflectionClass($absolute_class); + + self::_getClassProperties($reflection_class, $absolute_class); + + return isset(self::$_existing_properties[$property_id]); + } + protected static function _methodExists($method_id) { if (isset(self::$_existing_methods[$method_id])) { diff --git a/lib/StatementsSource.php b/lib/StatementsSource.php index 4ccca3b66..5218854dc 100644 --- a/lib/StatementsSource.php +++ b/lib/StatementsSource.php @@ -12,6 +12,8 @@ interface StatementsSource public function getClassName(); + public function getClassChecker(); + /** * @return \PhpParser\Node\Name */