|
13 | 13 | use PhpParser\Node\Stmt\Return_; |
14 | 14 | use PHPStan\Analyser\Scope; |
15 | 15 | use PHPStan\BetterReflection\Reflection\Adapter\ReflectionClass; |
| 16 | +use PHPStan\BetterReflection\Reflection\Adapter\ReflectionEnum; |
16 | 17 | use PHPStan\BetterReflection\Reflection\Adapter\ReflectionMethod; |
17 | 18 | use PHPStan\BetterReflection\Reflection\Adapter\ReflectionProperty; |
18 | 19 | use PHPStan\BetterReflection\Reflector\Exception\IdentifierNotFound; |
|
30 | 31 | use RecursiveDirectoryIterator; |
31 | 32 | use RecursiveIteratorIterator; |
32 | 33 | use ReflectionAttribute; |
33 | | -use ReflectionEnum; |
34 | 34 | use ReflectionNamedType; |
35 | 35 | use Reflector; |
36 | 36 | use ShipMonk\PHPStan\DeadCode\Enum\AccessType; |
@@ -162,6 +162,7 @@ public function getUsages( |
162 | 162 | $usages = [ |
163 | 163 | ...$usages, |
164 | 164 | ...$this->getMethodUsagesFromAttributeReflection($node, $scope), |
| 165 | + ...$this->getMapInputUsages($node), |
165 | 166 | ]; |
166 | 167 | } |
167 | 168 |
|
@@ -644,6 +645,123 @@ private function getMethodUsagesFromAttributeReflection( |
644 | 645 | return $usages; |
645 | 646 | } |
646 | 647 |
|
| 648 | + /** |
| 649 | + * @return list<ClassMethodUsage|ClassPropertyUsage> |
| 650 | + */ |
| 651 | + private function getMapInputUsages(InClassMethodNode $node): array |
| 652 | + { |
| 653 | + if ($node->getMethodReflection()->getName() !== '__invoke') { |
| 654 | + return []; |
| 655 | + } |
| 656 | + |
| 657 | + $nativeReflection = $node->getClassReflection()->getNativeReflection(); |
| 658 | + |
| 659 | + $isCommand = $this->hasAttribute($nativeReflection, 'Symfony\Component\Console\Attribute\AsCommand') |
| 660 | + || $nativeReflection->isSubclassOf('Symfony\Component\Console\Command\Command'); |
| 661 | + |
| 662 | + if (!$isCommand) { |
| 663 | + return []; |
| 664 | + } |
| 665 | + |
| 666 | + $usages = []; |
| 667 | + |
| 668 | + foreach ($node->getMethodReflection()->getParameters() as $parameter) { |
| 669 | + $isMapInput = false; |
| 670 | + |
| 671 | + foreach ($parameter->getAttributes() as $attributeReflection) { |
| 672 | + if ($attributeReflection->getName() === 'Symfony\Component\Console\Attribute\MapInput') { |
| 673 | + $isMapInput = true; |
| 674 | + break; |
| 675 | + } |
| 676 | + } |
| 677 | + |
| 678 | + if (!$isMapInput) { |
| 679 | + continue; |
| 680 | + } |
| 681 | + |
| 682 | + $parameterType = $parameter->getType(); |
| 683 | + |
| 684 | + if (!$parameterType->isObject()->yes()) { |
| 685 | + continue; |
| 686 | + } |
| 687 | + |
| 688 | + foreach ($parameterType->getObjectClassNames() as $dtoClassName) { |
| 689 | + $usages = [...$usages, ...$this->collectMapInputDtoUsages($dtoClassName)]; |
| 690 | + } |
| 691 | + } |
| 692 | + |
| 693 | + return $usages; |
| 694 | + } |
| 695 | + |
| 696 | + /** |
| 697 | + * @param array<string, true> $visited |
| 698 | + * @return list<ClassMethodUsage|ClassPropertyUsage> |
| 699 | + */ |
| 700 | + private function collectMapInputDtoUsages( |
| 701 | + string $dtoClassName, |
| 702 | + array &$visited = [], |
| 703 | + ): array |
| 704 | + { |
| 705 | + if (isset($visited[$dtoClassName])) { |
| 706 | + return []; |
| 707 | + } |
| 708 | + |
| 709 | + $visited[$dtoClassName] = true; |
| 710 | + |
| 711 | + if (!$this->reflectionProvider->hasClass($dtoClassName)) { |
| 712 | + return []; |
| 713 | + } |
| 714 | + |
| 715 | + $dtoReflection = $this->reflectionProvider->getClass($dtoClassName); |
| 716 | + $nativeReflection = $dtoReflection->getNativeReflection(); |
| 717 | + $note = 'Console input DTO via #[MapInput]'; |
| 718 | + $usages = []; |
| 719 | + |
| 720 | + foreach ($nativeReflection->getProperties() as $property) { |
| 721 | + if ($property->getDeclaringClass()->getName() !== $dtoClassName) { |
| 722 | + continue; |
| 723 | + } |
| 724 | + |
| 725 | + $isInputProperty = $this->hasAttribute($property, 'Symfony\Component\Console\Attribute\Argument') |
| 726 | + || $this->hasAttribute($property, 'Symfony\Component\Console\Attribute\Option'); |
| 727 | + $nestedMapInput = $this->hasAttribute($property, 'Symfony\Component\Console\Attribute\MapInput'); |
| 728 | + |
| 729 | + if (!$isInputProperty && !$nestedMapInput) { |
| 730 | + continue; |
| 731 | + } |
| 732 | + |
| 733 | + $usages[] = $this->createPropertyUsage($property, $note, AccessType::WRITE); |
| 734 | + $usages[] = $this->createPropertyUsage($property, $note, AccessType::READ); |
| 735 | + |
| 736 | + if (!$nestedMapInput) { |
| 737 | + continue; |
| 738 | + } |
| 739 | + |
| 740 | + $propertyType = $property->getType(); |
| 741 | + |
| 742 | + if (!$propertyType instanceof ReflectionNamedType || $propertyType->isBuiltin()) { |
| 743 | + continue; |
| 744 | + } |
| 745 | + |
| 746 | + $usages = [...$usages, ...$this->collectMapInputDtoUsages($propertyType->getName(), $visited)]; |
| 747 | + } |
| 748 | + |
| 749 | + foreach ($nativeReflection->getMethods() as $dtoMethod) { |
| 750 | + if ($dtoMethod->getDeclaringClass()->getName() !== $dtoClassName) { |
| 751 | + continue; |
| 752 | + } |
| 753 | + |
| 754 | + if ($this->hasAttribute($dtoMethod, 'Symfony\Component\Console\Attribute\Interact')) { |
| 755 | + $usages[] = new ClassMethodUsage( |
| 756 | + UsageOrigin::createVirtual($this, VirtualUsageData::withNote($note)), |
| 757 | + new ClassMethodRef($dtoClassName, $dtoMethod->getName(), possibleDescendant: false), |
| 758 | + ); |
| 759 | + } |
| 760 | + } |
| 761 | + |
| 762 | + return $usages; |
| 763 | + } |
| 764 | + |
647 | 765 | private function shouldMarkAsUsed(ReflectionMethod $method): ?string |
648 | 766 | { |
649 | 767 | if ($this->isBundleConstructor($method)) { |
@@ -1325,7 +1443,7 @@ private function isProbablySymfonyListener(ReflectionMethod $method): bool |
1325 | 1443 | } |
1326 | 1444 |
|
1327 | 1445 | /** |
1328 | | - * @param ReflectionClass|ReflectionMethod|ReflectionProperty $classOrMethod |
| 1446 | + * @param ReflectionClass|ReflectionMethod|ReflectionProperty|ReflectionEnum $classOrMethod |
1329 | 1447 | * @param ReflectionAttribute::IS_*|0 $flags |
1330 | 1448 | */ |
1331 | 1449 | private function hasAttribute( |
|
0 commit comments