Skip to content

Commit 6f5c135

Browse files
authored
[console] [7.3] Add type declaration based on default value type in InvokableCommandInputAttributeRector (#843)
* add fixture * add type declaration based on dfeault value type * add type support to option add array support
1 parent b5f2638 commit 6f5c135

File tree

8 files changed

+244
-19
lines changed

8 files changed

+244
-19
lines changed

rules-tests/Symfony73/Rector/Class_/InvokableCommandInputAttributeRector/Fixture/DefaultValue/argument_with_default_value.php.inc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@ class ArgumentWithDefaultValue
4747
#[\Symfony\Component\Console\Attribute\Argument(name: 'second')]
4848
string $second = 'required value',
4949
#[\Symfony\Component\Console\Attribute\Option(name: 'third')]
50-
$third = 'third value'
50+
string $third = 'third value'
5151
): int
5252
{
5353
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
<?php
2+
3+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandInputAttributeRector\Fixture\DefaultValue;
4+
5+
use Symfony\Component\Console\Attribute\AsCommand;
6+
use Symfony\Component\Console\Command\Command;
7+
use Symfony\Component\Console\Input\InputArgument;
8+
use Symfony\Component\Console\Input\InputInterface;
9+
use Symfony\Component\Console\Input\InputOption;
10+
use Symfony\Component\Console\Output\OutputInterface;
11+
12+
#[AsCommand(
13+
name: 'app:hello',
14+
)]
15+
class MatchScalarType extends Command
16+
{
17+
protected function configure(): void
18+
{
19+
$this->addArgument('first', InputArgument::OPTIONAL, null, 100);
20+
$this->addArgument('second', InputArgument::REQUIRED, null, 200.5);
21+
22+
$this->addOption('third', '', null, null, 200);
23+
$this->addOption('fourth', '', null, null, 400.5);
24+
}
25+
protected function execute(InputInterface $input, OutputInterface $output): int
26+
{
27+
}
28+
}
29+
30+
?>
31+
-----
32+
<?php
33+
34+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandInputAttributeRector\Fixture\DefaultValue;
35+
36+
use Symfony\Component\Console\Attribute\AsCommand;
37+
use Symfony\Component\Console\Command\Command;
38+
use Symfony\Component\Console\Input\InputArgument;
39+
use Symfony\Component\Console\Input\InputInterface;
40+
use Symfony\Component\Console\Input\InputOption;
41+
use Symfony\Component\Console\Output\OutputInterface;
42+
43+
#[AsCommand(
44+
name: 'app:hello',
45+
)]
46+
class MatchScalarType
47+
{
48+
public function __invoke(
49+
#[\Symfony\Component\Console\Attribute\Argument(name: 'first')]
50+
?int $first = 100,
51+
#[\Symfony\Component\Console\Attribute\Argument(name: 'second')]
52+
float $second = 200.5,
53+
#[\Symfony\Component\Console\Attribute\Option(name: 'third')]
54+
int $third = 200,
55+
#[\Symfony\Component\Console\Attribute\Option(name: 'fourth')]
56+
float $fourth = 400.5
57+
): int
58+
{
59+
}
60+
}
61+
62+
?>
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
<?php
2+
3+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandInputAttributeRector\Fixture\DefaultValue;
4+
5+
use Symfony\Component\Console\Attribute\AsCommand;
6+
use Symfony\Component\Console\Command\Command;
7+
use Symfony\Component\Console\Input\InputArgument;
8+
use Symfony\Component\Console\Input\InputInterface;
9+
use Symfony\Component\Console\Input\InputOption;
10+
use Symfony\Component\Console\Output\OutputInterface;
11+
12+
#[AsCommand(
13+
name: 'app:hello',
14+
)]
15+
class OptionWithOptionalValue extends Command
16+
{
17+
protected function configure(): void
18+
{
19+
$this->addOption('some-array', null, InputOption::VALUE_IS_ARRAY, '', ['third value']);
20+
$this->addOption('no-default-array', null, InputOption::VALUE_IS_ARRAY);
21+
}
22+
protected function execute(InputInterface $input, OutputInterface $output): int
23+
{
24+
}
25+
}
26+
27+
?>
28+
-----
29+
<?php
30+
31+
namespace Rector\Symfony\Tests\Symfony73\Rector\Class_\InvokableCommandInputAttributeRector\Fixture\DefaultValue;
32+
33+
use Symfony\Component\Console\Attribute\AsCommand;
34+
use Symfony\Component\Console\Command\Command;
35+
use Symfony\Component\Console\Input\InputArgument;
36+
use Symfony\Component\Console\Input\InputInterface;
37+
use Symfony\Component\Console\Input\InputOption;
38+
use Symfony\Component\Console\Output\OutputInterface;
39+
40+
#[AsCommand(
41+
name: 'app:hello',
42+
)]
43+
class OptionWithOptionalValue
44+
{
45+
public function __invoke(#[\Symfony\Component\Console\Attribute\Option(name: 'some-array', mode: InputOption::VALUE_IS_ARRAY)]
46+
array $someArray = ['third value'], #[\Symfony\Component\Console\Attribute\Option(name: 'no-default-array', mode: InputOption::VALUE_IS_ARRAY)]
47+
array $noDefaultArray): int
48+
{
49+
}
50+
}
51+
52+
?>

