Skip to content

Commit 2b901f4

Browse files
authored
Merge pull request #703 from phpDocumentor/fix/invalid-expression-type-resolving
Use type resolver to resolve types in expressions
2 parents 67b6bc5 + 6f01fae commit 2b901f4

File tree

6 files changed

+155
-15
lines changed

6 files changed

+155
-15
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
"nikic/php-parser": "~4.18 || ^5.0",
3030
"phpdocumentor/reflection-common": "^2.1",
3131
"phpdocumentor/reflection-docblock": "^5",
32-
"phpdocumentor/type-resolver": "^1.2",
32+
"phpdocumentor/type-resolver": "^1.4",
3333
"symfony/polyfill-php80": "^1.28",
3434
"webmozart/assert": "^1.7"
3535
},

composer.lock

Lines changed: 9 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/phpDocumentor/Reflection/Php/Expression/ExpressionPrinter.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
use phpDocumentor\Reflection\FqsenResolver;
1818
use phpDocumentor\Reflection\Php\Expression;
1919
use phpDocumentor\Reflection\Type;
20+
use phpDocumentor\Reflection\TypeResolver;
2021
use phpDocumentor\Reflection\Types\Context;
22+
use phpDocumentor\Reflection\Types\Object_;
2123
use PhpParser\Node\Expr;
2224
use PhpParser\Node\Name;
2325
use PhpParser\PrettyPrinter\Standard;
@@ -27,14 +29,16 @@ final class ExpressionPrinter extends Standard
2729
/** @var array<string, Fqsen|Type> */
2830
private array $parts = [];
2931
private Context|null $context = null;
30-
private FqsenResolver $fqsenResolver;
32+
private TypeResolver $typeResolver;
3133

3234
/** {@inheritDoc} */
3335
public function __construct(array $options = [])
3436
{
3537
parent::__construct($options);
3638

37-
$this->fqsenResolver = new FqsenResolver();
39+
$this->typeResolver = new TypeResolver(
40+
new FqsenResolver(),
41+
);
3842
}
3943

4044
protected function resetState(): void
@@ -53,9 +57,14 @@ public function prettyPrintExpr(Expr $node, Context|null $context = null): strin
5357

5458
protected function pName(Name $node): string
5559
{
56-
$renderedName = $this->fqsenResolver->resolve(parent::pName($node), $this->context);
60+
$renderedName = $this->typeResolver->resolve(parent::pName($node), $this->context);
5761
$placeholder = Expression::generatePlaceholder((string) $renderedName);
58-
$this->parts[$placeholder] = $renderedName;
62+
63+
if ($renderedName instanceof Object_ && $renderedName->getFqsen() !== null) {
64+
$this->parts[$placeholder] = $renderedName->getFqsen();
65+
} else {
66+
$this->parts[$placeholder] = $renderedName;
67+
}
5968

6069
return $placeholder;
6170
}

tests/integration/ProjectCreationTest.php

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
use phpDocumentor\Reflection\Php\Function_;
2121
use phpDocumentor\Reflection\Php\ProjectFactory;
2222
use phpDocumentor\Reflection\Types\Integer;
23+
use phpDocumentor\Reflection\Types\Null_;
2324
use phpDocumentor\Reflection\Types\Object_;
2425
use phpDocumentor\Reflection\Types\String_;
2526

@@ -129,6 +130,19 @@ public function testWithNamespacedClass() : void
129130
new Object_(new Fqsen('\\Luigi\\Pizza\Style')),
130131
$methods['\\Luigi\\Pizza::__construct()']->getArguments()[0]->getType()
131132
);
133+
134+
$sauceArgument = $methods['\\Luigi\\Pizza::__construct()']->getArguments()[1];
135+
$this->assertEquals('sauce', $sauceArgument->getName());
136+
$this->assertEquals(
137+
new Php\Expression(
138+
'{{ PHPDOC37a6259cc0c1dae299a7866489dff0bd }}',
139+
[
140+
'{{ PHPDOC37a6259cc0c1dae299a7866489dff0bd }}' => new Null_(),
141+
],
142+
),
143+
$sauceArgument->getDefault(false)
144+
);
145+
132146
}
133147

134148
public function testDocblockOfMethodIsProcessed() : void

tests/integration/data/Luigi/Pizza.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,9 @@
1111

1212
namespace Luigi;
1313

14+
use Luigi\Pizza\Sauce;
15+
use Luigi\Pizza\TomatoSauce;
16+
1417
#[\Food("Pizza")]
1518
#[\Food(country: "Italy", originDate: Pizza::class)]
1619
class Pizza extends \Pizza
@@ -46,7 +49,7 @@ class Pizza extends \Pizza
4649
/** @var string $deliveryMethod Is the customer picking this pizza up or must it be delivered? */
4750
$deliveryMethod;
4851

