diff --git a/src/CodeInspector/ClassChecker.php b/src/CodeInspector/ClassChecker.php index 262847d2f..a63c5d510 100644 --- a/src/CodeInspector/ClassChecker.php +++ b/src/CodeInspector/ClassChecker.php @@ -359,6 +359,8 @@ class ClassChecker implements StatementsSource public static function setThisClass($this_class) { self::$_this_class = $this_class; + + self::$_class_checkers = []; } public static function getThisClass() diff --git a/src/CodeInspector/FileChecker.php b/src/CodeInspector/FileChecker.php index eba0883fa..ed5b32353 100644 --- a/src/CodeInspector/FileChecker.php +++ b/src/CodeInspector/FileChecker.php @@ -96,7 +96,6 @@ class FileChecker implements StatementsSource public function checkWithClass($class_name, $method_vars = []) { $stmts = self::getStatements($this->_file_name); - $this->_class_name = $class_name; $class_method = new PhpParser\Node\Stmt\ClassMethod($class_name, ['stmts' => $stmts]); diff --git a/src/CodeInspector/FunctionChecker.php b/src/CodeInspector/FunctionChecker.php index 8bf38af47..8437fab43 100644 --- a/src/CodeInspector/FunctionChecker.php +++ b/src/CodeInspector/FunctionChecker.php @@ -16,9 +16,10 @@ class FunctionChecker implements StatementsSource protected $_absolute_class; protected $_statements_checker; protected $_source; + protected $_return_vars_in_scope = []; + protected $_return_vars_possibly_in_scope = []; protected $_function_params = []; - protected $_function_return_types = []; public function __construct(PhpParser\Node\FunctionLike $function, StatementsSource $source) { @@ -32,12 +33,9 @@ class FunctionChecker implements StatementsSource $this->_source = $source; } - public function check(&$extra_scope_vars = []) + public function check(&$vars_in_scope = [], &$vars_possibly_in_scope = []) { if ($this->_function->stmts) { - $vars_in_scope = $extra_scope_vars; - $vars_possibly_in_scope = $extra_scope_vars; - $statements_checker = new StatementsChecker($this, !empty($this->_function->params)); foreach ($this->_function->params as $param) { @@ -74,7 +72,42 @@ class FunctionChecker implements StatementsSource $statements_checker->check($this->_function->stmts, $vars_in_scope, $vars_possibly_in_scope); - $extra_scope_vars = $vars_in_scope; + if (isset($this->_return_vars_in_scope[''])) { + $vars_in_scope = TypeChecker::combineTypes($vars_in_scope, $this->_return_vars_in_scope['']); + } + + if (isset($this->_return_vars_possibly_in_scope[''])) { + $vars_possibly_in_scope = TypeChecker::combineTypes($vars_possibly_in_scope, $this->_return_vars_possibly_in_scope['']); + } + + foreach ($vars_in_scope as $var => $type) { + if (strpos($var, 'this->') !== 0) { + unset($vars_in_scope[$var]); + } + } + + foreach ($vars_in_scope as $var => $type) { + if (strpos($var, 'this->') !== 0) { + unset($vars_possibly_in_scope[$var]); + } + } + } + } + + public function addReturnTypes($return_type, $vars_in_scope, $vars_possibly_in_scope) + { + if (isset($this->_return_vars_in_scope[$return_type])) { + $this->_return_vars_in_scope[$return_type] = TypeChecker::combineTypes($vars_in_scope, $this->_return_vars_in_scope[$return_type]); + } + else { + $this->_return_vars_in_scope[$return_type] = $vars_in_scope; + } + + if (isset($this->_return_vars_possibly_in_scope[$return_type])) { + $this->_return_vars_possibly_in_scope[$return_type] = TypeChecker::combineTypes($vars_possibly_in_scope, $this->_return_vars_possibly_in_scope[$return_type]); + } + else { + $this->_return_vars_possibly_in_scope[$return_type] = $vars_possibly_in_scope; } } diff --git a/src/CodeInspector/StatementsChecker.php b/src/CodeInspector/StatementsChecker.php index f5182714b..28f40191d 100644 --- a/src/CodeInspector/StatementsChecker.php +++ b/src/CodeInspector/StatementsChecker.php @@ -269,6 +269,7 @@ class StatementsChecker if (!$has_ending_statments) { $vars = array_diff_key($if_vars_possibly_in_scope, $vars_possibly_in_scope); + // if we're leaving this block, add vars to outer for loop scope if ($has_leaving_statments) { $for_vars_possibly_in_scope = array_merge($for_vars_possibly_in_scope, $vars); } @@ -364,6 +365,7 @@ class StatementsChecker if (!$has_ending_statments) { $vars = array_diff_key($elseif_vars_possibly_in_scope, $vars_possibly_in_scope); + // if we're leaving this block, add vars to outer for loop scope if ($has_leaving_statements) { $for_vars_possibly_in_scope = array_merge($vars, $for_vars_possibly_in_scope); } @@ -902,7 +904,7 @@ class StatementsChecker if ($stmt->var instanceof PhpParser\Node\Expr\Variable) { if ($stmt->var->name === 'this') { if (is_string($stmt->name)) { - if (!FileChecker::shouldCheckClassProperties($this->_file_name)) { + if (!ClassChecker::getThisClass() && !FileChecker::shouldCheckClassProperties($this->_file_name)) { // ignore this property } else { $class_checker = $this->_source->getClassChecker(); @@ -915,7 +917,7 @@ class StatementsChecker $property_id = $this->_absolute_class . '::' . $stmt->name; $var_id = $stmt->var->name . '->' . $stmt->name; - $var_defined = isset($vars_in_scope[$var_id]); + $var_defined = isset($vars_in_scope[$var_id]) || isset($vars_possibly_in_scope[$var_id]); if ((ClassChecker::getThisClass() && !$var_defined) || (!ClassChecker::getThisClass() && !$var_defined && !self::_propertyExists($property_id))) { throw new UndefinedPropertyException('$' . $var_id . ' is not defined', $this->_file_name, $stmt->getLine()); @@ -1277,6 +1279,8 @@ class StatementsChecker $vars_in_scope[$var_id] = 'mixed'; } + $vars_possibly_in_scope[$var_id] = true; + // right now we have to settle for mixed self::$_this_assignments[$method_id][$stmt->var->name] = 'mixed'; //self::$_this_assignments[$method_id][$stmt->var->name] = $vars_in_scope[$property_id]; @@ -1338,24 +1342,26 @@ class StatementsChecker $method_id = ClassChecker::getThisClass() . '::' . $stmt->name; - $this_vars_in_scope = []; - - foreach ($vars_in_scope as $var => $type) { - if (strpos($var, 'this->') === 0) { - $this_vars_in_scope[$var] = $type; - } - } - $method_checker = ClassChecker::getMethodChecker($method_id); if ($method_checker->getMethodId() !== $this->_source->getMethodId()) { - $method_checker->check($this_vars_in_scope); + $this_vars_in_scope = []; - foreach ($this_vars_in_scope as $var => $type) { + $this_vars_possibly_in_scope = []; + + foreach ($vars_possibly_in_scope as $var => $type) { if (strpos($var, 'this->') === 0) { - $vars_in_scope[$var] = $type; + $this_vars_possibly_in_scope[$var] = true; } } + + foreach ($vars_in_scope as $var => $type) { + if (strpos($var, 'this->') === 0) { + $this_vars_in_scope[$var] = $type; + } + } + + $method_checker->check($this_vars_in_scope, $this_vars_possibly_in_scope); } } } @@ -1732,6 +1738,10 @@ class StatementsChecker else { $stmt->returnType = 'void'; } + + if ($this->_source instanceof FunctionChecker) { + $this->_source->addReturnTypes($stmt->expr ? $stmt->returnType : '', $vars_in_scope, $vars_possibly_in_scope); + } } protected function _checkTernary(PhpParser\Node\Expr\Ternary $stmt, array &$vars_in_scope, array &$vars_possibly_in_scope)