Skip to content

Commit ed8aedb

Browse files
authored
Support generics (#94)
* Support generics
1 parent 0a09132 commit ed8aedb

26 files changed

+872
-97
lines changed

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
"scripts": {
1111
"test": "vendor/bin/phpunit --no-coverage",
1212
"test:with-coverage": "XDEBUG_MODE=coverage vendor/bin/phpunit",
13-
"test:coverage-level": "vendor/bin/coverage-check ./coverage/clover.xml 86",
13+
"test:coverage-level": "vendor/bin/coverage-check ./coverage/clover.xml 85",
1414
"build-phar": "bin/build.sh",
1515
"build-phar:test": "php build/php-converter.phar --from=./tests/Fixtures/ --to=.",
1616
"test:update-snapshots": "vendor/bin/phpunit -d --update-snapshots --no-coverage",

ecs.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,10 @@
66
use PhpCsFixer\Fixer\Import\NoUnusedImportsFixer;
77
use PhpCsFixer\Fixer\Import\OrderedImportsFixer;
88
use PhpCsFixer\Fixer\Operator\NotOperatorWithSuccessorSpaceFixer;
9+
use PhpCsFixer\Fixer\Phpdoc\NoSuperfluousPhpdocTagsFixer;
910
use PhpCsFixer\Fixer\Phpdoc\PhpdocLineSpanFixer;
1011
use PhpCsFixer\Fixer\Strict\DeclareStrictTypesFixer;
12+
use SlevomatCodingStandard\Sniffs\Attributes\RequireAttributeAfterDocCommentSniff;
1113
use SlevomatCodingStandard\Sniffs\ControlStructures\EarlyExitSniff;
1214
use Symplify\EasyCodingStandard\Config\ECSConfig;
1315
use Symplify\EasyCodingStandard\ValueObject\Set\SetList;
@@ -24,6 +26,7 @@
2426
NoUnusedImportsFixer::class,
2527
DeclareStrictTypesFixer::class,
2628
GlobalNamespaceImportFixer::class,
29+
RequireAttributeAfterDocCommentSniff::class,
2730
]);
2831

