mirror of
https://github.com/danog/psalm.git
synced 2024-11-30 04:39:00 +01:00
Add support for checking existing properties
This commit is contained in:
parent
8d7d761a99
commit
32b4853c7b
@ -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;
|
||||
}
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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])) {
|
||||
|
@ -12,6 +12,8 @@ interface StatementsSource
|
||||
|
||||
public function getClassName();
|
||||
|
||||
public function getClassChecker();
|
||||
|
||||
/**
|
||||
* @return \PhpParser\Node\Name
|
||||
*/
|
||||
|
Loading…
Reference in New Issue
Block a user