1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-27 04:45:20 +01:00

Add array assignment checks and remove Error suffix

This commit is contained in:
Matthew Brown 2016-06-13 19:57:32 -04:00
parent 01247558d6
commit 933c60b06a
45 changed files with 500 additions and 268 deletions

View File

@ -2,9 +2,9 @@
namespace CodeInspector;
use CodeInspector\Issue\InvalidClassError;
use CodeInspector\Issue\UndefinedClassError;
use CodeInspector\Issue\UndefinedTraitError;
use CodeInspector\Issue\InvalidClass;
use CodeInspector\Issue\UndefinedClass;
use CodeInspector\Issue\UndefinedTrait;
use CodeInspector\ExceptionHandler;
use PhpParser;
use PhpParser\Error;
@ -97,7 +97,7 @@ class ClassChecker implements StatementsSource
$trait_name = self::getAbsoluteClassFromName($trait, $this->_namespace, $this->_aliased_classes);
if (!trait_exists($trait_name)) {
if (ExceptionHandler::accepts(
new UndefinedTraitError('Trait ' . $trait_name . ' does not exist', $this->_file_name, $trait->getLine())
new UndefinedTrait('Trait ' . $trait_name . ' does not exist', $this->_file_name, $trait->getLine())
)) {
return false;
}
@ -188,7 +188,7 @@ class ClassChecker implements StatementsSource
}
/**
* @return void
* @return false|null
*/
public static function checkAbsoluteClass($absolute_class, PhpParser\NodeAbstract $stmt, $file_name)
{
@ -204,7 +204,7 @@ class ClassChecker implements StatementsSource
if (!class_exists($absolute_class, true) && !interface_exists($absolute_class, true)) {
if (ExceptionHandler::accepts(
new UndefinedClassError('Class ' . $absolute_class . ' does not exist', $file_name, $stmt->getLine())
new UndefinedClass('Class ' . $absolute_class . ' does not exist', $file_name, $stmt->getLine())
)) {
return false;
}
@ -215,7 +215,7 @@ class ClassChecker implements StatementsSource
if ($reflection_class->getName() !== $absolute_class) {
if (ExceptionHandler::accepts(
new InvalidClassError('Class ' . $absolute_class . ' has wrong casing', $file_name, $stmt->getLine())
new InvalidClass('Class ' . $absolute_class . ' has wrong casing', $file_name, $stmt->getLine())
)) {
return false;
}
@ -321,11 +321,11 @@ class ClassChecker implements StatementsSource
*/
public function classImplements($absolute_class, $interface)
{
if (isset($_implementing_classes[$absolute_class][$interface])) {
if (isset(self::$_implementing_classes[$absolute_class][$interface])) {
return true;
}
if (isset($_implementing_classes[$absolute_class])) {
if (isset(self::$_implementing_classes[$absolute_class])) {
return false;
}
@ -335,7 +335,7 @@ class ClassChecker implements StatementsSource
return false;
}
$_implementing_classes[$absolute_class] = $class_implementations;
self::$_implementing_classes[$absolute_class] = $class_implementations;
return true;
}

View File

@ -2,9 +2,9 @@
namespace CodeInspector;
use CodeInspector\Issue\UndefinedMethodError;
use CodeInspector\Issue\InaccessibleMethodError;
use CodeInspector\Issue\ReturnTypeError;
use CodeInspector\Issue\UndefinedMethod;
use CodeInspector\Issue\InaccessibleMethod;
use CodeInspector\Issue\InvalidReturnType;
use PhpParser;
class ClassMethodChecker extends FunctionChecker
@ -42,7 +42,7 @@ class ClassMethodChecker extends FunctionChecker
}
/**
* @return void
* @return false|null
*/
public function checkReturnTypes($update_doc_comment = false)
{
@ -101,7 +101,7 @@ class ClassMethodChecker extends FunctionChecker
if ($truly_different) {
if (ExceptionHandler::accepts(
new ReturnTypeError(
new InvalidReturnType(
'The given return type for ' . $method_id . ' is incorrect, expecting ' . implode('|', $return_types),
$this->_file_name,
$this->_function->getLine()
@ -476,7 +476,7 @@ class ClassMethodChecker extends FunctionChecker
}
/**
* @return void
* @return false|null
*/
public static function checkMethodExists($method_id, $file_name, $stmt)
{
@ -492,7 +492,7 @@ class ClassMethodChecker extends FunctionChecker
}
if (ExceptionHandler::accepts(
new UndefinedMethodError('Method ' . $method_id . ' does not exist', $file_name, $stmt->getLine())
new UndefinedMethod('Method ' . $method_id . ' does not exist', $file_name, $stmt->getLine())
)) {
return false;
}
@ -511,7 +511,7 @@ class ClassMethodChecker extends FunctionChecker
}
/**
* @return void
* @return false|null
*/
public static function checkMethodVisibility($method_id, $calling_context, $file_name, $line_number)
{
@ -522,7 +522,7 @@ class ClassMethodChecker extends FunctionChecker
if (!isset(self::$_method_visibility[$method_id])) {
if (ExceptionHandler::accepts(
new InaccessibleMethodError('Cannot access method ' . $method_id, $file_name, $line_number)
new InaccessibleMethod('Cannot access method ' . $method_id, $file_name, $line_number)
)) {
return false;
}
@ -535,7 +535,7 @@ class ClassMethodChecker extends FunctionChecker
case self::VISIBILITY_PRIVATE:
if (!$calling_context || $method_class !== $calling_context) {
if (ExceptionHandler::accepts(
new InaccessibleMethodError('Cannot access private method ' . $method_id . ' from context ' . $calling_context, $file_name, $line_number)
new InaccessibleMethod('Cannot access private method ' . $method_id . ' from context ' . $calling_context, $file_name, $line_number)
)) {
return false;
}
@ -549,7 +549,7 @@ class ClassMethodChecker extends FunctionChecker
if (!$calling_context) {
if (ExceptionHandler::accepts(
new InaccessibleMethodError('Cannot access protected method ' . $method_id, $file_name, $line_number)
new InaccessibleMethod('Cannot access protected method ' . $method_id, $file_name, $line_number)
)) {
return false;
}
@ -561,7 +561,7 @@ class ClassMethodChecker extends FunctionChecker
if (!is_subclass_of($calling_context, $method_class)) {
if (ExceptionHandler::accepts(
new InaccessibleMethodError('Cannot access protected method ' . $method_id . ' from context ' . $calling_context, $file_name, $line_number)
new InaccessibleMethod('Cannot access protected method ' . $method_id . ' from context ' . $calling_context, $file_name, $line_number)
)) {
return false;
}

View File

@ -74,6 +74,12 @@ class EffectsAnalyser
$return_types = [];
}
$return_types = TypeChecker::reduceTypes($return_types);
$array_return_types = array_filter($return_types, function($return_type) {
return preg_match('/^array(\<|$)/', $return_type);
});
$return_types = array_flip($return_types);
if (count($return_types) > 1 && isset($return_types['void'])) {

View File

@ -135,6 +135,8 @@ class FileChecker implements StatementsSource
$from_cache = false;
$cache_location = null;
if (self::$_cache_dir) {
$key = md5($contents);

View File

@ -88,11 +88,11 @@ class FunctionChecker implements StatementsSource
$statements_checker->check($this->_function->stmts, $vars_in_scope, $vars_possibly_in_scope);
if (isset($this->_return_vars_in_scope[''])) {
$vars_in_scope = TypeChecker::combineTypes($vars_in_scope, $this->_return_vars_in_scope['']);
$vars_in_scope = TypeChecker::combineKeyedTypes($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['']);
$vars_possibly_in_scope = array_merge($vars_possibly_in_scope, $this->_return_vars_possibly_in_scope['']);
}
foreach ($vars_in_scope as $var => $type) {
@ -116,14 +116,14 @@ class FunctionChecker implements StatementsSource
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]);
$this->_return_vars_in_scope[$return_type] = TypeChecker::combineKeyedTypes($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]);
$this->_return_vars_possibly_in_scope[$return_type] = array_merge($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;
@ -131,7 +131,7 @@ class FunctionChecker implements StatementsSource
}
/**
* @return string
* @return null|string
*/
public function getMethodId()
{

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class FailedTypeResolution extends CodeError
{
}

View File

@ -2,6 +2,6 @@
namespace CodeInspector\Issue;
class IteratorError extends CodeError
class ForbiddenCode extends CodeError
{
}

View File

@ -2,6 +2,6 @@
namespace CodeInspector\Issue;
class ForbiddenCodeError extends CodeError
class InaccessibleMethod extends CodeError
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class InaccessibleMethodError extends CodeError
{
}

View File

@ -2,6 +2,6 @@
namespace CodeInspector\Issue;
class ReturnTypeError extends CodeError
class InvalidArgument extends CodeError
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class InvalidArgumentError extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class InvalidArrayAssignment extends CodeIssue
{
}

View File

@ -2,6 +2,6 @@
namespace CodeInspector\Issue;
class ScopeError extends CodeError
class InvalidClass extends CodeError
{
}

View File

@ -2,6 +2,6 @@
namespace CodeInspector\Issue;
class InvalidClassError extends CodeError
class InvalidIterator extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class InvalidNamespace extends CodeError
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class InvalidNamespaceError extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class InvalidReturnType extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class InvalidScope extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class InvalidStaticInvocation extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class InvalidStaticVariable extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class NullReference extends CodeError
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class NullReferenceError extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class ParentNotFound extends CodeError
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class ParentNotFoundError extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class PossiblyUndefinedVariable extends CodeIssue
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class PossiblyUndefinedVariableNotice extends CodeIssue
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class StaticInvocationError extends CodeError
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class StaticVariableError extends CodeError
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class TypeResolutionError extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class UndefinedClass extends CodeError
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class UndefinedClassError extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class UndefinedConstant extends CodeError
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class UndefinedConstantError extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class UndefinedFunction extends CodeError
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class UndefinedFunctionError extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class UndefinedMethod extends CodeError
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class UndefinedMethodError extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class UndefinedProperty extends CodeError
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class UndefinedPropertyError extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class UndefinedTrait extends CodeError
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class UndefinedTraitError extends CodeError
{
}

View File

@ -0,0 +1,7 @@
<?php
namespace CodeInspector\Issue;
class UndefinedVariable extends CodeError
{
}

View File

@ -1,7 +0,0 @@
<?php
namespace CodeInspector\Issue;
class UndefinedVariableError extends CodeError
{
}

View File

@ -5,21 +5,21 @@ namespace CodeInspector;
use PhpParser;
use CodeInspector\ExceptionHandler;
use CodeInspector\Issue\ForbiddenCodeError;
use CodeInspector\Issue\InvalidArgumentError;
use CodeInspector\Issue\InvalidNamespaceError;
use CodeInspector\Issue\IteratorError;
use CodeInspector\Issue\NullReferenceError;
use CodeInspector\Issue\ParentNotFoundError;
use CodeInspector\Issue\PossiblyUndefinedVariableNotice;
use CodeInspector\Issue\ScopeError;
use CodeInspector\Issue\StaticInvocationError;
use CodeInspector\Issue\StaticVariableError;
use CodeInspector\Issue\TypeResolutionError;
use CodeInspector\Issue\UndefinedConstantError;
use CodeInspector\Issue\UndefinedFunctionError;
use CodeInspector\Issue\UndefinedPropertyError;
use CodeInspector\Issue\UndefinedVariableError;
use CodeInspector\Issue\ForbiddenCode;
use CodeInspector\Issue\InvalidArgument;
use CodeInspector\Issue\InvalidNamespace;
use CodeInspector\Issue\InvalidIterator;
use CodeInspector\Issue\NullReference;
use CodeInspector\Issue\ParentNotFound;
use CodeInspector\Issue\PossiblyUndefinedVariable;
use CodeInspector\Issue\InvalidScope;
use CodeInspector\Issue\InvalidStaticInvocation;
use CodeInspector\Issue\InvalidStaticVariable;
use CodeInspector\Issue\FailedTypeResolution;
use CodeInspector\Issue\UndefinedConstant;
use CodeInspector\Issue\UndefinedFunction;
use CodeInspector\Issue\UndefinedProperty;
use CodeInspector\Issue\UndefinedVariable;
class StatementsChecker
{
@ -196,7 +196,7 @@ class StatementsChecker
} elseif ($stmt instanceof PhpParser\Node\Stmt\Namespace_) {
if ($this->_namespace) {
if (ExceptionHandler::accepts(
new InvalidNamespaceError('Cannot redeclare namespace', $this->_require_file_name, $stmt->getLine())
new InvalidNamespace('Cannot redeclare namespace', $this->_require_file_name, $stmt->getLine())
)) {
return false;
}
@ -254,7 +254,7 @@ class StatementsChecker
$new_vars = null;
$new_vars_possibly_in_scope = [];
$redefined_vars = null;
$possibly_redefined_vars = null;
$possibly_redefined_vars = [];
$has_left = false;
$post_type_assertions = [];
@ -540,9 +540,8 @@ class StatementsChecker
if ($vars_in_scope[$var] !== 'mixed' && $type !== 'mixed') {
$existing_types = explode('|', $vars_in_scope[$var]);
$new_types = explode('|', $type);
$new_types = array_merge($new_types, $existing_types);
$new_types = array_unique($new_types);
$vars_in_scope[$var] = implode('|', $new_types);
$new_types = array_unique(array_merge($new_types, $existing_types));
$vars_in_scope[$var] = implode('|', TypeChecker::reduceTypes($new_types));
}
else {
$vars_in_scope[$var] = 'mixed';
@ -909,7 +908,7 @@ class StatementsChecker
} elseif ($stmt instanceof PhpParser\Node\Expr\ShellExec) {
if (ExceptionHandler::accepts(
new ForbiddenCodeError('Use of shell_exec', $this->_file_name, $stmt->getLine())
new ForbiddenCode('Use of shell_exec', $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -932,7 +931,7 @@ class StatementsChecker
{
if ($this->_is_static && $stmt->name === 'this') {
if (ExceptionHandler::accepts(
new StaticVariableError('Invalid reference to $this in a static context', $this->_file_name, $stmt->getLine())
new InvalidStaticVariable('Invalid reference to $this in a static context', $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -977,7 +976,7 @@ class StatementsChecker
if (!isset($vars_in_scope[$var_name])) {
if (!isset($vars_possibly_in_scope[$var_name]) || !isset($this->_all_vars[$var_name])) {
if (ExceptionHandler::accepts(
new UndefinedVariableError('Cannot find referenced variable $' . $var_name, $this->_file_name, $stmt->getLine())
new UndefinedVariable('Cannot find referenced variable $' . $var_name, $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -987,7 +986,7 @@ class StatementsChecker
$this->_warn_vars[$var_name] = true;
if (ExceptionHandler::accepts(
new PossiblyUndefinedVariableNotice(
new PossiblyUndefinedVariable(
'Possibly undefined variable $' . $var_name .', first seen on line ' . $this->_all_vars[$var_name],
$this->_file_name,
$stmt->getLine()
@ -1065,7 +1064,7 @@ class StatementsChecker
if (!$class_checker) {
if (ExceptionHandler::accepts(
new ScopeError('Cannot use $this when not inside class', $this->_file_name, $stmt->getLine())
new InvalidScope('Cannot use $this when not inside class', $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -1081,7 +1080,7 @@ class StatementsChecker
if ((ClassChecker::getThisClass() && !$var_defined) || (!ClassChecker::getThisClass() && !$var_defined && !self::_propertyExists($property_id))) {
if (ExceptionHandler::accepts(
new UndefinedPropertyError('$' . $var_id . ' is not defined', $this->_file_name, $stmt->getLine())
new UndefinedProperty('$' . $var_id . ' is not defined', $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -1115,6 +1114,12 @@ class StatementsChecker
protected function _checkArray(PhpParser\Node\Expr\Array_ $stmt, array &$vars_in_scope, array &$vars_possibly_in_scope)
{
// if the array is empty, this special type allows us to match any other array type against it
if (empty($stmt->items)) {
$stmt->returnType = 'array<empty>';
return;
}
foreach ($stmt->items as $item) {
if ($item->key) {
if ($this->_checkExpression($item->key, $vars_in_scope, $vars_possibly_in_scope) === false) {
@ -1222,7 +1227,7 @@ class StatementsChecker
case 'null':
if (ExceptionHandler::accepts(
new NullReferenceError('Cannot iterate over ' . $return_type, $this->_file_name, $stmt->getLine())
new NullReference('Cannot iterate over ' . $return_type, $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -1232,7 +1237,7 @@ class StatementsChecker
case 'void':
case 'int':
if (ExceptionHandler::accepts(
new IteratorError('Cannot iterate over ' . $return_type, $this->_file_name, $stmt->getLine())
new InvalidIterator('Cannot iterate over ' . $return_type, $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -1345,6 +1350,10 @@ class StatementsChecker
// statements of the form if ($x && $x->foo())
$op_vars_in_scope = TypeChecker::reconcileTypes($left_type_assertions, $vars_in_scope, true, $this->_file_name, $stmt->getLine());
if ($op_vars_in_scope === false) {
return false;
}
if ($this->_checkExpression($stmt->right, $op_vars_in_scope, $vars_possibly_in_scope) === false) {
return false;
}
@ -1362,6 +1371,10 @@ class StatementsChecker
// statements of the form if ($x === null || $x->foo())
$op_vars_in_scope = TypeChecker::reconcileTypes($negated_type_assertions, $vars_in_scope, true, $this->_file_name, $stmt->getLine());
if ($op_vars_in_scope === false) {
return false;
}
if ($this->_checkExpression($stmt->right, $op_vars_in_scope, $vars_possibly_in_scope) === false) {
return false;
}
@ -1422,30 +1435,34 @@ class StatementsChecker
$comments = self::parseDocComment($doc_comment);
if ($comments && isset($comments['specials']['var'][0])) {
$type_in_comments = explode(' ', $comments['specials']['var'][0])[0];
$var_parts = array_filter(preg_split('/[\s\t]+/', $comments['specials']['var'][0]));
if ($type_in_comments[0] === strtoupper($type_in_comments[0])) {
$type_in_comments = ClassChecker::getAbsoluteClassFromString($type_in_comments, $this->_namespace, $this->_aliased_classes);
if ($var_parts && (count($var_parts) === 1 || $var_parts[1][0] !== '$')) {
$type_in_comments = $var_parts[0];
if ($type_in_comments[0] === strtoupper($type_in_comments[0])) {
$type_in_comments = ClassChecker::getAbsoluteClassFromString($type_in_comments, $this->_namespace, $this->_aliased_classes);
}
}
}
}
$var_id = self::_getVarId($stmt->var);
if ($type_in_comments) {
$return_type = $type_in_comments;
}
elseif (isset($stmt->expr->returnType)) {
$return_type = $stmt->expr->returnType;
}
else {
$return_type = 'mixed';
}
if ($stmt->var instanceof PhpParser\Node\Expr\Variable && is_string($stmt->var->name)) {
if ($type_in_comments) {
$vars_in_scope[$stmt->var->name] = $type_in_comments;
} elseif (isset($stmt->expr->returnType)) {
$var_name = $stmt->var->name;
if ($this->_typeAssignment($var_name, $stmt->expr, $vars_in_scope) === false) {
return false;
}
}
else {
$vars_in_scope[$stmt->var->name] = 'mixed';
}
$vars_possibly_in_scope[$stmt->var->name] = true;
$this->registerVariable($stmt->var->name, $stmt->var->getLine());
$vars_in_scope[$var_id] = $return_type;
$vars_possibly_in_scope[$var_id] = true;
$this->registerVariable($var_id, $stmt->var->getLine());
} elseif ($stmt->var instanceof PhpParser\Node\Expr\List_) {
foreach ($stmt->var->vars as $var) {
@ -1456,58 +1473,116 @@ class StatementsChecker
}
}
} 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] = 'mixed';
$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\ArrayDimFetch) {
if ($this->_checkArrayAssignment($stmt->var, $vars_in_scope, $vars_possibly_in_scope, $return_type) == false) {
return false;
}
} else if ($stmt->var instanceof PhpParser\Node\Expr\PropertyFetch) {
if ($stmt->var->var instanceof PhpParser\Node\Expr\Variable) {
if ($stmt->var->var->name === 'this' && is_string($stmt->var->name)) {
$method_id = $this->_source->getMethodId();
} else if ($stmt->var instanceof PhpParser\Node\Expr\PropertyFetch &&
$stmt->var->var instanceof PhpParser\Node\Expr\Variable &&
$stmt->var->var->name === 'this' &&
is_string($stmt->var->name)) {
if (!isset(self::$_this_assignments[$method_id])) {
self::$_this_assignments[$method_id] = [];
}
$method_id = $this->_source->getMethodId();
$property_id = $this->_absolute_class . '::' . $stmt->var->name;
$var_id = $stmt->var->var->name . '->' . $stmt->var->name;
self::$_existing_properties[$property_id] = 1;
if (!isset(self::$_this_assignments[$method_id])) {
self::$_this_assignments[$method_id] = [];
}
if ($type_in_comments) {
$vars_in_scope[$var_id] = $type_in_comments;
}
elseif (isset($stmt->expr->returnType)) {
if ($this->_typeAssignment($var_id, $stmt->expr, $vars_in_scope) === false) {
return false;
}
}
else {
$vars_in_scope[$var_id] = 'mixed';
}
$property_id = $this->_absolute_class . '::' . $stmt->var->name;
self::$_existing_properties[$property_id] = 1;
$vars_possibly_in_scope[$var_id] = true;
$vars_in_scope[$var_id] = $return_type;
$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];
}
// right now we have to settle for mixed
self::$_this_assignments[$method_id][$stmt->var->name] = 'mixed';
}
if ($var_id && isset($vars_in_scope[$var_id]) && $vars_in_scope[$var_id] === 'void') {
if (ExceptionHandler::accepts(
new FailedTypeResolution('Cannot assign $' . $var_id . ' to type void', $this->_file_name, $stmt->getLine())
)) {
return false;
}
}
}
protected function _typeAssignment($var_name, PhpParser\Node\Expr $expr, array &$vars_in_scope)
protected static function _getVarId(PhpParser\Node\Expr $stmt)
{
if ($expr->returnType === 'void') {
if (ExceptionHandler::accepts(
new TypeResolutionError('Cannot assign $' . $var_name . ' to type void', $this->_file_name, $expr->getLine())
)) {
return false;
if ($stmt instanceof PhpParser\Node\Expr\Variable && is_string($stmt->name)) {
return $stmt->name;
}
else if ($stmt instanceof PhpParser\Node\Expr\PropertyFetch &&
$stmt->var instanceof PhpParser\Node\Expr\Variable &&
is_string($stmt->var->name)) {
$object_id = self::_getVarId($stmt->var);
if (!$object_id) {
return null;
}
} else {
$vars_in_scope[$var_name] = $expr->returnType;
return $object_id . '->' . $stmt->var->name;
}
return null;
}
protected function _checkArrayAssignment(PhpParser\Node\Expr\ArrayDimFetch $stmt, array &$vars_in_scope, array &$vars_possibly_in_scope, $assignment_type)
{
if ($this->_checkExpression($stmt->var, $vars_in_scope, $vars_possibly_in_scope) === false) {
return false;
}
$var_id = self::_getVarId($stmt->var);
if (isset($stmt->var->returnType)) {
$return_type = $stmt->var->returnType;
if ($return_type !== 'mixed') {
if (preg_match('/^array\<(.*)\>$/', $return_type, $matches)) {
$array_type = $matches[0];
if ($array_type === 'empty') {
$array_type = $assignment_type;
}
else if ($array_type !== $assignment_type) {
$array_type = 'mixed';
}
$vars_in_scope[$var_id] = 'array<' . $array_type . '>';
}
else {
$type_parts = explode('|', $return_type);
foreach ($type_parts as $type) {
if ($type === 'array' || strpos($type, 'array<') === 0) {
continue;
}
if ($type === 'null') {
if (ExceptionHandler::accepts(
new NullReference('Cannot assign value on possibly null array ' . $var_id, $this->_file_name, $stmt->getLine())
)) {
return false;
}
continue;
}
if (!ClassChecker::classImplements($type, 'ArrayAccess')) {
if (ExceptionHandler::accepts(
new InvalidArrayAssignment('Cannot assign value on variable ' . $var_id . ' that does not implement ArrayAccess', $this->_file_name, $stmt->getLine())
)) {
return false;
}
continue;
}
}
}
}
}
}
@ -1537,7 +1612,7 @@ class StatementsChecker
}
else if ($stmt->var->name === 'this' && !$this->_class_name) {
if (ExceptionHandler::accepts(
new ScopeError('Use of $this in non-class context', $this->_file_name, $stmt->getLine())
new InvalidScope('Use of $this in non-class context', $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -1593,7 +1668,7 @@ class StatementsChecker
switch ($absolute_class) {
case 'null':
if (ExceptionHandler::accepts(
new NullReferenceError('Cannot call method ' . $stmt->name . ' on possibly null variable ' . $class_type, $this->_file_name, $stmt->getLine())
new NullReference('Cannot call method ' . $stmt->name . ' on possibly null variable ' . $class_type, $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -1603,7 +1678,7 @@ class StatementsChecker
case 'bool':
case 'array':
if (ExceptionHandler::accepts(
new InvalidArgumentError('Cannot call method ' . $stmt->name . ' on ' . $class_type . ' variable', $this->_file_name, $stmt->getLine())
new InvalidArgument('Cannot call method ' . $stmt->name . ' on ' . $class_type . ' variable', $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -1706,7 +1781,7 @@ class StatementsChecker
if (!isset($vars_possibly_in_scope[$use->var])) {
if (ExceptionHandler::accepts(
new UndefinedVariableError('Cannot find referenced variable $' . $use->var, $this->_file_name, $use->getLine())
new UndefinedVariable('Cannot find referenced variable $' . $use->var, $this->_file_name, $use->getLine())
)) {
return false;
}
@ -1716,7 +1791,7 @@ class StatementsChecker
if (!isset($this->_warn_vars[$use->var])) {
$this->_warn_vars[$use->var] = true;
if (ExceptionHandler::accepts(
new PossiblyUndefinedVariableNotice(
new PossiblyUndefinedVariable(
'Possibly undefined variable $' . $use->var . ', first seen on line ' . $this->_all_vars[$use->var],
$this->_file_name,
$use->getLine()
@ -1730,7 +1805,7 @@ class StatementsChecker
}
if (ExceptionHandler::accepts(
new UndefinedVariableError('Cannot find referenced variable $' . $use->var, $this->_file_name, $use->getLine())
new UndefinedVariable('Cannot find referenced variable $' . $use->var, $this->_file_name, $use->getLine())
)) {
return false;
}
@ -1756,7 +1831,7 @@ class StatementsChecker
if ($stmt->class->parts[0] === 'parent') {
if ($this->_class_extends === null) {
if (ExceptionHandler::accepts(
new ParentNotFoundError('Cannot call method on parent as this class does not extend another', $this->_file_name, $stmt->getLine())
new ParentNotFound('Cannot call method on parent as this class does not extend another', $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -1808,7 +1883,7 @@ class StatementsChecker
if ($this->_is_static) {
if (!ClassMethodChecker::isGivenMethodStatic($method_id)) {
if (ExceptionHandler::accepts(
new StaticInvocationError('Method ' . $method_id . ' is not static', $this->_file_name, $stmt->getLine())
new InvalidStaticInvocation('Method ' . $method_id . ' is not static', $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -1818,7 +1893,7 @@ class StatementsChecker
if ($stmt->class->parts[0] === 'self' && $stmt->name !== '__construct') {
if (!ClassMethodChecker::isGivenMethodStatic($method_id)) {
if (ExceptionHandler::accepts(
new StaticInvocationError('Cannot call non-static method ' . $method_id . ' as if it were static', $this->_file_name, $stmt->getLine())
new InvalidStaticInvocation('Cannot call non-static method ' . $method_id . ' as if it were static', $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -2007,7 +2082,7 @@ class StatementsChecker
if (!defined($const_id)) {
if (ExceptionHandler::accepts(
new UndefinedConstantError('Const ' . $const_id . ' is not defined', $this->_file_name, $stmt->getLine())
new UndefinedConstant('Const ' . $const_id . ' is not defined', $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -2055,7 +2130,7 @@ class StatementsChecker
if (!self::_staticVarExists($var_id)) {
if (ExceptionHandler::accepts(
new UndefinedVariableError('Static variable ' . $var_id . ' does not exist', $this->_file_name, $stmt->getLine())
new UndefinedVariable('Static variable ' . $var_id . ' does not exist', $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -2120,6 +2195,11 @@ class StatementsChecker
if ($stmt->if) {
$t_if_vars_in_scope = TypeChecker::reconcileTypes($if_types, $vars_in_scope, true, $this->_file_name, $stmt->getLine());
if ($t_if_vars_in_scope === false) {
return false;
}
if ($this->_checkExpression($stmt->if, $t_if_vars_in_scope, $vars_possibly_in_scope) === false) {
return false;
}
@ -2128,6 +2208,10 @@ class StatementsChecker
if ($can_negate_if_types) {
$negated_if_types = TypeChecker::negateTypes($if_types);
$t_else_vars_in_scope = TypeChecker::reconcileTypes($negated_if_types, $vars_in_scope, true, $this->_file_name, $stmt->getLine());
if ($t_else_vars_in_scope === false) {
return false;
}
}
else {
$t_else_vars_in_scope = $vars_in_scope;
@ -2193,9 +2277,9 @@ class StatementsChecker
$last_stmt = null;
if ($case->stmts) {
$switch_vars = $type_candidate_var && !empty($case_types) ?
[$type_candidate_var => implode('|', $case_types)] :
[];
$switch_vars = $type_candidate_var && !empty($case_types)
? [$type_candidate_var => implode('|', $case_types)]
: [];
$case_vars_in_scope = array_merge($vars_in_scope, $switch_vars);
$old_case_vars = $case_vars_in_scope;
@ -2295,7 +2379,7 @@ class StatementsChecker
if (isset($input_types['null']) && !$is_nullable) {
if (ExceptionHandler::accepts(
new NullReferenceError(
new NullReference(
'Argument ' . ($argument_offset + 1) . ' of ' . $method_id . ' cannot be null, possibly null value provided',
$file_name,
$line_number
@ -2319,7 +2403,7 @@ class StatementsChecker
}
if (ExceptionHandler::accepts(
new InvalidArgumentError(
new InvalidArgument(
'Argument ' . ($argument_offset + 1) . ' expects ' . $method_param_type . ', ' . $type . ' provided',
$file_name,
$line_number
@ -2353,7 +2437,7 @@ class StatementsChecker
} elseif ($method->parts === ['var_dump'] || $method->parts === ['die'] || $method->parts === ['exit']) {
if (ExceptionHandler::accepts(
new ForbiddenCodeError('Unsafe ' . implode('', $method->parts), $this->_file_name, $stmt->getLine())
new ForbiddenCode('Unsafe ' . implode('', $method->parts), $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -2482,7 +2566,7 @@ class StatementsChecker
}
catch (\ReflectionException $e) {
if (ExceptionHandler::accepts(
new UndefinedFunctionError('Function ' . $method_id . ' does not exist', $this->_file_name, $stmt->getLine())
new UndefinedFunction('Function ' . $method_id . ' does not exist', $this->_file_name, $stmt->getLine())
)) {
return false;
}
@ -2850,13 +2934,13 @@ class StatementsChecker
}
if (isset(self::$_this_assignments[$method_id])) {
$this_assignments = TypeChecker::combineTypes($this_assignments, self::$_this_assignments[$method_id]);
$this_assignments = TypeChecker::combineKeyedTypes($this_assignments, self::$_this_assignments[$method_id]);
}
if (isset(self::$_this_calls[$method_id])) {
foreach (self::$_this_calls[$method_id] as $call) {
$call_assingments = self::getThisAssignments($absolute_class . '::' . $call);
$this_assignments = TypeChecker::combineTypes($this_assignments, $call_assingments);
$this_assignments = TypeChecker::combineKeyedTypes($this_assignments, $call_assingments);
}
}

View File

@ -2,8 +2,8 @@
namespace CodeInspector;
use CodeInspector\Issue\InvalidArgumentError;
use CodeInspector\Issue\TypeResolutionError;
use CodeInspector\Issue\InvalidArgument;
use CodeInspector\Issue\FailedTypeResolution;
use CodeInspector\ExceptionHandler;
use PhpParser;
@ -35,7 +35,7 @@ class TypeChecker
if ($return_type === 'void') {
if (ExceptionHandler::accepts(
new TypeResolutionError(
new FailedTypeResolution(
'Argument ' . ($arg_offset + 1) . ' of ' . $method_id . ' cannot be void, but possibly void value was supplied',
$file_name,
$line_number
@ -63,7 +63,7 @@ class TypeChecker
}
if (ExceptionHandler::accepts(
new InvalidArgumentError(
new InvalidArgument(
'Argument ' . ($arg_offset + 1) . ' of ' . $method_id . ' cannot be null, but possibly null value was supplied',
$file_name,
$line_number
@ -93,7 +93,7 @@ class TypeChecker
}
if (ExceptionHandler::accepts(
new InvalidArgumentError(
new InvalidArgument(
'Argument ' . ($arg_offset + 1) . ' of ' . $method_id . ' has incorrect type of ' . $return_type . ', expecting ' . $expected_type,
$file_name,
$line_number
@ -174,6 +174,7 @@ class TypeChecker
}
else if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\Identical || $conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\Equal) {
$null_position = self::_hasNullVariable($conditional->expr);
$false_position = self::_hasNullVariable($conditional->expr);
if ($null_position !== null) {
if ($null_position === self::ASSIGNMENT_TO_RIGHT) {
@ -196,9 +197,31 @@ class TypeChecker
}
}
}
elseif ($false_position !== null) {
if ($false_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = $this->_getVariable($conditional->expr->left);
}
else if ($false_position === self::ASSIGNMENT_TO_LEFT) {
$var_name = $this->_getVariable($conditional->epxr->right);
}
else {
throw new \InvalidArgumentException('Bad null variable position');
}
if ($var_name) {
if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
$if_types[$var_name] = '!false';
}
else {
// we do this because == null gives us a weaker idea than === null
$if_types[$var_name] = '!empty';
}
}
}
}
else if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical || $conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\NotEqual) {
$null_position = self::_hasNullVariable($conditional->expr);
$false_position = self::_hasNullVariable($conditional->expr);
if ($null_position !== null) {
if ($null_position === self::ASSIGNMENT_TO_RIGHT) {
@ -220,6 +243,27 @@ class TypeChecker
}
}
}
elseif ($false_position !== null) {
if ($false_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = $this->_getVariable($conditional->expr->left);
}
else if ($false_position === self::ASSIGNMENT_TO_LEFT) {
$var_name = $this->_getVariable($conditional->epxr->right);
}
else {
throw new \InvalidArgumentException('Bad null variable position');
}
if ($var_name) {
if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
$if_types[$var_name] = 'false';
}
else {
// we do this because == null gives us a weaker idea than === null
$if_types[$var_name] = 'empty';
}
}
}
}
else if ($conditional->expr instanceof PhpParser\Node\Expr\Empty_) {
$var_name = $this->_getVariable($conditional->expr->expr);
@ -251,6 +295,7 @@ class TypeChecker
}
else if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical || $conditional instanceof PhpParser\Node\Expr\BinaryOp\Equal) {
$null_position = self::_hasNullVariable($conditional);
$false_position = self::_hasFalseVariable($conditional);
if ($null_position !== null) {
if ($null_position === self::ASSIGNMENT_TO_RIGHT) {
@ -272,9 +317,30 @@ class TypeChecker
}
}
}
elseif ($false_position) {
if ($false_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = $this->_getVariable($conditional->left);
}
else if ($false_position === self::ASSIGNMENT_TO_LEFT) {
$var_name = $this->_getVariable($conditional->right);
}
else {
throw new \InvalidArgumentException('Bad null variable position');
}
if ($var_name) {
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
$if_types[$var_name] = 'false';
}
else {
$if_types[$var_name] = 'empty';
}
}
}
}
else if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical || $conditional instanceof PhpParser\Node\Expr\BinaryOp\NotEqual) {
$null_position = self::_hasNullVariable($conditional);
$false_position = self::_hasFalseVariable($conditional);
if ($null_position !== null) {
if ($null_position === self::ASSIGNMENT_TO_RIGHT) {
@ -296,6 +362,26 @@ class TypeChecker
}
}
}
elseif ($false_position) {
if ($false_position === self::ASSIGNMENT_TO_RIGHT) {
$var_name = $this->_getVariable($conditional->left);
}
else if ($false_position === self::ASSIGNMENT_TO_LEFT) {
$var_name = $this->_getVariable($conditional->right);
}
else {
throw new \InvalidArgumentException('Bad null variable position');
}
if ($var_name) {
if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
$if_types[$var_name] = '!false';
}
else {
$if_types[$var_name] = '!empty';
}
}
}
}
else if (self::_hasNullCheck($conditional)) {
$var_name = $this->_getVariable($conditional->args[0]->value);
@ -418,6 +504,23 @@ class TypeChecker
return null;
}
protected static function _hasFalseVariable(PhpParser\Node\Expr $conditional)
{
if ($conditional->right instanceof PhpParser\Node\Expr\ConstFetch &&
$conditional->right->name instanceof PhpParser\Node\Name &&
$conditional->right->name->parts === ['false']) {
return self::ASSIGNMENT_TO_RIGHT;
}
if ($conditional->left instanceof PhpParser\Node\Expr\ConstFetch &&
$conditional->left->name instanceof PhpParser\Node\Name &&
$conditional->left->name->parts === ['false']) {
return self::ASSIGNMENT_TO_LEFT;
}
return null;
}
/**
* @return bool
*/
@ -461,7 +564,7 @@ class TypeChecker
*
* @param array $new_types
* @param array $existing_types
* @return array
* @return array|false
*/
public static function reconcileTypes(array $new_types, array $existing_types, $strict, $file_name, $line_number)
{
@ -508,7 +611,7 @@ class TypeChecker
continue;
}
$result_types[$key] = implode('|', $existing_var_types);
$result_types[$key] = implode('|', self::reduceTypes($existing_var_types));
continue;
}
@ -522,7 +625,7 @@ class TypeChecker
if (empty($existing_var_types)) {
if ($strict) {
if (ExceptionHandler::accepts(
new TypeResolutionError('Cannot resolve types for ' . $key, $file_name, $line_number)
new FailedTypeResolution('Cannot resolve types for ' . $key, $file_name, $line_number)
)) {
return false;
}
@ -530,7 +633,7 @@ class TypeChecker
}
}
$result_types[$key] = implode('|', $existing_var_types);
$result_types[$key] = implode('|', self::reduceTypes($existing_var_types));
continue;
}
@ -547,7 +650,7 @@ class TypeChecker
$existing_var_types[] = 'false';
}
$result_types[$key] = implode('|', $existing_var_types);
$result_types[$key] = implode('|', self::reduceTypes($existing_var_types));
continue;
}
@ -589,7 +692,7 @@ class TypeChecker
* @param array $existing_types
* @return array
*/
public static function combineTypes(array $new_types, array $existing_types)
public static function combineKeyedTypes(array $new_types, array $existing_types)
{
$keys = array_merge(array_keys($new_types), array_keys($existing_types));
$keys = array_unique($keys);
@ -615,21 +718,51 @@ class TypeChecker
continue;
}
$existing_var_types = isset($existing_types[$key]) ? explode('|', $existing_types[$key]) : null;
$new_var_types = isset($new_types[$key]) ? explode('|', $new_types[$key]) : null;
if (in_array('mixed', $existing_var_types) || in_array('mixed', $new_var_types)) {
$result_types[$key] = 'mixed';
continue;
if (!is_string($existing_types[$key]) || !is_string($new_types[$key])) {
throw new \InvalidArgumentException('combineTypes operates only on strings');
}
$all_types = array_merge($existing_var_types, $new_var_types);
$result_types[$key] = implode('|', array_unique($all_types));
$existing_var_types = explode('|', $existing_types[$key]);
$new_var_types = explode('|', $new_types[$key]);
if ($new_var_types === $existing_var_types) {
$combined_var_types = $new_var_types;
}
else {
$combined_var_types = self::reduceTypes(array_unique(array_merge($new_var_types, $existing_var_types)));
}
$result_types[$key] = implode('|', $combined_var_types);
}
return $result_types;
}
public static function reduceTypes(array $all_types)
{
if (in_array('mixed', $all_types)) {
return ['mixed'];
}
$array_types = array_filter($all_types, function($type) {
return preg_match('/^array(\<|$)/', $type);
});
$all_types = array_flip($all_types);
if (isset($all_types['array<empty>']) && count($array_types) > 1) {
unset($all_types['array<empty>']);
}
if (isset($all_types['array<mixed>'])) {
unset($all_types['array<mixed>']);
$all_types['array'] = true;
}
return array_keys($all_types);
}
public static function negateTypes(array $types)
{
return array_map(function ($type) {