2016-04-27 00:18:49 +02:00
|
|
|
<?php
|
2016-07-26 00:37:44 +02:00
|
|
|
namespace Psalm;
|
2016-04-27 00:18:49 +02:00
|
|
|
|
|
|
|
use PhpParser;
|
2017-01-15 01:06:58 +01:00
|
|
|
use Psalm\Type\Atomic;
|
2016-04-27 00:18:49 +02:00
|
|
|
|
|
|
|
/**
|
2016-11-02 07:29:00 +01:00
|
|
|
* A class for analysing a given method call's effects in relation to $this/self and also looking at return types
|
2016-04-27 00:18:49 +02:00
|
|
|
*/
|
|
|
|
class EffectsAnalyser
|
|
|
|
{
|
|
|
|
/**
|
|
|
|
* Gets the return types from a list of statements
|
|
|
|
*
|
2016-12-24 19:23:22 +01:00
|
|
|
* @param array<PhpParser\Node> $stmts
|
|
|
|
* @param array<int,Type\Atomic> $yield_types
|
2017-05-10 20:03:51 +02:00
|
|
|
* @param bool $ignore_nullable_issues
|
2016-12-24 19:23:22 +01:00
|
|
|
* @param bool $collapse_types
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2016-10-20 20:26:03 +02:00
|
|
|
* @return array<int,Type\Atomic> a list of return types
|
2016-04-27 00:18:49 +02:00
|
|
|
*/
|
2017-05-10 20:03:51 +02:00
|
|
|
public static function getReturnTypes(
|
|
|
|
array $stmts,
|
|
|
|
array &$yield_types,
|
|
|
|
&$ignore_nullable_issues = false,
|
|
|
|
$collapse_types = false
|
|
|
|
) {
|
2016-04-27 00:18:49 +02:00
|
|
|
$return_types = [];
|
|
|
|
|
2016-06-20 07:05:44 +02:00
|
|
|
$last_stmt = null;
|
|
|
|
|
2016-05-09 14:56:07 +02:00
|
|
|
foreach ($stmts as $stmt) {
|
2016-06-20 07:05:44 +02:00
|
|
|
if (!$stmt instanceof PhpParser\Node\Stmt\Nop) {
|
|
|
|
$last_stmt = $stmt;
|
|
|
|
}
|
|
|
|
|
2016-04-27 00:18:49 +02:00
|
|
|
if ($stmt instanceof PhpParser\Node\Stmt\Return_) {
|
2016-11-02 07:29:00 +01:00
|
|
|
if ($stmt->expr instanceof PhpParser\Node\Expr\Yield_ ||
|
|
|
|
$stmt->expr instanceof PhpParser\Node\Expr\YieldFrom) {
|
2016-10-20 21:26:02 +02:00
|
|
|
$yield_types = array_merge($yield_types, self::getYieldTypeFromExpression($stmt->expr));
|
2016-11-02 07:29:00 +01:00
|
|
|
} else {
|
2016-10-20 21:26:02 +02:00
|
|
|
if (isset($stmt->inferredType)) {
|
|
|
|
$return_types = array_merge(array_values($stmt->inferredType->types), $return_types);
|
2017-05-10 20:03:51 +02:00
|
|
|
|
|
|
|
if ($stmt->inferredType->ignore_nullable_issues) {
|
|
|
|
$ignore_nullable_issues = true;
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} else {
|
2017-01-15 01:06:58 +01:00
|
|
|
$return_types[] = new Atomic\TMixed();
|
2016-10-20 21:26:02 +02:00
|
|
|
}
|
2016-06-17 22:05:28 +02:00
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Expr\Yield_ || $stmt instanceof PhpParser\Node\Expr\YieldFrom) {
|
2016-10-20 21:26:02 +02:00
|
|
|
$yield_types = array_merge($yield_types, self::getYieldTypeFromExpression($stmt));
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\If_) {
|
2017-05-10 20:03:51 +02:00
|
|
|
$return_types = array_merge(
|
|
|
|
$return_types,
|
|
|
|
self::getReturnTypes($stmt->stmts, $yield_types, $ignore_nullable_issues)
|
|
|
|
);
|
2016-04-27 00:18:49 +02:00
|
|
|
|
|
|
|
foreach ($stmt->elseifs as $elseif) {
|
2017-05-10 20:03:51 +02:00
|
|
|
$return_types = array_merge(
|
|
|
|
$return_types,
|
|
|
|
self::getReturnTypes($elseif->stmts, $yield_types, $ignore_nullable_issues)
|
|
|
|
);
|
2016-04-27 00:18:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
if ($stmt->else) {
|
2017-05-10 20:03:51 +02:00
|
|
|
$return_types = array_merge(
|
|
|
|
$return_types,
|
|
|
|
self::getReturnTypes($stmt->else->stmts, $yield_types, $ignore_nullable_issues)
|
|
|
|
);
|
2016-04-27 00:18:49 +02:00
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\TryCatch) {
|
2017-05-10 20:03:51 +02:00
|
|
|
$return_types = array_merge(
|
|
|
|
$return_types,
|
|
|
|
self::getReturnTypes($stmt->stmts, $yield_types, $ignore_nullable_issues)
|
|
|
|
);
|
2016-04-27 00:18:49 +02:00
|
|
|
|
|
|
|
foreach ($stmt->catches as $catch) {
|
2017-05-10 20:03:51 +02:00
|
|
|
$return_types = array_merge(
|
|
|
|
$return_types,
|
|
|
|
self::getReturnTypes($catch->stmts, $yield_types, $ignore_nullable_issues)
|
|
|
|
);
|
2016-04-27 00:18:49 +02:00
|
|
|
}
|
|
|
|
|
2016-12-04 04:41:45 +01:00
|
|
|
if ($stmt->finally) {
|
2017-04-28 06:31:55 +02:00
|
|
|
$return_types = array_merge(
|
|
|
|
$return_types,
|
2017-05-10 20:03:51 +02:00
|
|
|
self::getReturnTypes($stmt->finally->stmts, $yield_types, $ignore_nullable_issues)
|
2017-04-28 06:31:55 +02:00
|
|
|
);
|
2016-04-27 00:18:49 +02:00
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\For_) {
|
2017-05-10 20:03:51 +02:00
|
|
|
$return_types = array_merge(
|
|
|
|
$return_types,
|
|
|
|
self::getReturnTypes($stmt->stmts, $yield_types, $ignore_nullable_issues)
|
|
|
|
);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Foreach_) {
|
2017-05-10 20:03:51 +02:00
|
|
|
$return_types = array_merge(
|
|
|
|
$return_types,
|
|
|
|
self::getReturnTypes($stmt->stmts, $yield_types, $ignore_nullable_issues)
|
|
|
|
);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\While_) {
|
2017-05-10 20:03:51 +02:00
|
|
|
$return_types = array_merge(
|
|
|
|
$return_types,
|
|
|
|
self::getReturnTypes($stmt->stmts, $yield_types, $ignore_nullable_issues)
|
|
|
|
);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Do_) {
|
2017-05-10 20:03:51 +02:00
|
|
|
$return_types = array_merge(
|
|
|
|
$return_types,
|
|
|
|
self::getReturnTypes($stmt->stmts, $yield_types, $ignore_nullable_issues)
|
|
|
|
);
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Stmt\Switch_) {
|
2016-04-27 00:18:49 +02:00
|
|
|
foreach ($stmt->cases as $case) {
|
2017-05-10 20:03:51 +02:00
|
|
|
$return_types = array_merge(
|
|
|
|
$return_types,
|
|
|
|
self::getReturnTypes($case->stmts, $yield_types, $ignore_nullable_issues)
|
|
|
|
);
|
2016-04-27 00:18:49 +02:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-06-20 07:05:44 +02:00
|
|
|
// if we're at the top level and we're not ending in a return, make sure to add possible null
|
2016-10-20 20:26:03 +02:00
|
|
|
if ($collapse_types) {
|
|
|
|
// if it's a generator, boil everything down to a single generator return type
|
2016-10-20 21:26:02 +02:00
|
|
|
if ($yield_types) {
|
2016-10-20 20:26:03 +02:00
|
|
|
$key_type = null;
|
|
|
|
$value_type = null;
|
|
|
|
|
2016-10-20 21:26:02 +02:00
|
|
|
foreach ($yield_types as $type) {
|
2017-01-15 01:06:58 +01:00
|
|
|
if ($type instanceof Type\Atomic\TArray || $type instanceof Type\Atomic\TGenericObject) {
|
2016-10-20 20:26:03 +02:00
|
|
|
$first_type_param = count($type->type_params) ? $type->type_params[0] : null;
|
|
|
|
$last_type_param = $type->type_params[count($type->type_params) - 1];
|
|
|
|
|
|
|
|
if ($value_type === null) {
|
|
|
|
$value_type = clone $last_type_param;
|
2016-11-02 07:29:00 +01:00
|
|
|
} else {
|
2016-10-20 20:26:03 +02:00
|
|
|
$value_type = Type::combineUnionTypes($value_type, $last_type_param);
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!$key_type || !$first_type_param) {
|
|
|
|
$key_type = $first_type_param ? clone $first_type_param : Type::getMixed();
|
2016-11-02 07:29:00 +01:00
|
|
|
} else {
|
2016-10-20 20:26:03 +02:00
|
|
|
$key_type = Type::combineUnionTypes($key_type, $first_type_param);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2016-10-20 21:26:02 +02:00
|
|
|
$yield_types = [
|
2017-01-15 01:06:58 +01:00
|
|
|
new Atomic\TGenericObject(
|
2016-10-20 20:26:03 +02:00
|
|
|
'Generator',
|
|
|
|
[
|
2016-11-21 05:31:10 +01:00
|
|
|
$key_type ?: Type::getMixed(),
|
2017-05-27 02:05:57 +02:00
|
|
|
$value_type ?: Type::getMixed(),
|
2016-10-20 20:26:03 +02:00
|
|
|
]
|
2017-05-27 02:05:57 +02:00
|
|
|
),
|
2016-10-20 20:26:03 +02:00
|
|
|
];
|
|
|
|
}
|
2016-10-20 21:26:02 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
if (!$last_stmt instanceof PhpParser\Node\Stmt\Return_ &&
|
|
|
|
!Checker\ScopeChecker::doesAlwaysReturnOrThrow($stmts) &&
|
2016-11-13 05:59:31 +01:00
|
|
|
!$yield_types &&
|
|
|
|
count($return_types)
|
2016-11-02 07:29:00 +01:00
|
|
|
) {
|
2016-11-13 05:59:31 +01:00
|
|
|
// only add null if we have a return statement elsewhere and it wasn't void
|
|
|
|
foreach ($return_types as $return_type) {
|
2017-01-15 01:06:58 +01:00
|
|
|
if (!$return_type instanceof Atomic\TVoid) {
|
|
|
|
$return_types[] = new Atomic\TNull();
|
2016-11-13 05:59:31 +01:00
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
2016-10-20 20:26:03 +02:00
|
|
|
}
|
2016-06-20 07:05:44 +02:00
|
|
|
}
|
|
|
|
|
2016-06-16 02:16:40 +02:00
|
|
|
return $return_types;
|
2016-04-27 00:18:49 +02:00
|
|
|
}
|
2016-10-20 21:26:02 +02:00
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
/**
|
|
|
|
* @param PhpParser\Node\Expr $stmt
|
2017-05-27 02:16:18 +02:00
|
|
|
*
|
2017-01-19 07:32:35 +01:00
|
|
|
* @return array<int, Atomic>
|
2016-11-02 07:29:00 +01:00
|
|
|
*/
|
2016-10-20 21:26:02 +02:00
|
|
|
protected static function getYieldTypeFromExpression($stmt)
|
|
|
|
{
|
|
|
|
if ($stmt instanceof PhpParser\Node\Expr\Yield_) {
|
|
|
|
$key_type = null;
|
|
|
|
|
|
|
|
if (isset($stmt->key->inferredType)) {
|
|
|
|
$key_type = $stmt->key->inferredType;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (isset($stmt->inferredType)) {
|
2017-01-15 01:06:58 +01:00
|
|
|
$generator_type = new Atomic\TGenericObject(
|
2016-10-20 21:26:02 +02:00
|
|
|
'Generator',
|
|
|
|
[
|
|
|
|
$key_type ?: Type::getInt(),
|
2017-05-27 02:05:57 +02:00
|
|
|
$stmt->inferredType,
|
2016-10-20 21:26:02 +02:00
|
|
|
]
|
|
|
|
);
|
|
|
|
|
|
|
|
return [$generator_type];
|
|
|
|
}
|
2017-05-27 02:05:57 +02:00
|
|
|
|
|
|
|
return [new Atomic\TMixed()];
|
2016-11-02 07:29:00 +01:00
|
|
|
} elseif ($stmt instanceof PhpParser\Node\Expr\YieldFrom) {
|
2016-10-20 21:26:02 +02:00
|
|
|
$key_type = null;
|
|
|
|
|
|
|
|
if (isset($stmt->inferredType)) {
|
2017-01-19 07:32:35 +01:00
|
|
|
return array_values($stmt->inferredType->types);
|
2016-10-20 21:26:02 +02:00
|
|
|
}
|
2017-05-27 02:05:57 +02:00
|
|
|
|
|
|
|
return [new Atomic\TMixed()];
|
2016-10-20 21:26:02 +02:00
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
|
2017-01-19 07:32:35 +01:00
|
|
|
return [];
|
2016-10-20 21:26:02 +02:00
|
|
|
}
|
2016-04-27 00:18:49 +02:00
|
|
|
}
|