mirror of
https://github.com/danog/psalm.git
synced 2024-11-27 04:45:20 +01:00
Infer loop types without @var comments
This commit is contained in:
parent
6f00d05271
commit
3d2be3410e
@ -37,7 +37,7 @@ class ForChecker
|
||||
$for_context->inside_conditional = false;
|
||||
}
|
||||
|
||||
$statements_checker->analyze($stmt->stmts, $for_context, $context);
|
||||
$statements_checker->analyzeLoop($stmt->stmts, $for_context, $context);
|
||||
|
||||
foreach ($stmt->loop as $expr) {
|
||||
if (ExpressionChecker::analyze($statements_checker, $expr, $for_context) === false) {
|
||||
|
@ -202,7 +202,7 @@ class ForeachChecker
|
||||
);
|
||||
}
|
||||
|
||||
$statements_checker->analyze($stmt->stmts, $foreach_context, $context);
|
||||
$statements_checker->analyzeLoop($stmt->stmts, $foreach_context, $context);
|
||||
|
||||
foreach ($context->vars_in_scope as $var => $type) {
|
||||
if ($type->isMixed()) {
|
||||
|
@ -242,7 +242,10 @@ class IfChecker
|
||||
|
||||
if ($if_scope->possibly_redefined_vars) {
|
||||
foreach ($if_scope->possibly_redefined_vars as $var => $type) {
|
||||
if ($context->hasVariable($var) && !isset($if_scope->updated_vars[$var])) {
|
||||
if (!$type->failed_reconciliation &&
|
||||
$context->hasVariable($var) &&
|
||||
!isset($if_scope->updated_vars[$var])
|
||||
) {
|
||||
$context->vars_in_scope[$var] = Type::combineUnionTypes($context->vars_in_scope[$var], $type);
|
||||
}
|
||||
}
|
||||
|
@ -67,7 +67,7 @@ class WhileChecker
|
||||
$while_context->vars_in_scope = $while_vars_in_scope_reconciled;
|
||||
}
|
||||
|
||||
if ($statements_checker->analyze($stmt->stmts, $while_context, $context) === false) {
|
||||
if ($statements_checker->analyzeLoop($stmt->stmts, $while_context, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -993,7 +993,7 @@ class FetchChecker
|
||||
|
||||
$array_type = $context_type;
|
||||
|
||||
for ($i = 0; $i < $nesting + 1; $i++) {
|
||||
for ($i = 0; $i < (int)$nesting + 1; $i++) {
|
||||
if (isset($array_type->types['array']) &&
|
||||
$array_type->types['array'] instanceof Type\Atomic\TArray
|
||||
) {
|
||||
|
@ -608,14 +608,8 @@ class ExpressionChecker
|
||||
}
|
||||
} else {
|
||||
$existing_type = $context->vars_in_scope[$var_id];
|
||||
if (TypeChecker::isContainedBy(
|
||||
$existing_type,
|
||||
$by_ref_type,
|
||||
$statements_checker->getFileChecker()
|
||||
) &&
|
||||
!$existing_type->isNull() &&
|
||||
(string)$existing_type !== 'array<empty, empty>'
|
||||
) {
|
||||
if ((string)$existing_type !== 'array<empty, empty>') {
|
||||
$context->vars_in_scope[$var_id] = $by_ref_type;
|
||||
$stmt->inferredType = $context->vars_in_scope[$var_id];
|
||||
return;
|
||||
}
|
||||
|
@ -114,8 +114,8 @@ class StatementsChecker extends SourceChecker implements StatementsSource
|
||||
}
|
||||
|
||||
/*
|
||||
if (isset($context->vars_in_scope['$storage'])) {
|
||||
var_dump($stmt->getLine() . ' ' . $context->vars_in_scope['$storage'], $context->referenced_vars);
|
||||
if (isset($context->vars_in_scope['$failed_reconciliation']) && !$stmt instanceof PhpParser\Node\Stmt\Nop) {
|
||||
var_dump($stmt->getLine() . ' ' . $context->vars_in_scope['$failed_reconciliation']);
|
||||
}
|
||||
*/
|
||||
|
||||
@ -325,6 +325,72 @@ class StatementsChecker extends SourceChecker implements StatementsSource
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks an array of statements in a loop
|
||||
*
|
||||
* @param array<PhpParser\Node\Stmt|PhpParser\Node\Expr> $stmts
|
||||
* @param Context $loop_context
|
||||
* @param Context $outer_context
|
||||
* @return void
|
||||
*/
|
||||
public function analyzeLoop(
|
||||
array $stmts,
|
||||
Context $loop_context,
|
||||
Context $outer_context
|
||||
) {
|
||||
// record all the vars that existed before we did the first pass through the loop
|
||||
$pre_loop_vars_in_scope = $loop_context->vars_in_scope;
|
||||
|
||||
IssueBuffer::startRecording();
|
||||
$this->analyze($stmts, $loop_context, $outer_context);
|
||||
$recorded_issues = IssueBuffer::clearRecordingLevel();
|
||||
IssueBuffer::stopRecording();
|
||||
|
||||
if ($recorded_issues) {
|
||||
do {
|
||||
$vars_to_remove = [];
|
||||
|
||||
// widen the foreach context type with the initial context type
|
||||
foreach ($loop_context->vars_in_scope as $var_id => $type) {
|
||||
if (isset($pre_loop_vars_in_scope[$var_id])) {
|
||||
$loop_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$type,
|
||||
$pre_loop_vars_in_scope[$var_id]
|
||||
);
|
||||
|
||||
if (isset($outer_context->vars_in_scope[$var_id])) {
|
||||
$loop_context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
||||
$loop_context->vars_in_scope[$var_id],
|
||||
$outer_context->vars_in_scope[$var_id]
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$vars_to_remove[] = $var_id;
|
||||
}
|
||||
}
|
||||
|
||||
// remove vars that were defined in the foreach
|
||||
foreach ($vars_to_remove as $var_id) {
|
||||
unset($loop_context->vars_in_scope[$var_id]);
|
||||
}
|
||||
|
||||
IssueBuffer::startRecording();
|
||||
$this->analyze($stmts, $loop_context, $outer_context);
|
||||
$last_recorded_issues_count = count($recorded_issues);
|
||||
$recorded_issues = IssueBuffer::clearRecordingLevel();
|
||||
|
||||
IssueBuffer::stopRecording();
|
||||
} while (count($recorded_issues) < $last_recorded_issues_count);
|
||||
|
||||
if ($recorded_issues) {
|
||||
foreach ($recorded_issues as $recorded_issue) {
|
||||
// if we're not in any loops then this will just result in the issue being emitted
|
||||
IssueBuffer::bubbleUp($recorded_issue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param PhpParser\Node\Stmt\Static_ $stmt
|
||||
* @param Context $context
|
||||
@ -408,7 +474,7 @@ class StatementsChecker extends SourceChecker implements StatementsSource
|
||||
{
|
||||
$do_context = clone $context;
|
||||
|
||||
if ($this->analyze($stmt->stmts, $do_context, $context) === false) {
|
||||
if ($this->analyzeLoop($stmt->stmts, $do_context, $context) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@ -428,6 +494,12 @@ class StatementsChecker extends SourceChecker implements StatementsSource
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($do_context->vars_in_scope as $var_id => $type) {
|
||||
if (!isset($context->vars_in_scope[$var_id])) {
|
||||
$context->vars_in_scope[$var_id] = $type;
|
||||
}
|
||||
}
|
||||
|
||||
$context->vars_possibly_in_scope = array_merge(
|
||||
$context->vars_possibly_in_scope,
|
||||
$do_context->vars_possibly_in_scope
|
||||
|
@ -96,6 +96,7 @@ class TypeChecker
|
||||
$right_clauses = [new Clause([], true)];
|
||||
}
|
||||
|
||||
/** @var array<string, array<string>> */
|
||||
$possibilities = [];
|
||||
|
||||
if ($left_clauses[0]->wedge && $right_clauses[0]->wedge) {
|
||||
@ -482,6 +483,8 @@ class TypeChecker
|
||||
|
||||
$before_adjustment = (string)$result_type;
|
||||
|
||||
$failed_reconciliation = false;
|
||||
|
||||
foreach ($new_type_parts as $new_type_part) {
|
||||
$result_type = self::reconcileTypes(
|
||||
(string) $new_type_part,
|
||||
@ -489,7 +492,8 @@ class TypeChecker
|
||||
$key,
|
||||
$file_checker,
|
||||
$code_location,
|
||||
$suppressed_issues
|
||||
$suppressed_issues,
|
||||
$failed_reconciliation
|
||||
);
|
||||
|
||||
// special case if result is just a simple array
|
||||
@ -510,6 +514,10 @@ class TypeChecker
|
||||
$changed_types[] = $key;
|
||||
}
|
||||
|
||||
if ($failed_reconciliation) {
|
||||
$result_type->failed_reconciliation = true;
|
||||
}
|
||||
|
||||
$existing_types[$key] = $result_type;
|
||||
}
|
||||
|
||||
@ -531,6 +539,7 @@ class TypeChecker
|
||||
* @param FileChecker $file_checker
|
||||
* @param CodeLocation $code_location
|
||||
* @param array $suppressed_issues
|
||||
* @param bool $failed_reconciliation if the types cannot be reconciled, we need to know
|
||||
* @return Type\Union|null|false
|
||||
*/
|
||||
public static function reconcileTypes(
|
||||
@ -539,7 +548,8 @@ class TypeChecker
|
||||
$key,
|
||||
FileChecker $file_checker,
|
||||
CodeLocation $code_location = null,
|
||||
array $suppressed_issues = []
|
||||
array $suppressed_issues = [],
|
||||
&$failed_reconciliation = false
|
||||
) {
|
||||
if ($existing_var_type === null) {
|
||||
if ($new_var_type === '^isset') {
|
||||
@ -595,6 +605,8 @@ class TypeChecker
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
$failed_reconciliation = true;
|
||||
}
|
||||
|
||||
return Type::getMixed();
|
||||
@ -614,8 +626,17 @@ class TypeChecker
|
||||
}
|
||||
|
||||
if (empty($existing_var_type->types)) {
|
||||
// @todo - I think there's a better way to handle this, but for the moment
|
||||
// mixed will have to do.
|
||||
if ($key && $code_location) {
|
||||
if (IssueBuffer::accepts(
|
||||
new FailedTypeResolution('Cannot resolve types for ' . $key, $code_location),
|
||||
$suppressed_issues
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
}
|
||||
|
||||
$failed_reconciliation = true;
|
||||
|
||||
return Type::getMixed();
|
||||
}
|
||||
|
||||
@ -638,6 +659,8 @@ class TypeChecker
|
||||
}
|
||||
}
|
||||
|
||||
$failed_reconciliation = true;
|
||||
|
||||
return Type::getMixed();
|
||||
}
|
||||
|
||||
@ -648,6 +671,8 @@ class TypeChecker
|
||||
$existing_var_type->removeType('null');
|
||||
|
||||
if (empty($existing_var_type->types)) {
|
||||
$failed_reconciliation = true;
|
||||
|
||||
// @todo - I think there's a better way to handle this, but for the moment
|
||||
// mixed will have to do.
|
||||
return Type::getMixed();
|
||||
@ -746,6 +771,8 @@ class TypeChecker
|
||||
)) {
|
||||
// fall through
|
||||
}
|
||||
|
||||
$failed_reconciliation = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -18,11 +18,6 @@ class Context
|
||||
*/
|
||||
public $inside_loop = false;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
public $has_loop_issues = false;
|
||||
|
||||
/**
|
||||
* Whether or not we're inside the conditional of an if/where etc.
|
||||
*
|
||||
@ -147,8 +142,6 @@ class Context
|
||||
foreach ($this->constants as &$constant) {
|
||||
$constant = clone $constant;
|
||||
}
|
||||
|
||||
$this->has_loop_issues = false;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -203,6 +196,7 @@ class Context
|
||||
|
||||
foreach ($original_context->vars_in_scope as $var => $context_type) {
|
||||
if (isset($new_context->vars_in_scope[$var]) &&
|
||||
!$new_context->vars_in_scope[$var]->failed_reconciliation &&
|
||||
(string)$new_context->vars_in_scope[$var] !== (string)$context_type
|
||||
) {
|
||||
$redefined_vars[$var] = $new_context->vars_in_scope[$var];
|
||||
|
@ -2,6 +2,7 @@
|
||||
namespace Psalm;
|
||||
|
||||
use Psalm\Checker\ProjectChecker;
|
||||
use Psalm\Issue\CodeIssue;
|
||||
|
||||
class IssueBuffer
|
||||
{
|
||||
@ -20,17 +21,23 @@ class IssueBuffer
|
||||
*/
|
||||
protected static $emitted = [];
|
||||
|
||||
/** @var int */
|
||||
protected static $recording_level = 0;
|
||||
|
||||
/** @var array<int, array<int, CodeIssue>> */
|
||||
protected static $recorded_issues = [];
|
||||
|
||||
/**
|
||||
* @var int
|
||||
*/
|
||||
protected static $start_time = 0;
|
||||
|
||||
/**
|
||||
* @param Issue\CodeIssue $e
|
||||
* @param array $suppressed_issues
|
||||
* @param CodeIssue $e
|
||||
* @param array $suppressed_issues
|
||||
* @return bool
|
||||
*/
|
||||
public static function accepts(Issue\CodeIssue $e, array $suppressed_issues = [])
|
||||
public static function accepts(CodeIssue $e, array $suppressed_issues = [])
|
||||
{
|
||||
$config = Config::getInstance();
|
||||
|
||||
@ -45,15 +52,20 @@ class IssueBuffer
|
||||
return false;
|
||||
}
|
||||
|
||||
if (self::$recording_level > 0) {
|
||||
self::$recorded_issues[self::$recording_level][] = $e;
|
||||
return false;
|
||||
}
|
||||
|
||||
return self::add($e);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Issue\CodeIssue $e
|
||||
* @param CodeIssue $e
|
||||
* @return bool
|
||||
* @throws Exception\CodeException
|
||||
*/
|
||||
public static function add(Issue\CodeIssue $e)
|
||||
public static function add(CodeIssue $e)
|
||||
{
|
||||
$config = Config::getInstance();
|
||||
$project_checker = ProjectChecker::getInstance();
|
||||
@ -121,11 +133,11 @@ class IssueBuffer
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Issue\CodeIssue $e
|
||||
* @param CodeIssue $e
|
||||
* @param string $severity
|
||||
* @return array
|
||||
*/
|
||||
protected static function getIssueArray(Issue\CodeIssue $e, $severity = Config::REPORT_ERROR)
|
||||
protected static function getIssueArray(CodeIssue $e, $severity = Config::REPORT_ERROR)
|
||||
{
|
||||
$location = $e->getLocation();
|
||||
$selection_bounds = $location->getSelectionBounds();
|
||||
@ -143,11 +155,11 @@ class IssueBuffer
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Issue\CodeIssue $e
|
||||
* @param CodeIssue $e
|
||||
* @param string $severity
|
||||
* @return string
|
||||
*/
|
||||
protected static function getEmacsOutput(Issue\CodeIssue $e, $severity = Config::REPORT_ERROR)
|
||||
protected static function getEmacsOutput(CodeIssue $e, $severity = Config::REPORT_ERROR)
|
||||
{
|
||||
$location = $e->getLocation();
|
||||
|
||||
@ -164,11 +176,11 @@ class IssueBuffer
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Issue\CodeIssue $e
|
||||
* @param CodeIssue $e
|
||||
* @param boolean $use_color
|
||||
* @return string
|
||||
*/
|
||||
protected static function getSnippet(Issue\CodeIssue $e, $use_color = true)
|
||||
protected static function getSnippet(CodeIssue $e, $use_color = true)
|
||||
{
|
||||
$location = $e->getLocation();
|
||||
|
||||
@ -252,5 +264,57 @@ class IssueBuffer
|
||||
self::$issue_data = [];
|
||||
self::$emitted = [];
|
||||
self::$error_count = 0;
|
||||
self::$recording_level = 0;
|
||||
self::$recorded_issues = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public static function startRecording()
|
||||
{
|
||||
self::$recording_level++;
|
||||
self::$recorded_issues[self::$recording_level] = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public static function stopRecording()
|
||||
{
|
||||
if (self::$recording_level === 0) {
|
||||
throw new \UnexpectedValueException('Cannot stop recording - already at base level');
|
||||
}
|
||||
|
||||
self::$recording_level--;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int, CodeIssue>
|
||||
*/
|
||||
public static function clearRecordingLevel()
|
||||
{
|
||||
if (self::$recording_level === 0) {
|
||||
throw new \UnexpectedValueException('Not currently recording');
|
||||
}
|
||||
|
||||
$recorded_issues = self::$recorded_issues[self::$recording_level];
|
||||
|
||||
self::$recorded_issues[self::$recording_level] = [];
|
||||
|
||||
return $recorded_issues;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return void
|
||||
*/
|
||||
public static function bubbleUp(CodeIssue $e)
|
||||
{
|
||||
if (self::$recording_level === 0) {
|
||||
self::add($e);
|
||||
return;
|
||||
}
|
||||
|
||||
self::$recorded_issues[self::$recording_level][] = $e;
|
||||
}
|
||||
}
|
||||
|
@ -449,12 +449,28 @@ abstract class Type
|
||||
*/
|
||||
public static function combineUnionTypes(Union $type_1, Union $type_2)
|
||||
{
|
||||
$both_failed_reconciliation = false;
|
||||
|
||||
if ($type_1->failed_reconciliation) {
|
||||
if ($type_2->failed_reconciliation) {
|
||||
$both_failed_reconciliation = true;
|
||||
} else {
|
||||
return $type_2;
|
||||
}
|
||||
} elseif ($type_2->failed_reconciliation) {
|
||||
return $type_1;
|
||||
}
|
||||
|
||||
$combined_type = self::combineTypes(array_merge(array_values($type_1->types), array_values($type_2->types)));
|
||||
|
||||
if (!$type_1->initialized || !$type_2->initialized) {
|
||||
$combined_type->initialized = false;
|
||||
}
|
||||
|
||||
if ($both_failed_reconciliation) {
|
||||
$combined_type->failed_reconciliation = true;
|
||||
}
|
||||
|
||||
return $combined_type;
|
||||
}
|
||||
|
||||
|
@ -33,6 +33,11 @@ class Union
|
||||
*/
|
||||
protected $checked = false;
|
||||
|
||||
/**
|
||||
* @var boolean
|
||||
*/
|
||||
public $failed_reconciliation = false;
|
||||
|
||||
/**
|
||||
* Constructs an Union instance
|
||||
* @param array<int, Atomic> $types
|
||||
|
Loading…
Reference in New Issue
Block a user