expr, $context) === false) { return false; } $foreach_context = clone $context; $project_checker = $statements_checker->getFileChecker()->project_checker; if ($project_checker->alter_code) { $foreach_context->branch_point = $foreach_context->branch_point ?: (int) $stmt->getAttribute('startFilePos'); } $key_type = null; $value_type = null; $var_id = ExpressionChecker::getVarId( $stmt->expr, $statements_checker->getFQCLN(), $statements_checker ); if (isset($stmt->expr->inferredType)) { /** @var Type\Union */ $iterator_type = $stmt->expr->inferredType; } elseif ($var_id && $foreach_context->hasVariable($var_id)) { $iterator_type = $foreach_context->vars_in_scope[$var_id]; } else { $iterator_type = null; } if ($iterator_type) { 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; } } foreach ($iterator_type->getTypes() as $iterator_type) { // if it's an empty array, we cannot iterate over it if ($iterator_type instanceof Type\Atomic\TArray && $iterator_type->type_params[1]->isEmpty() ) { continue; } if ($iterator_type instanceof Type\Atomic\TNull) { continue; } if ($iterator_type instanceof Type\Atomic\TArray || $iterator_type instanceof Type\Atomic\ObjectLike ) { if ($iterator_type instanceof Type\Atomic\ObjectLike) { $iterator_type = $iterator_type->getGenericArrayType(); } if (!$value_type) { $value_type = $iterator_type->type_params[1]; } else { $value_type = Type::combineUnionTypes($value_type, $iterator_type->type_params[1]); } $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 ($iterator_type instanceof Type\Atomic\Scalar || $iterator_type instanceof Type\Atomic\TVoid ) { if (IssueBuffer::accepts( new InvalidIterator( 'Cannot iterate over ' . $iterator_type->getKey(), new CodeLocation($statements_checker->getSource(), $stmt->expr) ), $statements_checker->getSuppressedIssues() )) { return false; } $value_type = Type::getMixed(); } elseif ($iterator_type instanceof Type\Atomic\TObject || $iterator_type instanceof Type\Atomic\TMixed || $iterator_type instanceof Type\Atomic\TEmpty ) { $value_type = Type::getMixed(); } elseif ($iterator_type instanceof Type\Atomic\TNamedObject) { if ($iterator_type->value !== 'Traversable' && $iterator_type->value !== $statements_checker->getClassName() ) { if (ClassLikeChecker::checkFullyQualifiedClassLikeName( $statements_checker, $iterator_type->value, new CodeLocation($statements_checker->getSource(), $stmt->expr), $statements_checker->getSuppressedIssues() ) === false) { return false; } } if ($iterator_type instanceof Type\Atomic\TGenericObject && (strtolower($iterator_type->value) === 'iterable' || strtolower($iterator_type->value) === 'traversable' || ClassChecker::classImplements( $project_checker, $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( $project_checker, $iterator_type->value, 'Iterator' ) || ( InterfaceChecker::interfaceExists($project_checker, $iterator_type->value) && InterfaceChecker::interfaceExtends( $project_checker, $iterator_type->value, 'Iterator' ) ) ) { $iterator_method = $iterator_type->value . '::current'; $self_class = $iterator_type->value; $iterator_class_type = MethodChecker::getMethodReturnType( $project_checker, $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(); } } elseif (ClassChecker::classImplements( $project_checker, $iterator_type->value, 'Traversable' ) || ( InterfaceChecker::interfaceExists($project_checker, $iterator_type->value) && InterfaceChecker::interfaceExtends( $project_checker, $iterator_type->value, 'Traversable' ) ) ) { // @todo try and get value type } elseif (!in_array( strtolower($iterator_type->value), ['iterator', 'iterable', 'traversable'], true )) { 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; } } } } } if ($stmt->keyVar && $stmt->keyVar instanceof PhpParser\Node\Expr\Variable && is_string($stmt->keyVar->name)) { $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; if (!$statements_checker->hasVariable($key_var_id)) { $statements_checker->registerVariable( $key_var_id, new CodeLocation($statements_checker, $stmt->keyVar), $foreach_context->branch_point ); } } 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); } AssignmentChecker::analyze( $statements_checker, $stmt->valueVar, null, $value_type ?: Type::getMixed(), $foreach_context, (string)$stmt->getDocComment() ); $doc_comment_text = (string)$stmt->getDocComment(); if ($doc_comment_text) { /** @var VarDocblockComment|null $var_comment */ $var_comment = null; try { $var_comment = CommentChecker::getTypeFromComment( $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 } } if ($var_comment && $var_comment->var_id) { $comment_type = ExpressionChecker::fleshOutType( $project_checker, $var_comment->type, $context->self, $context->self ); $foreach_context->vars_in_scope[$var_comment->var_id] = $comment_type; } } $loop_scope = new LoopScope($foreach_context, $context); $protected_var_ids = $context->protected_var_ids; if ($var_id) { $protected_var_ids[$var_id] = true; } $loop_scope->protected_var_ids = $protected_var_ids; LoopChecker::analyze($statements_checker, $stmt->stmts, [], [], $loop_scope); $context->vars_possibly_in_scope = array_merge( $foreach_context->vars_possibly_in_scope, $context->vars_possibly_in_scope ); $context->referenced_var_ids = array_merge( $foreach_context->referenced_var_ids, $context->referenced_var_ids ); if ($context->collect_references) { $context->unreferenced_vars = $foreach_context->unreferenced_vars; } return null; } }