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

Split code out from StatementsChecker so it isn't so monolithic

This commit is contained in:
Matthew Brown 2016-04-16 16:28:25 -04:00
parent f4910b0b96
commit 56d4d1c684
4 changed files with 783 additions and 742 deletions

View File

@ -6,4 +6,300 @@ use PhpParser;
class ClassMethodChecker extends FunctionChecker
{
protected static $_method_comments = [];
protected static $_method_files = [];
protected static $_method_params = [];
protected static $_method_namespaces = [];
protected static $_method_return_types = [];
protected static $_static_methods = [];
protected static $_declaring_classes = [];
protected static $_existing_methods = [];
const TYPE_REGEX = '(\\\?[A-Za-z0-9\<\>\[\]|\\\]+[A-Za-z0-9\<\>\[\]]|\$[a-zA-Z_0-9\<\>\[\]]+)';
public function __construct(PhpParser\Node\FunctionLike $function, StatementsSource $source)
{
parent::__construct($function, $source);
$this->_registerMethod($function);
}
public static function getMethodParams($method_id)
{
if (!isset(self::$_method_params[$method_id])) {
self::_extractReflectionMethodInfo($method_id);
}
return self::$_method_params[$method_id];
}
public static function getMethodReturnTypes($method_id)
{
if (!isset(self::$_method_return_types[$method_id])) {
self::_extractReflectionMethodInfo($method_id);
}
$return_types = self::$_method_return_types[$method_id];
return $return_types;
}
protected static function _extractReflectionMethodInfo($method_id)
{
$method = new \ReflectionMethod($method_id);
self::$_static_methods[$method_id] = $method->isStatic();
self::$_method_files[$method_id] = $method->getFileName();
self::$_method_namespaces[$method_id] = $method->getDeclaringClass()->getNamespaceName();
self::$_declaring_classes[$method_id] = $method->getDeclaringClass()->name;
$params = $method->getParameters();
self::$_method_params[$method_id] = [];
foreach ($params as $param) {
$param_type = null;
if ($param->isArray()) {
$param_type = 'array';
} elseif ($param->getClass() && self::$_method_files[$method_id]) {
$param_type = $param->getClass()->getName();
}
$is_nullable = false;
try {
$is_nullable = $param->getDefaultValue() === null;
}
catch (\ReflectionException $e) {
// do nothing
}
self::$_method_params[$method_id][] = [
'name' => $param->getName(),
'by_ref' => $param->isPassedByReference(),
'type' => $param_type,
'is_nullable' => $is_nullable
];
}
$return_types = [];
$comments = StatementsChecker::parseDocComment($method->getDocComment() ?: '');
if ($comments) {
if (isset($comments['specials']['return'])) {
$return_blocks = explode(' ', $comments['specials']['return'][0]);
foreach ($return_blocks as $block) {
if ($block && preg_match('/^' . self::TYPE_REGEX . '$/', $block)) {
$return_types = explode('|', $block);
break;
}
}
}
if (isset($comments['specials']['call'])) {
self::$_method_custom_calls[$method_id] = [];
$call_blocks = $comments['specials']['call'];
foreach ($comments['specials']['call'] as $block) {
if ($block) {
self::$_method_custom_calls[$method_id][] = trim($block);
}
}
}
$return_types = array_filter($return_types, function ($entry) {
return !empty($entry) && $entry !== '[type]';
});
if ($return_types) {
foreach ($return_types as &$return_type) {
$return_type = self::_fixUpReturnType($return_type, $method_id);
}
}
}
self::$_method_return_types[$method_id] = $return_types;
}
/**
* Determines whether a given method is static or not
* @param string $method_id
* @return boolean
*/
public static function isGivenMethodStatic($method_id)
{
if (!isset(self::$_static_methods[$method_id])) {
self::_extractReflectionMethodInfo($method_id);
}
return self::$_static_methods[$method_id];
}
protected function _registerMethod(PhpParser\Node\Stmt\ClassMethod $method)
{
$method_id = $this->_absolute_class . '::' . $method->name;
self::$_declaring_classes[$method_id] = $this->_absolute_class;
self::$_static_methods[$method_id] = $method->isStatic();
self::$_method_comments[$method_id] = $method->getDocComment() ?: '';
self::$_method_namespaces[$method_id] = $this->_namespace;
self::$_method_files[$method_id] = $this->_file_name;
self::$_existing_methods[$method_id] = 1;
if (!isset(self::$_method_return_types[$method_id])) {
$comments = StatementsChecker::parseDocComment($method->getDocComment());
$return_types = [];
if (isset($comments['specials']['return'])) {
$return_blocks = explode(' ', $comments['specials']['return'][0]);
foreach ($return_blocks as $block) {
if ($block) {
if ($block && preg_match('/^' . self::TYPE_REGEX . '$/', $block)) {
$return_types = explode('|', $block);
break;
}
}
}
}
if (isset($comments['specials']['call'])) {
self::$_method_custom_calls[$method_id] = [];
$call_blocks = $comments['specials']['call'];
foreach ($comments['specials']['call'] as $block) {
if ($block) {
self::$_method_custom_calls[$method_id][] = trim($block);
}
}
}
$return_types = array_filter($return_types, function ($entry) {
return !empty($entry) && $entry !== '[type]';
});
foreach ($return_types as &$return_type) {
$return_type = $this->_fixUpLocalReturnType($return_type, $method_id, $this->_namespace, $this->_aliased_classes);
}
self::$_method_return_types[$method_id] = $return_types;
}
self::$_method_params[$method_id] = [];
foreach ($method->getParams() as $param) {
$param_type = null;
if ($param->type) {
if (is_string($param->type)) {
$param_type = $param->type;
}
else {
if ($param->type instanceof PhpParser\Node\Name\FullyQualified) {
$param_type = implode('\\', $param->type->parts);
}
else {
$param_type = ClassChecker::getAbsoluteClassFromString(implode('\\', $param->type->parts), $this->_namespace, $this->_aliased_classes);
}
}
}
$is_nullable = $param->default !== null &&
$param->default instanceof \PhpParser\Node\Expr\ConstFetch &&
$param->default->name instanceof PhpParser\Node\Name &&
$param->default->name->parts = ['null'];
self::$_method_params[$method_id][] = [
'name' => $param->name,
'by_ref' => $param->byRef,
'type' => $param_type,
'is_nullable' => $is_nullable
];
}
}
protected static function _fixUpLocalReturnType($return_type, $method_id, $namespace, $aliased_classes)
{
if (strpos($return_type, '[') !== false) {
$return_type = TypeChecker::convertSquareBrackets($return_type);
}
$return_type_tokens = TypeChecker::tokenize($return_type);
foreach ($return_type_tokens as &$return_type_token) {
if ($return_type_token[0] === '\\') {
$return_type_token = substr($return_type_token, 1);
continue;
}
if (in_array($return_type_token, ['<', '>'])) {
continue;
}
if ($return_type_token[0] === strtoupper($return_type_token[0])) {
$absolute_class = explode('::', $method_id)[0];
if ($return_type === '$this') {
$return_type_token = $absolute_class;
continue;
}
$return_type_token = ClassChecker::getAbsoluteClassFromString($return_type_token, $namespace, $aliased_classes);
}
}
return implode('', $return_type_tokens);
}
protected static function _fixUpReturnType($return_type, $method_id)
{
if (strpos($return_type, '[') !== false) {
$return_type = TypeChecker::convertSquareBrackets($return_type);
}
$return_type_tokens = TypeChecker::tokenize($return_type);
foreach ($return_type_tokens as &$return_type_token) {
if ($return_type_token[0] === '\\') {
$return_type_token = substr($return_type_token, 1);
continue;
}
if (in_array($return_type_token, ['<', '>'])) {
continue;
}
if ($return_type_token[0] === strtoupper($return_type_token[0])) {
$absolute_class = explode('::', $method_id)[0];
if ($return_type_token === '$this') {
$return_type_token = $absolute_class;
continue;
}
$return_type_token = FileChecker::getAbsoluteClassFromNameInFile($return_type_token, self::$_method_namespaces[$method_id], self::$_method_files[$method_id]);
}
}
return implode('', $return_type_tokens);
}
public static function checkMethodExists($method_id, $file_name, $stmt)
{
if (isset(self::$_existing_methods[$method_id])) {
return;
}
try {
new \ReflectionMethod($method_id);
self::$_existing_methods[$method_id] = 1;
return;
} catch (\ReflectionException $e) {
throw new CodeException('Method ' . $method_id . ' does not exist', $file_name, $stmt->getLine());
}
}
}

