Skip to content

Commit a102064

Browse files
committed
implement string type resolver
1 parent 457e9fc commit a102064

File tree

8 files changed

+425
-22
lines changed

8 files changed

+425
-22
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Symfony\Component\TypeInfo\Tests\Fixtures;
4+
5+
abstract class AbstractDummy
6+
{
7+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
<?php
2+
3+
namespace Symfony\Component\TypeInfo\Tests\Fixtures;
4+
5+
final class Dummy extends AbstractDummy
6+
{
7+
}

src/Symfony/Component/TypeInfo/Tests/Type/ObjectTypeTest.php

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,10 @@
1212
namespace Symfony\Component\TypeInfo\Tests\Type;
1313

1414
use PHPUnit\Framework\TestCase;
15-
use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
1615
use Symfony\Component\TypeInfo\Type\ObjectType;
1716

1817
class ObjectTypeTest extends TestCase
1918
{
20-
public function testCannotCreateWithInvalidClass()
21-
{
22-
$this->expectException(InvalidArgumentException::class);
23-
new ObjectType('foo');
24-
}
25-
2619
public function testToString()
2720
{
2821
$this->assertSame(self::class, (string) new ObjectType(self::class));
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\TypeInfo\Tests\TypeResolver;
13+
14+
use PHPUnit\Framework\TestCase;
15+
use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
16+
use Symfony\Component\TypeInfo\Exception\UnsupportedException;
17+
use Symfony\Component\TypeInfo\Tests\Fixtures\AbstractDummy;
18+
use Symfony\Component\TypeInfo\Tests\Fixtures\Dummy;
19+
use Symfony\Component\TypeInfo\Type;
20+
use Symfony\Component\TypeInfo\TypeContext\TypeContext;
21+
use Symfony\Component\TypeInfo\TypeContext\TypeContextFactory;
22+
use Symfony\Component\TypeInfo\TypeResolver\StringTypeResolver;
23+
24+
class StringTypeResolverTest extends TestCase
25+
{
26+
private StringTypeResolver $resolver;
27+
28+
protected function setUp(): void
29+
{
30+
$this->resolver = new StringTypeResolver();
31+
}
32+
33+
/**
34+
* @dataProvider resolveDataProvider
35+
*/
36+
public function testResolve(Type $expectedType, string $string, TypeContext $typeContext = null)
37+
{
38+
$this->assertEquals($expectedType, $this->resolver->resolve($string, $typeContext));
39+
}
40+
41+
/**
42+
* @return iterable<array{0: Type, 1: string, 2?: TypeContext}>
43+
*/
44+
public function resolveDataProvider(): iterable
45+
{
46+
$typeContextFactory = new TypeContextFactory();
47+
48+
// callable
49+
yield [Type::callable(), 'callable(string, int): mixed'];
50+
51+
// array
52+
yield [Type::list(Type::bool()), 'bool[]'];
53+
54+
// array shape
55+
yield [Type::array(), 'array{0: true, 1: false}'];
56+
57+
// object shape
58+
yield [Type::object(), 'object{foo: true, bar: false}'];
59+
60+
// this
61+
yield [Type::object(Dummy::class), '$this', $typeContextFactory->create(Dummy::class, AbstractDummy::class)];
62+
63+
// const
64+
yield [Type::array(), 'array[1, 2, 3]'];
65+
yield [Type::false(), 'false'];
66+
yield [Type::float(), '1.23'];
67+
yield [Type::int(), '1'];
68+
yield [Type::null(), 'null'];
69+
yield [Type::string(), '"string"'];
70+
yield [Type::true(), 'true'];
71+
72+
// identifiers
73+
yield [Type::bool(), 'bool'];
74+
yield [Type::bool(), 'boolean'];
75+
yield [Type::true(), 'true'];
76+
yield [Type::false(), 'false'];
77+
yield [Type::int(), 'int'];
78+
yield [Type::int(), 'integer'];
79+
yield [Type::int(), 'positive-int'];
80+
yield [Type::int(), 'negative-int'];
81+
yield [Type::int(), 'non-positive-int'];
82+
yield [Type::int(), 'non-negative-int'];
83+
yield [Type::int(), 'non-zero-int'];
84+
yield [Type::float(), 'float'];
85+
yield [Type::float(), 'double'];
86+
yield [Type::string(), 'string'];
87+
yield [Type::string(), 'class-string'];
88+
yield [Type::string(), 'callable-string'];
89+
yield [Type::string(), 'numeric-string'];
90+
yield [Type::string(), 'non-empty-string'];
91+
yield [Type::string(), 'non-falsy-string'];
92+
yield [Type::string(), 'truthy-string'];
93+
yield [Type::string(), 'literal-string'];
94+
yield [Type::resource(), 'resource'];
95+
yield [Type::object(), 'object'];
96+
yield [Type::callable(), 'callable'];
97+
yield [Type::array(), 'array'];
98+
yield [Type::array(), 'non-empty-array'];
99+
yield [Type::list(), 'list'];
100+
yield [Type::list(), 'non-empty-list'];
101+
yield [Type::iterable(), 'iterable'];
102+
yield [Type::mixed(), 'mixed'];
103+
yield [Type::null(), 'null'];
104+
yield [Type::union(Type::int(), Type::string()), 'array-key'];
105+
yield [Type::union(Type::int(), Type::float(), Type::string(), Type::bool()), 'scalar'];
106+
yield [Type::object(AbstractDummy::class), 'self', $typeContextFactory->create(Dummy::class, AbstractDummy::class)];
107+
yield [Type::object(Dummy::class), 'static', $typeContextFactory->create(Dummy::class, AbstractDummy::class)];
108+
yield [Type::object(AbstractDummy::class), 'parent', $typeContextFactory->create(Dummy::class)];
109+
yield [Type::object(Dummy::class), 'Dummy', $typeContextFactory->create(Dummy::class)];
110+
yield [Type::template('T'), 'T'];
111+
112+
// nullable
113+
yield [Type::nullable(Type::int()), '?int'];
114+
115+
// generic
116+
yield [Type::generic(Type::int(), Type::string(), Type::bool()), 'int<string, bool>'];
117+
yield [Type::generic(Type::int(), Type::generic(Type::string(), Type::bool())), 'int<string<bool>>'];
118+
119+
// union
120+
yield [Type::union(Type::int(), Type::string()), 'int|string'];
121+
122+
// intersection
123+
yield [Type::intersection(Type::int(), Type::string()), 'int&string'];
124+
125+
// DNF
126+
yield [Type::union(Type::int(), Type::intersection(Type::string(), Type::bool())), 'int|(string&bool)'];
127+
}
128+
129+
public function testCannotResolveNonStringType()
130+
{
131+
$this->expectException(UnsupportedException::class);
132+
$this->resolver->resolve(123);
133+
}
134+
135+
/**
136+
* @dataProvider unhandledTypesDataProvider
137+
*/
138+
public function testCannotResolveInvalidTypes(string $string)
139+
{
140+
$this->expectException(UnsupportedException::class);
141+
$this->resolver->resolve($string);
142+
}
143+
144+
/**
145+
* @return iterable<array{0: string}>
146+
*/
147+
public function unhandledTypesDataProvider(): iterable
148+
{
149+
yield ['void'];
150+
yield ['never'];
151+
yield ['never-return'];
152+
yield ['never-returns'];
153+
yield ['no-return'];
154+
}
155+
156+
public function testCannotResolveThisWithoutTypeContext()
157+
{
158+
$this->expectException(InvalidArgumentException::class);
159+
$this->resolver->resolve('$this');
160+
}
161+
}

src/Symfony/Component/TypeInfo/Type/ObjectType.php

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111

1212
namespace Symfony\Component\TypeInfo\Type;
1313

14-
use Symfony\Component\TypeInfo\Exception\InvalidArgumentException;
1514
use Symfony\Component\TypeInfo\Type;
1615

1716
/**
@@ -26,9 +25,6 @@
2625
public function __construct(
2726
private string $className,
2827
) {
29-
if (!class_exists($className)) {
30-
throw new InvalidArgumentException(sprintf('"%s" is not a valid class.', $className));
31-
}
3228
}
3329

3430
/**

src/Symfony/Component/TypeInfo/TypeContext/TypeContext.php

Lines changed: 31 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111

1212
namespace Symfony\Component\TypeInfo\TypeContext;
1313

14+
use Symfony\Component\TypeInfo\Exception\LogicException;
15+
1416
/**
1517
* @author Mathias Arlaud <[email protected]>
1618
* @author Baptiste Leduc <[email protected]>
@@ -22,12 +24,13 @@ final class TypeContext
2224
*/
2325
public function __construct(
2426
public readonly string $calledClassName,
27+
public readonly string $declaringClassName,
2528
public readonly ?string $namespace = null,
2629
public readonly array $uses = []
2730
) {
2831
}
2932

30-
public function resolveStringName(string $name): string
33+
public function resolve(string $name): string
3134
{
3235
if (str_starts_with($name, '\\')) {
3336
return ltrim($name, '\\');
@@ -51,8 +54,33 @@ public function resolveStringName(string $name): string
5154
return $name;
5255
}
5356

54-
public function resolveRootClass(): string
57+
/**
58+
* @return class-string
59+
*/
60+
public function resolveDeclaringClass(): string
61+
{
62+
return $this->resolve($this->declaringClassName);
63+
}
64+
65+
/**
66+
* @return class-string
67+
*/
68+
public function resolveCalledClass(): string
69+
{
70+
return $this->resolve($this->calledClassName);
71+
}
72+
73+
/**
74+
* @return class-string
75+
*/
76+
public function resolveParentClass(): string
5577
{
56-
return $this->resolveStringName($this->calledClassName);
78+
$declaringClassName = $this->resolveDeclaringClass();
79+
80+
if (false === $parentClass = get_parent_class($declaringClassName)) {
81+
throw new LogicException(sprintf('"%s" do not extend any class.', $declaringClassName));
82+
}
83+
84+
return $this->resolve(str_replace($this->namespace.'\\', '', $parentClass));
5785
}
5886
}

src/Symfony/Component/TypeInfo/TypeContext/TypeContextFactory.php

Lines changed: 16 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -22,35 +22,43 @@
2222
*/
2323
final class TypeContextFactory
2424
{
25-
private ContextFactory $contextFactory;
26-
2725
public function __construct()
2826
{
29-
$this->contextFactory = new ContextFactory();
3027
}
3128

3229
public function create(string $calledClassName, string $declaringClassName = null): TypeContext
3330
{
31+
if (!class_exists(ContextFactory::class)) {
32+
throw new \LogicException(sprintf('Unable to call "%s()" as the "phpdocumentor/type-resolver" package is not installed. Try running composer require "phpdocumentor/type-resolver".', __METHOD__));
33+
}
34+
3435
$declaringClassName ??= $calledClassName;
3536

36-
$path = explode('\\', $calledClassName);
37-
$calledClassName = array_pop($path);
37+
$calledClassPath = explode('\\', $calledClassName);
38+
$declaringClassPath = explode('\\', $declaringClassName);
3839

3940
$declaringReflection = new \ReflectionClass($declaringClassName);
4041
[$declaringNamespace, $declaringUses] = $this->extractFromFullClassName($declaringReflection);
42+
4143
$declaringUses = array_merge($declaringUses, $this->collectUses($declaringReflection));
4244

43-
return new TypeContext($calledClassName, $declaringNamespace, $declaringUses);
45+
return new TypeContext(array_pop($calledClassPath), array_pop($declaringClassPath), $declaringNamespace, $declaringUses);
4446
}
4547

4648
public function createFromReflection(\ReflectionNamedType|\ReflectionParameter|\ReflectionProperty|\ReflectionFunctionAbstract $reflection): ?TypeContext
4749
{
4850
$declaringClass = $reflection instanceof \ReflectionMethod ? $reflection->getDeclaringClass() : $reflection->getClosureScopeClass();
4951

5052
if (null !== $declaringClass) {
51-
return new TypeContext($declaringClass->getName(), $declaringClass->getNamespaceName(), $this->collectUses($declaringClass));
53+
return new TypeContext(
54+
$declaringClass->getName(),
55+
$declaringClass->getName(),
56+
$declaringClass->getNamespaceName(),
57+
$this->collectUses($declaringClass),
58+
);
5259
}
5360

61+
// TODO throw instead
5462
return null;
5563
}
5664

@@ -85,7 +93,7 @@ private function extractFromFullClassName(\ReflectionClass $reflection): array
8593
throw new RuntimeException(sprintf('Unable to read file "%s".', $fileName));
8694
}
8795

88-
$context = $this->contextFactory->createForNamespace($namespace, $contents);
96+
$context = (new ContextFactory())->createForNamespace($namespace, $contents);
8997

9098
return [$namespace, $context->getNamespaceAliases()];
9199
}

0 commit comments

Comments
 (0)