2932
$ecsConfig->ruleWithConfiguration(EarlyExitSniff::class, [
@@ -42,6 +45,7 @@
4245
$ecsConfig->skip([
4346
OrderedImportsFixer::class,
4447
NotOperatorWithSuccessorSpaceFixer::class,
48+
NoSuperfluousPhpdocTagsFixer::class,
4549
PhpdocLineSpanFixer::class,
4650
]);
4751
};

src/Ast/DtoVisitor.php

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpTypeInterface;
1818
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpUnionType;
1919
use Exception;
20+
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpUnknownType;
2021
use function sprintf;
2122
use function get_class;
2223
use function array_map;
@@ -52,6 +53,7 @@ private function createDtoType(Class_|Enum_ $node): void
5253
{
5354
$properties = [];
5455
$expressionType = $this->resolveExpressionType($node);
56+
$classComments = $node->getDocComment()?->getText();
5557

5658
foreach ($node->stmts as $stmt) {
5759
if ($stmt instanceof Node\Stmt\ClassConst) {
@@ -72,11 +74,21 @@ private function createDtoType(Class_|Enum_ $node): void
7274
}
7375

7476
if ($stmt instanceof Node\Stmt\Property) {
75-
if ($stmt->type === null) {
76-
throw new Exception(sprintf("Property %s of class %s has no type. Please add PHP type", $stmt->props[0]->name->name, $node->name->name));
77+
$comment = $stmt->getDocComment()?->getText() ?? '';
78+
79+
/** @var PhpTypeInterface|null $docBlockType */
80+
$docBlockType = null;
81+
$nativeType = $stmt->type;
82+
83+
if ($comment) {
84+
$docBlockType = $this->phpDocTypeParser->parseVarOrReturn($comment);
85+
}
86+
87+
if (!$nativeType && !$docBlockType) {
88+
throw new Exception(sprintf("Property %s#%s has no type. Please add PHP type", $node->name->name, $stmt->props[0]->name->name));
7789
}
7890

79-
$type = $this->createSingleType($stmt->type, $stmt->getDocComment()?->getText());
91+
$type = $docBlockType ?? $this->createSingleType($nativeType);
8092

8193
$properties[] = new DtoClassProperty(
8294
type: $type,
@@ -85,19 +97,39 @@ private function createDtoType(Class_|Enum_ $node): void
8597
}
8698

8799
if ($stmt instanceof Node\Stmt\ClassMethod) {
100+
$classMethodComments = $stmt->getDocComment()?->getText();
101+
/** @var DtoClassProperty[]|null $classMethodCommentsParsed */
102+
$classMethodCommentsParsed = null;
88103
foreach ($stmt->params as $param) {
89104
if ($param->flags !== Node\Stmt\Class_::MODIFIER_PUBLIC) {
90105
continue;
91106
}
92107

93-
if ($param->type === null) {
94-
throw new Exception(sprintf("Property %s of class %s has no type. Please add PHP type", $param->var->name, $node->name->name));
108+
$paramType = $param->type;
109+
$paramName = $param->var->name;
110+
111+
if ($paramType === null && $classMethodComments) {
112+
if ($classMethodCommentsParsed === null) {
113+
$classMethodCommentsParsed = $this->phpDocTypeParser->parseMethodParams($classMethodComments);
114+
}
115+
116+
foreach ($classMethodCommentsParsed as $classMethodCommentParsed) {
117+
if ($classMethodCommentParsed->getName() === $paramName) {
118+
$properties[] = $classMethodCommentParsed;
119+
continue 2;
120+
}
121+
}
95122
}
96-
$type = $this->createSingleType($param->type, $param->getDocComment()?->getText());
123+
124+
if ($paramType === null) {
125+
throw new Exception(sprintf("Property %s#%s has no type. Please add PHP type", $node->name->name, $paramName));
126+
}
127+
128+
$type = $this->createSingleType($paramType, $param->getDocComment()?->getText());
97129

98130
$properties[] = new DtoClassProperty(
99131
type: $type,
100-
name: $param->var->name,
132+
name: $paramName,
101133
);
102134
}
103135
}
@@ -121,10 +153,17 @@ private function createDtoType(Class_|Enum_ $node): void
121153
);
122154
}
123155

156+
/** @var PhpUnknownType[] $generics */
157+
$generics = [];
158+
if ($classComments) {
159+
$generics = $this->phpDocTypeParser->parseClassComments($classComments);
160+
}
161+
124162
$this->converterResult->dtoList->add(new DtoType(
125163
name: $node->name->name,
126164
expressionType: $expressionType,
127165
properties: $properties,
166+
generics: $generics,
128167
));
129168
}
130169

@@ -133,18 +172,18 @@ private function createSingleType(
133172
?string $docComment = null,
134173
): PhpTypeInterface {
135174
if ($docComment) {
136-
$docBlockType = $this->phpDocTypeParser->parse($docComment);
175+
$docBlockType = $this->phpDocTypeParser->parseVarOrReturn($docComment);
137176
if ($docBlockType) {
138177
return $docBlockType;
139178
}
140179
}
141180

142181
if ($param instanceof Node\UnionType) {
143-
return new PhpUnionType(array_map(fn ($singleParam) => $this->createSingleType($singleParam, $docComment), $param->types));
182+
return new PhpUnionType(array_map(fn ($singleParam) => $this->createSingleType($singleParam), $param->types));
144183
}
145184

146185
if ($param instanceof Node\NullableType) {
147-
return PhpUnionType::nullable($this->createSingleType($param->type, $docComment));
186+
return PhpUnionType::nullable($this->createSingleType($param->type));
148187
}
149188

150189
$typeName = get_class($param) === Node\Name::class || get_class($param) === Node\Name\FullyQualified::class

src/Ast/PhpDocTypeParser.php

Lines changed: 90 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44

55
namespace Riverwaysoft\PhpConverter\Ast;
66

7+
use PHPStan\PhpDocParser\Ast\PhpDoc\ParamTagValueNode;
8+
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocChildNode;
9+
use PHPStan\PhpDocParser\Ast\PhpDoc\ReturnTagValueNode;
710
use PHPStan\PhpDocParser\Ast\PhpDoc\PhpDocTagNode;
11+
use PHPStan\PhpDocParser\Ast\PhpDoc\TemplateTagValueNode;
812
use PHPStan\PhpDocParser\Ast\PhpDoc\VarTagValueNode;
913
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
14+
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
1015
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
1116
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
1217
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
@@ -15,10 +20,12 @@
1520
use PHPStan\PhpDocParser\Parser\PhpDocParser;
1621
use PHPStan\PhpDocParser\Parser\TokenIterator;
1722
use PHPStan\PhpDocParser\Parser\TypeParser;
23+
use Riverwaysoft\PhpConverter\Dto\DtoClassProperty;
1824
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpListType;
1925
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpTypeFactory;
2026
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpTypeInterface;
2127
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpUnionType;
28+
use Riverwaysoft\PhpConverter\Dto\PhpType\PhpUnknownType;
2229
use function array_map;
2330

2431
class PhpDocTypeParser
@@ -35,27 +42,95 @@ public function __construct()
3542
$this->phpDocParser = new PhpDocParser($typeParser, $constExprParser);
3643
}
3744

38-
public function parse(string $input): PhpTypeInterface|null
45+
/** @return DtoClassProperty[] */
46+
public function parseMethodParams(string $input): array
3947
{
40-
$tokens = new TokenIterator($this->lexer->tokenize($input));
41-
$result = $this->phpDocParser->parse($tokens)->children;
42-
$varTagNode = null;
48+
$phpDocNodes = $this->commentToPhpDocNodes($input);
49+
/** @var DtoClassProperty[] $results */
50+
$results = [];
4351

44-
foreach ($result as $node) {
52+
foreach ($phpDocNodes as $node) {
4553
if (!$node instanceof PhpDocTagNode) {
4654
continue;
4755
}
4856

49-
if ($node->value instanceof VarTagValueNode) {
50-
$varTagNode = $node->value;
57+
if (!($node->value instanceof ParamTagValueNode)) {
58+
continue;
59+
}
60+
61+
$convertedType = $this->convertToDto($node->value->type);
62+
if (!$convertedType) {
63+
continue;
5164
}
65+
66+
$variableName = $this->getVariableNameWithoutDollarSign($node->value);
67+
if (!$variableName) {
68+
continue;
69+
}
70+
71+
$results[] = new DtoClassProperty(
72+
type: $convertedType,
73+
name: $variableName,
74+
);
5275
}
5376

54-
if (!$varTagNode) {
77+
return $results;
78+
}
79+
80+
private function getVariableNameWithoutDollarSign(ParamTagValueNode $nodeValue): string|null
81+
{
82+
$isParameterNameValid = str_starts_with($nodeValue->parameterName, '$') && mb_strlen($nodeValue->parameterName) > 1;
83+
if (!$isParameterNameValid) {
5584
return null;
5685
}
86+
return ltrim($nodeValue->parameterName, '$');
87+
}
88+
89+
/** @return PhpUnknownType[] */
90+
public function parseClassComments(string $input): array
91+
{
92+
$phpDocNodes = $this->commentToPhpDocNodes($input);
93+
/** @var PhpUnknownType[] $generics */
94+
$generics = [];
95+
96+
foreach ($phpDocNodes as $node) {
97+
if (!$node instanceof PhpDocTagNode) {
98+
continue;
99+
}
100+
101+
if (!($node->value instanceof TemplateTagValueNode)) {
102+
continue;
103+
}
104+
$generics[] = new PhpUnknownType($node->value->name);
105+
}
106+
107+
return $generics;
108+
}
57109

58-
return $this->convertToDto($varTagNode->type);
110+
/** @return PhpDocChildNode[] */
111+
private function commentToPhpDocNodes(string $input): array
112+
{
113+
$tokens = new TokenIterator($this->lexer->tokenize($input));
114+
return $this->phpDocParser->parse($tokens)->children;
115+
}
116+
117+
public function parseVarOrReturn(string $input): PhpTypeInterface|null
118+
{
119+
$phpDocNodes = $this->commentToPhpDocNodes($input);
120+
121+
foreach ($phpDocNodes as $node) {
122+
if (!$node instanceof PhpDocTagNode) {
123+
continue;
124+
}
125+
126+
if (!($node->value instanceof VarTagValueNode) && !($node->value instanceof ReturnTagValueNode)) {
127+
continue;
128+
}
129+
130+
return $this->convertToDto($node->value->type);
131+
}
132+
133+
return null;
59134
}
60135

61136
private function convertToDto(TypeNode $node): PhpTypeInterface|null
@@ -69,6 +144,12 @@ private function convertToDto(TypeNode $node): PhpTypeInterface|null
69144
if ($node instanceof UnionTypeNode) {
70145
return new PhpUnionType(array_map(fn (TypeNode $child) => $this->convertToDto($child), $node->types));
71146
}
147+
if ($node instanceof GenericTypeNode) {
148+
return PhpTypeFactory::create($node->type->name, [], array_map(
149+
fn (TypeNode $child) => $this->convertToDto($child),
150+
$node->genericTypes,
151+
));
152+
}
72153
return null;
73154
}
74155
}

0 commit comments

Comments
 (0)