Skip to content

Commit d76d366

Browse files
committed
Support nested #[MapInput] in console command DTOs
Recurse into DTO properties annotated with #[MapInput] so their reported as dead. The host property is marked read+written.
1 parent 17ce9d0 commit d76d366

2 files changed

Lines changed: 60 additions & 1 deletion

File tree

src/Provider/SymfonyUsageProvider.php

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -686,12 +686,24 @@ private function getMapInputUsages(InClassMethodNode $node): array
686686

687687
/**
688688
* @param list<ClassMethodUsage|ClassPropertyUsage> $usages
689+
* @param array<string, true> $visited
689690
*/
690691
private function collectMapInputDtoUsages(
691692
string $dtoClassName,
692693
array &$usages,
694+
array &$visited = [],
693695
): void
694696
{
697+
if (isset($visited[$dtoClassName])) {
698+
return;
699+
}
700+
701+
$visited[$dtoClassName] = true;
702+
703+
if (!$this->reflectionProvider->hasClass($dtoClassName)) {
704+
return;
705+
}
706+
695707
$dtoReflection = $this->reflectionProvider->getClass($dtoClassName);
696708
$origin = UsageOrigin::createVirtual($this, VirtualUsageData::withNote('Console input DTO via #[MapInput]'));
697709

@@ -702,8 +714,9 @@ private function collectMapInputDtoUsages(
702714

703715
$isInputProperty = $property->getAttributes('Symfony\Component\Console\Attribute\Argument') !== []
704716
|| $property->getAttributes('Symfony\Component\Console\Attribute\Option') !== [];
717+
$nestedMapInput = $property->getAttributes('Symfony\Component\Console\Attribute\MapInput') !== [];
705718

706-
if (!$isInputProperty) {
719+
if (!$isInputProperty && !$nestedMapInput) {
707720
continue;
708721
}
709722

@@ -717,6 +730,18 @@ private function collectMapInputDtoUsages(
717730
new ClassPropertyRef($dtoClassName, $property->getName(), possibleDescendant: false),
718731
AccessType::READ,
719732
);
733+
734+
if (!$nestedMapInput) {
735+
continue;
736+
}
737+
738+
$propertyType = $property->getType();
739+
740+
if (!$propertyType instanceof ReflectionNamedType || $propertyType->isBuiltin()) {
741+
continue;
742+
}
743+
744+
$this->collectMapInputDtoUsages($propertyType->getName(), $usages, $visited);
720745
}
721746

722747
foreach ($dtoReflection->getNativeReflection()->getMethods() as $dtoMethod) {

tests/Rule/data/providers/symfony.php

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,3 +265,37 @@ class OrphanedInput {
265265
#[Interact]
266266
public function askSomething(): void {} // error: Unused Symfony\OrphanedInput::askSomething
267267
}
268+
269+
class NestedFiltersInput {
270+
#[\Symfony\Component\Console\Attribute\Argument]
271+
public string $tag;
272+
273+
#[\Symfony\Component\Console\Attribute\Option]
274+
public bool $strict = false;
275+
276+
public string $notAnInput; // error: Property Symfony\NestedFiltersInput::$notAnInput is never read // error: Property Symfony\NestedFiltersInput::$notAnInput is never written
277+
278+
#[Interact]
279+
public function askForTag(): void {}
280+
281+
public function deadOnNested(): void {} // error: Unused Symfony\NestedFiltersInput::deadOnNested
282+
}
283+
284+
class WrappedImportInput {
285+
#[\Symfony\Component\Console\Attribute\Argument]
286+
public string $name;
287+
288+
#[\Symfony\Component\Console\Attribute\MapInput]
289+
public NestedFiltersInput $filters;
290+
}
291+
292+
#[AsCommand(name: 'app:import-wrapped')]
293+
class WrappedImportCommand extends Command {
294+
public function __invoke(
295+
#[\Symfony\Component\Console\Attribute\MapInput] WrappedImportInput $input,
296+
): int {
297+
echo $input->name;
298+
echo $input->filters->tag;
299+
return 0;
300+
}
301+
}

0 commit comments

Comments
 (0)