rules/Symfony73/NodeAnalyzer/CommandArgumentsResolver.php

Lines changed: 36 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,11 @@
44

55
namespace Rector\Symfony\Symfony73\NodeAnalyzer;
66

7+
use PhpParser\Node\Arg;
78
use PhpParser\Node\Expr;
89
use PhpParser\Node\Stmt\ClassMethod;
10+
use PHPStan\Type\Type;
11+
use Rector\NodeTypeResolver\NodeTypeResolver;
912
use Rector\PhpParser\Node\Value\ValueResolver;
1013
use Rector\Symfony\Symfony73\NodeFinder\MethodCallFinder;
1114
use Rector\Symfony\Symfony73\ValueObject\CommandArgument;
@@ -14,7 +17,8 @@
1417
{
1518
public function __construct(
1619
private MethodCallFinder $methodCallFinder,
17-
private ValueResolver $valueResolver
20+
private ValueResolver $valueResolver,
21+
private NodeTypeResolver $nodeTypeResolver
1822
) {
1923
}
2024

@@ -31,25 +35,47 @@ public function resolve(ClassMethod $configureClassMethod): array
3135

3236
$argumentName = $this->valueResolver->getValue($addArgumentArgs[0]->value);
3337

34-
$modeExpr = $addArgumentArgs[1]->value ?? null;
35-
36-
$isArray = false;
37-
if ($modeExpr instanceof Expr) {
38-
$modeValue = $this->valueResolver->getValue($modeExpr);
39-
// binary check for InputArgument::IS_ARRAY
40-
$isArray = (bool) ($modeValue & 4);
41-
}
38+
$isArray = $this->isArrayMode($addArgumentArgs);
4239

4340
$commandArguments[] = new CommandArgument(
4441
$argumentName,
4542
$addArgumentArgs[0]->value,
4643
$addArgumentArgs[1]->value ?? null,
4744
$addArgumentArgs[2]->value ?? null,
4845
$addArgumentArgs[3]->value ?? null,
49-
$isArray
46+
$isArray,
47+
$this->resolveDefaultType($addArgumentArgs)
5048
);
5149
}
5250

5351
return $commandArguments;
5452
}
53+
54+
/**
55+
* @param Arg[] $args
56+
*/
57+
private function resolveDefaultType(array $args): ?Type
58+
{
59+
$defaultArg = $args[3] ?? null;
60+
if (! $defaultArg instanceof Arg) {
61+
return null;
62+
}
63+
64+
return $this->nodeTypeResolver->getType($defaultArg->value);
65+
}
66+
67+
/**
68+
* @param Arg[] $args
69+
*/
70+
private function isArrayMode(array $args): bool
71+
{
72+
$modeExpr = $args[1]->value ?? null;
73+
if (! $modeExpr instanceof Expr) {
74+
return false;
75+
}
76+
77+
$modeValue = $this->valueResolver->getValue($modeExpr);
78+
// binary check for InputArgument::IS_ARRAY
79+
return (bool) ($modeValue & 4);
80+
}
5581
}

rules/Symfony73/NodeAnalyzer/CommandOptionsResolver.php

Lines changed: 37 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,11 @@
44

55
namespace Rector\Symfony\Symfony73\NodeAnalyzer;
66

7+
use PhpParser\Node\Arg;
8+
use PhpParser\Node\Expr;
79
use PhpParser\Node\Stmt\ClassMethod;
10+
use PHPStan\Type\Type;
11+
use Rector\NodeTypeResolver\NodeTypeResolver;
812
use Rector\PhpParser\Node\Value\ValueResolver;
913
use Rector\Symfony\Symfony73\NodeFinder\MethodCallFinder;
1014
use Rector\Symfony\Symfony73\ValueObject\CommandOption;
@@ -13,7 +17,8 @@
1317
{
1418
public function __construct(
1519
private MethodCallFinder $methodCallFinder,
16-
private ValueResolver $valueResolver
20+
private ValueResolver $valueResolver,
21+
private NodeTypeResolver $nodeTypeResolver
1722
) {
1823
}
1924

@@ -37,10 +42,40 @@ public function resolve(ClassMethod $configureClassMethod): array
3742
$addOptionArgs[1]->value ?? null,
3843
$addOptionArgs[2]->value ?? null,
3944
$addOptionArgs[3]->value ?? null,
40-
$addOptionArgs[4]->value ?? null
45+
$addOptionArgs[4]->value ?? null,
46+
$this->isArrayMode($addOptionArgs),
47+
$this->resolveDefaultType($addOptionArgs)
4148
);
4249
}
4350

