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

Fix #90 - add genericised stubs for common array functions

This commit is contained in:
Matt Brown 2017-02-10 18:12:59 -05:00
parent 8ffd45407c
commit c0a6fc9125
10 changed files with 276 additions and 188 deletions

View File

@ -12,6 +12,7 @@
<directory name="examples" />
<ignoreFiles>
<file name="src/Psalm/CallMap.php" />
<directory name="src/Psalm/Stubs" />
<directory name="tests/stubs" />
<!-- @todo fix this -->
<file name="examples/TemplateChecker.php" />

View File

@ -27,6 +27,11 @@ class FunctionChecker extends FunctionLikeChecker
*/
protected static $builtin_functions = [];
/**
* @var array<string, FunctionLikeStorage>
*/
public static $stubbed_functions = [];
/**
* @param mixed $function
* @param StatementsSource $source
@ -61,6 +66,10 @@ class FunctionChecker extends FunctionLikeChecker
return true;
}
if (isset(self::$stubbed_functions[$function_id])) {
return true;
}
if (self::extractReflectionInfo($function_id) === false) {
return false;
}
@ -75,7 +84,11 @@ class FunctionChecker extends FunctionLikeChecker
*/
public static function getStorage($function_id, $file_path)
{
if (isset(self::$builtin_functions[$function_id]) && self::$builtin_functions[$function_id]) {
if (isset(self::$stubbed_functions[$function_id])) {
return self::$stubbed_functions[$function_id];
}
if (isset(self::$builtin_functions[$function_id])) {
return self::$builtin_functions[$function_id];
}
@ -149,18 +162,22 @@ class FunctionChecker extends FunctionLikeChecker
*/
public static function getFunctionReturnType($function_id, $file_path, array $function_template_types = null)
{
if (!isset(FileChecker::$storage[$file_path])) {
return null;
if (isset(self::$stubbed_functions[$function_id])) {
$function_return_type = self::$stubbed_functions[$function_id]->return_type;
} else {
if (!isset(FileChecker::$storage[$file_path])) {
return null;
}
$file_storage = FileChecker::$storage[$file_path];
if (!isset($file_storage->functions[$function_id])) {
throw new \InvalidArgumentException('Do not know function ' . $function_id . ' in file ' . $file_path);
}
$function_return_type = $file_storage->functions[$function_id]->return_type;
}
$file_storage = FileChecker::$storage[$file_path];
if (!isset($file_storage->functions[$function_id])) {
throw new \InvalidArgumentException('Do not know function ' . $function_id . ' in file ' . $file_path);
}
$function_return_type = $file_storage->functions[$function_id]->return_type;
if ($function_return_type) {
if ($function_template_types) {
$type_tokens = Type::tokenize((string)$function_return_type);
@ -433,45 +450,6 @@ class FunctionChecker extends FunctionLikeChecker
? $first_arg->inferredType->types['array']
: null;
if ($call_map_key === 'array_values' ||
$call_map_key === 'array_unique' ||
$call_map_key === 'array_intersect' ||
$call_map_key === 'array_slice'
) {
if ($first_arg_array_generic || $first_arg_array_objectlike) {
if ($first_arg_array_generic) {
$inner_type = clone $first_arg_array_generic->type_params[1];
} else {
/** @var Type\Atomic\ObjectLike $first_arg_array_objectlike */
$inner_type = $first_arg_array_objectlike->getGenericTypeParam();
}
return new Type\Union([
new Type\Atomic\TArray([
Type::getInt(),
$inner_type
])
]);
}
}
if ($call_map_key === 'array_keys') {
if ($first_arg_array_generic || $first_arg_array_objectlike) {
if ($first_arg_array_generic) {
$inner_type = clone $first_arg_array_generic->type_params[0];
} else {
$inner_type = Type::getString();
}
return new Type\Union([
new Type\Atomic\TArray([
Type::getInt(),
$inner_type
])
]);
}
}
if ($call_map_key === 'array_merge') {
$inner_value_types = [];
$inner_key_types = [];
@ -510,62 +488,6 @@ class FunctionChecker extends FunctionLikeChecker
return Type::getArray();
}
if ($call_map_key === 'array_combine') {
$second_arg_array_generic = $second_arg
&& isset($second_arg->inferredType)
&& isset($second_arg->inferredType->types['array'])
&& $second_arg->inferredType->types['array'] instanceof Type\Atomic\TArray
? $second_arg->inferredType->types['array']
: null;
$second_arg_array_objectlike = $second_arg
&& isset($second_arg->inferredType)
&& isset($second_arg->inferredType->types['array'])
&& $second_arg->inferredType->types['array'] instanceof Type\Atomic\ObjectLike
? $second_arg->inferredType->types['array']
: null;
if ($second_arg_array_generic || $second_arg_array_objectlike) {
if ($first_arg && isset($first_arg->inferredType) && $first_arg->inferredType->hasArray()) {
if ($first_arg_array_generic) {
$keys_inner_type = clone $first_arg_array_generic->type_params[1];
} else {
/** @var Type\Atomic\ObjectLike $first_arg_array_objectlike */
$keys_inner_type = $first_arg_array_objectlike->getGenericTypeParam();
}
} else {
$keys_inner_type = Type::getMixed();
}
if ($second_arg_array_generic) {
$values_inner_type = clone $second_arg_array_generic->type_params[1];
} else {
/** @var Type\Atomic\ObjectLike $second_arg_array_objectlike */
$values_inner_type = $second_arg_array_objectlike->getGenericTypeParam();
}
return new Type\Union([
new Type\Atomic\TArray([
$keys_inner_type,
$values_inner_type
])
]);
}
}
if ($call_map_key === 'array_diff') {
if (!$first_arg_array_generic) {
return Type::getArray();
}
return new Type\Union([
new Type\Atomic\TArray([
Type::getInt(),
clone $first_arg_array_generic->type_params[1]
])
]);
}
if ($call_map_key === 'array_filter') {
if (!$first_arg_array_generic) {
return Type::getArray();
@ -588,22 +510,6 @@ class FunctionChecker extends FunctionLikeChecker
]);
}
if ($call_map_key === 'array_diff_key') {
if (!$first_arg_array_generic) {
return Type::getArray();
}
return clone $first_arg->inferredType;
}
if ($call_map_key === 'array_shift' || $call_map_key === 'array_pop') {
if (!$first_arg_array_generic) {
return Type::getMixed();
}
return clone $first_arg_array_generic->type_params[1];
}
return null;
}
@ -780,5 +686,6 @@ class FunctionChecker extends FunctionLikeChecker
public static function clearCache()
{
self::$builtin_functions = [];
self::$stubbed_functions = [];
}
}

View File

@ -485,13 +485,19 @@ abstract class FunctionLikeChecker extends SourceChecker implements StatementsSo
$cased_function_id = ($namespace ? $namespace . '\\' : '') . $function->name;
$function_id = strtolower($cased_function_id);
$file_storage = FileChecker::$storage[$source->getFilePath()];
$project_checker = $source->getFileChecker()->project_checker;
if (isset($file_storage->functions[$function_id])) {
return $file_storage->functions[$function_id];
if ($project_checker->register_global_functions) {
$storage = FunctionChecker::$stubbed_functions[$function_id] = new FunctionLikeStorage();
} else {
$file_storage = FileChecker::$storage[$source->getFilePath()];
if (isset($file_storage->functions[$function_id])) {
return $file_storage->functions[$function_id];
}
$storage = $file_storage->functions[$function_id] = new FunctionLikeStorage();
}
$storage = $file_storage->functions[$function_id] = new FunctionLikeStorage();
} elseif ($function instanceof PhpParser\Node\Stmt\ClassMethod) {
$fq_class_name = (string)$source->getFQCLN();

View File

@ -155,6 +155,13 @@ class ProjectChecker
*/
public $fake_files = [];
/**
* Whether to log functions just at the file level or globally (for stubs)
*
* @var boolean
*/
public $register_global_functions = false;
const TYPE_CONSOLE = 'console';
const TYPE_JSON = 'json';
const TYPE_EMACS = 'emacs';

View File

@ -58,41 +58,36 @@ class ForeachChecker
}
if ($iterator_type) {
foreach ($iterator_type->types as $return_type) {
foreach ($iterator_type->types as $iterator_type) {
// if it's an empty array, we cannot iterate over it
if ((string) $return_type === 'array<empty, empty>') {
if ((string) $iterator_type === 'array<empty, empty>') {
continue;
}
if ($return_type instanceof Type\Atomic\TArray || $return_type instanceof Type\Atomic\TGenericObject) {
$value_index = count($return_type->type_params) - 1;
$value_type_part = $return_type->type_params[$value_index];
if ($iterator_type instanceof Type\Atomic\TArray) {
if (!$value_type) {
$value_type = $value_type_part;
$value_type = $iterator_type->type_params[1];
} else {
$value_type = Type::combineUnionTypes($value_type, $value_type_part);
$value_type = Type::combineUnionTypes($value_type, $iterator_type->type_params[1]);
}
if ($value_index) {
$key_type_part = $return_type->type_params[0];
$key_type_part = $iterator_type->type_params[0];
if (!$key_type) {
$key_type = $key_type_part;
} else {
$key_type = Type::combineUnionTypes($key_type, $key_type_part);
}
if (!$key_type) {
$key_type = $key_type_part;
} else {
$key_type = Type::combineUnionTypes($key_type, $key_type_part);
}
continue;
}
if ($return_type instanceof Type\Atomic\Scalar ||
$return_type instanceof Type\Atomic\TNull ||
$return_type instanceof Type\Atomic\TVoid
if ($iterator_type instanceof Type\Atomic\Scalar ||
$iterator_type instanceof Type\Atomic\TNull ||
$iterator_type instanceof Type\Atomic\TVoid
) {
if (IssueBuffer::accepts(
new InvalidIterator(
'Cannot iterate over ' . $return_type->getKey(),
'Cannot iterate over ' . $iterator_type->getKey(),
new CodeLocation($statements_checker->getSource(), $stmt->expr)
),
$statements_checker->getSuppressedIssues()
@ -101,19 +96,18 @@ class ForeachChecker
}
$value_type = Type::getMixed();
} elseif ($return_type instanceof Type\Atomic\TArray ||
$return_type instanceof Type\Atomic\TObject ||
$return_type instanceof Type\Atomic\TMixed ||
$return_type instanceof Type\Atomic\TEmpty ||
($return_type instanceof Type\Atomic\TNamedObject && $return_type->value === 'Generator')
} elseif ($iterator_type instanceof Type\Atomic\TArray ||
$iterator_type instanceof Type\Atomic\TObject ||
$iterator_type instanceof Type\Atomic\TMixed ||
$iterator_type instanceof Type\Atomic\TEmpty
) {
$value_type = Type::getMixed();
} elseif ($return_type instanceof Type\Atomic\TNamedObject) {
if ($return_type->value !== 'Traversable' &&
$return_type->value !== $statements_checker->getClassName()
} elseif ($iterator_type instanceof Type\Atomic\TNamedObject) {
if ($iterator_type->value !== 'Traversable' &&
$iterator_type->value !== $statements_checker->getClassName()
) {
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
$return_type->value,
$iterator_type->value,
$statements_checker->getFileChecker(),
new CodeLocation($statements_checker->getSource(), $stmt->expr),
$statements_checker->getSuppressedIssues()
@ -122,18 +116,47 @@ class ForeachChecker
}
}
if ($iterator_type instanceof Type\Atomic\TGenericObject &&
(strtolower($iterator_type->value) === 'iterable' ||
strtolower($iterator_type->value) === 'traversable' ||
ClassChecker::classImplements(
$iterator_type->value,
'Traversable'
))
) {
$value_index = count($iterator_type->type_params) - 1;
$value_type_part = $iterator_type->type_params[$value_index];
if (!$value_type) {
$value_type = $value_type_part;
} else {
$value_type = Type::combineUnionTypes($value_type, $value_type_part);
}
if ($value_index) {
$key_type_part = $iterator_type->type_params[0];
if (!$key_type) {
$key_type = $key_type_part;
} else {
$key_type = Type::combineUnionTypes($key_type, $key_type_part);
}
}
continue;
}
if (ClassChecker::classImplements(
$return_type->value,
$iterator_type->value,
'Iterator'
)) {
$iterator_method = $return_type->value . '::current';
$iterator_method = $iterator_type->value . '::current';
$iterator_class_type = MethodChecker::getMethodReturnType($iterator_method);
if ($iterator_class_type) {
$value_type_part = ExpressionChecker::fleshOutTypes(
$iterator_class_type,
[],
$return_type->value,
$iterator_type->value,
$iterator_method
);

View File

@ -123,6 +123,9 @@ class CallChecker
if ($context->check_functions) {
$in_call_map = false;
$is_stubbed = false;
$function_storage = null;
$function_params = null;
$code_location = new CodeLocation($statements_checker->getSource(), $stmt);
@ -185,6 +188,7 @@ class CallChecker
$method_id = implode('\\', $stmt->name->parts);
$in_call_map = FunctionChecker::inCallMap($method_id);
$is_stubbed = isset(FunctionChecker::$stubbed_functions[strtolower($method_id)]);
$is_predefined = true;
@ -201,7 +205,7 @@ class CallChecker
//$method_id = $statements_checker->getFQCLN() . '::' . $method_id;
}
if (!$in_call_map) {
if (!$in_call_map && !$is_stubbed) {
if (self::checkFunctionExists(
$statements_checker,
$method_id,
@ -211,7 +215,9 @@ class CallChecker
) {
return false;
}
}
if (!$in_call_map || $is_stubbed) {
$function_storage = FunctionChecker::getStorage(
strtolower($method_id),
$statements_checker->getFilePath()
@ -220,12 +226,11 @@ class CallChecker
$function_params = $function_storage->params;
if (!$is_predefined) {
$defined_constants = FunctionChecker::getDefinedConstants(
$method_id,
$statements_checker->getFilePath()
);
$defined_constants = $function_storage->defined_constants;
}
} else {
}
if ($in_call_map && !$is_stubbed) {
$function_params = FunctionLikeChecker::getFunctionParamsFromCallMapById(
$method_id,
$stmt->args,
@ -243,19 +248,10 @@ class CallChecker
// fall through
}
$function_storage = null;
$generic_params = null;
if ($stmt->name instanceof PhpParser\Node\Name && $method_id) {
if (!$in_call_map) {
$function_storage = FunctionChecker::getStorage(
strtolower($method_id),
$statements_checker->getFilePath()
);
$function_params = $function_storage->params;
} else {
if (!$is_stubbed && $in_call_map) {
$function_params = FunctionLikeChecker::getFunctionParamsFromCallMapById(
$method_id,
$stmt->args,
@ -282,14 +278,7 @@ class CallChecker
}
if ($stmt->name instanceof PhpParser\Node\Name && $method_id) {
if ($in_call_map) {
$stmt->inferredType = FunctionChecker::getReturnTypeFromCallMapWithArgs(
$method_id,
$stmt->args,
$code_location,
$statements_checker->getSuppressedIssues()
);
} else {
if (!$in_call_map || $is_stubbed) {
try {
$stmt->inferredType = FunctionChecker::getFunctionReturnType(
$method_id,
@ -300,6 +289,13 @@ class CallChecker
// this can happen when the function was defined in the Config startup script
$stmt->inferredType = Type::getMixed();
}
} else {
$stmt->inferredType = FunctionChecker::getReturnTypeFromCallMapWithArgs(
$method_id,
$stmt->args,
$code_location,
$statements_checker->getSuppressedIssues()
);
}
}
@ -1556,7 +1552,6 @@ class CallChecker
}
}
$closure_params = $closure_type->params;
$closure_return_type = $closure_type->return_type;

View File

@ -574,10 +574,26 @@ class Config
*/
public function visitStubFiles(ProjectChecker $project_checker)
{
$project_checker->register_global_functions = true;
$generic_stubs = realpath(__DIR__ . '/Stubs/CoreGenericFunctions.php');
if ($generic_stubs) {
$generic_stub_checker = new FileChecker(
$generic_stubs,
$project_checker
);
$generic_stub_checker->visit();
} else {
throw new \UnexpectedValueException('Cannot locate core generic stubs');
}
foreach ($this->stub_files as $stub_file) {
$stub_checker = new FileChecker($stub_file, $project_checker);
$stub_checker->visit();
}
$project_checker->register_global_functions = false;
}
/**

View File

@ -0,0 +1,94 @@
<?php
/**
* @template T
*
* @param array<T, mixed> $arr
* @return array<int, T>
*/
function array_keys(array $arr) {}
/**
* @template T
*
* @param array<mixed, T> $arr
* @return array<int, T>
*/
function array_values(array $arr) {}
/**
* @template T
*
* @param array<mixed, T> $arr
* @return array<int, T>
*/
function array_unique(array $arr) {}
/**
* @template T
*
* @param array<mixed, T> $arr
* @return array<int, T>
*/
function array_slice(array $arr) {}
/**
* @template TKey
* @template TValue
*
* @param array<TKey, TValue> $arr
* @param array $arr2
* @param array|null $arr3
* @param array|null $arr4
* @return array<TKey, TValue>
*/
function array_intersect(array $arr, array $arr2, array $arr3 = null, array $arr4 = null) {}
/**
* @template TKey
* @template TValue
*
* @param array<mixed, TKey> $arr
* @param array<mixed, TValue> $arr2
* @return array<TKey, TValue>
*/
function array_combine(array $arr, array $arr2) {}
/**
* @template TKey
* @template TValue
*
* @param array<TKey, TValue> $arr
* @param array $arr2
* @param array|null $arr3
* @param array|null $arr4
* @return array<TKey, TValue>
*/
function array_diff(array $arr, array $arr2, array $arr3 = null, array $arr4 = null) {}
/**
* @template TKey
* @template TValue
*
* @param array<TKey, TValue> $arr
* @param array $arr2
* @param array|null $arr3
* @param array|null $arr4
* @return array<TKey, TValue>
*/
function array_diff_key(array $arr, array $arr2, array $arr3 = null, array $arr4 = null) {}
/**
* @template TValue
*
* @param array<mixed, TValue> $arr
* @return TValue
*/
function array_shift(array $arr) {}
/**
* @template TValue
*
* @param array<mixed, TValue> $arr
* @return TValue
*/
function array_pop(array $arr) {}

View File

@ -327,21 +327,60 @@ class FunctionCallTest extends PHPUnit_Framework_TestCase
/**
* @return void
*/
public function testArrayFunctions()
public function testArrayKeys()
{
$stmts = self::$parser->parse('<?php
$a = array_keys(["a" => 1, "b" => 2]);
$b = array_values(["a" => 1, "b" => 2]);
$c = array_combine(["a", "b", "c"], [1, 2, 3]);
$d = array_merge(["a", "b", "c"], [1, 2, 3]);
');
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
$context = new Context();
$file_checker->visitAndAnalyzeMethods($context);
$this->assertEquals('array<int, string>', (string) $context->vars_in_scope['$a']);
}
/**
* @return void
*/
public function testArrayValues()
{
$stmts = self::$parser->parse('<?php
$b = array_values(["a" => 1, "b" => 2]);
');
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
$context = new Context();
$file_checker->visitAndAnalyzeMethods($context);
$this->assertEquals('array<int, int>', (string) $context->vars_in_scope['$b']);
}
/**
* @return void
*/
public function testArrayCombine()
{
$stmts = self::$parser->parse('<?php
$c = array_combine(["a", "b", "c"], [1, 2, 3]);
');
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
$context = new Context();
$file_checker->visitAndAnalyzeMethods($context);
$this->assertEquals('array<string, int>', (string) $context->vars_in_scope['$c']);
}
/**
* @return void
*/
public function testArrayMerge()
{
$stmts = self::$parser->parse('<?php
$d = array_merge(["a", "b", "c"], [1, 2, 3]);
');
$file_checker = new FileChecker('somefile.php', $this->project_checker, $stmts);
$context = new Context();
$file_checker->visitAndAnalyzeMethods($context);
$this->assertEquals('array<int, int|string>', (string) $context->vars_in_scope['$d']);
}
}

View File

@ -379,7 +379,7 @@ class Php71Test extends PHPUnit_Framework_TestCase
{
$stmts = self::$parser->parse('<?php
/**
* @param iterable<int> $iter
* @param iterable<int, int> $iter
*/
function iterator(iterable $iter) : void
{