Skip to content

Commit 951a57d

Browse files
committed
refactor: use semantic nodes instead of literal type definitions
1 parent 91f181a commit 951a57d

38 files changed

Lines changed: 649 additions & 180 deletions

docs/2-features/18-typescript.md

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -29,31 +29,33 @@ This command scans your marked classes, generates the corresponding TypeScript d
2929

3030
Tempest provides several built-in type resolvers for common types: strings, numbers, dates, enums and class references.
3131

32-
You can add your own resolver by providing implementations of {b`Tempest\Generation\TypeScript\TypeResolvers\TypeResolver`}. This interface requires a `canResolve()` method to determine if the resolver can handle a given type, and a `resolve()` method to perform the actual resolution.
32+
You can add your own resolver by implementing {b`Tempest\Generation\TypeScript\TypeResolver`}. This interface requires a `canResolve()` method to determine if the resolver can handle a given type, and a `resolve()` method that returns a type node.
3333

34-
The following is the actual implementation of the built-in resolver that handles scalar types:
34+
The following is the actual implementation of the built-in resolver that handles enum cases:
3535

36-
```php ScalarTypeResolver.php
36+
```php EnumCaseTypeResolver.php
3737
#[Priority(Priority::LOW)]
38-
final class ScalarTypeResolver implements TypeResolver
38+
final class EnumCaseTypeResolver implements TypeResolver
3939
{
4040
public function canResolve(TypeReflector $type): bool
4141
{
42-
return $type->isBuiltIn()
43-
&& in_array($type->getName(), ['string', 'int', 'float', 'bool'], strict: true);
42+
return $type->isEnumCase();
4443
}
4544

46-
public function resolve(TypeReflector $type, TypeScriptGenerator $generator): ResolvedType
45+
public function resolve(TypeReflector $type, TypeScriptGenerator $generator): TypeNode
4746
{
48-
return new ResolvedType(match ($type->getName()) {
49-
'string' => 'string',
50-
'int', 'float' => 'number',
51-
'bool' => 'boolean',
52-
});
47+
$case = $type->asEnumCase()->getValue();
48+
$value = $case instanceof BackedEnum
49+
? $case->value
50+
: $case->name;
51+
52+
return new LiteralTypeNode($value);
5353
}
5454
}
5555
```
5656

57+
Resolvers may return any supported semantic node depending on your use case, such as {b`Tempest\Generation\TypeScript\TypeNodes\PrimitiveTypeNode`}, {b`Tempest\Generation\TypeScript\TypeNodes\LiteralTypeNode`}, {b`Tempest\Generation\TypeScript\TypeNodes\SymbolTypeNode`}, {b`Tempest\Generation\TypeScript\TypeNodes\ArrayTypeNode`}, {b`Tempest\Generation\TypeScript\TypeNodes\UnionTypeNode`}, {b`Tempest\Generation\TypeScript\TypeNodes\IntersectionTypeNode`}, {b`Tempest\Generation\TypeScript\TypeNodes\ObjectTypeNode`} or {b`Tempest\Generation\TypeScript\TypeNodes\RawTypeNode`}.
58+
5759
:::info
5860
Type resolvers are automatically [discovered](../1-essentials/05-discovery.md) and do not need to be registered manually.
5961
:::

packages/generation/src/TypeScript/PropertyDefinition.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,20 +4,21 @@
44

55
namespace Tempest\Generation\TypeScript;
66

7+
use Tempest\Generation\TypeScript\TypeNodes\TypeNode;
8+
79
/**
810
* Represents a property in a TypeScript interface.
911
*/
1012
final readonly class PropertyDefinition
1113
{
1214
/**
1315
* @param string $name The name of the property.
14-
* @param string $definition The TypeScript definition of the property.
16+
* @param TypeNode $type The TypeScript type of the property.
1517
* @param null|string $fqcn The PHP FQCN of the original type.
1618
*/
1719
public function __construct(
1820
public string $name,
19-
public string $definition,
21+
public TypeNode $type,
2022
public bool $isNullable,
21-
public ?string $fqcn = null,
2223
) {}
2324
}

packages/generation/src/TypeScript/ResolvedType.php

Lines changed: 0 additions & 20 deletions
This file was deleted.

packages/generation/src/TypeScript/StructureResolvers/ClassStructureResolver.php

Lines changed: 19 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@
77
use Tempest\Container\Container;
88
use Tempest\Generation\TypeScript\InterfaceDefinition;
99
use Tempest\Generation\TypeScript\PropertyDefinition;
10-
use Tempest\Generation\TypeScript\ResolvedType;
1110
use Tempest\Generation\TypeScript\StructureResolver;
11+
use Tempest\Generation\TypeScript\TypeNodes\ArrayTypeNode;
12+
use Tempest\Generation\TypeScript\TypeNodes\IntersectionTypeNode;
13+
use Tempest\Generation\TypeScript\TypeNodes\PrimitiveTypeNode;
14+
use Tempest\Generation\TypeScript\TypeNodes\TypeNode;
15+
use Tempest\Generation\TypeScript\TypeNodes\UnionTypeNode;
1216
use Tempest\Generation\TypeScript\TypeScriptGenerationConfig;
1317
use Tempest\Generation\TypeScript\TypeScriptGenerator;
1418
use Tempest\Reflection\PropertyReflector;
@@ -48,62 +52,56 @@ private function resolveProperty(PropertyReflector $property, TypeScriptGenerato
4852
$elementTypeReflector = $property->getIterableType();
4953

5054
if ($elementTypeReflector instanceof TypeReflector) {
51-
$result = $this->resolveType($elementTypeReflector, $generator);
55+
$resolvedType = $this->resolveType($elementTypeReflector, $generator);
5256

5357
return new PropertyDefinition(
5458
name: $property->getName(),
55-
definition: $result->type . '[]',
59+
type: new ArrayTypeNode($resolvedType),
5660
isNullable: $property->isNullable(),
57-
fqcn: $result->fqcn,
5861
);
5962
}
6063

6164
return new PropertyDefinition(
6265
name: $property->getName(),
63-
definition: 'any[]',
66+
type: new ArrayTypeNode(new PrimitiveTypeNode('any')),
6467
isNullable: $property->isNullable(),
6568
);
6669
}
6770

6871
if ($type->isUnion() || $type->isIntersection()) {
6972
$parts = $type->split();
7073
$resolvedTypes = [];
71-
$referencedClasses = [];
7274

7375
foreach ($parts as $part) {
7476
if ($part->getName() === 'null') {
7577
continue;
7678
}
7779

78-
$result = $this->resolveType($part, $generator);
79-
$resolvedTypes[] = $result->type;
80-
81-
if ($result->fqcn !== null) {
82-
$referencedClasses[] = $result->fqcn;
83-
}
80+
$resolvedTypes[] = $this->resolveType($part, $generator);
8481
}
8582

86-
$symbol = $type->isIntersection() ? '&' : '|';
87-
8883
return new PropertyDefinition(
8984
name: $property->getName(),
90-
definition: implode(" {$symbol} ", $resolvedTypes),
85+
type: match (true) {
86+
$resolvedTypes === [] => new PrimitiveTypeNode('any'),
87+
count($resolvedTypes) === 1 => $resolvedTypes[0],
88+
$type->isIntersection() => new IntersectionTypeNode($resolvedTypes),
89+
default => new UnionTypeNode($resolvedTypes),
90+
},
9191
isNullable: $property->isNullable(),
92-
fqcn: count($referencedClasses) === 1 ? $referencedClasses[0] : null,
9392
);
9493
}
9594

96-
$result = $this->resolveType($type, $generator);
95+
$resolvedType = $this->resolveType($type, $generator);
9796

9897
return new PropertyDefinition(
9998
name: $property->getName(),
100-
definition: $result->type,
99+
type: $resolvedType,
101100
isNullable: $property->isNullable(),
102-
fqcn: $result->fqcn,
103101
);
104102
}
105103

106-
private function resolveType(TypeReflector $type, TypeScriptGenerator $generator): ResolvedType
104+
private function resolveType(TypeReflector $type, TypeScriptGenerator $generator): TypeNode
107105
{
108106
foreach ($this->config->resolvers as $resolverClass) {
109107
$resolver = $this->container->get($resolverClass);
@@ -113,6 +111,6 @@ private function resolveType(TypeReflector $type, TypeScriptGenerator $generator
113111
}
114112
}
115113

116-
return new ResolvedType('any');
114+
return new PrimitiveTypeNode('any');
117115
}
118116
}

packages/generation/src/TypeScript/StructureResolvers/EnumStructureResolver.php

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010
use Tempest\Container\Container;
1111
use Tempest\Generation\TypeScript\StructureResolver;
1212
use Tempest\Generation\TypeScript\TypeDefinition;
13+
use Tempest\Generation\TypeScript\TypeNodes\TypeNode;
14+
use Tempest\Generation\TypeScript\TypeNodes\UnionTypeNode;
1315
use Tempest\Generation\TypeScript\TypeScriptGenerationConfig;
1416
use Tempest\Generation\TypeScript\TypeScriptGenerator;
1517
use Tempest\Reflection\TypeReflector;
@@ -26,29 +28,30 @@ public function __construct(
2628

2729
public function resolve(TypeReflector $type, TypeScriptGenerator $generator): TypeDefinition
2830
{
29-
$typeScriptType = implode(
30-
separator: ' | ',
31-
array: array_map(
32-
callback: fn (ReflectionEnumUnitCase|ReflectionEnumBackedCase $case) => $this->resolveType(new TypeReflector($case), $generator),
33-
array: $type->asEnum()->getReflectionCases(),
34-
),
31+
$types = array_map(
32+
callback: fn (ReflectionEnumUnitCase|ReflectionEnumBackedCase $case) => $this->resolveType(new TypeReflector($case), $generator),
33+
array: $type->asEnum()->getReflectionCases(),
3534
);
3635

36+
$resolvedType = count($types) === 1
37+
? $types[0]
38+
: new UnionTypeNode($types);
39+
3740
return new TypeDefinition(
3841
class: $type->getName(),
3942
originalType: $type,
40-
definition: $typeScriptType,
43+
type: $resolvedType,
4144
isNullable: $type->isNullable(),
4245
);
4346
}
4447

45-
private function resolveType(TypeReflector $type, TypeScriptGenerator $generator): string
48+
private function resolveType(TypeReflector $type, TypeScriptGenerator $generator): TypeNode
4649
{
4750
foreach ($this->config->resolvers as $resolverClass) {
4851
$resolver = $this->container->get($resolverClass);
4952

5053
if ($resolver->canResolve($type)) {
51-
return $resolver->resolve($type, $generator)->type;
54+
return $resolver->resolve($type, $generator);
5255
}
5356
}
5457

packages/generation/src/TypeScript/TypeDefinition.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
namespace Tempest\Generation\TypeScript;
66

7+
use Tempest\Generation\TypeScript\TypeNodes\TypeNode;
78
use Tempest\Reflection\TypeReflector;
89
use Tempest\Support\Str;
910

@@ -25,7 +26,7 @@ final class TypeDefinition
2526
public function __construct(
2627
public string $class,
2728
public TypeReflector $originalType,
28-
public string $definition,
29+
public TypeNode $type,
2930
public bool $isNullable,
3031
) {}
3132
}
Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Generation\TypeScript;
6+
7+
use Tempest\Generation\TypeScript\TypeNodes\ArrayTypeNode;
8+
use Tempest\Generation\TypeScript\TypeNodes\IntersectionTypeNode;
9+
use Tempest\Generation\TypeScript\TypeNodes\LiteralTypeNode;
10+
use Tempest\Generation\TypeScript\TypeNodes\ObjectTypeNode;
11+
use Tempest\Generation\TypeScript\TypeNodes\ObjectTypePropertyNode;
12+
use Tempest\Generation\TypeScript\TypeNodes\PrimitiveTypeNode;
13+
use Tempest\Generation\TypeScript\TypeNodes\RawTypeNode;
14+
use Tempest\Generation\TypeScript\TypeNodes\SymbolTypeNode;
15+
use Tempest\Generation\TypeScript\TypeNodes\TypeNode;
16+
use Tempest\Generation\TypeScript\TypeNodes\UnionTypeNode;
17+
18+
/**
19+
* Renders semantic type nodes to TypeScript text.
20+
*/
21+
final readonly class TypeNodeRenderer
22+
{
23+
/**
24+
* @param callable(string):string $symbolRenderer
25+
*/
26+
public function render(TypeNode $type, callable $symbolRenderer): string
27+
{
28+
return match (true) {
29+
$type instanceof PrimitiveTypeNode => $type->name,
30+
$type instanceof RawTypeNode => $type->expression,
31+
$type instanceof LiteralTypeNode => $this->renderLiteral($type),
32+
$type instanceof SymbolTypeNode => $symbolRenderer($type->fqcn),
33+
$type instanceof ArrayTypeNode => $this->render($type->type, $symbolRenderer) . '[]',
34+
$type instanceof ObjectTypeNode => $this->renderObjectType($type, $symbolRenderer),
35+
$type instanceof UnionTypeNode => implode(' | ', array_map(
36+
callback: fn (TypeNode $part): string => $this->render($part, $symbolRenderer),
37+
array: $type->types,
38+
)),
39+
$type instanceof IntersectionTypeNode => implode(' & ', array_map(
40+
callback: fn (TypeNode $part): string => $this->render($part, $symbolRenderer),
41+
array: $type->types,
42+
)),
43+
default => 'any',
44+
};
45+
}
46+
47+
private function renderLiteral(LiteralTypeNode $type): string
48+
{
49+
if (is_string($type->value)) {
50+
return sprintf("'%s'", addcslashes($type->value, "\\'\n\r\t\v\f"));
51+
}
52+
53+
if (is_bool($type->value)) {
54+
return $type->value ? 'true' : 'false';
55+
}
56+
57+
return (string) $type->value;
58+
}
59+
60+
/**
61+
* @param callable(string):string $symbolRenderer
62+
*/
63+
private function renderObjectType(ObjectTypeNode $type, callable $symbolRenderer): string
64+
{
65+
$properties = array_map(
66+
callback: fn (ObjectTypePropertyNode $property): string => vsprintf('%s%s: %s', [
67+
$this->renderObjectPropertyName($property->name),
68+
$property->optional ? '?' : '',
69+
$this->render($property->type, $symbolRenderer),
70+
]),
71+
array: $type->properties,
72+
);
73+
74+
return '{ ' . implode('; ', $properties) . '; }';
75+
}
76+
77+
private function renderObjectPropertyName(string $name): string
78+
{
79+
if (preg_match('/^[A-Za-z_$][A-Za-z0-9_$]*$/', $name) === 1) {
80+
return $name;
81+
}
82+
83+
return sprintf("'%s'", addcslashes($name, "\\'\n\r\t\v\f"));
84+
}
85+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Generation\TypeScript\TypeNodes;
6+
7+
final class ArrayTypeNode implements TypeNode
8+
{
9+
public function __construct(
10+
public readonly TypeNode $type,
11+
) {}
12+
13+
public array $references {
14+
get => $this->type->references;
15+
}
16+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tempest\Generation\TypeScript\TypeNodes;
6+
7+
final class IntersectionTypeNode implements TypeNode
8+
{
9+
/**
10+
* @param TypeNode[] $types
11+
*/
12+
public function __construct(
13+
public readonly array $types,
14+
) {}
15+
16+
public array $references {
17+
get {
18+
$fqcns = [];
19+
20+
foreach ($this->types as $type) {
21+
$fqcns = [...$fqcns, ...$type->references];
22+
}
23+
24+
return array_values(array_unique($fqcns));
25+
}
26+
}
27+
}

0 commit comments

Comments
 (0)