Conditions | 251 |
Paths | > 20000 |
Total Lines | 1120 |
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:
Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.
There are several approaches to avoid long parameter lists:
1 | <?php |
||
362 | public static function getArrayAccessTypeGivenOffset( |
||
363 | StatementsAnalyzer $statements_analyzer, |
||
364 | PhpParser\Node\Expr\ArrayDimFetch $stmt, |
||
365 | Type\Union $array_type, |
||
366 | Type\Union $offset_type, |
||
367 | bool $in_assignment, |
||
368 | ?string $array_var_id, |
||
369 | Context $context, |
||
370 | PhpParser\Node\Expr $assign_value = null, |
||
371 | Type\Union $replacement_type = null |
||
372 | ) { |
||
373 | $codebase = $statements_analyzer->getCodebase(); |
||
374 | |||
375 | $has_array_access = false; |
||
376 | $non_array_types = []; |
||
377 | |||
378 | $has_valid_offset = false; |
||
379 | $expected_offset_types = []; |
||
380 | |||
381 | $key_values = []; |
||
382 | |||
383 | if ($stmt->dim instanceof PhpParser\Node\Scalar\String_ |
||
384 | || $stmt->dim instanceof PhpParser\Node\Scalar\LNumber |
||
385 | ) { |
||
386 | $key_values[] = $stmt->dim->value; |
||
387 | } elseif ($stmt->dim && ($stmt_dim_type = $statements_analyzer->node_data->getType($stmt->dim))) { |
||
388 | $string_literals = $stmt_dim_type->getLiteralStrings(); |
||
389 | $int_literals = $stmt_dim_type->getLiteralInts(); |
||
390 | |||
391 | $all_atomic_types = $stmt_dim_type->getAtomicTypes(); |
||
392 | |||
393 | if (count($string_literals) + count($int_literals) === count($all_atomic_types)) { |
||
394 | foreach ($string_literals as $string_literal) { |
||
395 | $key_values[] = $string_literal->value; |
||
396 | } |
||
397 | |||
398 | foreach ($int_literals as $int_literal) { |
||
399 | $key_values[] = $int_literal->value; |
||
400 | } |
||
401 | } |
||
402 | } |
||
403 | |||
404 | $array_access_type = null; |
||
405 | |||
406 | if ($offset_type->isNull()) { |
||
407 | if (IssueBuffer::accepts( |
||
408 | new NullArrayOffset( |
||
409 | 'Cannot access value on variable ' . $array_var_id . ' using null offset', |
||
410 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
411 | ), |
||
412 | $statements_analyzer->getSuppressedIssues() |
||
413 | )) { |
||
414 | // fall through |
||
415 | } |
||
416 | |||
417 | if ($in_assignment) { |
||
418 | $offset_type->removeType('null'); |
||
419 | $offset_type->addType(new TLiteralInt(0)); |
||
420 | } |
||
421 | } |
||
422 | |||
423 | if ($offset_type->isNullable() && !$context->inside_isset) { |
||
424 | if (!$offset_type->ignore_nullable_issues) { |
||
425 | if (IssueBuffer::accepts( |
||
426 | new PossiblyNullArrayOffset( |
||
427 | 'Cannot access value on variable ' . $array_var_id |
||
428 | . ' using possibly null offset ' . $offset_type, |
||
429 | new CodeLocation($statements_analyzer->getSource(), $stmt->var) |
||
430 | ), |
||
431 | $statements_analyzer->getSuppressedIssues() |
||
432 | )) { |
||
433 | // fall through |
||
434 | } |
||
435 | } |
||
436 | |||
437 | if ($in_assignment) { |
||
438 | $offset_type->removeType('null'); |
||
439 | |||
440 | if (!$offset_type->ignore_nullable_issues) { |
||
441 | $offset_type->addType(new TLiteralInt(0)); |
||
442 | } |
||
443 | } |
||
444 | } |
||
445 | |||
446 | foreach ($array_type->getAtomicTypes() as $type_string => $type) { |
||
447 | $original_type = $type; |
||
448 | |||
449 | if ($type instanceof TMixed || $type instanceof TTemplateParam || $type instanceof TEmpty) { |
||
450 | if (!$type instanceof TTemplateParam || $type->as->isMixed() || !$type->as->isSingle()) { |
||
451 | if (!$context->collect_initializations |
||
452 | && !$context->collect_mutations |
||
453 | && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() |
||
454 | && (!(($parent_source = $statements_analyzer->getSource()) |
||
455 | instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) |
||
456 | || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) |
||
457 | ) { |
||
458 | $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath()); |
||
459 | } |
||
460 | |||
461 | if (!$context->inside_isset) { |
||
462 | if ($in_assignment) { |
||
463 | if (IssueBuffer::accepts( |
||
464 | new MixedArrayAssignment( |
||
465 | 'Cannot access array value on mixed variable ' . $array_var_id, |
||
466 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
467 | ), |
||
468 | $statements_analyzer->getSuppressedIssues() |
||
469 | )) { |
||
470 | // fall through |
||
471 | } |
||
472 | } else { |
||
473 | if (IssueBuffer::accepts( |
||
474 | new MixedArrayAccess( |
||
475 | 'Cannot access array value on mixed variable ' . $array_var_id, |
||
476 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
477 | ), |
||
478 | $statements_analyzer->getSuppressedIssues() |
||
479 | )) { |
||
480 | // fall through |
||
481 | } |
||
482 | } |
||
483 | } |
||
484 | |||
485 | $has_valid_offset = true; |
||
486 | if (!$array_access_type) { |
||
487 | $array_access_type = Type::getMixed( |
||
488 | $type instanceof TEmpty |
||
489 | ); |
||
490 | } else { |
||
491 | $array_access_type = Type::combineUnionTypes( |
||
492 | $array_access_type, |
||
493 | Type::getMixed($type instanceof TEmpty) |
||
494 | ); |
||
495 | } |
||
496 | |||
497 | continue; |
||
498 | } |
||
499 | |||
500 | $type = clone array_values($type->as->getAtomicTypes())[0]; |
||
501 | } |
||
502 | |||
503 | if ($type instanceof TNull) { |
||
504 | if ($array_type->ignore_nullable_issues) { |
||
505 | continue; |
||
506 | } |
||
507 | |||
508 | if ($in_assignment) { |
||
509 | if ($replacement_type) { |
||
510 | if ($array_access_type) { |
||
511 | $array_access_type = Type::combineUnionTypes($array_access_type, $replacement_type); |
||
512 | } else { |
||
513 | $array_access_type = clone $replacement_type; |
||
514 | } |
||
515 | } else { |
||
516 | if (IssueBuffer::accepts( |
||
517 | new PossiblyNullArrayAssignment( |
||
518 | 'Cannot access array value on possibly null variable ' . $array_var_id . |
||
519 | ' of type ' . $array_type, |
||
520 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
521 | ), |
||
522 | $statements_analyzer->getSuppressedIssues() |
||
523 | )) { |
||
524 | // fall through |
||
525 | } |
||
526 | |||
527 | $array_access_type = new Type\Union([new TEmpty]); |
||
528 | } |
||
529 | } else { |
||
530 | if (!$context->inside_isset) { |
||
531 | if (IssueBuffer::accepts( |
||
532 | new PossiblyNullArrayAccess( |
||
533 | 'Cannot access array value on possibly null variable ' . $array_var_id . |
||
534 | ' of type ' . $array_type, |
||
535 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
536 | ), |
||
537 | $statements_analyzer->getSuppressedIssues() |
||
538 | )) { |
||
539 | // fall through |
||
540 | } |
||
541 | } |
||
542 | |||
543 | if ($array_access_type) { |
||
544 | $array_access_type = Type::combineUnionTypes($array_access_type, Type::getNull()); |
||
545 | } else { |
||
546 | $array_access_type = Type::getNull(); |
||
547 | } |
||
548 | } |
||
549 | |||
550 | continue; |
||
551 | } |
||
552 | |||
553 | if ($type instanceof TArray |
||
554 | || $type instanceof ObjectLike |
||
555 | || $type instanceof TList |
||
556 | || $type instanceof TClassStringMap |
||
557 | ) { |
||
558 | $has_array_access = true; |
||
559 | |||
560 | if ($in_assignment |
||
561 | && $type instanceof TArray |
||
562 | && (($type->type_params[0]->isEmpty() && $type->type_params[1]->isEmpty()) |
||
563 | || ($type->type_params[1]->hasMixed() |
||
564 | && count($key_values) === 1 |
||
565 | && \is_string($key_values[0]))) |
||
566 | ) { |
||
567 | $from_empty_array = $type->type_params[0]->isEmpty() && $type->type_params[1]->isEmpty(); |
||
568 | |||
569 | if (count($key_values) === 1) { |
||
570 | $from_mixed_array = $type->type_params[1]->isMixed(); |
||
571 | |||
572 | $previous_key_type = $type->type_params[0]; |
||
573 | $previous_value_type = $type->type_params[1]; |
||
574 | |||
575 | // ok, type becomes an ObjectLike |
||
576 | $array_type->removeType($type_string); |
||
577 | $type = new ObjectLike([ |
||
578 | $key_values[0] => $from_mixed_array ? Type::getMixed() : Type::getEmpty() |
||
579 | ]); |
||
580 | |||
581 | $type->sealed = $from_empty_array; |
||
582 | |||
583 | if (!$from_empty_array) { |
||
584 | $type->previous_value_type = clone $previous_value_type; |
||
585 | $type->previous_key_type = clone $previous_key_type; |
||
586 | } |
||
587 | |||
588 | $array_type->addType($type); |
||
589 | } elseif (!$stmt->dim && $from_empty_array && $replacement_type) { |
||
590 | $array_type->removeType($type_string); |
||
591 | $array_type->addType(new Type\Atomic\TNonEmptyList($replacement_type)); |
||
592 | continue; |
||
593 | } |
||
594 | } elseif ($in_assignment |
||
595 | && $type instanceof ObjectLike |
||
596 | && $type->previous_value_type |
||
597 | && $type->previous_value_type->isMixed() |
||
598 | && count($key_values) === 1 |
||
599 | ) { |
||
600 | $type->properties[$key_values[0]] = Type::getMixed(); |
||
601 | } |
||
602 | |||
603 | $offset_type = self::replaceOffsetTypeWithInts($offset_type); |
||
604 | |||
605 | if ($type instanceof TList |
||
606 | && (($in_assignment && $stmt->dim) |
||
607 | || $original_type instanceof TTemplateParam |
||
608 | || !$offset_type->isInt()) |
||
609 | ) { |
||
610 | $type = new TArray([Type::getInt(), $type->type_param]); |
||
611 | } |
||
612 | |||
613 | if ($type instanceof TArray) { |
||
614 | // if we're assigning to an empty array with a key offset, refashion that array |
||
615 | if ($in_assignment) { |
||
616 | if ($type->type_params[0]->isEmpty()) { |
||
617 | $type->type_params[0] = $offset_type->isMixed() |
||
618 | ? Type::getArrayKey() |
||
619 | : $offset_type; |
||
620 | } |
||
621 | } elseif (!$type->type_params[0]->isEmpty()) { |
||
622 | $expected_offset_type = $type->type_params[0]->hasMixed() |
||
623 | ? new Type\Union([ new TArrayKey ]) |
||
624 | : $type->type_params[0]; |
||
625 | |||
626 | $templated_offset_type = null; |
||
627 | |||
628 | foreach ($offset_type->getAtomicTypes() as $offset_atomic_type) { |
||
629 | if ($offset_atomic_type instanceof TTemplateParam) { |
||
630 | $templated_offset_type = $offset_atomic_type; |
||
631 | } |
||
632 | } |
||
633 | |||
634 | $union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult(); |
||
635 | |||
636 | if ($original_type instanceof TTemplateParam && $templated_offset_type) { |
||
637 | foreach ($templated_offset_type->as->getAtomicTypes() as $offset_as) { |
||
638 | if ($offset_as instanceof Type\Atomic\TTemplateKeyOf |
||
639 | && $offset_as->param_name === $original_type->param_name |
||
640 | && $offset_as->defining_class === $original_type->defining_class |
||
641 | ) { |
||
642 | /** @psalm-suppress PropertyTypeCoercion */ |
||
643 | $type->type_params[1] = new Type\Union([ |
||
644 | new Type\Atomic\TTemplateIndexedAccess( |
||
645 | $offset_as->param_name, |
||
646 | $templated_offset_type->param_name, |
||
647 | $offset_as->defining_class |
||
648 | ) |
||
649 | ]); |
||
650 | |||
651 | $has_valid_offset = true; |
||
652 | } |
||
653 | } |
||
654 | } else { |
||
655 | $offset_type_contained_by_expected = TypeAnalyzer::isContainedBy( |
||
656 | $codebase, |
||
657 | $offset_type, |
||
658 | $expected_offset_type, |
||
659 | true, |
||
660 | $offset_type->ignore_falsable_issues, |
||
661 | $union_comparison_results |
||
662 | ); |
||
663 | |||
664 | if ($codebase->config->ensure_array_string_offsets_exist |
||
665 | && $offset_type_contained_by_expected |
||
666 | ) { |
||
667 | self::checkLiteralStringArrayOffset( |
||
668 | $offset_type, |
||
669 | $expected_offset_type, |
||
670 | $array_var_id, |
||
671 | $stmt, |
||
672 | $context, |
||
673 | $statements_analyzer |
||
674 | ); |
||
675 | } |
||
676 | |||
677 | if ($codebase->config->ensure_array_int_offsets_exist |
||
678 | && $offset_type_contained_by_expected |
||
679 | ) { |
||
680 | self::checkLiteralIntArrayOffset( |
||
681 | $offset_type, |
||
682 | $expected_offset_type, |
||
683 | $array_var_id, |
||
684 | $stmt, |
||
685 | $context, |
||
686 | $statements_analyzer |
||
687 | ); |
||
688 | } |
||
689 | |||
690 | if ((!$offset_type_contained_by_expected |
||
691 | && !$union_comparison_results->type_coerced_from_scalar) |
||
692 | || $union_comparison_results->to_string_cast |
||
693 | ) { |
||
694 | if ($union_comparison_results->type_coerced_from_mixed |
||
695 | && !$offset_type->isMixed() |
||
696 | ) { |
||
697 | if (IssueBuffer::accepts( |
||
698 | new MixedArrayTypeCoercion( |
||
699 | 'Coercion from array offset type \'' . $offset_type->getId() . '\' ' |
||
700 | . 'to the expected type \'' . $expected_offset_type->getId() . '\'', |
||
701 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
702 | ), |
||
703 | $statements_analyzer->getSuppressedIssues() |
||
704 | )) { |
||
705 | // fall through |
||
706 | } |
||
707 | } else { |
||
708 | $expected_offset_types[] = $expected_offset_type->getId(); |
||
709 | } |
||
710 | |||
711 | if (TypeAnalyzer::canExpressionTypesBeIdentical( |
||
712 | $codebase, |
||
713 | $offset_type, |
||
714 | $expected_offset_type |
||
715 | )) { |
||
716 | $has_valid_offset = true; |
||
717 | } |
||
718 | } else { |
||
719 | $has_valid_offset = true; |
||
720 | } |
||
721 | } |
||
722 | } |
||
723 | |||
724 | if (!$stmt->dim && $type instanceof TNonEmptyArray && $type->count !== null) { |
||
725 | $type->count++; |
||
726 | } |
||
727 | |||
728 | if ($in_assignment && $replacement_type) { |
||
729 | /** @psalm-suppress PropertyTypeCoercion */ |
||
730 | $type->type_params[1] = Type::combineUnionTypes( |
||
731 | $type->type_params[1], |
||
732 | $replacement_type, |
||
733 | $codebase |
||
734 | ); |
||
735 | } |
||
736 | |||
737 | if (!$array_access_type) { |
||
738 | $array_access_type = $type->type_params[1]; |
||
739 | } else { |
||
740 | $array_access_type = Type::combineUnionTypes( |
||
741 | $array_access_type, |
||
742 | $type->type_params[1] |
||
743 | ); |
||
744 | } |
||
745 | |||
746 | if ($array_access_type->isEmpty() |
||
747 | && !$array_type->hasMixed() |
||
748 | && !$in_assignment |
||
749 | && !$context->inside_isset |
||
750 | ) { |
||
751 | if (IssueBuffer::accepts( |
||
752 | new EmptyArrayAccess( |
||
753 | 'Cannot access value on empty array variable ' . $array_var_id, |
||
754 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
755 | ), |
||
756 | $statements_analyzer->getSuppressedIssues() |
||
757 | )) { |
||
758 | return Type::getMixed(true); |
||
759 | } |
||
760 | |||
761 | if (!IssueBuffer::isRecording()) { |
||
762 | $array_access_type = Type::getMixed(true); |
||
763 | } |
||
764 | } |
||
765 | } elseif ($type instanceof TList) { |
||
766 | // if we're assigning to an empty array with a key offset, refashion that array |
||
767 | if (!$in_assignment) { |
||
768 | if (!$type instanceof TNonEmptyList |
||
769 | || (count($key_values) === 1 |
||
770 | && is_int($key_values[0]) |
||
771 | && $key_values[0] > 0 |
||
772 | && $key_values[0] > ($type->count - 1)) |
||
773 | ) { |
||
774 | $expected_offset_type = Type::getInt(); |
||
775 | |||
776 | if ($codebase->config->ensure_array_int_offsets_exist) { |
||
777 | self::checkLiteralIntArrayOffset( |
||
778 | $offset_type, |
||
779 | $expected_offset_type, |
||
780 | $array_var_id, |
||
781 | $stmt, |
||
782 | $context, |
||
783 | $statements_analyzer |
||
784 | ); |
||
785 | } |
||
786 | } |
||
787 | |||
788 | $has_valid_offset = true; |
||
789 | } |
||
790 | |||
791 | if ($in_assignment && $type instanceof Type\Atomic\TNonEmptyList && $type->count !== null) { |
||
792 | $type->count++; |
||
793 | } |
||
794 | |||
795 | if ($in_assignment && $replacement_type) { |
||
796 | $type->type_param = Type::combineUnionTypes( |
||
797 | $type->type_param, |
||
798 | $replacement_type, |
||
799 | $codebase |
||
800 | ); |
||
801 | } |
||
802 | |||
803 | if (!$array_access_type) { |
||
804 | $array_access_type = $type->type_param; |
||
805 | } else { |
||
806 | $array_access_type = Type::combineUnionTypes( |
||
807 | $array_access_type, |
||
808 | $type->type_param |
||
809 | ); |
||
810 | } |
||
811 | } elseif ($type instanceof TClassStringMap) { |
||
812 | $offset_type_parts = array_values($offset_type->getAtomicTypes()); |
||
813 | |||
814 | foreach ($offset_type_parts as $offset_type_part) { |
||
815 | if ($offset_type_part instanceof Type\Atomic\TClassString) { |
||
816 | if ($offset_type_part instanceof Type\Atomic\TTemplateParamClass) { |
||
817 | $template_result_get = new TemplateResult( |
||
818 | [], |
||
819 | [ |
||
820 | $type->param_name => [ |
||
821 | 'class-string-map' => [ |
||
822 | new Type\Union([ |
||
823 | new TTemplateParam( |
||
824 | $offset_type_part->param_name, |
||
825 | $offset_type_part->as_type |
||
826 | ? new Type\Union([$offset_type_part->as_type]) |
||
827 | : Type::getObject(), |
||
828 | $offset_type_part->defining_class |
||
829 | ) |
||
830 | ]) |
||
831 | ] |
||
832 | ] |
||
833 | ] |
||
834 | ); |
||
835 | |||
836 | $template_result_set = new TemplateResult( |
||
837 | [], |
||
838 | [ |
||
839 | $offset_type_part->param_name => [ |
||
840 | $offset_type_part->defining_class => [ |
||
841 | new Type\Union([ |
||
842 | new TTemplateParam( |
||
843 | $type->param_name, |
||
844 | $type->as_type |
||
845 | ? new Type\Union([$type->as_type]) |
||
846 | : Type::getObject(), |
||
847 | 'class-string-map' |
||
848 | ) |
||
849 | ]) |
||
850 | ] |
||
851 | ] |
||
852 | ] |
||
853 | ); |
||
854 | } else { |
||
855 | $template_result_get = new TemplateResult( |
||
856 | [], |
||
857 | [ |
||
858 | $type->param_name => [ |
||
859 | 'class-string-map' => [ |
||
860 | new Type\Union([ |
||
861 | $offset_type_part->as_type |
||
862 | ?: new Type\Atomic\TObject() |
||
863 | ]) |
||
864 | ] |
||
865 | ] |
||
866 | ] |
||
867 | ); |
||
868 | $template_result_set = new TemplateResult( |
||
869 | [], |
||
870 | [] |
||
871 | ); |
||
872 | } |
||
873 | |||
874 | $expected_value_param_get = clone $type->value_param; |
||
875 | |||
876 | $expected_value_param_get->replaceTemplateTypesWithArgTypes( |
||
877 | $template_result_get, |
||
878 | $codebase |
||
879 | ); |
||
880 | |||
881 | if ($replacement_type) { |
||
882 | $expected_value_param_set = clone $type->value_param; |
||
883 | |||
884 | $replacement_type->replaceTemplateTypesWithArgTypes( |
||
885 | $template_result_set, |
||
886 | $codebase |
||
887 | ); |
||
888 | |||
889 | $type->value_param = Type::combineUnionTypes( |
||
890 | $replacement_type, |
||
891 | $expected_value_param_set, |
||
892 | $codebase |
||
893 | ); |
||
894 | } |
||
895 | |||
896 | if (!$array_access_type) { |
||
897 | $array_access_type = $expected_value_param_get; |
||
898 | } else { |
||
899 | $array_access_type = Type::combineUnionTypes( |
||
900 | $array_access_type, |
||
901 | $expected_value_param_get, |
||
902 | $codebase |
||
903 | ); |
||
904 | } |
||
905 | } |
||
906 | } |
||
907 | } else { |
||
908 | $generic_key_type = $type->getGenericKeyType(); |
||
909 | |||
910 | if (!$stmt->dim && $type->sealed && $type->is_list) { |
||
911 | $key_values[] = count($type->properties); |
||
912 | } |
||
913 | |||
914 | if ($key_values) { |
||
915 | foreach ($key_values as $key_value) { |
||
916 | if (isset($type->properties[$key_value]) || $replacement_type) { |
||
917 | $has_valid_offset = true; |
||
918 | |||
919 | if ($replacement_type) { |
||
920 | if (isset($type->properties[$key_value])) { |
||
921 | $type->properties[$key_value] = Type::combineUnionTypes( |
||
922 | $type->properties[$key_value], |
||
923 | $replacement_type |
||
924 | ); |
||
925 | } else { |
||
926 | $type->properties[$key_value] = $replacement_type; |
||
927 | } |
||
928 | } |
||
929 | |||
930 | if (!$array_access_type) { |
||
931 | $array_access_type = clone $type->properties[$key_value]; |
||
932 | } else { |
||
933 | $array_access_type = Type::combineUnionTypes( |
||
934 | $array_access_type, |
||
935 | $type->properties[$key_value] |
||
936 | ); |
||
937 | } |
||
938 | } elseif ($in_assignment) { |
||
939 | $type->properties[$key_value] = new Type\Union([new TEmpty]); |
||
940 | |||
941 | if (!$array_access_type) { |
||
942 | $array_access_type = clone $type->properties[$key_value]; |
||
943 | } else { |
||
944 | $array_access_type = Type::combineUnionTypes( |
||
945 | $array_access_type, |
||
946 | $type->properties[$key_value] |
||
947 | ); |
||
948 | } |
||
949 | } elseif ($type->previous_value_type) { |
||
950 | if ($codebase->config->ensure_array_string_offsets_exist) { |
||
951 | self::checkLiteralStringArrayOffset( |
||
952 | $offset_type, |
||
953 | $type->getGenericKeyType(), |
||
954 | $array_var_id, |
||
955 | $stmt, |
||
956 | $context, |
||
957 | $statements_analyzer |
||
958 | ); |
||
959 | } |
||
960 | |||
961 | if ($codebase->config->ensure_array_int_offsets_exist) { |
||
962 | self::checkLiteralIntArrayOffset( |
||
963 | $offset_type, |
||
964 | $type->getGenericKeyType(), |
||
965 | $array_var_id, |
||
966 | $stmt, |
||
967 | $context, |
||
968 | $statements_analyzer |
||
969 | ); |
||
970 | } |
||
971 | |||
972 | $type->properties[$key_value] = clone $type->previous_value_type; |
||
973 | |||
974 | $array_access_type = clone $type->previous_value_type; |
||
975 | } elseif ($array_type->hasMixed()) { |
||
976 | $has_valid_offset = true; |
||
977 | |||
978 | $array_access_type = Type::getMixed(); |
||
979 | } else { |
||
980 | if ($type->sealed || !$context->inside_isset) { |
||
981 | $object_like_keys = array_keys($type->properties); |
||
982 | |||
983 | if (count($object_like_keys) === 1) { |
||
984 | $expected_keys_string = '\'' . $object_like_keys[0] . '\''; |
||
985 | } else { |
||
986 | $last_key = array_pop($object_like_keys); |
||
987 | $expected_keys_string = '\'' . implode('\', \'', $object_like_keys) . |
||
988 | '\' or \'' . $last_key . '\''; |
||
989 | } |
||
990 | |||
991 | $expected_offset_types[] = $expected_keys_string; |
||
992 | } |
||
993 | |||
994 | $array_access_type = Type::getMixed(); |
||
995 | } |
||
996 | } |
||
997 | } else { |
||
998 | $key_type = $generic_key_type->hasMixed() |
||
999 | ? Type::getArrayKey() |
||
1000 | : $generic_key_type; |
||
1001 | |||
1002 | $union_comparison_results = new \Psalm\Internal\Analyzer\TypeComparisonResult(); |
||
1003 | |||
1004 | $is_contained = TypeAnalyzer::isContainedBy( |
||
1005 | $codebase, |
||
1006 | $offset_type, |
||
1007 | $key_type, |
||
1008 | true, |
||
1009 | $offset_type->ignore_falsable_issues, |
||
1010 | $union_comparison_results |
||
1011 | ); |
||
1012 | |||
1013 | if ($context->inside_isset && !$is_contained) { |
||
1014 | $is_contained = TypeAnalyzer::isContainedBy( |
||
1015 | $codebase, |
||
1016 | $key_type, |
||
1017 | $offset_type, |
||
1018 | true, |
||
1019 | $offset_type->ignore_falsable_issues |
||
1020 | ) |
||
1021 | || TypeAnalyzer::canBeContainedBy( |
||
1022 | $codebase, |
||
1023 | $offset_type, |
||
1024 | $key_type, |
||
1025 | true, |
||
1026 | $offset_type->ignore_falsable_issues |
||
1027 | ); |
||
1028 | } |
||
1029 | |||
1030 | if (($is_contained |
||
1031 | || $union_comparison_results->type_coerced_from_scalar |
||
1032 | || $union_comparison_results->type_coerced_from_mixed |
||
1033 | || $in_assignment) |
||
1034 | && !$union_comparison_results->to_string_cast |
||
1035 | ) { |
||
1036 | if ($replacement_type) { |
||
1037 | $generic_params = Type::combineUnionTypes( |
||
1038 | $type->getGenericValueType(), |
||
1039 | $replacement_type |
||
1040 | ); |
||
1041 | |||
1042 | $new_key_type = Type::combineUnionTypes( |
||
1043 | $generic_key_type, |
||
1044 | $offset_type->isMixed() ? Type::getArrayKey() : $offset_type |
||
1045 | ); |
||
1046 | |||
1047 | $property_count = $type->sealed ? count($type->properties) : null; |
||
1048 | |||
1049 | if (!$stmt->dim && $property_count) { |
||
1050 | ++$property_count; |
||
1051 | $array_type->removeType($type_string); |
||
1052 | $type = new TNonEmptyArray([ |
||
1053 | $new_key_type, |
||
1054 | $generic_params, |
||
1055 | ]); |
||
1056 | $array_type->addType($type); |
||
1057 | $type->count = $property_count; |
||
1058 | } else { |
||
1059 | $array_type->removeType($type_string); |
||
1060 | $type = new TArray([ |
||
1061 | $new_key_type, |
||
1062 | $generic_params, |
||
1063 | ]); |
||
1064 | $array_type->addType($type); |
||
1065 | } |
||
1066 | |||
1067 | if (!$array_access_type) { |
||
1068 | $array_access_type = clone $generic_params; |
||
1069 | } else { |
||
1070 | $array_access_type = Type::combineUnionTypes( |
||
1071 | $array_access_type, |
||
1072 | $generic_params |
||
1073 | ); |
||
1074 | } |
||
1075 | } else { |
||
1076 | if (!$array_access_type) { |
||
1077 | $array_access_type = $type->getGenericValueType(); |
||
1078 | } else { |
||
1079 | $array_access_type = Type::combineUnionTypes( |
||
1080 | $array_access_type, |
||
1081 | $type->getGenericValueType() |
||
1082 | ); |
||
1083 | } |
||
1084 | } |
||
1085 | |||
1086 | $has_valid_offset = true; |
||
1087 | } else { |
||
1088 | if (!$context->inside_isset |
||
1089 | || ($type->sealed && !$union_comparison_results->type_coerced) |
||
1090 | ) { |
||
1091 | $expected_offset_types[] = $generic_key_type->getId(); |
||
1092 | } |
||
1093 | |||
1094 | $array_access_type = Type::getMixed(); |
||
1095 | } |
||
1096 | } |
||
1097 | } |
||
1098 | continue; |
||
1099 | } |
||
1100 | |||
1101 | if ($type instanceof TString) { |
||
1102 | if ($in_assignment && $replacement_type) { |
||
1103 | if ($replacement_type->hasMixed()) { |
||
1104 | if (!$context->collect_initializations |
||
1105 | && !$context->collect_mutations |
||
1106 | && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() |
||
1107 | && (!(($parent_source = $statements_analyzer->getSource()) |
||
1108 | instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) |
||
1109 | || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) |
||
1110 | ) { |
||
1111 | $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath()); |
||
1112 | } |
||
1113 | |||
1114 | if (IssueBuffer::accepts( |
||
1115 | new MixedStringOffsetAssignment( |
||
1116 | 'Right-hand-side of string offset assignment cannot be mixed', |
||
1117 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
1118 | ), |
||
1119 | $statements_analyzer->getSuppressedIssues() |
||
1120 | )) { |
||
1121 | // fall through |
||
1122 | } |
||
1123 | } else { |
||
1124 | if (!$context->collect_initializations |
||
1125 | && !$context->collect_mutations |
||
1126 | && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() |
||
1127 | && (!(($parent_source = $statements_analyzer->getSource()) |
||
1128 | instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) |
||
1129 | || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) |
||
1130 | ) { |
||
1131 | $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath()); |
||
1132 | } |
||
1133 | } |
||
1134 | } |
||
1135 | |||
1136 | if ($type instanceof TSingleLetter) { |
||
1137 | $valid_offset_type = Type::getInt(false, 0); |
||
1138 | } elseif ($type instanceof TLiteralString) { |
||
1139 | if (!strlen($type->value)) { |
||
1140 | $valid_offset_type = Type::getEmpty(); |
||
1141 | } elseif (strlen($type->value) < 10) { |
||
1142 | $valid_offsets = []; |
||
1143 | |||
1144 | for ($i = -strlen($type->value), $l = strlen($type->value); $i < $l; $i++) { |
||
1145 | $valid_offsets[] = new TLiteralInt($i); |
||
1146 | } |
||
1147 | |||
1148 | if (!$valid_offsets) { |
||
1149 | throw new \UnexpectedValueException('This is weird'); |
||
1150 | } |
||
1151 | |||
1152 | $valid_offset_type = new Type\Union($valid_offsets); |
||
1153 | } else { |
||
1154 | $valid_offset_type = Type::getInt(); |
||
1155 | } |
||
1156 | } else { |
||
1157 | $valid_offset_type = Type::getInt(); |
||
1158 | } |
||
1159 | |||
1160 | if (!TypeAnalyzer::isContainedBy( |
||
1161 | $codebase, |
||
1162 | $offset_type, |
||
1163 | $valid_offset_type, |
||
1164 | true |
||
1165 | )) { |
||
1166 | $expected_offset_types[] = $valid_offset_type->getId(); |
||
1167 | |||
1168 | $array_access_type = Type::getMixed(); |
||
1169 | } else { |
||
1170 | $has_valid_offset = true; |
||
1171 | |||
1172 | if (!$array_access_type) { |
||
1173 | $array_access_type = Type::getSingleLetter(); |
||
1174 | } else { |
||
1175 | $array_access_type = Type::combineUnionTypes( |
||
1176 | $array_access_type, |
||
1177 | Type::getSingleLetter() |
||
1178 | ); |
||
1179 | } |
||
1180 | } |
||
1181 | |||
1182 | continue; |
||
1183 | } |
||
1184 | |||
1185 | if (!$context->collect_initializations |
||
1186 | && !$context->collect_mutations |
||
1187 | && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() |
||
1188 | && (!(($parent_source = $statements_analyzer->getSource()) |
||
1189 | instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) |
||
1190 | || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) |
||
1191 | ) { |
||
1192 | $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath()); |
||
1193 | } |
||
1194 | |||
1195 | if ($type instanceof Type\Atomic\TFalse && $array_type->ignore_falsable_issues) { |
||
1196 | continue; |
||
1197 | } |
||
1198 | |||
1199 | if ($type instanceof TNamedObject) { |
||
1200 | if (strtolower($type->value) === 'simplexmlelement') { |
||
1201 | $call_array_access_type = new Type\Union([new TNamedObject('SimpleXMLElement')]); |
||
1202 | } elseif (strtolower($type->value) === 'domnodelist' && $stmt->dim) { |
||
1203 | $old_data_provider = $statements_analyzer->node_data; |
||
1204 | |||
1205 | $statements_analyzer->node_data = clone $statements_analyzer->node_data; |
||
1206 | |||
1207 | $fake_method_call = new PhpParser\Node\Expr\MethodCall( |
||
1208 | $stmt->var, |
||
1209 | new PhpParser\Node\Identifier('item', $stmt->var->getAttributes()), |
||
1210 | [ |
||
1211 | new PhpParser\Node\Arg($stmt->dim) |
||
1212 | ] |
||
1213 | ); |
||
1214 | |||
1215 | $suppressed_issues = $statements_analyzer->getSuppressedIssues(); |
||
1216 | |||
1217 | if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) { |
||
1218 | $statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']); |
||
1219 | } |
||
1220 | |||
1221 | if (!in_array('MixedMethodCall', $suppressed_issues, true)) { |
||
1222 | $statements_analyzer->addSuppressedIssues(['MixedMethodCall']); |
||
1223 | } |
||
1224 | |||
1225 | \Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze( |
||
1226 | $statements_analyzer, |
||
1227 | $fake_method_call, |
||
1228 | $context |
||
1229 | ); |
||
1230 | |||
1231 | if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) { |
||
1232 | $statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']); |
||
1233 | } |
||
1234 | |||
1235 | if (!in_array('MixedMethodCall', $suppressed_issues, true)) { |
||
1236 | $statements_analyzer->removeSuppressedIssues(['MixedMethodCall']); |
||
1237 | } |
||
1238 | |||
1239 | $call_array_access_type = $statements_analyzer->node_data->getType( |
||
1240 | $fake_method_call |
||
1241 | ) ?: Type::getMixed(); |
||
1242 | |||
1243 | $statements_analyzer->node_data = $old_data_provider; |
||
1244 | } else { |
||
1245 | $suppressed_issues = $statements_analyzer->getSuppressedIssues(); |
||
1246 | |||
1247 | if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) { |
||
1248 | $statements_analyzer->addSuppressedIssues(['PossiblyInvalidMethodCall']); |
||
1249 | } |
||
1250 | |||
1251 | if (!in_array('MixedMethodCall', $suppressed_issues, true)) { |
||
1252 | $statements_analyzer->addSuppressedIssues(['MixedMethodCall']); |
||
1253 | } |
||
1254 | |||
1255 | if ($in_assignment) { |
||
1256 | $old_node_data = $statements_analyzer->node_data; |
||
1257 | |||
1258 | $statements_analyzer->node_data = clone $statements_analyzer->node_data; |
||
1259 | |||
1260 | $fake_set_method_call = new PhpParser\Node\Expr\MethodCall( |
||
1261 | $stmt->var, |
||
1262 | new PhpParser\Node\Identifier('offsetSet', $stmt->var->getAttributes()), |
||
1263 | [ |
||
1264 | new PhpParser\Node\Arg( |
||
1265 | $stmt->dim |
||
1266 | ? $stmt->dim |
||
1267 | : new PhpParser\Node\Expr\ConstFetch( |
||
1268 | new PhpParser\Node\Name('null'), |
||
1269 | $stmt->var->getAttributes() |
||
1270 | ) |
||
1271 | ), |
||
1272 | new PhpParser\Node\Arg( |
||
1273 | $assign_value |
||
1274 | ?: new PhpParser\Node\Expr\ConstFetch( |
||
1275 | new PhpParser\Node\Name('null'), |
||
1276 | $stmt->var->getAttributes() |
||
1277 | ) |
||
1278 | ), |
||
1279 | ] |
||
1280 | ); |
||
1281 | |||
1282 | \Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze( |
||
1283 | $statements_analyzer, |
||
1284 | $fake_set_method_call, |
||
1285 | $context |
||
1286 | ); |
||
1287 | |||
1288 | $statements_analyzer->node_data = $old_node_data; |
||
1289 | } |
||
1290 | |||
1291 | if ($stmt->dim) { |
||
1292 | $old_node_data = $statements_analyzer->node_data; |
||
1293 | |||
1294 | $statements_analyzer->node_data = clone $statements_analyzer->node_data; |
||
1295 | |||
1296 | $fake_get_method_call = new PhpParser\Node\Expr\MethodCall( |
||
1297 | $stmt->var, |
||
1298 | new PhpParser\Node\Identifier('offsetGet', $stmt->var->getAttributes()), |
||
1299 | [ |
||
1300 | new PhpParser\Node\Arg( |
||
1301 | $stmt->dim |
||
1302 | ) |
||
1303 | ] |
||
1304 | ); |
||
1305 | |||
1306 | \Psalm\Internal\Analyzer\Statements\Expression\Call\MethodCallAnalyzer::analyze( |
||
1307 | $statements_analyzer, |
||
1308 | $fake_get_method_call, |
||
1309 | $context |
||
1310 | ); |
||
1311 | |||
1312 | $call_array_access_type = $statements_analyzer->node_data->getType($fake_get_method_call) |
||
1313 | ?: Type::getMixed(); |
||
1314 | |||
1315 | $statements_analyzer->node_data = $old_node_data; |
||
1316 | } else { |
||
1317 | $call_array_access_type = Type::getVoid(); |
||
1318 | } |
||
1319 | |||
1320 | $has_array_access = true; |
||
1321 | |||
1322 | if (!in_array('PossiblyInvalidMethodCall', $suppressed_issues, true)) { |
||
1323 | $statements_analyzer->removeSuppressedIssues(['PossiblyInvalidMethodCall']); |
||
1324 | } |
||
1325 | |||
1326 | if (!in_array('MixedMethodCall', $suppressed_issues, true)) { |
||
1327 | $statements_analyzer->removeSuppressedIssues(['MixedMethodCall']); |
||
1328 | } |
||
1329 | } |
||
1330 | |||
1331 | if (!$array_access_type) { |
||
1332 | $array_access_type = $call_array_access_type; |
||
1333 | } else { |
||
1334 | $array_access_type = Type::combineUnionTypes( |
||
1335 | $array_access_type, |
||
1336 | $call_array_access_type |
||
1337 | ); |
||
1338 | } |
||
1339 | } elseif (!$array_type->hasMixed()) { |
||
1340 | $non_array_types[] = (string)$type; |
||
1341 | } |
||
1342 | } |
||
1343 | |||
1344 | if ($non_array_types) { |
||
1345 | if ($has_array_access) { |
||
1346 | if ($in_assignment) { |
||
1347 | if (IssueBuffer::accepts( |
||
1348 | new PossiblyInvalidArrayAssignment( |
||
1349 | 'Cannot access array value on non-array variable ' . |
||
1350 | $array_var_id . ' of type ' . $non_array_types[0], |
||
1351 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
1352 | ), |
||
1353 | $statements_analyzer->getSuppressedIssues() |
||
1354 | ) |
||
1355 | ) { |
||
1356 | // do nothing |
||
1357 | } |
||
1358 | } elseif (!$context->inside_isset) { |
||
1359 | if (IssueBuffer::accepts( |
||
1360 | new PossiblyInvalidArrayAccess( |
||
1361 | 'Cannot access array value on non-array variable ' . |
||
1362 | $array_var_id . ' of type ' . $non_array_types[0], |
||
1363 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
1364 | ), |
||
1365 | $statements_analyzer->getSuppressedIssues() |
||
1366 | ) |
||
1367 | ) { |
||
1368 | // do nothing |
||
1369 | } |
||
1370 | } |
||
1371 | } else { |
||
1372 | if ($in_assignment) { |
||
1373 | if (IssueBuffer::accepts( |
||
1374 | new InvalidArrayAssignment( |
||
1375 | 'Cannot access array value on non-array variable ' . |
||
1376 | $array_var_id . ' of type ' . $non_array_types[0], |
||
1377 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
1378 | ), |
||
1379 | $statements_analyzer->getSuppressedIssues() |
||
1380 | )) { |
||
1381 | // fall through |
||
1382 | } |
||
1383 | } else { |
||
1384 | if (IssueBuffer::accepts( |
||
1385 | new InvalidArrayAccess( |
||
1386 | 'Cannot access array value on non-array variable ' . |
||
1387 | $array_var_id . ' of type ' . $non_array_types[0], |
||
1388 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
1389 | ), |
||
1390 | $statements_analyzer->getSuppressedIssues() |
||
1391 | )) { |
||
1392 | // fall through |
||
1393 | } |
||
1394 | } |
||
1395 | |||
1396 | $array_access_type = Type::getMixed(); |
||
1397 | } |
||
1398 | } |
||
1399 | |||
1400 | if ($offset_type->hasMixed()) { |
||
1401 | if (!$context->collect_initializations |
||
1402 | && !$context->collect_mutations |
||
1403 | && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() |
||
1404 | && (!(($parent_source = $statements_analyzer->getSource()) |
||
1405 | instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) |
||
1406 | || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) |
||
1407 | ) { |
||
1408 | $codebase->analyzer->incrementMixedCount($statements_analyzer->getFilePath()); |
||
1409 | } |
||
1410 | |||
1411 | if (IssueBuffer::accepts( |
||
1412 | new MixedArrayOffset( |
||
1413 | 'Cannot access value on variable ' . $array_var_id . ' using mixed offset', |
||
1414 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
1415 | ), |
||
1416 | $statements_analyzer->getSuppressedIssues() |
||
1417 | )) { |
||
1418 | // fall through |
||
1419 | } |
||
1420 | } else { |
||
1421 | if (!$context->collect_initializations |
||
1422 | && !$context->collect_mutations |
||
1423 | && $statements_analyzer->getFilePath() === $statements_analyzer->getRootFilePath() |
||
1424 | && (!(($parent_source = $statements_analyzer->getSource()) |
||
1425 | instanceof \Psalm\Internal\Analyzer\FunctionLikeAnalyzer) |
||
1426 | || !$parent_source->getSource() instanceof \Psalm\Internal\Analyzer\TraitAnalyzer) |
||
1427 | ) { |
||
1428 | $codebase->analyzer->incrementNonMixedCount($statements_analyzer->getFilePath()); |
||
1429 | } |
||
1430 | |||
1431 | if ($expected_offset_types) { |
||
1432 | $invalid_offset_type = $expected_offset_types[0]; |
||
1433 | |||
1434 | $used_offset = 'using a ' . $offset_type->getId() . ' offset'; |
||
1435 | |||
1436 | if ($key_values) { |
||
1437 | $used_offset = 'using offset value of ' |
||
1438 | . (is_int($key_values[0]) ? $key_values[0] : '\'' . $key_values[0] . '\''); |
||
1439 | } |
||
1440 | |||
1441 | if ($has_valid_offset && $context->inside_isset) { |
||
1442 | // do nothing |
||
1443 | } elseif ($has_valid_offset) { |
||
1444 | if (!$context->inside_unset) { |
||
1445 | if (IssueBuffer::accepts( |
||
1446 | new PossiblyInvalidArrayOffset( |
||
1447 | 'Cannot access value on variable ' . $array_var_id . ' ' . $used_offset |
||
1448 | . ', expecting ' . $invalid_offset_type, |
||
1449 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
1450 | ), |
||
1451 | $statements_analyzer->getSuppressedIssues() |
||
1452 | )) { |
||
1453 | // fall through |
||
1454 | } |
||
1455 | } |
||
1456 | } else { |
||
1457 | if (IssueBuffer::accepts( |
||
1458 | new InvalidArrayOffset( |
||
1459 | 'Cannot access value on variable ' . $array_var_id . ' ' . $used_offset |
||
1460 | . ', expecting ' . $invalid_offset_type, |
||
1461 | new CodeLocation($statements_analyzer->getSource(), $stmt) |
||
1462 | ), |
||
1463 | $statements_analyzer->getSuppressedIssues() |
||
1464 | )) { |
||
1465 | // fall through |
||
1466 | } |
||
1467 | } |
||
1468 | } |
||
1469 | } |
||
1470 | |||
1471 | if ($array_access_type === null) { |
||
1472 | // shouldn’t happen, but don’t crash |
||
1473 | return Type::getMixed(); |
||
1474 | } |
||
1475 | |||
1476 | if ($in_assignment) { |
||
1477 | $array_type->bustCache(); |
||
1478 | } |
||
1479 | |||
1480 | return $array_access_type; |
||
1481 | } |
||
1482 | |||
1625 |