WhileAnalyzer::getAndExpressions()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
nc 2
nop 1
dl 0
loc 12
rs 9.8666
c 0
b 0
f 0
1
<?php
2
namespace Psalm\Internal\Analyzer\Statements\Block;
3
4
use PhpParser;
5
use Psalm\Internal\Analyzer\ScopeAnalyzer;
6
use Psalm\Internal\Analyzer\StatementsAnalyzer;
7
use Psalm\Internal\Analyzer\Statements\ExpressionAnalyzer;
8
use Psalm\Context;
9
use Psalm\Internal\Scope\LoopScope;
10
use Psalm\Type;
11
use function in_array;
12
use function array_merge;
13
14
/**
15
 * @internal
16
 */
17
class WhileAnalyzer
18
{
19
    /**
20
     * @param   StatementsAnalyzer           $statements_analyzer
21
     * @param   PhpParser\Node\Stmt\While_  $stmt
22
     * @param   Context                     $context
23
     *
24
     * @return  false|null
25
     */
26
    public static function analyze(
27
        StatementsAnalyzer $statements_analyzer,
28
        PhpParser\Node\Stmt\While_ $stmt,
29
        Context $context
30
    ) {
31
        $while_true = ($stmt->cond instanceof PhpParser\Node\Expr\ConstFetch && $stmt->cond->name->parts === ['true'])
32
            || ($stmt->cond instanceof PhpParser\Node\Scalar\LNumber && $stmt->cond->value > 0);
33
34
        $pre_context = null;
35
36
        if ($while_true) {
37
            $pre_context = clone $context;
38
        }
39
40
        $while_context = clone $context;
41
42
        $while_context->inside_loop = true;
43
        $while_context->break_types[] = 'loop';
44
45
        $codebase = $statements_analyzer->getCodebase();
46
47
        if ($codebase->alter_code) {
48
            $while_context->branch_point = $while_context->branch_point ?: (int) $stmt->getAttribute('startFilePos');
49
        }
50
51
        $loop_scope = new LoopScope($while_context, $context);
52
        $loop_scope->protected_var_ids = $context->protected_var_ids;
53
54
        if (LoopAnalyzer::analyze(
55
            $statements_analyzer,
56
            $stmt->stmts,
57
            self::getAndExpressions($stmt->cond),
58
            [],
59
            $loop_scope,
60
            $inner_loop_context
61
        ) === false) {
62
            return false;
63
        }
64
65
        if (!$inner_loop_context) {
66
            throw new \UnexpectedValueException('Should always enter loop');
67
        }
68
69
        $always_enters_loop = false;
70
71
        if ($stmt_cond_type = $statements_analyzer->node_data->getType($stmt->cond)) {
72
            $always_enters_loop = true;
73
74
            foreach ($stmt_cond_type->getAtomicTypes() as $iterator_type) {
75
                if ($iterator_type instanceof Type\Atomic\TArray
76
                    || $iterator_type instanceof Type\Atomic\ObjectLike
77
                ) {
78
                    if ($iterator_type instanceof Type\Atomic\ObjectLike) {
79
                        if (!$iterator_type->sealed) {
80
                            $always_enters_loop = false;
81
                        }
82
                    } elseif (!$iterator_type instanceof Type\Atomic\TNonEmptyArray) {
83
                        $always_enters_loop = false;
84
                    }
85
86
                    continue;
87
                }
88
89
                if ($iterator_type instanceof Type\Atomic\TTrue) {
90
                    continue;
91
                }
92
93
                if ($iterator_type instanceof Type\Atomic\TLiteralString
94
                    && $iterator_type->value
95
                ) {
96
                    continue;
97
                }
98
99
                if ($iterator_type instanceof Type\Atomic\TLiteralInt
100
                    && $iterator_type->value
101
                ) {
102
                    continue;
103
                }
104
105
                $always_enters_loop = false;
106
                break;
107
            }
108
        }
109
110
        $can_leave_loop = !$while_true
111
            || in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true);
112
113 View Code Duplication
        if ($always_enters_loop && $can_leave_loop) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
114
            foreach ($inner_loop_context->vars_in_scope as $var_id => $type) {
115
                // if there are break statements in the loop it's not certain
116
                // that the loop has finished executing, so the assertions at the end
117
                // the loop in the while conditional may not hold
118
                if (in_array(ScopeAnalyzer::ACTION_BREAK, $loop_scope->final_actions, true)
119
                    || in_array(ScopeAnalyzer::ACTION_CONTINUE, $loop_scope->final_actions, true)
120
                ) {
121
                    if (isset($loop_scope->possibly_defined_loop_parent_vars[$var_id])) {
122
                        $context->vars_in_scope[$var_id] = Type::combineUnionTypes(
123
                            $type,
124
                            $loop_scope->possibly_defined_loop_parent_vars[$var_id]
125
                        );
126
                    }
127
                } else {
128
                    $context->vars_in_scope[$var_id] = $type;
129
                }
130
            }
131
        }
