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

Add support for closure checks

This commit is contained in:
Matt Brown 2016-12-07 14:13:39 -05:00
parent 7aac9985de
commit 55a060b53a
20 changed files with 688 additions and 330 deletions

View File

@ -440,7 +440,19 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
$method_checker->check(clone $class_context);
if (!$config->excludeIssueInFile('InvalidReturnType', $this->file_name)) {
$method_checker->checkReturnTypes($update_docblocks);
/** @var string */
$method_id = $method_checker->getMethodId();
$return_type_location = MethodChecker::getMethodReturnTypeLocation(
$method_id,
$secondary_return_type_location
);
$method_checker->checkReturnTypes(
$update_docblocks,
MethodChecker::getMethodReturnType($method_id),
$return_type_location,
$secondary_return_type_location
);
}
}
}
@ -531,10 +543,10 @@ abstract class ClassLikeChecker extends SourceChecker implements StatementsSourc
$class_context->self . '::' . $this->getMappedMethodName(strtolower($stmt->name)),
$method_id
);
}
self::$class_methods[$class_context->self][strtolower($stmt->name)] = true;
}
}
/**
* @param PhpParser\Node\Stmt\TraitUse $stmt

View File

@ -228,7 +228,8 @@ class FileChecker extends SourceChecker implements StatementsSource
);
$this->namespace_aliased_classes[$namespace_name] = $namespace_checker->getAliasedClasses();
$this->namespace_aliased_classes_flipped[$namespace_name] = $namespace_checker->getAliasedClassesFlipped();
$this->namespace_aliased_classes_flipped[$namespace_name] =
$namespace_checker->getAliasedClassesFlipped();
$this->declared_classes = array_merge($namespace_checker->getDeclaredClasses());
} elseif ($stmt instanceof PhpParser\Node\Stmt\Function_ && $check_functions) {
@ -236,7 +237,24 @@ class FileChecker extends SourceChecker implements StatementsSource
$function_checkers[$stmt->name]->check($function_context, $file_context);
if (!$config->excludeIssueInFile('InvalidReturnType', $this->file_name)) {
$function_checkers[$stmt->name]->checkReturnTypes();
/** @var string */
$method_id = $function_checkers[$stmt->name]->getMethodId();
$return_type = FunctionChecker::getFunctionReturnType(
$method_id,
$this->file_name
);
$return_type_location = FunctionChecker::getFunctionReturnTypeLocation(
$method_id,
$this->file_name
);
$function_checkers[$stmt->name]->checkReturnTypes(
false,
$return_type,
$return_type_location
);
}
}
} else {
@ -333,7 +351,9 @@ class FileChecker extends SourceChecker implements StatementsSource
$stmts = [];
$root_cache_directory = Config::getInstance()->getCacheDirectory();
$parser_cache_directory = $root_cache_directory ? $root_cache_directory . '/' . self::PARSER_CACHE_DIRECTORY : null;
$parser_cache_directory = $root_cache_directory
? $root_cache_directory . '/' . self::PARSER_CACHE_DIRECTORY
: null;
$from_cache = false;
$cache_location = null;
@ -347,7 +367,8 @@ class FileChecker extends SourceChecker implements StatementsSource
if (self::$file_content_hashes === null) {
/** @var array<string, string> */
self::$file_content_hashes = $root_cache_directory && is_readable($root_cache_directory . '/' . self::FILE_HASHES)
self::$file_content_hashes = $root_cache_directory &&
is_readable($root_cache_directory . '/' . self::FILE_HASHES)
? unserialize((string)file_get_contents($root_cache_directory . '/' . self::FILE_HASHES))
: [];
}
@ -390,7 +411,10 @@ class FileChecker extends SourceChecker implements StatementsSource
self::$file_content_hashes[$name_cache_key] = $file_content_hash;
file_put_contents($root_cache_directory . '/' . self::FILE_HASHES, serialize(self::$file_content_hashes));
file_put_contents(
$root_cache_directory . '/' . self::FILE_HASHES,
serialize(self::$file_content_hashes)
);
}
}
@ -660,8 +684,12 @@ class FileChecker extends SourceChecker implements StatementsSource
if (self::$deleted_files === null) {
self::$deleted_files = array_filter(
array_keys(self::$file_references),
/**
* @param string $file_name
* @return bool
*/
function ($file_name) {
return !file_exists((string)$file_name);
return !file_exists($file_name);
}
);
}

