Conditions | 205 |
Paths | > 20000 |
Total Lines | 925 |
Lines | 0 |
Ratio | 0 % |
Changes | 0 |
Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.
For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.
Commonly applied refactorings include:
If many parameters/temporary variables are present:
1 | <?php |
||
57 | public static function analyze( |
||
58 | StatementsAnalyzer $statements_analyzer, |
||
59 | PhpParser\Node\Expr $assign_var, |
||
60 | $assign_value, |
||
61 | $assign_value_type, |
||
62 | Context $context, |
||
63 | ?PhpParser\Comment\Doc $doc_comment |
||
64 | ) { |
||
65 | $var_id = ExpressionIdentifier::getVarId( |
||
66 | $assign_var, |
||
67 | $statements_analyzer->getFQCLN(), |
||
68 | $statements_analyzer |
||
69 | ); |
||
70 | |||
71 | // gets a variable id that *may* contain array keys |
||
72 | $array_var_id = ExpressionIdentifier::getArrayVarId( |
||
73 | $assign_var, |
||
74 | $statements_analyzer->getFQCLN(), |
||
75 | $statements_analyzer |
||
76 | ); |
||
77 | |||
78 | $var_comments = []; |
||
79 | $comment_type = null; |
||
80 | $comment_type_location = null; |
||
81 | |||
82 | $was_in_assignment = $context->inside_assignment; |
||
83 | |||
84 | $context->inside_assignment = true; |
||
85 | |||
86 | $codebase = $statements_analyzer->getCodebase(); |
||
87 | |||
88 | $removed_taints = []; |
||
89 | |||
90 | if ($doc_comment) { |
||
91 | $file_path = $statements_analyzer->getRootFilePath(); |
||
92 | |||
93 | $file_storage_provider = $codebase->file_storage_provider; |
||
94 | |||
95 | $file_storage = $file_storage_provider->get($file_path); |
||
96 | |||
97 | $template_type_map = $statements_analyzer->getTemplateTypeMap(); |
||
98 | |||
99 | try { |
||
100 | $var_comments = CommentAnalyzer::getTypeFromComment( |
||
101 | $doc_comment, |
||
102 | $statements_analyzer->getSource(), |
||
103 | $statements_analyzer->getAliases(), |
||
104 | $template_type_map, |
||
105 | $file_storage->type_aliases |
||
106 | ); |
||
107 | } catch (IncorrectDocblockException $e) { |
||
108 | if (IssueBuffer::accepts( |
||
109 | new MissingDocblockType( |
||
110 | (string)$e->getMessage(), |
||
111 | new CodeLocation($statements_analyzer->getSource(), $assign_var) |
||
112 | ) |
||
113 | )) { |
||
114 | // fall through |
||
115 | } |
||
116 | } catch (DocblockParseException $e) { |
||
117 | if (IssueBuffer::accepts( |
||
118 | new InvalidDocblock( |
||
119 | (string)$e->getMessage(), |
||
120 | new CodeLocation($statements_analyzer->getSource(), $assign_var) |
||
121 | ) |
||
122 | )) { |
||
123 | // fall through |
||
124 | } |
||
125 | } |
||
126 | |||
127 | foreach ($var_comments as $var_comment) { |
||
128 | if ($var_comment->removed_taints) { |
||
129 | $removed_taints = $var_comment->removed_taints; |
||
130 | } |
||
131 | |||
132 | if (!$var_comment->type) { |
||
133 | continue; |
||
134 | } |
||
135 | |||
136 | try { |
||
137 | $var_comment_type = \Psalm\Internal\Type\TypeExpander::expandUnion( |
||
138 | $codebase, |
||
139 | $var_comment->type, |
||
140 | $context->self, |
||
141 | $context->self, |
||
142 | $statements_analyzer->getParentFQCLN() |
||
143 | ); |
||
144 | |||
145 | $var_comment_type->setFromDocblock(); |
||
146 | |||
147 | $var_comment_type->check( |
||
148 | $statements_analyzer, |
||
149 | new CodeLocation($statements_analyzer->getSource(), $assign_var), |
||
150 | $statements_analyzer->getSuppressedIssues(), |
||
151 | [], |
||
152 | false, |
||
153 | false, |
||
154 | false, |
||
155 | $context->calling_method_id |
||
156 | ); |
||
157 | |||
158 | $type_location = null; |
||
159 | |||
160 | if ($var_comment->type_start |
||
161 | && $var_comment->type_end |
||
162 | && $var_comment->line_number |
||
163 | ) { |
||
164 | $type_location = new CodeLocation\DocblockTypeLocation( |
||
165 | $statements_analyzer, |
||
166 | $var_comment->type_start, |
||
167 | $var_comment->type_end, |
||
168 | $var_comment->line_number |
||
169 | ); |
||
170 | |||
171 | if ($codebase->alter_code) { |
||
172 | $codebase->classlikes->handleDocblockTypeInMigration( |
||
173 | $codebase, |
||
174 | $statements_analyzer, |
||
175 | $var_comment_type, |
||
176 | $type_location, |
||
177 | $context->calling_method_id |
||
178 | ); |
||
179 | } |
||
180 | } |
||
181 | |||
182 | if (!$var_comment->var_id || $var_comment->var_id === $var_id) { |
||
183 | $comment_type = $var_comment_type; |
||
184 | $comment_type_location = $type_location; |
||
185 | continue; |
||
186 | } |
||
187 | |||
188 | if ($codebase->find_unused_variables |
||
189 | && $type_location |
||
190 | && isset($context->vars_in_scope[$var_comment->var_id]) |
||
191 | && $context->vars_in_scope[$var_comment->var_id]->getId() === $var_comment_type->getId() |
||
192 | && !$var_comment_type->isMixed() |
||
193 | ) { |
||
194 | $project_analyzer = $statements_analyzer->getProjectAnalyzer(); |
||
195 | |||
196 | if ($codebase->alter_code |
||
197 | && isset($project_analyzer->getIssuesToFix()['UnnecessaryVarAnnotation']) |
||
198 | ) { |
||
199 | FileManipulationBuffer::addVarAnnotationToRemove($type_location); |
||
200 | } elseif (IssueBuffer::accepts( |
||
201 | new UnnecessaryVarAnnotation( |
||
202 | 'The @var ' . $var_comment_type . ' annotation for ' |
||
203 | . $var_comment->var_id . ' is unnecessary', |
||
204 | $type_location |
||
205 | ), |
||
206 | $statements_analyzer->getSuppressedIssues(), |
||
207 | true |
||
208 | )) { |
||
209 | // fall through |
||
210 | } |
||
211 | } |
||
212 | |||
213 | $parent_nodes = $context->vars_in_scope[$var_comment->var_id]->parent_nodes ?? []; |
||
214 | $var_comment_type->parent_nodes = $parent_nodes; |
||
215 | |||
216 | $context->vars_in_scope[$var_comment->var_id] = $var_comment_type; |
||
217 | } catch (\UnexpectedValueException $e) { |
||
218 | if (IssueBuffer::accepts( |
||
219 | new InvalidDocblock( |
||
220 | (string)$e->getMessage(), |
||
221 | new CodeLocation($statements_analyzer->getSource(), $assign_var) |
||
222 | ) |
||
223 | )) { |
||
224 | // fall through |
||
225 | } |
||
226 | } |
||
227 | } |
||
228 | } |
||
229 | |||
230 | if ($array_var_id) { |
||
231 | unset($context->referenced_var_ids[$array_var_id]); |
||
232 | $context->assigned_var_ids[$array_var_id] = true; |
||
233 | $context->possibly_assigned_var_ids[$array_var_id] = true; |
||
234 | } |
||
235 | |||
236 | if ($assign_value) { |
||
237 | if ($var_id && $assign_value instanceof PhpParser\Node\Expr\Closure) { |
||
238 | foreach ($assign_value->uses as $closure_use) { |
||
239 | if ($closure_use->byRef |
||
240 | && is_string($closure_use->var->name) |
||
241 | && $var_id === '$' . $closure_use->var->name |
||
242 | ) { |
||
243 | $context->vars_in_scope[$var_id] = Type::getClosure(); |
||
244 | $context->vars_possibly_in_scope[$var_id] = true; |
||
245 | } |
||
246 | } |
||
247 | } |
||
248 | |||
249 | if (ExpressionAnalyzer::analyze($statements_analyzer, $assign_value, $context) === false) { |
||
250 | if ($var_id) { |
||
251 | if ($array_var_id) { |
||
252 | $context->removeDescendents($array_var_id, null, $assign_value_type); |
||
253 | } |
||
254 | |||
255 | // if we're not exiting immediately, make everything mixed |
||
256 | $context->vars_in_scope[$var_id] = $comment_type ?: Type::getMixed(); |
||
257 | } |
||
258 | |||
259 | return false; |
||
260 | } |
||
261 | } |
||
262 | |||
263 | if ($comment_type && $comment_type_location) { |
||
264 | $temp_assign_value_type = $assign_value_type |
||
265 | ? $assign_value_type |
||
266 | : ($assign_value ? $statements_analyzer->node_data->getType($assign_value) : null); |
||
267 | |||
268 | if ($codebase->find_unused_variables |
||
269 | && $temp_assign_value_type |
||
270 | && $array_var_id |
||
271 | && $temp_assign_value_type->getId() === $comment_type->getId() |
||
272 | && !$comment_type->isMixed() |
||
273 | ) { |
||
274 | if ($codebase->alter_code |
||
275 | && isset($statements_analyzer->getProjectAnalyzer()->getIssuesToFix()['UnnecessaryVarAnnotation']) |
||
276 | ) { |
||
277 | FileManipulationBuffer::addVarAnnotationToRemove($comment_type_location); |
||
278 | } elseif (IssueBuffer::accepts( |
||
279 | new UnnecessaryVarAnnotation( |
||
280 | 'The @var ' . $comment_type . ' annotation for ' |
||
281 | . $array_var_id . ' is unnecessary', |
||
282 | $comment_type_location |
||
283 | ) |
||
284 | )) { |
||
285 | // fall through |
||
286 | } |
||
287 | } |
||
288 | |||
289 | $assign_value_type = $comment_type; |
||
290 | $assign_value_type->parent_nodes = $temp_assign_value_type->parent_nodes ?? []; |
||
291 | } elseif (!$assign_value_type) { |
||
292 | $assign_value_type = $assign_value |
||
293 | ? ($statements_analyzer->node_data->getType($assign_value) ?: Type::getMixed()) |
||
294 | : Type::getMixed(); |
||
295 | } |
||
296 | |||
297 | if ($array_var_id && isset($context->vars_in_scope[$array_var_id])) { |
||
298 | if ($context->vars_in_scope[$array_var_id]->by_ref) { |
||
299 | if ($context->mutation_free) { |
||
300 | if (IssueBuffer::accepts( |
||
301 | new ImpureByReferenceAssignment( |
||
302 | 'Variable ' . $array_var_id . ' cannot be assigned to as it is passed by reference', |
||
303 | new CodeLocation($statements_analyzer->getSource(), $assign_var) |
||
304 | ) |
||
305 | )) { |
||
306 | // fall through |
||
307 | } |
||
308 | } |
||
309 | |||
310 | $assign_value_type->by_ref = true; |
||
311 | } |
||
312 | |||
313 | // removes dependent vars from $context |
||
314 | $context->removeDescendents( |
||
315 | $array_var_id, |
||
316 | $context->vars_in_scope[$array_var_id], |
||
317 | $assign_value_type, |
||
318 | $statements_analyzer |
||
319 | ); |
||
320 | } else { |
||
321 | $root_var_id = ExpressionIdentifier::getRootVarId( |
||
322 | $assign_var, |
||
323 | $statements_analyzer->getFQCLN(), |
||
324 | $statements_analyzer |
||
325 | ); |
||
326 | |||
327 | if ($root_var_id && isset($context->vars_in_scope[$root_var_id])) { |
||
328 | $context->removeVarFromConflictingClauses( |
||
329 | $root_var_id, |
||
330 | $context->vars_in_scope[$root_var_id], |
||
331 | $statements_analyzer |
||
332 | ); |
||
333 | } |
||
334 | } |
||
335 | |||
336 | $codebase = $statements_analyzer->getCodebase(); |
||
337 | |||
338 | if ($assign_value_type->hasMixed()) { |
||
339 | $root_var_id = ExpressionIdentifier::getRootVarId( |
||
340 | $assign_var, |
||
341 | $statements_analyzer->getFQCLN(), |
||
342 | $statements_analyzer |
||
343 | ); |
||
344 | |||
345 | if (!$context->collect_initializations |
||
346 | && !$context->collect_mutations |
||
347 | && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() |
||
348 | && (!(($parent_source = $statements_analyzer->getSource()) |
||
349 | instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) |
||
350 | || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) |
||
351 | ) { |
||
352 | $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath()); |
||
353 | } |
||
354 | |||
355 | if (!$assign_var instanceof PhpParser\Node\Expr\PropertyFetch |
||
356 | && !strpos($root_var_id ?? '', '->') |
||
357 | && !$comment_type |
||
358 | && substr($var_id ?? '', 0, 2) !== '$_' |
||
359 | ) { |
||
360 | if (IssueBuffer::accepts( |
||
361 | new MixedAssignment( |
||
362 | $var_id |
||
363 | ? 'Unable to determine the type that ' . $var_id . ' is being assigned to' |
||
364 | : 'Unable to determine the type of this assignment', |
||
365 | new CodeLocation($statements_analyzer->getSource(), $assign_var) |
||
366 | ), |
||
367 | $statements_analyzer->getSuppressedIssues() |
||
368 | )) { |
||
369 | // fall through |
||
370 | } |
||
371 | } |
||
372 | } else { |
||
373 | if (!$context->collect_initializations |
||
374 | && !$context->collect_mutations |
||
375 | && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() |
||
376 | && (!(($parent_source = $statements_analyzer->getSource()) |
||
377 | instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) |
||
378 | || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) |
||
379 | ) { |
||
380 | $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath()); |
||
381 | } |
||
382 | |||
383 | if ($var_id |
||
384 | && isset($context->byref_constraints[$var_id]) |
||
385 | && ($outer_constraint_type = $context->byref_constraints[$var_id]->type) |
||
386 | ) { |
||
387 | if (!TypeAnalyzer::isContainedBy( |
||
388 | $codebase, |
||
389 | $assign_value_type, |
||
390 | $outer_constraint_type, |
||
391 | $assign_value_type->ignore_nullable_issues, |
||
392 | $assign_value_type->ignore_falsable_issues |
||
393 | ) |
||
394 | ) { |
||
395 | if (IssueBuffer::accepts( |
||
396 | new ReferenceConstraintViolation( |
||
397 | 'Variable ' . $var_id . ' is limited to values of type ' |
||
398 | . $context->byref_constraints[$var_id]->type |
||
399 | . ' because it is passed by reference, ' |
||
400 | . $assign_value_type->getId() . ' type found', |
||
401 | new CodeLocation($statements_analyzer->getSource(), $assign_var) |
||
402 | ), |
||
403 | $statements_analyzer->getSuppressedIssues() |
||
404 | )) { |
||
405 | // fall through |
||
406 | } |
||
407 | } |
||
408 | } |
||
409 | } |
||
410 | |||
411 | if ($var_id === '$this' && IssueBuffer::accepts( |
||
412 | new InvalidScope( |
||
413 | 'Cannot re-assign ' . $var_id, |
||
414 | new CodeLocation($statements_analyzer->getSource(), $assign_var) |
||
415 | ), |
||
416 | $statements_analyzer->getSuppressedIssues() |
||
417 | )) { |
||
418 | return false; |
||
419 | } |
||
420 | |||
421 | if (isset($context->protected_var_ids[$var_id])) { |
||
422 | if (IssueBuffer::accepts( |
||
423 | new LoopInvalidation( |
||
424 | 'Variable ' . $var_id . ' has already been assigned in a for/foreach loop', |
||
425 | new CodeLocation($statements_analyzer->getSource(), $assign_var) |
||
426 | ), |
||
427 | $statements_analyzer->getSuppressedIssues() |
||
428 | )) { |
||
429 | // fall through |
||
430 | } |
||
431 | } |
||
432 | |||
433 | if ($assign_var instanceof PhpParser\Node\Expr\Variable) { |
||
434 | if (is_string($assign_var->name)) { |
||
435 | if ($var_id) { |
||
436 | $context->vars_in_scope[$var_id] = $assign_value_type; |
||
437 | $context->vars_possibly_in_scope[$var_id] = true; |
||
438 | |||
439 | $location = new CodeLocation($statements_analyzer, $assign_var); |
||
440 | |||
441 | if ($codebase->find_unused_variables) { |
||
442 | $context->unreferenced_vars[$var_id] = [$location->getHash() => $location]; |
||
443 | } |
||
444 | |||
445 | if (!$statements_analyzer->hasVariable($var_id)) { |
||
446 | $statements_analyzer->registerVariable( |
||
447 | $var_id, |
||
448 | $location, |
||
449 | $context->branch_point |
||
450 | ); |
||
451 | } else { |
||
452 | $statements_analyzer->registerVariableAssignment( |
||
453 | $var_id, |
||
454 | $location |
||
455 | ); |
||
456 | } |
||
457 | |||
458 | if ($codebase->store_node_types |
||
459 | && !$context->collect_initializations |
||
460 | && !$context->collect_mutations |
||
461 | ) { |
||
462 | $location = new CodeLocation($statements_analyzer, $assign_var); |
||
463 | $codebase->analyzer->addNodeReference( |
||
464 | $statements_analyzer->getFilePath(), |
||
465 | $assign_var, |
||
466 | $location->raw_file_start |
||
467 | . '-' . $location->raw_file_end |
||
468 | . ':' . $assign_value_type->getId() |
||
469 | ); |
||
470 | } |
||
471 | |||
472 | if (isset($context->byref_constraints[$var_id]) || $assign_value_type->by_ref) { |
||
473 | $statements_analyzer->registerVariableUses([$location->getHash() => $location]); |
||
474 | } |
||
475 | } |
||
476 | } else { |
||
477 | if (ExpressionAnalyzer::analyze($statements_analyzer, $assign_var->name, $context) === false) { |
||
478 | return false; |
||
479 | } |
||
480 | } |
||
481 | } elseif ($assign_var instanceof PhpParser\Node\Expr\List_ |
||
482 | || $assign_var instanceof PhpParser\Node\Expr\Array_ |
||
483 | ) { |
||
484 | if (!$assign_value_type->hasArray() |
||
485 | && !$assign_value_type->isMixed() |
||
486 | && !$assign_value_type->hasArrayAccessInterface($codebase) |
||
487 | ) { |
||
488 | if (IssueBuffer::accepts( |
||
489 | new InvalidArrayOffset( |
||
490 | 'Cannot destructure non-array of type ' . $assign_value_type->getId(), |
||
491 | new CodeLocation($statements_analyzer->getSource(), $assign_var) |
||
492 | ), |
||
493 | $statements_analyzer->getSuppressedIssues() |
||
494 | )) { |
||
495 | // fall through |
||
496 | } |
||
497 | } |
||
498 | |||
499 | $can_be_empty = true; |
||
500 | |||
501 | foreach ($assign_var->items as $offset => $assign_var_item) { |
||
502 | // $assign_var_item can be null e.g. list($a, ) = ['a', 'b'] |
||
503 | if (!$assign_var_item) { |
||
504 | continue; |
||
505 | } |
||
506 | |||
507 | $var = $assign_var_item->value; |
||
508 | |||
509 | if ($assign_value instanceof PhpParser\Node\Expr\Array_ |
||
510 | && $statements_analyzer->node_data->getType($assign_var_item->value) |
||
511 | ) { |
||
512 | self::analyze( |
||
513 | $statements_analyzer, |
||
514 | $var, |
||
515 | $assign_var_item->value, |
||
516 | null, |
||
517 | $context, |
||
518 | $doc_comment |
||
519 | ); |
||
520 | |||
521 | continue; |
||
522 | } |
||
523 | |||
524 | $list_var_id = ExpressionIdentifier::getArrayVarId( |
||
525 | $var, |
||
526 | $statements_analyzer->getFQCLN(), |
||
527 | $statements_analyzer |
||
528 | ); |
||
529 | |||
530 | $new_assign_type = null; |
||
531 | $assigned = false; |
||
532 | $has_null = false; |
||
533 | |||
534 | foreach ($assign_value_type->getAtomicTypes() as $assign_value_atomic_type) { |
||
535 | if ($assign_value_atomic_type instanceof Type\Atomic\ObjectLike |
||
536 | && !$assign_var_item->key |
||
537 | ) { |
||
538 | // if object-like has int offsets |
||
539 | if (isset($assign_value_atomic_type->properties[$offset])) { |
||
540 | $offset_type = $assign_value_atomic_type->properties[(string)$offset]; |
||
541 | |||
542 | if ($offset_type->possibly_undefined) { |
||
543 | if (IssueBuffer::accepts( |
||
544 | new PossiblyUndefinedArrayOffset( |
||
545 | 'Possibly undefined array key', |
||
546 | new CodeLocation($statements_analyzer->getSource(), $var) |
||
547 | ), |
||
548 | $statements_analyzer->getSuppressedIssues() |
||
549 | )) { |
||
550 | // fall through |
||
551 | } |
||
552 | |||
553 | $offset_type = clone $offset_type; |
||
554 | $offset_type->possibly_undefined = false; |
||
555 | } |
||
556 | |||
557 | self::analyze( |
||
558 | $statements_analyzer, |
||
559 | $var, |
||
560 | null, |
||
561 | $offset_type, |
||
562 | $context, |
||
563 | $doc_comment |
||
564 | ); |
||
565 | |||
566 | $assigned = true; |
||
567 | |||
568 | continue; |
||
569 | } |
||
570 | |||
571 | if ($assign_value_atomic_type->sealed) { |
||
572 | if (IssueBuffer::accepts( |
||
573 | new InvalidArrayOffset( |
||
574 | 'Cannot access value with offset ' . $offset, |
||
575 | new CodeLocation($statements_analyzer->getSource(), $var) |
||
576 | ), |
||
577 | $statements_analyzer->getSuppressedIssues() |
||
578 | )) { |
||
579 | // fall through |
||
580 | } |
||
581 | } |
||
582 | } |
||
583 | |||
584 | if ($assign_value_atomic_type instanceof Type\Atomic\TMixed) { |
||
585 | if (IssueBuffer::accepts( |
||
586 | new MixedArrayAccess( |
||
587 | 'Cannot access array value on mixed variable ' . $array_var_id, |
||
588 | new CodeLocation($statements_analyzer->getSource(), $var) |
||
589 | ), |
||
590 | $statements_analyzer->getSuppressedIssues() |
||
591 | )) { |
||
592 | // fall through |
||
593 | } |
||
594 | } elseif ($assign_value_atomic_type instanceof Type\Atomic\TNull) { |
||
595 | $has_null = true; |
||
596 | |||
597 | if (IssueBuffer::accepts( |
||
598 | new PossiblyNullArrayAccess( |
||
599 | 'Cannot access array value on null variable ' . $array_var_id, |
||
600 | new CodeLocation($statements_analyzer->getSource(), $var) |
||
601 | ), |
||
602 | $statements_analyzer->getSuppressedIssues() |
||
603 | ) |
||
604 | ) { |
||
605 | // do nothing |
||
606 | } |
||
607 | } elseif (!$assign_value_atomic_type instanceof Type\Atomic\TArray |
||
608 | && !$assign_value_atomic_type instanceof Type\Atomic\ObjectLike |
||
609 | && !$assign_value_atomic_type instanceof Type\Atomic\TList |
||
610 | && !$assign_value_type->hasArrayAccessInterface($codebase) |
||
611 | ) { |
||
612 | if ($assign_value_type->hasArray()) { |
||
613 | if (($assign_value_atomic_type instanceof Type\Atomic\TFalse |
||
614 | && $assign_value_type->ignore_falsable_issues) |
||
615 | || ($assign_value_atomic_type instanceof Type\Atomic\TNull |
||
616 | && $assign_value_type->ignore_nullable_issues) |
||
617 | ) { |
||
618 | // do nothing |
||
619 | } elseif (IssueBuffer::accepts( |
||
620 | new PossiblyInvalidArrayAccess( |
||
621 | 'Cannot access array value on non-array variable ' |
||
622 | . $array_var_id . ' of type ' . $assign_value_atomic_type->getId(), |
||
623 | new CodeLocation($statements_analyzer->getSource(), $var) |
||
624 | ), |
||
625 | $statements_analyzer->getSuppressedIssues() |
||
626 | ) |
||
627 | ) { |
||
628 | // do nothing |
||
629 | } |
||
630 | } else { |
||
631 | if (IssueBuffer::accepts( |
||
632 | new InvalidArrayAccess( |
||
633 | 'Cannot access array value on non-array variable ' |
||
634 | . $array_var_id . ' of type ' . $assign_value_atomic_type->getId(), |
||
635 | new CodeLocation($statements_analyzer->getSource(), $var) |
||
636 | ), |
||
637 | $statements_analyzer->getSuppressedIssues() |
||
638 | ) |
||
639 | ) { |
||
640 | // do nothing |
||
641 | } |
||
642 | } |
||
643 | } |
||
644 | |||
645 | if ($var instanceof PhpParser\Node\Expr\List_ |
||
646 | || $var instanceof PhpParser\Node\Expr\Array_ |
||
647 | ) { |
||
648 | if ($assign_value_atomic_type instanceof Type\Atomic\ObjectLike) { |
||
649 | $assign_value_atomic_type = $assign_value_atomic_type->getGenericArrayType(); |
||
650 | } |
||
651 | |||
652 | if ($assign_value_atomic_type instanceof Type\Atomic\TList) { |
||
653 | $assign_value_atomic_type = new Type\Atomic\TArray([ |
||
654 | Type::getInt(), |
||
655 | $assign_value_atomic_type->type_param |
||
656 | ]); |
||
657 | } |
||
658 | |||
659 | self::analyze( |
||
660 | $statements_analyzer, |
||
661 | $var, |
||
662 | null, |
||
663 | $assign_value_atomic_type instanceof Type\Atomic\TArray |
||
664 | ? clone $assign_value_atomic_type->type_params[1] |
||
665 | : Type::getMixed(), |
||
666 | $context, |
||
667 | $doc_comment |
||
668 | ); |
||
669 | |||
670 | continue; |
||
671 | } |
||
672 | |||
673 | if ($list_var_id) { |
||
674 | $context->vars_possibly_in_scope[$list_var_id] = true; |
||
675 | $context->assigned_var_ids[$list_var_id] = true; |
||
676 | $context->possibly_assigned_var_ids[$list_var_id] = true; |
||
677 | |||
678 | $already_in_scope = isset($context->vars_in_scope[$list_var_id]); |
||
679 | |||
680 | if (strpos($list_var_id, '-') === false && strpos($list_var_id, '[') === false) { |
||
681 | $location = new CodeLocation($statements_analyzer, $var); |
||
682 | |||
683 | if ($codebase->find_unused_variables) { |
||
684 | $context->unreferenced_vars[$list_var_id] = [$location->getHash() => $location]; |
||
685 | } |
||
686 | |||
687 | if (!$statements_analyzer->hasVariable($list_var_id)) { |
||
688 | $statements_analyzer->registerVariable( |
||
689 | $list_var_id, |
||
690 | $location, |
||
691 | $context->branch_point |
||
692 | ); |
||
693 | } else { |
||
694 | $statements_analyzer->registerVariableAssignment( |
||
695 | $list_var_id, |
||
696 | $location |
||
697 | ); |
||
698 | } |
||
699 | |||
700 | if (isset($context->byref_constraints[$list_var_id])) { |
||
701 | $statements_analyzer->registerVariableUses([$location->getHash() => $location]); |
||
702 | } |
||
703 | } |
||
704 | |||
705 | if ($assign_value_atomic_type instanceof Type\Atomic\TArray) { |
||
706 | $new_assign_type = clone $assign_value_atomic_type->type_params[1]; |
||
707 | |||
708 | $can_be_empty = !$assign_value_atomic_type instanceof Type\Atomic\TNonEmptyArray; |
||
709 | } elseif ($assign_value_atomic_type instanceof Type\Atomic\TList) { |
||
710 | $new_assign_type = clone $assign_value_atomic_type->type_param; |
||
711 | |||
712 | $can_be_empty = !$assign_value_atomic_type instanceof Type\Atomic\TNonEmptyList; |
||
713 | } elseif ($assign_value_atomic_type instanceof Type\Atomic\ObjectLike) { |
||
714 | if ($assign_var_item->key |
||
715 | && ($assign_var_item->key instanceof PhpParser\Node\Scalar\String_ |
||
716 | || $assign_var_item->key instanceof PhpParser\Node\Scalar\LNumber) |
||
717 | && isset($assign_value_atomic_type->properties[$assign_var_item->key->value]) |
||
718 | ) { |
||
719 | $new_assign_type = |
||
720 | clone $assign_value_atomic_type->properties[$assign_var_item->key->value]; |
||
721 | |||
722 | if ($new_assign_type->possibly_undefined) { |
||
723 | if (IssueBuffer::accepts( |
||
724 | new PossiblyUndefinedArrayOffset( |
||
725 | 'Possibly undefined array key', |
||
726 | new CodeLocation($statements_analyzer->getSource(), $var) |
||
727 | ), |
||
728 | $statements_analyzer->getSuppressedIssues() |
||
729 | )) { |
||
730 | // fall through |
||
731 | } |
||
732 | |||
733 | $new_assign_type->possibly_undefined = false; |
||
734 | } |
||
735 | } |
||
736 | |||
737 | $can_be_empty = !$assign_value_atomic_type->sealed; |
||
738 | } elseif ($assign_value_atomic_type->hasArrayAccessInterface($codebase)) { |
||
739 | ForeachAnalyzer::getKeyValueParamsForTraversableObject( |
||
740 | $assign_value_atomic_type, |
||
741 | $codebase, |
||
742 | $array_access_key_type, |
||
743 | $array_access_value_type |
||
744 | ); |
||
745 | |||
746 | $new_assign_type = $array_access_value_type; |
||
747 | } |
||
748 | |||
749 | if ($already_in_scope) { |
||
750 | // removes dependennt vars from $context |
||
751 | $context->removeDescendents( |
||
752 | $list_var_id, |
||
753 | $context->vars_in_scope[$list_var_id], |
||
754 | $new_assign_type, |
||
755 | $statements_analyzer |
||
756 | ); |
||
757 | } |
||
758 | } |
||
759 | } |
||
760 | |||
761 | if (!$assigned) { |
||
762 | foreach ($var_comments as $var_comment) { |
||
763 | if (!$var_comment->type) { |
||
764 | continue; |
||
765 | } |
||
766 | |||
767 | try { |
||
768 | if ($var_comment->var_id === $list_var_id) { |
||
769 | $var_comment_type = \Psalm\Internal\Type\TypeExpander::expandUnion( |
||
770 | $codebase, |
||
771 | $var_comment->type, |
||
772 | $context->self, |
||
773 | $context->self, |
||
774 | $statements_analyzer->getParentFQCLN() |
||
775 | ); |
||
776 | |||
777 | $var_comment_type->setFromDocblock(); |
||
778 | |||
779 | $new_assign_type = $var_comment_type; |
||
780 | break; |
||
781 | } |
||
782 | } catch (\UnexpectedValueException $e) { |
||
783 | if (IssueBuffer::accepts( |
||
784 | new InvalidDocblock( |
||
785 | (string)$e->getMessage(), |
||
786 | new CodeLocation($statements_analyzer->getSource(), $assign_var) |
||
787 | ) |
||
788 | )) { |
||
789 | // fall through |
||
790 | } |
||
791 | } |
||
792 | } |
||
793 | |||
794 | if ($list_var_id) { |
||
795 | $context->vars_in_scope[$list_var_id] = $new_assign_type ?: Type::getMixed(); |
||
796 | |||
797 | if (($context->error_suppressing && ($offset || $can_be_empty)) |
||
798 | || $has_null |
||
799 | ) { |
||
800 | $context->vars_in_scope[$list_var_id]->addType(new Type\Atomic\TNull); |
||
801 | } |
||
802 | } |
||
803 | } |
||
804 | } |
||
805 | } elseif ($assign_var instanceof PhpParser\Node\Expr\ArrayDimFetch) { |
||
806 | ArrayAssignmentAnalyzer::analyze( |
||
807 | $statements_analyzer, |
||
808 | $assign_var, |
||
809 | $context, |
||
810 | $assign_value, |
||
811 | $assign_value_type |
||
812 | ); |
||
813 | } elseif ($assign_var instanceof PhpParser\Node\Expr\PropertyFetch) { |
||
814 | if (!$assign_var->name instanceof PhpParser\Node\Identifier) { |
||
815 | // this can happen when the user actually means to type $this-><autocompleted>, but there's |
||
816 | // a variable on the next line |
||
817 | if (ExpressionAnalyzer::analyze($statements_analyzer, $assign_var->var, $context) === false) { |
||
818 | return false; |
||
819 | } |
||
820 | |||
821 | if (ExpressionAnalyzer::analyze($statements_analyzer, $assign_var->name, $context) === false) { |
||
822 | return false; |
||
823 | } |
||
824 | } |
||
825 | |||
826 | if ($assign_var->name instanceof PhpParser\Node\Identifier) { |
||
827 | $prop_name = $assign_var->name->name; |
||
828 | } elseif (($assign_var_name_type = $statements_analyzer->node_data->getType($assign_var->name)) |
||
829 | && $assign_var_name_type->isSingleStringLiteral() |
||
830 | ) { |
||
831 | $prop_name = $assign_var_name_type->getSingleStringLiteral()->value; |
||
832 | } else { |
||
833 | $prop_name = null; |
||
834 | } |
||
835 | |||
836 | if ($prop_name) { |
||
837 | InstancePropertyAssignmentAnalyzer::analyze( |
||
838 | $statements_analyzer, |
||
839 | $assign_var, |
||
840 | $prop_name, |
||
841 | $assign_value, |
||
842 | $assign_value_type, |
||
843 | $context |
||
844 | ); |
||
845 | } else { |
||
846 | if (ExpressionAnalyzer::analyze($statements_analyzer, $assign_var->var, $context) === false) { |
||
847 | return false; |
||
848 | } |
||
849 | |||
850 | if (($assign_var_type = $statements_analyzer->node_data->getType($assign_var->var)) |
||
851 | && !$context->ignore_variable_property |
||
852 | ) { |
||
853 | $stmt_var_type = $assign_var_type; |
||
854 | |||
855 | if ($stmt_var_type->hasObjectType()) { |
||
856 | foreach ($stmt_var_type->getAtomicTypes() as $type) { |
||
857 | if ($type instanceof Type\Atomic\TNamedObject) { |
||
858 | $codebase->analyzer->addMixedMemberName( |
||
859 | strtolower($type->value) . '::$', |
||
860 | $context->calling_method_id ?: $statements_analyzer->getFileName() |
||
861 | ); |
||
862 | } |
||
863 | } |
||
864 | } |
||
865 | } |
||
866 | } |
||
867 | |||
868 | if ($var_id) { |
||
869 | $context->vars_possibly_in_scope[$var_id] = true; |
||
870 | } |
||
871 | |||
872 | $property_var_pure_compatible = $statements_analyzer->node_data->isPureCompatible($assign_var->var); |
||
873 | |||
874 | // prevents writing to any properties in a mutation-free context |
||
875 | if (($context->mutation_free || $context->external_mutation_free) |
||
876 | && !$property_var_pure_compatible |
||
877 | && !$context->collect_mutations |
||
878 | && !$context->collect_initializations |
||
879 | ) { |
||
880 | if (IssueBuffer::accepts( |
||
881 | new ImpurePropertyAssignment( |
||
882 | 'Cannot assign to a property from a mutation-free context', |
||
883 | new CodeLocation($statements_analyzer, $assign_var) |
||
884 | ), |
||
885 | $statements_analyzer->getSuppressedIssues() |
||
886 | )) { |
||
887 | // fall through |
||
888 | } |
||
889 | } |
||
890 | } elseif ($assign_var instanceof PhpParser\Node\Expr\StaticPropertyFetch && |
||
891 | $assign_var->class instanceof PhpParser\Node\Name |
||
892 | ) { |
||
893 | if (ExpressionAnalyzer::analyze($statements_analyzer, $assign_var, $context) === false) { |
||
894 | return false; |
||
895 | } |
||
896 | |||
897 | if ($context->check_classes) { |
||
898 | StaticPropertyAssignmentAnalyzer::analyze( |
||
899 | $statements_analyzer, |
||
900 | $assign_var, |
||
901 | $assign_value, |
||
902 | $assign_value_type, |
||
903 | $context |
||
904 | ); |
||
905 | } |
||
906 | |||
907 | if ($var_id) { |
||
908 | $context->vars_possibly_in_scope[$var_id] = true; |
||
909 | } |
||
910 | } |
||
911 | |||
912 | if ($var_id && isset($context->vars_in_scope[$var_id])) { |
||
913 | if ($context->vars_in_scope[$var_id]->isVoid()) { |
||
914 | if (IssueBuffer::accepts( |
||
915 | new AssignmentToVoid( |
||
916 | 'Cannot assign ' . $var_id . ' to type void', |
||
917 | new CodeLocation($statements_analyzer->getSource(), $assign_var) |
||
918 | ), |
||
919 | $statements_analyzer->getSuppressedIssues() |
||
920 | )) { |
||
921 | // fall through |
||
922 | } |
||
923 | |||
924 | $context->vars_in_scope[$var_id] = Type::getNull(); |
||
925 | |||
926 | if (!$was_in_assignment) { |
||
927 | $context->inside_assignment = false; |
||
928 | } |
||
929 | |||
930 | return $context->vars_in_scope[$var_id]; |
||
931 | } |
||
932 | |||
933 | if ($context->vars_in_scope[$var_id]->isNever()) { |
||
934 | if (IssueBuffer::accepts( |
||
935 | new NoValue( |
||
936 | 'This function or method call never returns output', |
||
937 | new CodeLocation($statements_analyzer->getSource(), $assign_var) |
||
938 | ), |
||
939 | $statements_analyzer->getSuppressedIssues() |
||
940 | )) { |
||
941 | return false; |
||
942 | } |
||
943 | |||
944 | $context->vars_in_scope[$var_id] = Type::getEmpty(); |
||
945 | |||
946 | if (!$was_in_assignment) { |
||
947 | $context->inside_assignment = false; |
||
948 | } |
||
949 | |||
950 | return $context->vars_in_scope[$var_id]; |
||
951 | } |
||
952 | |||
953 | if ($codebase->taint |
||
954 | && $codebase->config->trackTaintsInPath($statements_analyzer->getFilePath()) |
||
955 | ) { |
||
956 | if ($context->vars_in_scope[$var_id]->parent_nodes) { |
||
957 | if (\in_array('TaintedInput', $statements_analyzer->getSuppressedIssues())) { |
||
958 | $context->vars_in_scope[$var_id]->parent_nodes = []; |
||
959 | } else { |
||
960 | $var_location = new CodeLocation($statements_analyzer->getSource(), $assign_var); |
||
961 | |||
962 | $new_parent_node = \Psalm\Internal\Taint\TaintNode::getForAssignment($var_id, $var_location); |
||
963 | |||
964 | $codebase->taint->addTaintNode($new_parent_node); |
||
965 | |||
966 | foreach ($context->vars_in_scope[$var_id]->parent_nodes as $parent_node) { |
||
967 | $codebase->taint->addPath($parent_node, $new_parent_node, '=', [], $removed_taints); |
||
968 | } |
||
969 | |||
970 | $context->vars_in_scope[$var_id]->parent_nodes = [$new_parent_node]; |
||
971 | } |
||
972 | } |
||
973 | } |
||
974 | } |
||
975 | |||
976 | if (!$was_in_assignment) { |
||
977 | $context->inside_assignment = false; |
||
978 | } |
||
979 | |||
980 | return $assign_value_type; |
||
981 | } |
||
982 | |||
1396 |
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.