View File

@ -36,10 +36,6 @@ class FunctionChecker implements StatementsSource
}
$this->_statements_checker = new StatementsChecker($this, substr($this->_file_name, -4) === '.php');
if ($function instanceof PhpParser\Node\Stmt\ClassMethod) {
$this->_statements_checker->registerMethod($function);
}
}
public function check($extra_scope_vars = [])

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,436 @@
<?php
namespace CodeInspector;
use PhpParser;
class TypeChecker
{
protected $_absolute_class;
protected $_namespace;
protected $_checker;
public function __construct(StatementsSource $source, StatementsChecker $statements_checker)
{
$this->_absolute_class = $source->getAbsoluteClass();
$this->_namespace = $source->getNamespace();
$this->_checker = $statements_checker;
}
public static function check($return_type, $method_id, $arg_offset, $current_class, $file_name, $line_number)
{
if ($return_type === 'mixed') {
return true;
}
$method_params = ClassMethodChecker::getMethodParams($method_id);
if ($arg_offset >= count($method_params)) {
return true;
}
$expected_type = $method_params[$arg_offset]['type'];
if (!$expected_type) {
return true;
}
if ($return_type === 'null') {
if ($method_params[$arg_offset]['is_nullable']) {
return true;
}
throw new CodeException('Argument ' . ($arg_offset + 1) . ' of ' . $method_id . ' cannot be null, but possibly null value was supplied', $file_name, $line_number);
}
// Remove generic type
$return_type = preg_replace('/\<[A-Za-z0-9' . '\\\\' . ']+\>/', '', $return_type);
if ($return_type === $expected_type) {
return true;
}
if (StatementsChecker::isMock($return_type)) {
return true;
}
if (!is_subclass_of($return_type, $expected_type, true)) {
if (is_subclass_of($expected_type, $return_type, true)) {
//echo('Warning: dangerous type coercion in ' . $file_name . ' on line ' . $line_number . PHP_EOL);
return true;
}
throw new CodeException('Argument ' . ($arg_offset + 1) . ' of ' . $method_id . ' has incorrect type of ' . $return_type . ', expecting ' . $expected_type, $file_name, $line_number);
}
return true;
}
public function getType(PhpParser\Node\Expr $stmt, array $vars_in_scope)
{
if ($stmt instanceof PhpParser\Node\Expr\Variable && is_string($stmt->name)) {
if ($stmt->name === 'this') {
return $this->_absolute_class;
}
elseif (isset($vars_in_scope[$stmt->name]) && is_string($vars_in_scope[$stmt->name])) {
return $vars_in_scope[$stmt->name];
}
}
elseif ($stmt instanceof PhpParser\Node\Expr\PropertyFetch &&
$stmt->var instanceof PhpParser\Node\Expr\Variable &&
$stmt->var->name === 'this' &&
is_string($stmt->name)
) {
$property_id = $this->_absolute_class . '::' . $stmt->name;
if (isset($vars_in_scope[$property_id])) {
return $vars_in_scope[$property_id];
}
}
if (isset($stmt->returnType)) {
return $stmt->returnType;
}
return null;
}
/**
* Gets all the type assertions in a conditional
*
* @param PhpParser\Node\Expr $stmt
* @return array
*/
public function getTypeAssertions(PhpParser\Node\Expr $conditional, $check_boolean_and = false)
{
$if_types = [];
if ($conditional instanceof PhpParser\Node\Expr\Instanceof_) {
$instanceof_type = $this->_getInstanceOfTypes($conditional);
if ($instanceof_type) {
$var_name = $this->_getVariable($conditional->expr);
if ($var_name) {
$if_types[$var_name] = $instanceof_type;
}
}
}
else if ($var_name = $this->_getVariable($conditional)) {
$if_types[$var_name] = '!empty';
}
else if ($conditional instanceof PhpParser\Node\Expr\BooleanNot) {
if ($conditional->expr instanceof PhpParser\Node\Expr\Instanceof_) {
$instanceof_type = $this->_getInstanceOfTypes($conditional->expr);
if ($instanceof_type) {
$var_name = $this->_getVariable($conditional->expr->expr);
if ($var_name) {
$if_types[$var_name] = '!' . $instanceof_type;
}
}
}
else if ($var_name = $this->_getVariable($conditional->expr)) {
$if_types[$var_name] = 'empty';
}
else if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
if (self::_hasNullVariable($conditional->expr)) {
$var_name = $this->_getVariable($conditional->expr->left);
if ($var_name) {
$if_types[$var_name] = '!null';
}
}
}
else if ($conditional->expr instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical) {
if (self::_hasNullVariable($conditional->expr)) {
$var_name = $this->_getVariable($conditional->expr->left);
if ($var_name) {
$if_types[$var_name] = 'null';
}
}
}
else if ($conditional->expr instanceof PhpParser\Node\Expr\Empty_) {
$var_name = $this->_getVariable($conditional->expr->expr);
if ($var_name) {
$if_types[$var_name] = '!empty';
}
}
else if (self::_hasNullCheck($conditional->expr)) {
$var_name = $this->_getVariable($conditional->expr->args[0]->value);
$if_types[$var_name] = '!null';
}
else if (self::_hasArrayCheck($conditional->expr)) {
$var_name = $this->_getVariable($conditional->expr->args[0]->value);
$if_types[$var_name] = '!array';
}
}
else if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
if (self::_hasNullVariable($conditional)) {
$var_name = $this->_getVariable($conditional->left);
if ($var_name) {
$if_types[$var_name] = 'null';
}
}
}
else if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\NotIdentical) {
if (self::_hasNullVariable($conditional)) {
$var_name = $this->_getVariable($conditional->left);
if ($var_name) {
$if_types[$var_name] = '!null';
}
}
}
else if (self::_hasNullCheck($conditional)) {
$var_name = $this->_getVariable($conditional->args[0]->value);
$if_types[$var_name] = 'null';
}
else if (self::_hasArrayCheck($conditional)) {
$var_name = $this->_getVariable($conditional->args[0]->value);
$if_types[$var_name] = 'array';
}
else if ($conditional instanceof PhpParser\Node\Expr\Empty_) {
$var_name = $this->_getVariable($conditional->expr);
if ($var_name) {
$if_types[$var_name] = 'empty';
}
}
else if ($conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanOr) {
$left_assertions = $this->getTypeAssertions($conditional->left, false);
$right_assertions = $this->getTypeAssertions($conditional->right, false);
$keys = array_merge(array_keys($left_assertions), array_keys($right_assertions));
$keys = array_unique($keys);
foreach ($keys as $key) {
if (isset($left_assertions[$key]) && isset($right_assertions[$key])) {
$if_types[$key] = $left_assertions[$key] . '|' . $right_assertions[$key];
}
else if (isset($left_assertions[$key])) {
$if_types[$key] = $left_assertions[$key];
}
else {
$if_types[$key] = $right_assertions[$key];
}
}
}
else if ($check_boolean_and && $conditional instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd) {
$left_assertions = $this->getTypeAssertions($conditional->left, $check_boolean_and);
$right_assertions = $this->getTypeAssertions($conditional->right, $check_boolean_and);
$keys = array_merge(array_keys($left_assertions), array_keys($right_assertions));
$keys = array_unique($keys);
foreach ($keys as $key) {
if (isset($left_assertions[$key]) && isset($right_assertions[$key])) {
if ($left_assertions[$key][0] !== '!' && $right_assertions[$key][0] !== '!') {
$if_types[$key] = $left_assertions[$key] . '&' . $right_assertions[$key];
}
else {
$if_types[$key] = $right_assertions[$key];
}
}
else if (isset($left_assertions[$key])) {
$if_types[$key] = $left_assertions[$key];
}
else {
$if_types[$key] = $right_assertions[$key];
}
}
}
return $if_types;
}
protected function _getInstanceOfTypes(PhpParser\Node\Expr\Instanceof_ $stmt)
{
if ($stmt->class instanceof PhpParser\Node\Name) {
if (!in_array($stmt->class->parts[0], ['self', 'static', 'parent'])) {
$instanceof_class = ClassChecker::getAbsoluteClassFromName($stmt->class, $this->_namespace, $this->_checker->getAliasedClasses());
return $instanceof_class;
} elseif ($stmt->class->parts === ['self']) {
return $this->_absolute_class;
}
}
return null;
}
protected function _getVariable(PhpParser\Node\Expr $stmt)
{
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 &&
$stmt->var->name === 'this' &&
is_string($stmt->name)) {
return $this->_absolute_class . '::' . $stmt->name;
}
return null;
}
protected static function _hasNullVariable(PhpParser\Node\Expr $conditional)
{
return $conditional->right instanceof PhpParser\Node\Expr\ConstFetch &&
$conditional->right->name instanceof PhpParser\Node\Name &&
$conditional->right->name->parts === ['null'];
}
protected static function _hasNullCheck(PhpParser\Node\Expr $stmt)
{
if ($stmt instanceof PhpParser\Node\Expr\FuncCall && $stmt->name instanceof PhpParser\Node\Name && $stmt->name->parts === ['is_null']) {
return true;
}
return false;
}
protected static function _hasArrayCheck(PhpParser\Node\Expr $stmt)
{
if ($stmt instanceof PhpParser\Node\Expr\FuncCall && $stmt->name instanceof PhpParser\Node\Name && $stmt->name->parts === ['is_array']) {
return true;
}
return false;
}
/**
* Takes two arrays and consolidates them, removing null values from existing types where applicable
*
* @param array $new_types
* @param array $existing_types
* @return array
*/
public static function reconcileTypes(array $new_types, array $existing_types, $strict, $file_name, $line_number)
{
$keys = array_merge(array_keys($new_types), array_keys($existing_types));
$keys = array_unique($keys);
$result_types = [];
if (empty($new_types)) {
return $existing_types;
}
foreach ($keys as $key) {
$existing_type = isset($existing_types[$key]) && is_string($existing_types[$key]) ? explode('|', $existing_types[$key]) : null;
if (isset($new_types[$key])) {
if (is_string($new_types[$key]) && $new_types[$key][0] === '!') {
if ($existing_type) {
if ($new_types[$key] === '!empty' || $new_types[$key] === '!null') {
$null_pos = array_search('null', $existing_type);
if ($null_pos !== false) {
array_splice($existing_type, $null_pos, 1);
if (empty($existing_type)) {
// @todo - I think there's a better way to handle this, but for the moment
// mixed will have to do.
$result_types[$key] = 'mixed';
}
else {
$result_types[$key] = implode('|', $existing_type);
}
}
else {
// if we cannot find a null declaration to remove, just use existing type
$result_types[$key] = $existing_types[$key];
}
}
else {
$negated_type = substr($new_types[$key], 1);
$type_pos = array_search($negated_type, $existing_type);
if ($type_pos !== false) {
array_splice($existing_type, $type_pos, 1);
if (empty($existing_type)) {
if ($strict) {
throw new CodeException('Cannot resolve types for ' . $key, $file_name, $line_number);
}
$result_types[$key] = $existing_types[$key];
}
$result_types[$key] = implode('|', $existing_type);
}
else {
// if we cannot find a type to negate, just use the existing type
$result_types[$key] = $existing_types[$key];
}
}
}
else if (isset($existing_types[$key])) {
$result_types[$key] = $existing_types[$key];
}
}
else {
$result_types[$key] = $new_types[$key];
}
}
else {
$result_types[$key] = $existing_types[$key];
}
}
return $result_types;
}
public static function negateTypes(array $types)
{
return array_map(function ($type) {
return $type[0] === '!' ? substr($type, 1) : '!' . $type;
}, $types);
}
public static function tokenize($return_type)
{
$return_type_tokens = [''];
$was_char = false;
foreach (str_split($return_type) as $char) {
if ($was_char) {
$return_type_tokens[] = '';
}
if ($char === '<' || $char === '>') {
if ($return_type_tokens[count($return_type_tokens) - 1] === '') {
$return_type_tokens[count($return_type_tokens) - 1] = $char;
}
else {
$return_type_tokens[] = $char;
}
$was_char = true;
}
else {
$return_type_tokens[count($return_type_tokens) - 1] .= $char;
$was_char = false;
}
}
return $return_type_tokens;
}
public static function convertSquareBrackets($type)
{
return preg_replace_callback(
'/([a-zA-Z\<\>]+)((\[\])+)/',
function ($matches) {
$inner_type = $matches[1];
$dimensionality = strlen($matches[2]) / 2;
for ($i = 0; $i < $dimensionality; $i++) {
$inner_type = 'array<' . $inner_type . '>';
}
return $inner_type;
},
$type
);
}
}