4451
return $commandOptions;
4552
}
53+
54+
/**
55+
* @param Arg[] $args
56+
*/
57+
private function resolveDefaultType(array $args): ?Type
58+
{
59+
$defaultArg = $args[4] ?? null;
60+
if (! $defaultArg instanceof Arg) {
61+
return null;
62+
}
63+
64+
return $this->nodeTypeResolver->getType($defaultArg->value);
65+
}
66+
67+
/**
68+
* @param Arg[] $args
69+
*/
70+
private function isArrayMode(array $args): bool
71+
{
72+
$modeExpr = $args[2]->value ?? null;
73+
if (! $modeExpr instanceof Expr) {
74+
return false;
75+
}
76+
77+
$modeValue = $this->valueResolver->getValue($modeExpr);
78+
// binary check for InputOptions::VALUE_IS_ARRAY
79+
return (bool) ($modeValue & 8);
80+
}
4681
}

rules/Symfony73/NodeFactory/CommandInvokeParamsFactory.php

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Rector\Symfony\Symfony73\NodeFactory;
66

7+
use PhpParser\Node;
78
use PhpParser\Node\Arg;
89
use PhpParser\Node\Attribute;
910
use PhpParser\Node\AttributeGroup;
@@ -13,7 +14,10 @@
1314
use PhpParser\Node\Name\FullyQualified;
1415
use PhpParser\Node\NullableType;
1516
use PhpParser\Node\Param;
17+
use PHPStan\Type\Type;
1618
use Rector\PhpParser\Node\Value\ValueResolver;
19+
use Rector\PHPStanStaticTypeMapper\Enum\TypeKind;
20+
use Rector\StaticTypeMapper\StaticTypeMapper;
1721
use Rector\Symfony\Enum\SymfonyAttribute;
1822
use Rector\Symfony\Symfony73\ValueObject\CommandArgument;
1923
use Rector\Symfony\Symfony73\ValueObject\CommandOption;
@@ -22,6 +26,7 @@
2226
{
2327
public function __construct(
2428
private ValueResolver $valueResolver,
29+
private StaticTypeMapper $staticTypeMapper
2530
) {
2631
}
2732

@@ -50,11 +55,7 @@ private function createArgumentParams(array $commandArguments): array
5055
$variableName = $this->createCamelCase($commandArgument->getNameValue());
5156
$argumentParam = new Param(new Variable($variableName));
5257

53-
if ($commandArgument->isArray()) {
54-
$argumentParam->type = new Identifier('array');
55-
} else {
56-
$argumentParam->type = new Identifier('string');
57-
}
58+
$this->decorateParamType($argumentParam, $commandArgument);
5859

5960
if ($commandArgument->getDefault() instanceof Expr) {
6061
$argumentParam->default = $commandArgument->getDefault();
@@ -100,6 +101,8 @@ private function createOptionParams(array $commandOptions): array
100101
$optionParam->default = $commandOption->getDefault();
101102
}
102103

104+
$this->decorateParamType($optionParam, $commandOption);
105+
103106
$optionArgs = [new Arg(value: $commandOption->getName(), name: new Identifier('name'))];
104107

105108
if ($this->isNonEmptyExpr($commandOption->getShortcut())) {
@@ -157,4 +160,31 @@ private function isNonEmptyExpr(?Expr $expr): bool
157160

158161
return ! $this->valueResolver->isValue($expr, '');
159162
}
163+
164+
private function decorateParamType(
165+
Param $argumentParam,
166+
CommandArgument|CommandOption $commandArgumentOrOption
167+
): void {
168+
if ($commandArgumentOrOption->isArray()) {
169+
$argumentParam->type = new Identifier('array');
170+
return;
171+
}
172+
173+
$defaultType = $commandArgumentOrOption->getDefaultType();
174+
if ($defaultType instanceof Type) {
175+
$paramType = $this->staticTypeMapper->mapPHPStanTypeToPhpParserNode($defaultType, TypeKind::PARAM);
176+
177+
if ($paramType instanceof Node) {
178+
$argumentParam->type = $paramType;
179+
return;
180+
}
181+
}
182+
183+
// fallback
184+
if ($commandArgumentOrOption instanceof CommandOption) {
185+
return;
186+
}
187+
188+
$argumentParam->type = new Identifier('string');
189+
}
160190
}

rules/Symfony73/ValueObject/CommandArgument.php

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
namespace Rector\Symfony\Symfony73\ValueObject;
66

77
use PhpParser\Node\Expr;
8+
use PHPStan\Type\Type;
89

910
final readonly class CommandArgument
1011
{
@@ -14,7 +15,8 @@ public function __construct(
1415
private ?Expr $mode,
1516
private ?Expr $description,
1617
private ?Expr $default,
17-
private bool $isArray
18+
private bool $isArray,
19+
private ?Type $defaultType
1820
) {
1921
}
2022

@@ -47,4 +49,9 @@ public function isArray(): bool
4749
{
4850
return $this->isArray;
4951
}
52+
53+
public function getDefaultType(): ?Type
54+
{
55+
return $this->defaultType;
56+
}
5057
}

0 commit comments

Comments
 (0)