132
133
        $while_context->loop_scope = null;
134
135 View Code Duplication
        if ($can_leave_loop) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
136
            $context->vars_possibly_in_scope = array_merge(
137
                $context->vars_possibly_in_scope,
138
                $while_context->vars_possibly_in_scope
139
            );
140
        } elseif ($pre_context) {
141
            $context->vars_possibly_in_scope = $pre_context->vars_possibly_in_scope;
142
        }
143
144
        $context->referenced_var_ids = array_merge(
145
            $context->referenced_var_ids,
146
            $while_context->referenced_var_ids
147
        );
148
149
        if ($codebase->find_unused_variables) {
150
            $suppressed_issues = $statements_analyzer->getSuppressedIssues();
151
152
            if (!in_array('RedundantCondition', $suppressed_issues, true)) {
153
                $statements_analyzer->addSuppressedIssues(['RedundantCondition']);
154
            }
155
            if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
156
                $statements_analyzer->addSuppressedIssues(['RedundantConditionGivenDocblockType']);
157
            }
158
            if (!in_array('TypeDoesNotContainType', $suppressed_issues, true)) {
159
                $statements_analyzer->addSuppressedIssues(['TypeDoesNotContainType']);
160
            }
161
162
            $while_context->inside_conditional = true;
163
            ExpressionAnalyzer::analyze($statements_analyzer, $stmt->cond, $while_context);
164
            $while_context->inside_conditional = false;
165
166
            if (!in_array('RedundantCondition', $suppressed_issues, true)) {
167
                $statements_analyzer->removeSuppressedIssues(['RedundantCondition']);
168
            }
169
            if (!in_array('RedundantConditionGivenDocblockType', $suppressed_issues, true)) {
170
                $statements_analyzer->removeSuppressedIssues(['RedundantConditionGivenDocblockType']);
171
            }
172
            if (!in_array('TypeDoesNotContainType', $suppressed_issues, true)) {
173
                $statements_analyzer->removeSuppressedIssues(['TypeDoesNotContainType']);
174
            }
175
176
            $context->unreferenced_vars = $while_context->unreferenced_vars;
177
        }
178
179
        return null;
180
    }
181
182
    /**
183
     * @return list<PhpParser\Node\Expr>
0 ignored issues
show
Documentation introduced by
The doc-type list<PhpParser\Node\Expr> could not be parsed: Expected "|" or "end of type", but got "<" at position 4. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
184
     */
185
    private static function getAndExpressions(
186
        PhpParser\Node\Expr $expr
187
    ) : array {
188
        if ($expr instanceof PhpParser\Node\Expr\BinaryOp\BooleanAnd) {
189
            return array_merge(
190
                self::getAndExpressions($expr->left),
191
                self::getAndExpressions($expr->right)
192
            );
193
        }
194
195
        return [$expr];
196
    }
197
}
198