2016-10-22 19:23:18 +02:00
|
|
|
|
<?php
|
|
|
|
|
namespace Psalm\Checker\Statements\Block;
|
|
|
|
|
|
|
|
|
|
use PhpParser;
|
|
|
|
|
use Psalm\Checker\ClassLikeChecker;
|
|
|
|
|
use Psalm\Checker\CommentChecker;
|
2018-11-10 22:10:59 +01:00
|
|
|
|
use Psalm\Checker\ScopeChecker;
|
2016-12-04 20:14:00 +01:00
|
|
|
|
use Psalm\Checker\Statements\Expression\AssignmentChecker;
|
2017-05-19 06:48:26 +02:00
|
|
|
|
use Psalm\Checker\Statements\ExpressionChecker;
|
2016-11-02 07:29:00 +01:00
|
|
|
|
use Psalm\Checker\StatementsChecker;
|
2018-09-10 02:10:50 +02:00
|
|
|
|
use Psalm\Checker\TypeChecker;
|
2017-05-19 06:48:26 +02:00
|
|
|
|
use Psalm\CodeLocation;
|
|
|
|
|
use Psalm\Context;
|
2017-11-15 03:43:31 +01:00
|
|
|
|
use Psalm\Exception\DocblockParseException;
|
|
|
|
|
use Psalm\Issue\InvalidDocblock;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
use Psalm\Issue\InvalidIterator;
|
2017-05-22 17:59:58 +02:00
|
|
|
|
use Psalm\Issue\NullIterator;
|
2018-03-21 03:59:22 +01:00
|
|
|
|
use Psalm\Issue\PossiblyFalseIterator;
|
|
|
|
|
use Psalm\Issue\PossiblyInvalidIterator;
|
2017-05-22 17:59:58 +02:00
|
|
|
|
use Psalm\Issue\PossiblyNullIterator;
|
2017-11-15 04:55:48 +01:00
|
|
|
|
use Psalm\Issue\RawObjectIteration;
|
2017-05-25 04:07:49 +02:00
|
|
|
|
use Psalm\IssueBuffer;
|
2017-12-03 00:28:18 +01:00
|
|
|
|
use Psalm\Scope\LoopScope;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
use Psalm\Type;
|
|
|
|
|
|
|
|
|
|
class ForeachChecker
|
|
|
|
|
{
|
|
|
|
|
/**
|
2016-11-02 07:29:00 +01:00
|
|
|
|
* @param StatementsChecker $statements_checker
|
|
|
|
|
* @param PhpParser\Node\Stmt\Foreach_ $stmt
|
|
|
|
|
* @param Context $context
|
2017-05-27 02:16:18 +02:00
|
|
|
|
*
|
2016-11-02 07:29:00 +01:00
|
|
|
|
* @return false|null
|
2016-10-22 19:23:18 +02:00
|
|
|
|
*/
|
2017-01-07 21:09:47 +01:00
|
|
|
|
public static function analyze(
|
2016-11-02 07:29:00 +01:00
|
|
|
|
StatementsChecker $statements_checker,
|
|
|
|
|
PhpParser\Node\Stmt\Foreach_ $stmt,
|
|
|
|
|
Context $context
|
|
|
|
|
) {
|
2017-01-07 21:09:47 +01:00
|
|
|
|
if (ExpressionChecker::analyze($statements_checker, $stmt->expr, $context) === false) {
|
2016-10-22 19:23:18 +02:00
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-21 22:24:20 +01:00
|
|
|
|
$project_checker = $statements_checker->getFileChecker()->project_checker;
|
2018-02-01 06:50:01 +01:00
|
|
|
|
$codebase = $project_checker->codebase;
|
2018-01-21 22:24:20 +01:00
|
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
|
$key_type = null;
|
|
|
|
|
$value_type = null;
|
2018-11-10 22:10:59 +01:00
|
|
|
|
$always_non_empty_array = true;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
|
|
|
|
$var_id = ExpressionChecker::getVarId(
|
|
|
|
|
$stmt->expr,
|
2016-11-08 01:16:51 +01:00
|
|
|
|
$statements_checker->getFQCLN(),
|
2017-01-07 20:35:07 +01:00
|
|
|
|
$statements_checker
|
2016-10-22 19:23:18 +02:00
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (isset($stmt->expr->inferredType)) {
|
|
|
|
|
$iterator_type = $stmt->expr->inferredType;
|
2018-11-10 22:10:59 +01:00
|
|
|
|
} elseif ($var_id && $context->hasVariable($var_id, $statements_checker)) {
|
|
|
|
|
$iterator_type = $context->vars_in_scope[$var_id];
|
2016-11-02 07:29:00 +01:00
|
|
|
|
} else {
|
2016-10-22 19:23:18 +02:00
|
|
|
|
$iterator_type = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($iterator_type) {
|
2017-05-22 17:59:58 +02:00
|
|
|
|
if ($iterator_type->isNull()) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new NullIterator(
|
|
|
|
|
'Cannot iterate over null',
|
|
|
|
|
new CodeLocation($statements_checker->getSource(), $stmt->expr)
|
|
|
|
|
),
|
|
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} elseif ($iterator_type->isNullable() && !$iterator_type->ignore_nullable_issues) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new PossiblyNullIterator(
|
|
|
|
|
'Cannot iterate over nullable var ' . $iterator_type,
|
|
|
|
|
new CodeLocation($statements_checker->getSource(), $stmt->expr)
|
|
|
|
|
),
|
|
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2018-03-17 05:19:55 +01:00
|
|
|
|
} elseif ($iterator_type->isFalsable() && !$iterator_type->ignore_falsable_issues) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
2018-03-21 03:59:22 +01:00
|
|
|
|
new PossiblyFalseIterator(
|
2018-03-17 05:19:55 +01:00
|
|
|
|
'Cannot iterate over falsable var ' . $iterator_type,
|
|
|
|
|
new CodeLocation($statements_checker->getSource(), $stmt->expr)
|
|
|
|
|
),
|
|
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2017-05-22 17:59:58 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-03-21 03:59:22 +01:00
|
|
|
|
$has_valid_iterator = false;
|
|
|
|
|
$invalid_iterator_types = [];
|
|
|
|
|
|
2018-01-09 21:05:48 +01:00
|
|
|
|
foreach ($iterator_type->getTypes() as $iterator_type) {
|
2016-10-22 19:23:18 +02:00
|
|
|
|
// if it's an empty array, we cannot iterate over it
|
2017-12-10 22:17:27 +01:00
|
|
|
|
if ($iterator_type instanceof Type\Atomic\TArray
|
|
|
|
|
&& $iterator_type->type_params[1]->isEmpty()
|
|
|
|
|
) {
|
2018-11-10 22:10:59 +01:00
|
|
|
|
$always_non_empty_array = false;
|
2018-03-21 03:59:22 +01:00
|
|
|
|
$has_valid_iterator = true;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2018-03-17 05:19:55 +01:00
|
|
|
|
if ($iterator_type instanceof Type\Atomic\TNull || $iterator_type instanceof Type\Atomic\TFalse) {
|
2018-11-10 22:10:59 +01:00
|
|
|
|
$always_non_empty_array = false;
|
2017-05-22 17:59:58 +02:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2017-12-19 00:47:17 +01:00
|
|
|
|
if ($iterator_type instanceof Type\Atomic\TArray
|
|
|
|
|
|| $iterator_type instanceof Type\Atomic\ObjectLike
|
|
|
|
|
) {
|
|
|
|
|
if ($iterator_type instanceof Type\Atomic\ObjectLike) {
|
2018-11-10 22:10:59 +01:00
|
|
|
|
if (!$iterator_type->sealed) {
|
|
|
|
|
$always_non_empty_array = false;
|
|
|
|
|
}
|
2017-12-19 00:47:17 +01:00
|
|
|
|
$iterator_type = $iterator_type->getGenericArrayType();
|
2018-11-10 22:10:59 +01:00
|
|
|
|
} elseif (!$iterator_type instanceof Type\Atomic\TNonEmptyArray) {
|
|
|
|
|
$always_non_empty_array = false;
|
2017-12-19 00:47:17 +01:00
|
|
|
|
}
|
|
|
|
|
|
2016-10-22 19:23:18 +02:00
|
|
|
|
if (!$value_type) {
|
2017-02-11 00:12:59 +01:00
|
|
|
|
$value_type = $iterator_type->type_params[1];
|
2016-11-02 07:29:00 +01:00
|
|
|
|
} else {
|
2017-02-11 00:12:59 +01:00
|
|
|
|
$value_type = Type::combineUnionTypes($value_type, $iterator_type->type_params[1]);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
}
|
|
|
|
|
|
2017-02-11 00:12:59 +01:00
|
|
|
|
$key_type_part = $iterator_type->type_params[0];
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
2017-02-11 00:12:59 +01:00
|
|
|
|
if (!$key_type) {
|
|
|
|
|
$key_type = $key_type_part;
|
|
|
|
|
} else {
|
|
|
|
|
$key_type = Type::combineUnionTypes($key_type, $key_type_part);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
}
|
2018-03-21 03:59:22 +01:00
|
|
|
|
|
|
|
|
|
$has_valid_iterator = true;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 22:10:59 +01:00
|
|
|
|
$always_non_empty_array = false;
|
|
|
|
|
|
2017-02-11 00:12:59 +01:00
|
|
|
|
if ($iterator_type instanceof Type\Atomic\Scalar ||
|
|
|
|
|
$iterator_type instanceof Type\Atomic\TVoid
|
2017-01-15 01:06:58 +01:00
|
|
|
|
) {
|
2018-03-21 03:59:22 +01:00
|
|
|
|
$invalid_iterator_types[] = $iterator_type->getKey();
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
2017-01-15 01:06:58 +01:00
|
|
|
|
$value_type = Type::getMixed();
|
2017-08-08 00:38:38 +02:00
|
|
|
|
} elseif ($iterator_type instanceof Type\Atomic\TObject ||
|
2017-02-11 00:12:59 +01:00
|
|
|
|
$iterator_type instanceof Type\Atomic\TMixed ||
|
|
|
|
|
$iterator_type instanceof Type\Atomic\TEmpty
|
2017-01-15 01:06:58 +01:00
|
|
|
|
) {
|
2018-03-21 03:59:22 +01:00
|
|
|
|
$has_valid_iterator = true;
|
2017-01-15 01:06:58 +01:00
|
|
|
|
$value_type = Type::getMixed();
|
2017-02-11 00:12:59 +01:00
|
|
|
|
} elseif ($iterator_type instanceof Type\Atomic\TNamedObject) {
|
|
|
|
|
if ($iterator_type->value !== 'Traversable' &&
|
|
|
|
|
$iterator_type->value !== $statements_checker->getClassName()
|
2017-01-15 01:06:58 +01:00
|
|
|
|
) {
|
|
|
|
|
if (ClassLikeChecker::checkFullyQualifiedClassLikeName(
|
2018-01-02 02:04:03 +01:00
|
|
|
|
$statements_checker,
|
2017-02-11 00:12:59 +01:00
|
|
|
|
$iterator_type->value,
|
2017-01-15 01:06:58 +01:00
|
|
|
|
new CodeLocation($statements_checker->getSource(), $stmt->expr),
|
2016-10-22 19:23:18 +02:00
|
|
|
|
$statements_checker->getSuppressedIssues()
|
2017-01-15 01:06:58 +01:00
|
|
|
|
) === false) {
|
2016-10-22 19:23:18 +02:00
|
|
|
|
return false;
|
|
|
|
|
}
|
2017-01-15 01:06:58 +01:00
|
|
|
|
}
|
2018-11-02 18:08:56 +01:00
|
|
|
|
|
2018-09-10 02:10:50 +02:00
|
|
|
|
if (TypeChecker::isAtomicContainedBy(
|
|
|
|
|
$codebase,
|
|
|
|
|
$iterator_type,
|
|
|
|
|
new Type\Atomic\TNamedObject('iterable')
|
|
|
|
|
)) {
|
|
|
|
|
if ($iterator_type->extra_types) {
|
|
|
|
|
$iterator_type_copy = clone $iterator_type;
|
|
|
|
|
$iterator_type_copy->extra_types = [];
|
|
|
|
|
$iterator_types = [$iterator_type_copy];
|
|
|
|
|
$iterator_types = array_merge($iterator_types, $iterator_type->extra_types);
|
|
|
|
|
} else {
|
|
|
|
|
$iterator_types = [$iterator_type];
|
|
|
|
|
}
|
2016-11-02 07:29:00 +01:00
|
|
|
|
|
2018-09-10 02:10:50 +02:00
|
|
|
|
foreach ($iterator_types as $iterator_type) {
|
2018-11-02 18:08:56 +01:00
|
|
|
|
if ($iterator_type instanceof Type\Atomic\TGenericParam) {
|
|
|
|
|
throw new \UnexpectedValueException('Shouldn’t get a generic param here');
|
|
|
|
|
}
|
|
|
|
|
|
2018-09-10 02:10:50 +02:00
|
|
|
|
$has_valid_iterator = true;
|
2018-03-21 03:59:22 +01:00
|
|
|
|
|
2018-11-02 18:08:56 +01:00
|
|
|
|
if ($iterator_type instanceof Type\Atomic\TGenericObject
|
|
|
|
|
&& (strtolower($iterator_type->value) === 'iterable'
|
|
|
|
|
|| strtolower($iterator_type->value) === 'traversable'
|
|
|
|
|
|| $codebase->classImplements(
|
2018-09-10 02:10:50 +02:00
|
|
|
|
$iterator_type->value,
|
|
|
|
|
'Traversable'
|
|
|
|
|
))
|
|
|
|
|
) {
|
|
|
|
|
$value_index = count($iterator_type->type_params) - 1;
|
|
|
|
|
$value_type_part = $iterator_type->type_params[$value_index];
|
2017-02-11 00:12:59 +01:00
|
|
|
|
|
2018-09-10 02:10:50 +02:00
|
|
|
|
if (!$value_type) {
|
|
|
|
|
$value_type = $value_type_part;
|
|
|
|
|
} else {
|
|
|
|
|
$value_type = Type::combineUnionTypes($value_type, $value_type_part);
|
|
|
|
|
}
|
2017-02-11 00:12:59 +01:00
|
|
|
|
|
2018-09-10 02:10:50 +02:00
|
|
|
|
if ($value_index) {
|
|
|
|
|
$key_type_part = $iterator_type->type_params[0];
|
2017-02-11 00:12:59 +01:00
|
|
|
|
|
2018-09-10 02:10:50 +02:00
|
|
|
|
if (!$key_type) {
|
|
|
|
|
$key_type = $key_type_part;
|
|
|
|
|
} else {
|
|
|
|
|
$key_type = Type::combineUnionTypes($key_type, $key_type_part);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
continue 2;
|
2017-02-11 00:12:59 +01:00
|
|
|
|
}
|
|
|
|
|
|
2018-09-10 02:10:50 +02:00
|
|
|
|
if (!$codebase->classlikes->classOrInterfaceExists($iterator_type->value)) {
|
|
|
|
|
continue 2;
|
|
|
|
|
}
|
2018-03-03 22:52:04 +01:00
|
|
|
|
|
2018-09-10 02:10:50 +02:00
|
|
|
|
if ($codebase->classImplements(
|
2018-04-19 05:47:21 +02:00
|
|
|
|
$iterator_type->value,
|
|
|
|
|
'IteratorAggregate'
|
2018-09-10 02:10:50 +02:00
|
|
|
|
) ||
|
|
|
|
|
(
|
|
|
|
|
$codebase->interfaceExists($iterator_type->value)
|
|
|
|
|
&& $codebase->interfaceExtends(
|
|
|
|
|
$iterator_type->value,
|
|
|
|
|
'IteratorAggregate'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
$iterator_method = $iterator_type->value . '::getIterator';
|
|
|
|
|
$self_class = $iterator_type->value;
|
|
|
|
|
$iterator_class_type = $codebase->methods->getMethodReturnType(
|
|
|
|
|
$iterator_method,
|
|
|
|
|
$self_class
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ($iterator_class_type) {
|
|
|
|
|
$array_type = ExpressionChecker::fleshOutType(
|
|
|
|
|
$project_checker,
|
|
|
|
|
$iterator_class_type,
|
|
|
|
|
$self_class,
|
|
|
|
|
$self_class
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
foreach ($array_type->getTypes() as $array_atomic_type) {
|
|
|
|
|
if ($array_atomic_type instanceof Type\Atomic\TArray
|
|
|
|
|
|| $array_atomic_type instanceof Type\Atomic\ObjectLike
|
|
|
|
|
) {
|
|
|
|
|
if ($array_atomic_type instanceof Type\Atomic\ObjectLike) {
|
|
|
|
|
$array_atomic_type = $array_atomic_type->getGenericArrayType();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$key_type_part = $array_atomic_type->type_params[0];
|
|
|
|
|
$value_type_part = $array_atomic_type->type_params[1];
|
|
|
|
|
} elseif ($array_atomic_type instanceof Type\Atomic\TGenericObject) {
|
|
|
|
|
$type_param_count = count($array_atomic_type->type_params);
|
|
|
|
|
|
|
|
|
|
$value_type_part = $array_atomic_type->type_params[$type_param_count - 1];
|
|
|
|
|
$key_type_part = $type_param_count > 1
|
|
|
|
|
? $array_atomic_type->type_params[0]
|
|
|
|
|
: Type::getMixed();
|
|
|
|
|
} else {
|
|
|
|
|
$key_type = Type::getMixed();
|
|
|
|
|
$value_type = Type::getMixed();
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$key_type) {
|
|
|
|
|
$key_type = $key_type_part;
|
|
|
|
|
} else {
|
|
|
|
|
$key_type = Type::combineUnionTypes($key_type, $key_type_part);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!$value_type) {
|
|
|
|
|
$value_type = $value_type_part;
|
|
|
|
|
} else {
|
|
|
|
|
$value_type = Type::combineUnionTypes($value_type, $value_type_part);
|
|
|
|
|
}
|
2018-04-19 05:47:21 +02:00
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$value_type = Type::getMixed();
|
|
|
|
|
}
|
2018-09-10 02:10:50 +02:00
|
|
|
|
} elseif ($codebase->classImplements(
|
2017-11-24 18:57:00 +01:00
|
|
|
|
$iterator_type->value,
|
|
|
|
|
'Iterator'
|
2018-09-10 02:10:50 +02:00
|
|
|
|
) ||
|
|
|
|
|
(
|
|
|
|
|
$codebase->interfaceExists($iterator_type->value)
|
|
|
|
|
&& $codebase->interfaceExtends(
|
|
|
|
|
$iterator_type->value,
|
|
|
|
|
'Iterator'
|
|
|
|
|
)
|
|
|
|
|
)
|
|
|
|
|
) {
|
|
|
|
|
$iterator_method = $iterator_type->value . '::current';
|
|
|
|
|
$self_class = $iterator_type->value;
|
|
|
|
|
$iterator_class_type = $codebase->methods->getMethodReturnType(
|
|
|
|
|
$iterator_method,
|
|
|
|
|
$self_class
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if ($iterator_class_type) {
|
|
|
|
|
$value_type_part = ExpressionChecker::fleshOutType(
|
|
|
|
|
$project_checker,
|
|
|
|
|
$iterator_class_type,
|
|
|
|
|
$self_class,
|
|
|
|
|
$self_class
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
if (!$value_type) {
|
|
|
|
|
$value_type = $value_type_part;
|
|
|
|
|
} else {
|
|
|
|
|
$value_type = Type::combineUnionTypes($value_type, $value_type_part);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$value_type = Type::getMixed();
|
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-09-10 02:10:50 +02:00
|
|
|
|
} else {
|
2017-11-15 04:55:48 +01:00
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new RawObjectIteration(
|
|
|
|
|
'Possibly undesired iteration over regular object ' . $iterator_type->value,
|
|
|
|
|
new CodeLocation($statements_checker->getSource(), $stmt->expr)
|
|
|
|
|
),
|
|
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2017-01-15 01:06:58 +01:00
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
}
|
|
|
|
|
}
|
2018-03-21 03:59:22 +01:00
|
|
|
|
|
|
|
|
|
if ($invalid_iterator_types) {
|
|
|
|
|
if ($has_valid_iterator) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new PossiblyInvalidIterator(
|
|
|
|
|
'Cannot iterate over ' . $invalid_iterator_types[0],
|
|
|
|
|
new CodeLocation($statements_checker->getSource(), $stmt->expr)
|
|
|
|
|
),
|
|
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new InvalidIterator(
|
|
|
|
|
'Cannot iterate over ' . $invalid_iterator_types[0],
|
|
|
|
|
new CodeLocation($statements_checker->getSource(), $stmt->expr)
|
|
|
|
|
),
|
|
|
|
|
$statements_checker->getSuppressedIssues()
|
|
|
|
|
)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-11-10 22:10:59 +01:00
|
|
|
|
$foreach_context = clone $context;
|
|
|
|
|
|
|
|
|
|
$foreach_context->inside_loop = true;
|
|
|
|
|
|
|
|
|
|
if ($project_checker->alter_code) {
|
|
|
|
|
$foreach_context->branch_point =
|
|
|
|
|
$foreach_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
|
|
|
|
|
}
|
|
|
|
|
|
2016-12-29 06:32:12 +01:00
|
|
|
|
if ($stmt->keyVar && $stmt->keyVar instanceof PhpParser\Node\Expr\Variable && is_string($stmt->keyVar->name)) {
|
2017-02-08 00:09:12 +01:00
|
|
|
|
$key_var_id = '$' . $stmt->keyVar->name;
|
|
|
|
|
$foreach_context->vars_in_scope[$key_var_id] = $key_type ?: Type::getMixed();
|
|
|
|
|
$foreach_context->vars_possibly_in_scope[$key_var_id] = true;
|
|
|
|
|
|
2018-01-28 23:28:34 +01:00
|
|
|
|
$location = new CodeLocation($statements_checker, $stmt->keyVar);
|
|
|
|
|
|
|
|
|
|
if ($context->collect_references && !isset($foreach_context->byref_constraints[$key_var_id])) {
|
2018-06-17 02:01:33 +02:00
|
|
|
|
$foreach_context->unreferenced_vars[$key_var_id] = [$location->getHash() => $location];
|
2018-01-28 23:28:34 +01:00
|
|
|
|
}
|
|
|
|
|
|
2017-02-08 00:09:12 +01:00
|
|
|
|
if (!$statements_checker->hasVariable($key_var_id)) {
|
|
|
|
|
$statements_checker->registerVariable(
|
|
|
|
|
$key_var_id,
|
2018-01-28 23:28:34 +01:00
|
|
|
|
$location,
|
2018-01-21 22:24:20 +01:00
|
|
|
|
$foreach_context->branch_point
|
2017-02-08 00:09:12 +01:00
|
|
|
|
);
|
2018-06-17 06:52:32 +02:00
|
|
|
|
} else {
|
|
|
|
|
$statements_checker->registerVariableAssignment(
|
|
|
|
|
$key_var_id,
|
|
|
|
|
$location
|
|
|
|
|
);
|
2017-02-08 00:09:12 +01:00
|
|
|
|
}
|
2018-01-28 23:28:34 +01:00
|
|
|
|
|
2018-06-17 02:01:33 +02:00
|
|
|
|
if ($stmt->byRef && $context->collect_references) {
|
|
|
|
|
$statements_checker->registerVariableUses([$location->getHash() => $location]);
|
2018-01-28 23:28:34 +01:00
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
}
|
|
|
|
|
|
2018-01-25 07:04:26 +01:00
|
|
|
|
if ($context->collect_references
|
|
|
|
|
&& $stmt->byRef
|
|
|
|
|
&& $stmt->valueVar instanceof PhpParser\Node\Expr\Variable
|
|
|
|
|
&& is_string($stmt->valueVar->name)
|
|
|
|
|
) {
|
|
|
|
|
$foreach_context->byref_constraints['$' . $stmt->valueVar->name]
|
|
|
|
|
= new \Psalm\ReferenceConstraint($value_type);
|
|
|
|
|
}
|
|
|
|
|
|
2017-01-07 21:09:47 +01:00
|
|
|
|
AssignmentChecker::analyze(
|
2016-12-04 20:14:00 +01:00
|
|
|
|
$statements_checker,
|
|
|
|
|
$stmt->valueVar,
|
|
|
|
|
null,
|
|
|
|
|
$value_type ?: Type::getMixed(),
|
|
|
|
|
$foreach_context,
|
|
|
|
|
(string)$stmt->getDocComment()
|
|
|
|
|
);
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
2017-03-02 04:27:52 +01:00
|
|
|
|
$doc_comment_text = (string)$stmt->getDocComment();
|
|
|
|
|
|
|
|
|
|
if ($doc_comment_text) {
|
2018-02-08 05:33:31 +01:00
|
|
|
|
$var_comments = [];
|
2017-12-04 14:50:59 +01:00
|
|
|
|
|
2017-11-15 03:43:31 +01:00
|
|
|
|
try {
|
2018-02-08 05:33:31 +01:00
|
|
|
|
$var_comments = CommentChecker::getTypeFromComment(
|
2017-11-15 03:43:31 +01:00
|
|
|
|
$doc_comment_text,
|
|
|
|
|
$statements_checker->getSource(),
|
|
|
|
|
$statements_checker->getSource()->getAliases()
|
|
|
|
|
);
|
|
|
|
|
} catch (DocblockParseException $e) {
|
|
|
|
|
if (IssueBuffer::accepts(
|
|
|
|
|
new InvalidDocblock(
|
|
|
|
|
(string)$e->getMessage(),
|
|
|
|
|
new CodeLocation($statements_checker, $stmt)
|
|
|
|
|
)
|
|
|
|
|
)) {
|
|
|
|
|
// fall through
|
|
|
|
|
}
|
|
|
|
|
}
|
2017-05-25 07:32:34 +02:00
|
|
|
|
|
2018-02-08 05:33:31 +01:00
|
|
|
|
foreach ($var_comments as $var_comment) {
|
|
|
|
|
if (!$var_comment->var_id) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
2017-10-07 16:22:52 +02:00
|
|
|
|
$comment_type = ExpressionChecker::fleshOutType(
|
|
|
|
|
$project_checker,
|
2018-01-02 02:04:03 +01:00
|
|
|
|
$var_comment->type,
|
2018-01-26 19:51:00 +01:00
|
|
|
|
$context->self,
|
2017-10-07 16:22:52 +02:00
|
|
|
|
$context->self
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
$foreach_context->vars_in_scope[$var_comment->var_id] = $comment_type;
|
2017-05-25 07:32:34 +02:00
|
|
|
|
}
|
2017-03-02 04:27:52 +01:00
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
2017-12-03 00:28:18 +01:00
|
|
|
|
$loop_scope = new LoopScope($foreach_context, $context);
|
|
|
|
|
|
2017-12-17 16:58:03 +01:00
|
|
|
|
$protected_var_ids = $context->protected_var_ids;
|
|
|
|
|
if ($var_id) {
|
|
|
|
|
$protected_var_ids[$var_id] = true;
|
|
|
|
|
}
|
|
|
|
|
$loop_scope->protected_var_ids = $protected_var_ids;
|
|
|
|
|
|
2018-11-10 22:10:59 +01:00
|
|
|
|
LoopChecker::analyze($statements_checker, $stmt->stmts, [], [], $loop_scope, $inner_loop_context);
|
|
|
|
|
|
|
|
|
|
if (!$inner_loop_context) {
|
|
|
|
|
throw new \UnexpectedValueException('There should be an inner loop context');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($always_non_empty_array) {
|
|
|
|
|
foreach ($inner_loop_context->vars_in_scope as $var_id => $type) {
|
|
|
|
|
// if there are break statements in the loop it's not certain
|
|
|
|
|
// that the loop has finished executing, so the assertions at the end
|
|
|
|
|
// the loop in the while conditional may not hold
|
|
|
|
|
if (in_array(ScopeChecker::ACTION_BREAK, $loop_scope->final_actions, true)
|
|
|
|
|
|| in_array(ScopeChecker::ACTION_CONTINUE, $loop_scope->final_actions, true)
|
|
|
|
|
) {
|
|
|
|
|
if (isset($loop_scope->possibly_defined_loop_parent_vars[$var_id])) {
|
|
|
|
|
$context->vars_in_scope[$var_id] = Type::combineUnionTypes(
|
|
|
|
|
$type,
|
|
|
|
|
$loop_scope->possibly_defined_loop_parent_vars[$var_id]
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
$context->vars_in_scope[$var_id] = $type;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2016-10-22 19:23:18 +02:00
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
|
$context->vars_possibly_in_scope = array_merge(
|
|
|
|
|
$foreach_context->vars_possibly_in_scope,
|
|
|
|
|
$context->vars_possibly_in_scope
|
|
|
|
|
);
|
|
|
|
|
|
2017-11-24 18:17:28 +01:00
|
|
|
|
$context->referenced_var_ids = array_merge(
|
|
|
|
|
$foreach_context->referenced_var_ids,
|
|
|
|
|
$context->referenced_var_ids
|
|
|
|
|
);
|
2017-02-01 05:24:33 +01:00
|
|
|
|
|
2018-06-22 07:13:49 +02:00
|
|
|
|
if ($context->collect_exceptions) {
|
|
|
|
|
$context->possibly_thrown_exceptions += $foreach_context->possibly_thrown_exceptions;
|
|
|
|
|
}
|
|
|
|
|
|
2018-01-25 07:04:26 +01:00
|
|
|
|
if ($context->collect_references) {
|
2018-06-17 03:14:19 +02:00
|
|
|
|
foreach ($foreach_context->unreferenced_vars as $var_id => $locations) {
|
|
|
|
|
if (isset($context->unreferenced_vars[$var_id])) {
|
|
|
|
|
$context->unreferenced_vars[$var_id] += $locations;
|
|
|
|
|
} else {
|
|
|
|
|
$context->unreferenced_vars[$var_id] = $locations;
|
|
|
|
|
}
|
|
|
|
|
}
|
2018-01-25 07:04:26 +01:00
|
|
|
|
}
|
|
|
|
|
|
2016-11-02 07:29:00 +01:00
|
|
|
|
return null;
|
2016-10-22 19:23:18 +02:00
|
|
|
|
}
|
|
|
|
|
}
|