View File

@ -315,7 +315,7 @@ class FunctionChecker extends FunctionLikeChecker
}
}
self::$function_return_type_locations[$file_name][$function_id] =
self::$function_return_type_locations[$file_name][$function_id] = $return_type_location ?: false;
self::$function_return_types[$file_name][$function_id] = $return_type ?: false;
return null;
@ -619,15 +619,13 @@ class FunctionChecker extends FunctionLikeChecker
if (isset($call_args[$function_index])) {
$function_call_arg = $call_args[$function_index];
if ($function_call_arg->value instanceof PhpParser\Node\Expr\Closure) {
$closure_yield_types = [];
$closure_return_types = EffectsAnalyser::getReturnTypes(
$function_call_arg->value->stmts,
$closure_yield_types,
true
);
if ($function_call_arg->value instanceof PhpParser\Node\Expr\Closure &&
isset($function_call_arg->value->inferredType) &&
$function_call_arg->value->inferredType->types['Closure'] instanceof Type\Fn
) {
$closure_return_type = $function_call_arg->value->inferredType->types['Closure']->return_type;
if (!$closure_return_types) {
if ($closure_return_type->isVoid()) {
IssueBuffer::accepts(
new InvalidReturnType(
'No return type could be found in the closure passed to ' . $call_map_key,
@ -640,7 +638,7 @@ class FunctionChecker extends FunctionLikeChecker
}
if ($call_map_key === 'array_map') {
$inner_type = new Type\Union($closure_return_types);
$inner_type = clone $closure_return_type;
return new Type\Union([new Type\Generic('array', [Type::getInt(), $inner_type])]);
}

View File

@ -108,11 +108,15 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
*/
public function check(Context $context, Context $global_context = null)
{
if ($function_stmts = $this->function->getStmts()) {
$function_stmts = $this->function->getStmts() ?: [];
$statements_checker = new StatementsChecker($this);
$hash = null;
$closure_return_type = null;
$closure_return_type_location = null;
if ($this instanceof MethodChecker) {
if (ClassLikeChecker::getThisClass()) {
$hash = $this->getMethodId() . json_encode([
@ -220,6 +224,35 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$doc_comment = $this->function->getDocComment();
if ($this->function->returnType) {
$parser_return_type = $this->function->returnType;
$suffix = '';
if ($parser_return_type instanceof PhpParser\Node\NullableType) {
$suffix = '|null';
$parser_return_type = $parser_return_type->type;
}
$closure_return_type = Type::parseString(
(is_string($parser_return_type)
? $parser_return_type
: ClassLikeChecker::getFQCLNFromNameObject(
$parser_return_type,
$this->namespace,
$this->getAliasedClasses()
)
) . $suffix
);
$closure_return_type_location = new CodeLocation(
$this->getSource(),
$this->function,
false,
self::RETURN_TYPE_REGEX
);
}
if ($doc_comment) {
try {
$docblock_info = CommentChecker::extractDocblockInfo(
@ -244,7 +277,7 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
if ($config->use_docblock_types) {
if ($docblock_info->return_type) {
$return_type =
$closure_return_type =
Type::parseString(
self::fixUpLocalType(
(string)$docblock_info->return_type,
@ -253,6 +286,16 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$this->getAliasedClasses()
)
);
if (!$closure_return_type_location) {
$closure_return_type_location = new CodeLocation(
$this->getSource(),
$this->function,
true
);
}
$closure_return_type_location->setCommentLine($docblock_info->return_type_line_number);
}
if ($docblock_info->params) {
@ -266,6 +309,14 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
}
}
}
$this->function->inferredType = new Type\Union([
new Type\Fn(
'Closure',
array_values($function_param_names),
$closure_return_type ?: Type::getMixed()
)
]);
}
foreach ($function_params as $function_param) {
@ -296,11 +347,39 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$context->vars_in_scope['$' . $function_param->name] = $param_type;
$statements_checker->registerVariable($function_param->name, $function_param->code_location->line_number);
$statements_checker->registerVariable(
$function_param->name,
$function_param->code_location->line_number
);
}
$statements_checker->check($function_stmts, $context, null, $global_context);
if ($this->function instanceof Closure) {
$closure_yield_types = [];
$this->checkReturnTypes(
false,
$closure_return_type,
$closure_return_type_location
);
if (!$closure_return_type || $closure_return_type->isMixed()) {
$closure_yield_types = [];
$closure_return_types = EffectsAnalyser::getReturnTypes(
$this->function->stmts,
$closure_yield_types,
true
);
if ($closure_return_types) {
$this->function->inferredType->types['Closure']->return_type =
new Type\Union($closure_return_types);
}
}
}
if (isset($this->return_vars_in_scope[''])) {
$context->vars_in_scope = TypeChecker::combineKeyedTypes(
$context->vars_in_scope,
@ -333,7 +412,6 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$context->vars_possibly_in_scope
];
}
}
return null;
}
@ -458,11 +536,22 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
/**
* @param bool $update_docblock
* @param Type\Union|null $return_type
* @param CodeLocation|null $return_type_location
* @param CodeLocation|null $secondary_return_type_location
* @return false|null
*/
public function checkReturnTypes($update_docblock = false)
{
if (!$this->function->getStmts()) {
public function checkReturnTypes(
$update_docblock = false,
Type\Union $return_type = null,
CodeLocation $return_type_location = null,
CodeLocation $secondary_return_type_location = null
) {
if (!$this->function->getStmts() &&
($this->function instanceof ClassMethod &&
($this->getSource() instanceof InterfaceChecker || $this->function->isAbstract())
)
) {
return null;
}
@ -474,21 +563,6 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$method_id = (string)$this->getMethodId();
$cased_method_id = $this instanceof MethodChecker ? MethodChecker::getCasedMethodId($method_id) : $method_id;
$secondary_return_type_location = null;
if ($this instanceof MethodChecker) {
$return_type = MethodChecker::getMethodReturnType($method_id);
$return_type_location = MethodChecker::getMethodReturnTypeLocation($method_id, $secondary_return_type_location);
} else {
try {
$return_type = FunctionChecker::getFunctionReturnType($method_id, $this->file_name);
$return_type_location = FunctionChecker::getFunctionReturnTypeLocation($method_id, $this->file_name);
} catch (\Exception $e) {
$return_type = null;
$return_type_location = null;
}
}
if (!$return_type_location) {
$return_type_location = new CodeLocation($this, $this->function, true);
}
@ -530,8 +604,16 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$this->file_name,
$this->function->getLine(),
(string)$this->function->getDocComment(),
$inferred_return_type->toNamespacedString($this->getAliasedClassesFlipped(), $this->getFQCLN(), false),
$inferred_return_type->toNamespacedString($this->getAliasedClassesFlipped(), $this->getFQCLN(), true)
$inferred_return_type->toNamespacedString(
$this->getAliasedClassesFlipped(),
$this->getFQCLN(),
false
),
$inferred_return_type->toNamespacedString(
$this->getAliasedClassesFlipped(),
$this->getFQCLN(),
true
)
);
}
@ -601,8 +683,16 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$this->file_name,
$this->function->getLine(),
(string)$this->function->getDocComment(),
$inferred_return_type->toNamespacedString($this->getAliasedClassesFlipped(), $this->getFQCLN(), false),
$inferred_return_type->toNamespacedString($this->getAliasedClassesFlipped(), $this->getFQCLN(), true)
$inferred_return_type->toNamespacedString(
$this->getAliasedClassesFlipped(),
$this->getFQCLN(),
false
),
$inferred_return_type->toNamespacedString(
$this->getAliasedClassesFlipped(),
$this->getFQCLN(),
true
)
);
}

View File

@ -59,6 +59,7 @@ class TryChecker
array_map(
/**
* @param string $fq_catch_class
* @return Type\Atomic
*/
function ($fq_catch_class) {
return new Type\Atomic($fq_catch_class);

View File

@ -118,6 +118,15 @@ class CallChecker
}
if (self::checkFunctionArguments(
$statements_checker,
$stmt->args,
$method_id,
$context
) === false) {
// fall through
}
if (self::checkFunctionArgumentsMatch(
$statements_checker,
$stmt->args,
$method_id,
@ -225,13 +234,22 @@ class CallChecker
$method_id = $fq_class_name . '::__construct';
if (self::checkFunctionArguments(
$statements_checker,
$stmt->args,
$method_id,
$context
) === false) {
return false;
}
if (self::checkFunctionArgumentsMatch(
$statements_checker,
$stmt->args,
$method_id,
$context,
new CodeLocation($statements_checker->getSource(), $stmt)
) === false) {
return false;
// fall through
}
if ($fq_class_name === 'ArrayIterator' && isset($stmt->args[0]->value->inferredType)) {
@ -499,6 +517,15 @@ class CallChecker
}
if (self::checkFunctionArguments(
$statements_checker,
$stmt->args,
$method_id,
$context
) === false) {
return false;
}
if (self::checkFunctionArgumentsMatch(
$statements_checker,
$stmt->args,
$method_id,
@ -692,6 +719,15 @@ class CallChecker
}
if (self::checkFunctionArguments(
$statements_checker,
$stmt->args,
$method_id,
$context
) === false) {
return false;
}
if (self::checkFunctionArgumentsMatch(
$statements_checker,
$stmt->args,
$method_id,
@ -711,24 +747,16 @@ class CallChecker
* @param array<int, PhpParser\Node\Arg> $args
* @param string|null $method_id
* @param Context $context
* @param CodeLocation $code_location
* @param boolean $is_mock
* @return false|null
*/
protected static function checkFunctionArguments(
StatementsChecker $statements_checker,
array $args,
$method_id,
Context $context,
CodeLocation $code_location,
$is_mock = false
Context $context
) {
$function_params = null;
$is_variadic = false;
$fq_class_name = null;
$in_call_map = $method_id ? FunctionChecker::inCallMap($method_id) : false;
if ($method_id) {
@ -737,13 +765,6 @@ class CallChecker
$args,
$statements_checker->getFileName()
);
if ($in_call_map || !strpos($method_id, '::')) {
$is_variadic = FunctionChecker::isVariadic(strtolower($method_id), $statements_checker->getFileName());
} else {
$fq_class_name = explode('::', $method_id)[0];
$is_variadic = $is_mock || MethodChecker::isVariadic($method_id);
}
}
foreach ($args as $argument_offset => $arg) {
@ -826,13 +847,48 @@ class CallChecker
}
}
return null;
}
/**
* @param StatementsChecker $statements_checker
* @param array<int, PhpParser\Node\Arg> $args
* @param string|null $method_id
* @param Context $context
* @param CodeLocation $code_location
* @param boolean $is_mock
* @return false|null
*/
protected static function checkFunctionArgumentsMatch(
StatementsChecker $statements_checker,
array $args,
$method_id,
Context $context,
CodeLocation $code_location,
$is_mock = false
) {
// we need to do this calculation after the above vars have already processed
$function_params = $method_id
? FunctionLikeChecker::getParamsById($method_id, $args, $statements_checker->getFileName())
: [];
$in_call_map = $method_id ? FunctionChecker::inCallMap($method_id) : false;
$cased_method_id = $method_id;
$is_variadic = false;
$fq_class_name = null;
if ($method_id) {
if ($in_call_map || !strpos($method_id, '::')) {
$is_variadic = FunctionChecker::isVariadic(strtolower($method_id), $statements_checker->getFileName());
} else {
$fq_class_name = explode('::', $method_id)[0];
$is_variadic = $is_mock || MethodChecker::isVariadic($method_id);
}
}
if ($method_id && strpos($method_id, '::') && !$in_call_map) {
$cased_method_id = MethodChecker::getCasedMethodId($method_id);
}
@ -902,16 +958,23 @@ class CallChecker
: 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
/** @var PhpParser\Node\Arg */
$closure_arg = isset($args[$closure_index]) ? $args[$closure_index] : null;
/** @var Type\Union|null */
$closure_arg_type = $closure_arg && isset($closure_arg->value->inferredType)
? $closure_arg->value->inferredType
: null;
if ($closure_arg) {
if ($closure_arg_type) {
$expected_closure_param_count = $method_id === 'array_filter' ? 1 : count($array_arg_types);
if (count($closure_arg->params) > $expected_closure_param_count) {
foreach ($closure_arg_type->types as $closure_type) {
if (!$closure_type instanceof Type\Fn) {
continue;
}
if (count($closure_type->parameters) > $expected_closure_param_count) {
if (IssueBuffer::accepts(
new TooManyArguments(
'Too many arguments in closure for ' . ($cased_method_id ?: $method_id),
@ -921,7 +984,7 @@ class CallChecker
)) {
return false;
}
} elseif (count($closure_arg->params) < $expected_closure_param_count) {
} elseif (count($closure_type->parameters) < $expected_closure_param_count) {
if (IssueBuffer::accepts(
new TooFewArguments(
'You must supply a param in the closure for ' . ($cased_method_id ?: $method_id),
@ -933,7 +996,10 @@ class CallChecker
}
}
foreach ($closure_arg->params as $i => $closure_param) {
$closure_params = $closure_type->parameters;
$closure_return_type = $closure_type->return_type;
foreach ($closure_params as $i => $closure_param_type) {
if (!$array_arg_types[$i]) {
continue;
}
@ -941,12 +1007,6 @@ class CallChecker
/** @var Type\Generic */
$array_arg_type = $array_arg_types[$i];
$translated_param = FunctionLikeChecker::getTranslatedParam(
$closure_param,
$statements_checker->getSource()
);
$param_type = $translated_param->type;
$input_type = $array_arg_type->type_params[1];
if ($input_type->isMixed()) {
@ -955,7 +1015,7 @@ class CallChecker
$type_match_found = FunctionLikeChecker::doesParamMatch(
$input_type,
$param_type,
$closure_param_type,
$scalar_type_match_found,
$coerced_type
);
@ -964,8 +1024,8 @@ class CallChecker
if (IssueBuffer::accepts(
new TypeCoercion(
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' .
$param_type . ', parent type ' . $input_type . ' provided',
new CodeLocation($statements_checker->getSource(), $closure_param)
$closure_param_type . ', parent type ' . $input_type . ' provided',
new CodeLocation($statements_checker->getSource(), $closure_arg)
),
$statements_checker->getSuppressedIssues()
)) {
@ -978,8 +1038,8 @@ class CallChecker
if (IssueBuffer::accepts(
new InvalidScalarArgument(
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' .
$param_type . ', ' . $input_type . ' provided',
new CodeLocation($statements_checker->getSource(), $closure_param)
$closure_param_type . ', ' . $input_type . ' provided',
new CodeLocation($statements_checker->getSource(), $closure_arg)
),
$statements_checker->getSuppressedIssues()
)) {
@ -988,8 +1048,8 @@ class CallChecker
} elseif (IssueBuffer::accepts(
new InvalidArgument(
'First parameter of closure passed to function ' . $cased_method_id . ' expects ' .
$param_type . ', ' . $input_type . ' provided',
new CodeLocation($statements_checker->getSource(), $closure_param)
$closure_param_type . ', ' . $input_type . ' provided',
new CodeLocation($statements_checker->getSource(), $closure_arg)
),
$statements_checker->getSuppressedIssues()
)) {
@ -999,6 +1059,7 @@ class CallChecker
}
}
}
}
if ($method_id) {
if (!$is_variadic
@ -1038,8 +1099,6 @@ class CallChecker
}
}
}
return null;
}
/**

View File

@ -231,7 +231,9 @@ class ExpressionChecker
$closure_checker->check($use_context);
if (!isset($stmt->inferredType)) {
$stmt->inferredType = Type::getClosure();
}
} elseif ($stmt instanceof PhpParser\Node\Expr\ArrayDimFetch) {
if (FetchChecker::checkArrayAccess(
$statements_checker,

View File

@ -1450,9 +1450,16 @@ class TypeChecker
return ['mixed'];
}
$array_types = array_filter($all_types, function ($type) {
return preg_match('/^array(\<|$)/', (string)$type);
});
$array_types = array_filter(
$all_types,
/**
* @param string $type
* @return bool
*/
function ($type) {
return (bool)preg_match('/^array(\<|$)/', $type);
}
);
$all_types = array_flip($all_types);
@ -1476,6 +1483,10 @@ class TypeChecker
public static function negateTypes(array $types)
{
return array_map(
/**
* @param string $type
* @return string
*/
function ($type) {
if ($type === 'mixed') {
return $type;
@ -1513,6 +1524,10 @@ class TypeChecker
$simple_declared_types = array_filter(
array_keys($declared_type->types),
/**
* @param string $type_value
* @return bool
*/
function ($type_value) {
return $type_value !== 'null';
}
@ -1520,6 +1535,10 @@ class TypeChecker
$simple_inferred_types = array_filter(
array_keys($inferred_type->types),
/**
* @param string $type_value
* @return bool
*/
function ($type_value) {
return $type_value !== 'null';
}

View File

@ -105,6 +105,9 @@ abstract class Type
$generic_type = array_shift($parse_tree->children);
$generic_params = array_map(
/**
* @return Union
*/
function (ParseTree $child_tree) {
$tree_type = self::getTypeFromTree($child_tree);
return $tree_type instanceof Union ? $tree_type : new Union([$tree_type]);
@ -137,8 +140,19 @@ abstract class Type
if ($parse_tree->value === ParseTree::UNION) {
$union_types = array_map(
/**
* @return Atomic
*/
function (ParseTree $child_tree) {
return self::getTypeFromTree($child_tree);
$atomic_type = self::getTypeFromTree($child_tree);
if (!$atomic_type instanceof Atomic) {
throw new \UnexpectedValueException(
'Was expecting an atomic type, got ' . get_class($atomic_type)
);
}
return $atomic_type;
},
$parse_tree->children
);
@ -224,6 +238,9 @@ abstract class Type
$class_chars = '[a-zA-Z\<\>\\\\_]+';
return preg_replace_callback(
'/(' . $class_chars . '|' . '\((' . $class_chars . '(\|' . $class_chars . ')*' . ')\))((\[\])+)/',
/**
* @return string
*/
function (array $matches) {
$inner_type = str_replace(['(', ')'], '', (string)$matches[1]);

33
src/Psalm/Type/Fn.php Normal file
View File

@ -0,0 +1,33 @@
<?php
namespace Psalm\Type;
class Fn extends Atomic
{
/**
* @var string
*/
public $value = 'Closure';
/**
* @var array<Union>
*/
public $parameters = [];
/**
* @var Union
*/
public $return_type;
/**
* Constructs a new instance of a generic type
*
* @param string $value
* @param array<int, Union> $parameters
* @param Union $return_type
*/
public function __construct($value, array $parameters, Union $return_type)
{
$this->parameters = $parameters;
$this->return_type = $return_type;
}
}

View File

@ -30,6 +30,9 @@ class Generic extends Atomic
implode(
', ',
array_map(
/**
* @return string
*/
function (Union $type_param) {
return (string)$type_param;
},
@ -72,6 +75,9 @@ class Generic extends Atomic
implode(
', ',
array_map(
/**
* @return string
*/
function (Union $type_param) use ($aliased_classes, $this_class) {
return $type_param->toNamespacedString($aliased_classes, $this_class, false);
},

View File

@ -34,6 +34,9 @@ class ObjectLike extends Atomic
implode(
', ',
array_map(
/**
* @return string
*/
function ($name, $type) {
return $name . ':' . $type;
},
@ -61,6 +64,9 @@ class ObjectLike extends Atomic
implode(
', ',
array_map(
/**
* @return string
*/
function ($name, Union $type) use ($aliased_classes, $this_class, $use_phpdoc_format) {
return $name . ':' . $type->toNamespacedString($aliased_classes, $this_class, $use_phpdoc_format);
},

View File

@ -33,8 +33,12 @@ class Union extends Type
return implode(
'|',
array_map(
/**
* @param string $type
* @return string
*/
function ($type) {
return (string)$type;
return $type;
},
$this->types
)
@ -52,6 +56,9 @@ class Union extends Type
return implode(
'|',
array_map(
/**
* @return string
*/
function (Atomic $type) use ($aliased_classes, $this_class, $use_phpdoc_format) {
return $type->toNamespacedString($aliased_classes, $this_class, $use_phpdoc_format);
},

View File

@ -38,6 +38,9 @@ class ClosureTest extends PHPUnit_Framework_TestCase
/** @return void */
function fn() {
run_function(
/**
* @return void
*/
function() use(&$data) {
$data = 1;
}
@ -63,7 +66,7 @@ class ClosureTest extends PHPUnit_Framework_TestCase
$bar = ["foo", "bar"];
$bam = array_map(
function(int $a) {
function(int $a) : int {
return $a + 1;
},
$bar
@ -74,4 +77,46 @@ class ClosureTest extends PHPUnit_Framework_TestCase
$context = new Context('somefile.php');
$file_checker->check(true, true, $context);
}
/**
* @expectedException \Psalm\Exception\CodeException
* @expectedExceptionMessage InvalidReturnType
*/
public function testNoReturn()
{
$stmts = self::$parser->parse('<?php
$bar = ["foo", "bar"];
$bam = array_map(
function(string $a) : string {
},
$bar
);
');
$file_checker = new FileChecker('somefile.php', $stmts);
$context = new Context('somefile.php');
$file_checker->check(true, true, $context);
}
public function testInferredArg()
{
$stmts = self::$parser->parse('<?php
$bar = ["foo", "bar"];
$bam = array_map(
/**
* @psalm-suppress MissingReturnType
*/
function(string $a) {
return $a . "blah";
},
$bar
);
');
$file_checker = new FileChecker('somefile.php', $stmts);
$context = new Context('somefile.php');
$file_checker->check(true, true, $context);
}
}

View File

@ -38,6 +38,9 @@ class InterfaceTest extends PHPUnit_Framework_TestCase
interface B
{
/**
* @return string
*/
public function bar();
}
@ -53,14 +56,17 @@ class InterfaceTest extends PHPUnit_Framework_TestCase
{
public function foo()
{
return "hello";
}
public function bar()
{
return "goodbye";
}
public function baz()
{
return "hello again";
}
}
@ -99,13 +105,19 @@ class InterfaceTest extends PHPUnit_Framework_TestCase
{
public function foo()
{
return "hello";
}
public function baz()
{
return "goodbye";
}
}
/**
* @param A $a
* @return void
*/
function qux(A $a) {
}

View File

@ -29,8 +29,11 @@ class Php40Test extends PHPUnit_Framework_TestCase
{
$stmts = self::$parser->parse('<?php
class A {
/**
* @return string
*/
public function A() {
return "hello";
}
}

View File

@ -76,6 +76,9 @@ class Php56Test extends PHPUnit_Framework_TestCase
public function testVariadic()
{
$stmts = self::$parser->parse('<?php
/**
* @return void
*/
function f($req, $opt = null, ...$params) {
}
@ -94,11 +97,19 @@ class Php56Test extends PHPUnit_Framework_TestCase
public function testVariadicArray()
{
$stmts = self::$parser->parse('<?php
/** @return array<int> */
/**
* @return array<int>
*/
function f(int ...$a_list) {
return array_map(function (int $a) {
return array_map(
/**
* @return int
*/
function (int $a) {
return $a + 1;
}, $a_list);
},
$a_list
);
}
f(1);

View File

@ -475,9 +475,9 @@ class ReturnTypeTest extends PHPUnit_Framework_TestCase
public function testOverrideReturnTypeInGrandparent()
{
$stmts = self::$parser->parse('<?php
class A {
abstract class A {
/** @return string|null */
public function blah();
abstract public function blah();
}
class B extends A {

View File

@ -31,12 +31,18 @@ class SwitchTypeTest extends PHPUnit_Framework_TestCase
{
$stmts = self::$parser->parse('<?php
class A {
/**
* @return void
*/
public function foo() {
}
}
class B {
/**
* @return void
*/
public function bar() {
}
@ -68,12 +74,14 @@ class SwitchTypeTest extends PHPUnit_Framework_TestCase
{
$stmts = self::$parser->parse('<?php
class A {
/** @return void */
public function foo() {
}
}
class B {
/** @return void */
public function bar() {
}

View File

@ -1236,6 +1236,7 @@ class TypeTest extends PHPUnit_Framework_TestCase
{
$stmts = self::$parser->parse('<?php
class One {
/** @return void */
public function foo() {}
}