49-
private function __construct(Pizza\Style $style)
52+
private function __construct(Pizza\Style $style, Sauce|null $sauce = null)
5053
{
5154
$this->style = $style;
5255
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace unit\phpDocumentor\Reflection\Php\Expression;
6+
7+
use phpDocumentor\Reflection\Fqsen;
8+
use phpDocumentor\Reflection\Php\Expression\ExpressionPrinter;
9+
use phpDocumentor\Reflection\PseudoTypes\True_;
10+
use phpDocumentor\Reflection\Types\Null_;
11+
use PhpParser\ParserFactory;
12+
use PHPUnit\Framework\Attributes\CoversClass;
13+
use PHPUnit\Framework\Attributes\DataProvider;
14+
use PHPUnit\Framework\TestCase;
15+
16+
#[CoversClass(ExpressionPrinter::class)]
17+
final class ExpressionPrinterTest extends TestCase
18+
{
19+
/** @param array<string, Type> $expectedParts */
20+
#[DataProvider('argumentProvider')]
21+
public function testArgumentIsParsed(string $code, string $expectedExpression, array $expectedParts): void
22+
{
23+
$parser = (new ParserFactory())->createForHostVersion();
24+
$node = $parser->parse($code);
25+
$printer = new ExpressionPrinter();
26+
27+
$expression = $printer->prettyPrintExpr($node[0]->params[0]->default);
28+
29+
self::assertSame($expectedExpression, $expression);
30+
self::assertEquals($expectedParts, $printer->getParts());
31+
}
32+
33+
/**
34+
* @return array<string, array{
35+
* 'code': string,
36+
* 'expectedExpression': string,
37+
* 'expectedParts': array<string, Type>
38+
* }>
39+
*/
40+
public static function argumentProvider(): array
41+
{
42+
return [
43+
'myClassDefault' => [
44+
'code' => '<?php function foo(MyClass $arg = new MyClass()) {}',
45+
'expectedExpression' => 'new {{ PHPDOC9f1f93179bc4ea54e11fc3cda63a284f }}()',
46+
'expectedParts' => [
47+
'{{ PHPDOC9f1f93179bc4ea54e11fc3cda63a284f }}' => new Fqsen('\MyClass'),
48+
],
49+
],
50+
// Enum case default.
51+
// After first run, replace ENUM_PLACEHOLDER below with the actual placeholder shown in the failure output.
52+
'enumCaseDefault' => [
53+
'code' => '<?php function foo(MyEnum $arg = MyEnum::CaseA) {}',
54+
'expectedExpression' => '{{ PHPDOC8844445ee68bb81ea3fd9529f906598b }}',
55+
'expectedParts' => [
56+
'{{ PHPDOC8844445ee68bb81ea3fd9529f906598b }}' => new Fqsen('\MyEnum::CaseA'),
57+
],
58+
],
59+
'classConstantDefault' => [
60+
'code' => '<?php function foo(MyClass $arg = MyClass::SOME_CONST) {}',
61+
'expectedExpression' => '{{ PHPDOCe54b7c24dd0f847c3193039223751b3d }}',
62+
'expectedParts' => [
63+
'{{ PHPDOCe54b7c24dd0f847c3193039223751b3d }}' => new Fqsen('\MyClass::SOME_CONST'),
64+
],
65+
],
66+
'stringDefault' => [
67+
'code' => '<?php function foo(string $arg = \'hello\') {}',
68+
'expectedExpression' => "'hello'",
69+
'expectedParts' => [],
70+
],
71+
'intDefault' => [
72+
'code' => '<?php function foo(int $arg = 42) {}',
73+
'expectedExpression' => '42',
74+
'expectedParts' => [],
75+
],
76+
'booleanDefault' => [
77+
'code' => '<?php function foo(bool $arg = true) {}',
78+
'expectedExpression' => '{{ PHPDOCb326b5062b2f0e69046810717534cb09 }}',
79+
'expectedParts' => [
80+
'{{ PHPDOCb326b5062b2f0e69046810717534cb09 }}' => new True_(),
81+
],
82+
],
83+
'nullDefault' => [
84+
'code' => '<?php function foo(bool|null $arg = null) {}',
85+
'expectedExpression' => '{{ PHPDOC37a6259cc0c1dae299a7866489dff0bd }}',
86+
'expectedParts' => [
87+
'{{ PHPDOC37a6259cc0c1dae299a7866489dff0bd }}' => new Null_(),
88+
],
89+
],
90+
'emptyArrayDefault' => [
91+
'code' => '<?php function foo(array $arg = []) {}',
92+
'expectedExpression' => '[]',
93+
'expectedParts' => [],
94+
],
95+
'intArrayDefault' => [
96+
'code' => '<?php function foo(array $arg = [1, 2]) {}',
97+
'expectedExpression' => '[1, 2]',
98+
'expectedParts' => [],
99+
],
100+
'stringArrayDefault' => [
101+
'code' => '<?php function foo(array $arg = [\'hello\', \'world\']) {}',
102+
'expectedExpression' => "['hello', 'world']",
103+
'expectedParts' => [],
104+
],
105+
'objectArrayDefault' => [
106+
'code' => '<?php function foo(array $arg = [new MyClass(), new MyClass()]) {}',
107+
'expectedExpression' => '[new {{ PHPDOC9f1f93179bc4ea54e11fc3cda63a284f }}(), new {{ PHPDOC9f1f93179bc4ea54e11fc3cda63a284f }}()]',
108+
'expectedParts' => [
109+
'{{ PHPDOC9f1f93179bc4ea54e11fc3cda63a284f }}' => new Fqsen('\MyClass'),
110+
],
111+
],
112+
];
113+
}
114+
}

0 commit comments

Comments
 (0)