1
0
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:
Matt Brown 2017-03-13 18:06:56 -04:00
parent 6f00d05271
commit 3d2be3410e
12 changed files with 213 additions and 38 deletions

View File

@ -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) {

View File

@ -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()) {

View File

@ -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);
}
}

View File

@ -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;
}

View File

@ -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
) {

View File

@ -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;
}

View File

@ -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

View File

@ -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;
}
}

View File

@ -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];

View File

@ -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;
}
}

View File

@ -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;
}

View File

@ -33,6 +33,11 @@ class Union
*/
protected $checked = false;
/**
* @var boolean
*/
public $failed_reconciliation = false;
/**
* Constructs an Union instance
* @param array<int, Atomic> $types