1
0
mirror of https://github.com/danog/psalm.git synced 2024-11-26 20:34:47 +01:00

Fix more errors caught by Psalm

This commit is contained in:
Matthew Brown 2016-10-30 12:46:18 -04:00
parent ad228e4d7e
commit 604c875d0c
19 changed files with 300 additions and 128 deletions

View File

@ -13,7 +13,7 @@ use Psalm\Checker\ProjectChecker;
use Psalm\IssueBuffer;
// show all errors
error_reporting(E_ALL);
error_reporting(-1);
ini_set('display_errors', 1);
ini_set('display_startup_errors', 1);
ini_set('memory_limit', '2048M');

View File

@ -766,6 +766,10 @@ abstract class ClassLikeChecker implements StatementsSource
return true;
}
if (!$class_name || strpos($class_name, '::') !== false) {
throw new \InvalidArgumentException('Invalid class name ' . $class_name);
}
try {
$old_level = error_reporting();
error_reporting(0);

View File

@ -139,7 +139,7 @@ class FunctionChecker extends FunctionLikeChecker
/**
* @param PhpParser\Node\Stmt\Function_ $function
* @param string $file_name
* @return void
* @return null|false
*/
protected function registerFunction(PhpParser\Node\Stmt\Function_ $function, $file_name)
{
@ -284,7 +284,9 @@ class FunctionChecker extends FunctionLikeChecker
$arg_name,
$by_reference,
$arg_type ? Type::parseString($arg_type) : Type::getMixed(),
$optional
$optional,
false,
$arg_name === '...'
);
}
@ -359,16 +361,16 @@ class FunctionChecker extends FunctionLikeChecker
$closure_return_types = \Psalm\EffectsAnalyser::getReturnTypes($function_call_arg->value->stmts, $closure_yield_types, true);
if (!$closure_return_types) {
if (IssueBuffer::accepts(
IssueBuffer::accepts(
new InvalidReturnType(
'No return type could be found in the closure passed to ' . $call_map_key,
$file_name,
$line_number
),
$suppressed_issues
)) {
return false;
}
);
return Type::getArray();
}
else {
if ($call_map_key === 'array_map') {

View File

@ -812,12 +812,13 @@ abstract class FunctionLikeChecker implements StatementsSource
*/
public static function getParamsById($method_id, array $args, $file_name)
{
$absolute_class = strpos($method_id, '::') ? explode($method_id, '::')[0] : null;
$absolute_class = strpos($method_id, '::') !== false ? explode('::', $method_id)[0] : null;
if ($absolute_class && ClassLikeChecker::isUserDefined($absolute_class)) {
return MethodChecker::getMethodParams($method_id);
}
elseif (!$absolute_class && FunctionChecker::inCallMap($method_id)) {
/** @var array<array<FunctionLikeParameter>> */
$function_param_options = FunctionChecker::getParamsFromCallMap($method_id);
}
elseif ($absolute_class) {

View File

@ -85,6 +85,11 @@ class InterfaceChecker extends ClassLikeChecker
self::$existing_interfaces_ci = [];
}
/**
* @param string $interface_name
* @param string $possible_parent
* @return boolean
*/
public static function interfaceExtends($interface_name, $possible_parent)
{
return in_array($possible_parent, self::getParentInterfaces($interface_name));

View File

@ -65,7 +65,12 @@ class MethodChecker extends FunctionLikeChecker
const VISIBILITY_PROTECTED = 2;
const VISIBILITY_PRIVATE = 3;
public function __construct(PhpParser\Node\FunctionLike $function, StatementsSource $source, array $this_vars = [])
/**
* @param PhpParser\Node\FunctionLike $function
* @param StatementsSource $source
* @param array $this_vars
*/
public function __construct($function, StatementsSource $source, array $this_vars = [])
{
if (!$function instanceof PhpParser\Node\Stmt\ClassMethod) {
throw new \InvalidArgumentException('Must be called with a ClassMethod');

View File

@ -1275,7 +1275,10 @@ class ExpressionChecker
$first_arg_type = $stmt->args[0]->value->inferredType;
if ($first_arg_type->hasGeneric()) {
/** @var Type\Union|null */
$key_type = null;
/** @var Type\Union|null */
$value_type = null;
foreach ($first_arg_type->types as $type) {
@ -2016,7 +2019,7 @@ class ExpressionChecker
(
$this_class === $statements_checker->getAbsoluteClass() ||
ClassChecker::classExtends($this_class, $statements_checker->getAbsoluteClass()) ||
trait_exists($statements_checker->getAbsoluteClass())
TraitChecker::traitExists($statements_checker->getAbsoluteClass())
)) {
$method_id = $statements_checker->getAbsoluteClass() . '::' . strtolower($stmt->name);
@ -2034,6 +2037,7 @@ class ExpressionChecker
$has_mock = false;
if ($class_type && is_string($stmt->name)) {
/** @var Type\Union|null */
$return_type = null;
foreach ($class_type->types as $type) {
@ -2381,7 +2385,8 @@ class ExpressionChecker
return false;
}
if ($stmt->class->parts[0] !== 'parent'
if ($stmt->class instanceof PhpParser\Node\Name
&& $stmt->class->parts[0] !== 'parent'
&& ($statements_checker->isStatic() || !ClassChecker::classExtends($context->self, $absolute_class))
) {
if (MethodChecker::checkMethodStatic($method_id, $statements_checker->getCheckedFileName(), $stmt->getLine(), $statements_checker->getSuppressedIssues()) === false) {
@ -2396,7 +2401,14 @@ class ExpressionChecker
$return_types = MethodChecker::getMethodReturnTypes($method_id);
if ($return_types) {
$return_types = self::fleshOutTypes($return_types, $stmt->args, $stmt->class->parts === ['parent'] ? $statements_checker->getAbsoluteClass() : $absolute_class, $method_id);
$return_types = self::fleshOutTypes(
$return_types,
$stmt->args,
$stmt->class instanceof PhpParser\Node\Name && $stmt->class->parts === ['parent']
? $statements_checker->getAbsoluteClass()
: $absolute_class,
$method_id
);
if (isset($stmt->inferredType)) {
$stmt->inferredType = Type::combineUnionTypes($stmt->inferredType, $return_types);
@ -2416,11 +2428,11 @@ class ExpressionChecker
}
/**
* @param PhpParser\Node\Arg[] $args
* @param string|null $method_id
* @param Context $context
* @param int $line_number
* @param boolean $is_mock
* @param array<int, PhpParser\Node\Arg> $args
* @param string|null $method_id
* @param Context $context
* @param int $line_number
* @param boolean $is_mock
* @return false|null
*/
protected static function checkFunctionArguments(StatementsChecker $statements_checker, array $args, $method_id, Context $context, $line_number, $is_mock = false)
@ -2521,7 +2533,6 @@ class ExpressionChecker
}
}
$has_packed_var = false;
foreach ($args as $arg) {
@ -2559,24 +2570,38 @@ class ExpressionChecker
}
if ($method_id === 'array_map' || $method_id === 'array_filter') {
$array_index = $method_id === 'array_map' ? 1 : 0;
$closure_index = $method_id === 'array_map' ? 0 : 1;
$array_arg = isset($args[$array_index]->value) ? $args[$array_index]->value : null;
$array_arg_types = [];
$array_arg_type = $array_arg
&& isset($array_arg->inferredType)
&& isset($array_arg->inferredType->types['array'])
&& $array_arg->inferredType->types['array'] instanceof Type\Generic
? $array_arg->inferredType->types['array']
: null;
foreach ($args as $i => $arg) {
if ($i === 0 && $method_id === 'array_map') {
continue;
}
if ($i === 1 && $method_id === 'array_filter') {
break;
}
$array_arg = isset($args[$i]->value) ? $args[$i]->value : null;
$array_arg_types[] = $array_arg
&& isset($array_arg->inferredType)
&& isset($array_arg->inferredType->types['array'])
&& $array_arg->inferredType->types['array'] instanceof Type\Generic
? $array_arg->inferredType->types['array']
: null;
}
/** @var PhpParser\Node\Expr\Closure|null */
$closure_arg = isset($args[$closure_index]) && $args[$closure_index]->value instanceof PhpParser\Node\Expr\Closure
? $args[$closure_index]->value
: null;
if ($array_arg_type && !$array_arg_type->type_params[1]->isMixed() && $closure_arg) {
if (count($closure_arg->params) > 1) {
if ($closure_arg) {
$expected_closure_param_count = $method_id === 'array_filter' ? 1 : count($array_arg_types);
if (count($closure_arg->params) > $expected_closure_param_count) {
if (IssueBuffer::accepts(
new TooManyArguments(
'Too many arguments in closure for ' . ($cased_method_id ?: $method_id),
@ -2588,8 +2613,7 @@ class ExpressionChecker
return false;
}
}
if (count($closure_arg->params) === 0) {
elseif (count($closure_arg->params) < $expected_closure_param_count) {
if (IssueBuffer::accepts(
new TooFewArguments(
'You must supply a param in the closure for ' . ($cased_method_id ?: $method_id),
@ -2602,38 +2626,25 @@ class ExpressionChecker
}
}
$closure_param = $closure_arg->params[0];
$translated_param = FunctionLikeChecker::getTranslatedParam(
$closure_param,
$statements_checker->getAbsoluteClass(),
$statements_checker->getNamespace(),
$statements_checker->getAliasedClasses()
);
$param_type = $translated_param->type;
$input_type = $array_arg_type->type_params[1];
foreach ($closure_arg->params as $i => $closure_param) {
$translated_param = FunctionLikeChecker::getTranslatedParam(
$closure_param,
$statements_checker->getAbsoluteClass(),
$statements_checker->getNamespace(),
$statements_checker->getAliasedClasses()
);
$type_match_found = FunctionLikeChecker::doesParamMatch($input_type, $param_type, $scalar_type_match_found, $coerced_type);
$param_type = $translated_param->type;
$input_type = $array_arg_types[$i]->type_params[1];
if ($coerced_type) {
if (IssueBuffer::accepts(
new TypeCoercion(
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' . $param_type . ', parent type ' . $input_type . ' provided',
$statements_checker->getCheckedFileName(),
$closure_param->getLine()
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
}
$type_match_found = FunctionLikeChecker::doesParamMatch($input_type, $param_type, $scalar_type_match_found, $coerced_type);
if (!$type_match_found) {
if ($scalar_type_match_found) {
if ($coerced_type) {
if (IssueBuffer::accepts(
new InvalidScalarArgument(
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' . $param_type . ', ' . $input_type . ' provided',
new TypeCoercion(
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' . $param_type . ', parent type ' . $input_type . ' provided',
$statements_checker->getCheckedFileName(),
$closure_param->getLine()
),
@ -2642,15 +2653,30 @@ class ExpressionChecker
return false;
}
}
else if (IssueBuffer::accepts(
new InvalidArgument(
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' . $param_type . ', ' . $input_type . ' provided',
$statements_checker->getCheckedFileName(),
$closure_param->getLine()
),
$statements_checker->getSuppressedIssues()
)) {
return false;
if (!$type_match_found) {
if ($scalar_type_match_found) {
if (IssueBuffer::accepts(
new InvalidScalarArgument(
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' . $param_type . ', ' . $input_type . ' provided',
$statements_checker->getCheckedFileName(),
$closure_param->getLine()
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
}
else if (IssueBuffer::accepts(
new InvalidArgument(
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' . $param_type . ', ' . $input_type . ' provided',
$statements_checker->getCheckedFileName(),
$closure_param->getLine()
),
$statements_checker->getSuppressedIssues()
)) {
return false;
}
}
}
}

View File

@ -386,10 +386,8 @@ class StatementsChecker
elseif ($stmt instanceof PhpParser\Node\Expr\UnaryMinus || $stmt instanceof PhpParser\Node\Expr\UnaryPlus) {
return self::getSimpleType($stmt->expr);
}
else {
var_dump('Unrecognised default property type');
var_dump($stmt);
}
return null;
}

View File

@ -49,4 +49,13 @@ class TraitChecker extends ClassLikeChecker
return $method_name;
}
/**
* @param string $trait_name
* @return boolean
*/
public static function traitExists($trait_name)
{
return trait_exists($trait_name);
}
}

View File

@ -64,7 +64,7 @@ class TypeChecker
return self::combineTypeAssertions($left_assertions, $right_assertions);
}
return self::getTypeAssertions($conditional, $this_class_name, $namespace, $aliased_classes);
return self::getTypeAssertions($conditional, $this_class_name, $namespace, $aliased_classes, true);
}
/**
@ -124,10 +124,16 @@ class TypeChecker
* @param string $this_class_name
* @param string $namespace
* @param array<string> $aliased_classes
* @param bool $allow_non_negatable Allow type assertions that should not be negated
* @return array<string,string>
*/
public static function getTypeAssertions(PhpParser\Node\Expr $conditional, $this_class_name, $namespace, array $aliased_classes)
{
public static function getTypeAssertions(
PhpParser\Node\Expr $conditional,
$this_class_name,
$namespace,
array $aliased_classes,
$allow_non_negatable = false
) {
$if_types = [];
if ($conditional instanceof PhpParser\Node\Expr\Instanceof_) {
@ -283,6 +289,7 @@ class TypeChecker
$null_position = self::hasNullVariable($conditional);
$false_position = self::hasFalseVariable($conditional);
$gettype_position = self::hasGetTypeCheck($conditional);
$scalar_value_position = $allow_non_negatable ? self::hasScalarValueComparison($conditional) : false;
$var_name = null;
@ -354,6 +361,42 @@ class TypeChecker
$var_type = $conditional->right->value;
}
if ($var_name && $var_type) {
$if_types[$var_name] = $var_type;
}
}
elseif ($scalar_value_position) {
$var_type = null;
if ($scalar_value_position === self::ASSIGNMENT_TO_RIGHT) {
/** @var PhpParser\Node\Expr $conditional->right */
$var_name = ExpressionChecker::getArrayVarId($conditional->left, $this_class_name, $namespace, $aliased_classes);
if ($conditional->right instanceof PhpParser\Node\Scalar\String_) {
$var_type = 'string';
}
elseif ($conditional->right instanceof PhpParser\Node\Scalar\LNumber) {
$var_type = 'int';
}
elseif ($conditional->right instanceof PhpParser\Node\Scalar\DNumber) {
$var_type = 'float';
}
}
else if ($scalar_value_position === self::ASSIGNMENT_TO_LEFT) {
/** @var PhpParser\Node\Expr $conditional->left */
$var_name = ExpressionChecker::getArrayVarId($conditional->right, $this_class_name, $namespace, $aliased_classes);
if ($conditional->left instanceof PhpParser\Node\Scalar\String_) {
$var_type = 'string';
}
elseif ($conditional->left instanceof PhpParser\Node\Scalar\LNumber) {
$var_type = 'int';
}
elseif ($conditional->left instanceof PhpParser\Node\Scalar\DNumber) {
$var_type = 'float';
}
}
if ($var_name && $var_type) {
$if_types[$var_name] = $var_type;
}
@ -631,6 +674,26 @@ class TypeChecker
return false;
}
/**
* @return bool
*/
protected static function hasScalarValueComparison(PhpParser\Node\Expr\BinaryOp $conditional)
{
if (!$conditional instanceof PhpParser\Node\Expr\BinaryOp\Identical) {
return false;
}
if ($conditional->right instanceof PhpParser\Node\Scalar) {
return self::ASSIGNMENT_TO_RIGHT;
}
if ($conditional->left instanceof PhpParser\Node\Scalar) {
return self::ASSIGNMENT_TO_LEFT;
}
return false;
}
/**
* @return bool
*/

View File

@ -193,6 +193,10 @@ class Config
return new self();
}
/**
* @param array<\SimpleXMLElement> $extensions
* @return void
*/
protected function loadFileExtensions($extensions)
{
foreach ($extensions as $extension) {

View File

@ -44,28 +44,28 @@ class FileFilter
if ($e->directory) {
foreach ($e->directory as $directory) {
$filter->include_dirs[] = self::slashify($directory['name']);
$filter->include_dirs[] = self::slashify((string)$directory['name']);
}
}
if ($e->file) {
foreach ($e->file as $file) {
$filter->include_files[] = $file['name'];
$filter->include_files_lowercase[] = strtolower($file['name']);
$filter->include_files_lowercase[] = strtolower((string)$file['name']);
}
}
}
else {
if ($e->directory) {
foreach ($e->directory as $directory) {
$filter->exclude_dirs[] = self::slashify($directory['name']);
$filter->exclude_dirs[] = self::slashify((string)$directory['name']);
}
}
if ($e->file) {
foreach ($e->file as $file) {
$filter->exclude_files[] = (string)$file['name'];
$filter->exclude_files_lowercase[] = strtolower($file['name']);
$filter->exclude_files_lowercase[] = strtolower((string)$file['name']);
}
}
}
@ -73,11 +73,20 @@ class FileFilter
return $filter;
}
/**
* @param string $str
* @return string
*/
protected static function slashify($str)
{
return preg_replace('/\/?$/', '/', $str);
}
/**
* @param string $file_name
* @param boolean $case_sensitive
* @return boolean
*/
public function allows($file_name, $case_sensitive = false)
{
if ($this->inclusive) {

View File

@ -137,6 +137,10 @@ class Context
return $redefined_vars;
}
/**
* @param string $remove_var_id
* @return void
*/
public function remove($remove_var_id)
{
if (isset($this->vars_in_scope[$remove_var_id])) {
@ -147,6 +151,11 @@ class Context
}
}
/**
* @param string $remove_var_id
* @param \Psalm\Type\Union|null $type
* @return void
*/
public function removeDescendents($remove_var_id, \Psalm\Type\Union $type = null)
{
if (!$type && isset($this->vars_in_scope[$remove_var_id])) {

View File

@ -107,7 +107,9 @@ class EffectsAnalyser
if ($collapse_types) {
// if it's a generator, boil everything down to a single generator return type
if ($yield_types) {
/** @var Type\Union */
$key_type = null;
/** @var Type\Union */
$value_type = null;
foreach ($yield_types as $type) {

View File

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

View File

@ -79,6 +79,10 @@ class IssueBuffer
}
}
/**
* @param string $message
* @return bool
*/
protected static function alreadyEmitted($message)
{
$sham = sha1($message);

View File

@ -73,6 +73,10 @@ abstract class Type
private static function getTypeFromTree(ParseTree $parse_tree)
{
if (!$parse_tree->value) {
throw new \InvalidArgumentException('Parse tree must have a value');
}
if ($parse_tree->value === ParseTree::GENERIC) {
$generic_type = array_shift($parse_tree->children);
@ -84,6 +88,10 @@ abstract class Type
$parse_tree->children
);
if (!$generic_type->value) {
throw new \InvalidArgumentException('Generic type must have a value');
}
$generic_type_value = self::fixScalarTerms($generic_type->value);
if (($generic_type_value === 'array' || $generic_type_value === 'Generator') && count($generic_params) === 1) {
@ -125,7 +133,11 @@ abstract class Type
$properties[$property_branch->children[0]->value] = $property_type;
}
return new ObjectLike($type, $properties);
if (!$type->value) {
throw new \InvalidArgumentException('Object-like type must have a value');
}
return new ObjectLike($type->value, $properties);
}
$atomic_type = self::fixScalarTerms($parse_tree->value);
@ -138,6 +150,7 @@ abstract class Type
}
/**
* @param string $return_type
* @return array<int,string>
*/
public static function tokenize($return_type)
@ -170,15 +183,18 @@ abstract class Type
return $return_type_tokens;
}
/** @return string */
/**
* @param string $type
* @return string
*/
public static function convertSquareBrackets($type)
{
return preg_replace_callback(
'/([a-zA-Z\<\>\\\\_\(\)|]+)((\[\])+)/',
function ($matches) {
$inner_type = str_replace(['(', ')'], '', $matches[1]);
$inner_type = str_replace(['(', ')'], '', (string)$matches[1]);
$dimensionality = strlen($matches[2]) / 2;
$dimensionality = strlen((string)$matches[2]) / 2;
for ($i = 0; $i < $dimensionality; $i++) {
$inner_type = 'array<mixed, ' . $inner_type . '>';
@ -298,54 +314,6 @@ abstract class Type
return new Union([$type]);
}
/** @return bool */
public function isMixed()
{
if ($this instanceof Atomic) {
return $this->value === 'mixed';
}
if ($this instanceof Union) {
return isset($this->types['mixed']);
}
}
/** @return bool */
public function isNull()
{
if ($this instanceof Atomic) {
return $this->value === 'null';
}
if ($this instanceof Union) {
return count($this->types) === 1 && isset($this->types['null']);
}
}
/** @return bool */
public function isVoid()
{
if ($this instanceof Atomic) {
return $this->value === 'void';
}
if ($this instanceof Union) {
return isset($this->types['void']);
}
}
/** @return bool */
public function isEmpty()
{
if ($this instanceof Atomic) {
return $this->value === 'empty';
}
if ($this instanceof Union) {
return isset($this->types['empty']);
}
}
/**
* @param array<Union> $redefined_vars
* @param Context $context
@ -433,6 +401,7 @@ abstract class Type
throw new \InvalidArgumentException('You must pass at least one type to combineTypes');
}
/** @var array<string,array<string,Union>> */
$key_types = [];
/** @var array<string,array<string,Union>> */
@ -478,6 +447,7 @@ abstract class Type
}
elseif ($type instanceof ObjectLike) {
if (!isset($value_types['object-like'])) {
/** @var array<string,Union> */
$value_types['object-like'] = [];
}

View File

@ -148,4 +148,28 @@ class Atomic extends Type
{
return $this->value === 'Generator';
}
/** @return bool */
public function isMixed()
{
return $this->value === 'mixed';
}
/** @return bool */
public function isNull()
{
return $this->value === 'null';
}
/** @return bool */
public function isVoid()
{
return $this->value === 'void';
}
/** @return bool */
public function isEmpty()
{
return $this->value === 'empty';
}
}

View File

@ -139,6 +139,36 @@ class Union extends Type
return isset($this->types['int']) && count($this->types) === 1;
}
/** @return bool */
public function isMixed()
{
return isset($this->types['mixed']);
}
/** @return bool */
public function isNull()
{
return count($this->types) === 1 && isset($this->types['null']);
}
/** @return bool */
public function isVoid()
{
return isset($this->types['void']);
}
/** @return bool */
public function isEmpty()
{
if ($this instanceof Atomic) {
return $this->value === 'empty';
}
if ($this instanceof Union) {
return isset($this->types['empty']);
}
}
public function removeObjects()
{
foreach ($this->types as $key => $type) {