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