Skip to content

Commit 32fea15

Browse files
authored
Fix #339: Add support for PHPStan/Psalm syntax and intersection types. Fix replacing static with FQCN
1 parent 90ddaa3 commit 32fea15

File tree

64 files changed

+9730
-584
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

64 files changed

+9730
-584
lines changed

CHANGELOG.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
Yii Framework 2 apidoc extension Change Log
22
===========================================
33

4-
3.0.9 under development
4+
4.0.0 under development
55
-----------------------
66

77
- Bug #338: Fix deprecation error `Using null as an array offset is deprecated, use an empty string instead` (mspirkov)
88
- Enh #337: Log invalid tags (mspirkov)
9+
- Enh #339: Add support for PHPStan/Psalm syntax (mspirkov)
10+
- Enh #339: Add support for intersection types (mspirkov)
11+
- Bug #339: Fix the mechanism for replacing `static` with FQCN (mspirkov)
12+
- Bug #339: Fix processing of multidimensional arrays (mspirkov)
913

1014

1115
3.0.8 November 24, 2025

commands/GuideController.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
use yii\apidoc\renderers\GuideRenderer;
1414
use yii\helpers\Console;
1515
use yii\helpers\FileHelper;
16-
use Yii;
1716
use yii\helpers\Json;
1817

1918
/**

composer.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@
2424
"php": "^7.4 || ^8.0",
2525
"yiisoft/yii2": "~2.0.16",
2626
"yiisoft/yii2-bootstrap": "~2.0.0",
27-
"phpdocumentor/reflection": "^5.1.0 || ^6.0.0",
27+
"phpdocumentor/reflection": "^5.3.0 || ^6.0.0",
28+
"phpdocumentor/type-resolver": "^1.11",
2829
"nikic/php-parser": "^4.0 || ^5.0",
2930
"cebe/js-search": "~0.9.0",
3031
"cebe/markdown": "^1.0",

helpers/TypeHelper.php

Lines changed: 30 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
namespace yii\apidoc\helpers;
99

10+
use phpDocumentor\Reflection\PseudoTypes\Conditional;
11+
use phpDocumentor\Reflection\PseudoTypes\ConditionalForParameter;
1012
use phpDocumentor\Reflection\Type;
1113
use phpDocumentor\Reflection\Types\AggregatedType;
1214

@@ -18,24 +20,41 @@
1820
final class TypeHelper
1921
{
2022
/**
21-
* @return string[]
23+
* @return Type[]
2224
*/
23-
public static function splitType(?Type $type): array
25+
public static function getTypesByAggregatedType(AggregatedType $compound): array
2426
{
25-
if ($type === null) {
26-
return [];
27+
$types = [];
28+
foreach ($compound as $type) {
29+
$types[] = $type;
2730
}
2831

29-
// TODO: Don't split the Intersection
30-
if (!$type instanceof AggregatedType) {
31-
return [(string) $type];
32-
}
32+
return $types;
33+
}
3334

35+
/**
36+
* @param Conditional|ConditionalForParameter $type
37+
* @return Type[] Possible unique types.
38+
*/
39+
public static function getPossibleTypesByConditionalType(Type $type): array
40+
{
3441
$types = [];
35-
foreach ($type as $childType) {
36-
$types[] = (string) $childType;
42+
43+
foreach ([$type->getIf(), $type->getElse()] as $innerType) {
44+
if ($innerType instanceof Conditional || $innerType instanceof ConditionalForParameter) {
45+
$types = array_merge($types, self::getPossibleTypesByConditionalType($innerType));
46+
} elseif ($innerType instanceof AggregatedType) {
47+
$types = array_merge($types, self::getTypesByAggregatedType($innerType));
48+
} else {
49+
$types[] = $innerType;
50+
}
3751
}
3852

39-
return $types;
53+
$uniqueTypes = [];
54+
foreach ($types as $innerType) {
55+
$uniqueTypes[(string) $innerType] = $innerType;
56+
}
57+
58+
return array_values($uniqueTypes);
4059
}
4160
}

models/BaseDoc.php

Lines changed: 97 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,15 @@
1313
use phpDocumentor\Reflection\DocBlock\Tags\Generic;
1414
use phpDocumentor\Reflection\DocBlock\Tags\InvalidTag;
1515
use phpDocumentor\Reflection\DocBlock\Tags\Since;
16+
use phpDocumentor\Reflection\DocBlock\Tags\Template;
17+
use phpDocumentor\Reflection\FqsenResolver;
1618
use phpDocumentor\Reflection\Php\Class_;
1719
use phpDocumentor\Reflection\Php\Constant;
1820
use phpDocumentor\Reflection\Php\Interface_;
1921
use phpDocumentor\Reflection\Php\Method;
2022
use phpDocumentor\Reflection\Php\Property;
2123
use phpDocumentor\Reflection\Php\Trait_;
24+
use phpDocumentor\Reflection\TypeResolver;
2225
use yii\base\BaseObject;
2326
use yii\helpers\StringHelper;
2427

@@ -32,14 +35,26 @@
3235
*/
3336
class BaseDoc extends BaseObject
3437
{
38+
private const PHPSTAN_TYPE_ANNOTATION_NAME = 'phpstan-type';
39+
private const PSALM_TYPE_ANNOTATION_NAME = 'psalm-type';
40+
41+
private const PHPSTAN_IMPORT_TYPE_ANNOTATION_NAME = 'phpstan-import-type';
42+
private const PSALM_IMPORT_TYPE_ANNOTATION_NAME = 'psalm-import-type';
43+
3544
private const INHERITDOC_TAG_NAME = 'inheritdoc';
3645
private const TODO_TAG_NAME = 'todo';
3746

3847
/**
3948
* @var \phpDocumentor\Reflection\Types\Context|null
4049
*/
4150
public $phpDocContext;
51+
/**
52+
* @var string|null
53+
*/
4254
public $name;
55+
/**
56+
* @var string|null
57+
*/
4358
public $fullName;
4459
public $sourceFile;
4560
public $startLine;
@@ -64,6 +79,30 @@ class BaseDoc extends BaseObject
6479
* @var Generic[]
6580
*/
6681
public $todos = [];
82+
/**
83+
* @var array<string, Template>
84+
*/
85+
public $templates = [];
86+
/**
87+
* @var self|null
88+
*/
89+
public $parent = null;
90+
/**
91+
* @var array<string, PseudoTypeDoc>
92+
*/
93+
public array $phpStanTypes = [];
94+
/**
95+
* @var array<string, PseudoTypeDoc>
96+
*/
97+
public array $psalmTypes = [];
98+
/**
99+
* @var array<string, PseudoTypeImportDoc>
100+
*/
101+
public array $phpStanTypeImports = [];
102+
/**
103+
* @var array<string, PseudoTypeImportDoc>
104+
*/
105+
public array $psalmTypeImports = [];
67106

68107
/**
69108
* Checks if doc has tag of a given name
@@ -126,18 +165,24 @@ public function getPackageName()
126165
}
127166

128167
/**
168+
* @param self|null $parent
129169
* @param Class_|Method|Trait_|Interface_|Property|Constant|null $reflector
130170
* @param Context|null $context
131171
* @param array $config
132172
*/
133-
public function __construct($reflector = null, $context = null, $config = [])
173+
public function __construct($parent = null, $reflector = null, $context = null, $config = [])
134174
{
135175
parent::__construct($config);
136176

177+
$this->parent = $parent;
178+
137179
if ($reflector === null) {
138180
return;
139181
}
140182

183+
$fqsenResolver = new FqsenResolver();
184+
$typeResolver = new TypeResolver($fqsenResolver);
185+
141186
// base properties
142187
$this->fullName = trim((string) $reflector->getFqsen(), '\\()');
143188

@@ -160,7 +205,7 @@ public function __construct($reflector = null, $context = null, $config = [])
160205
return;
161206
}
162207

163-
$this->shortDescription = StringHelper::mb_ucfirst($docBlock->getSummary());;
208+
$this->shortDescription = StringHelper::mb_ucfirst($docBlock->getSummary());
164209
if (empty($this->shortDescription) && !($this instanceof PropertyDoc) && $context !== null && !$docBlock->getTagsByName(self::INHERITDOC_TAG_NAME)) {
165210
$context->warnings[] = [
166211
'line' => $this->startLine,
@@ -192,9 +237,57 @@ public function __construct($reflector = null, $context = null, $config = [])
192237
$this->deprecatedSince = $tag->getVersion();
193238
$this->deprecatedReason = (string) $tag->getDescription();
194239
unset($this->tags[$i]);
195-
} elseif ($tag instanceof Generic && $tag->getName() === self::TODO_TAG_NAME) {
196-
$this->todos[] = $tag;
240+
} elseif ($tag instanceof Template) {
241+
$fqsen = $fqsenResolver->resolve($tag->getTemplateName(), $this->phpDocContext);
242+
$this->templates[(string) $fqsen] = $tag;
197243
unset($this->tags[$i]);
244+
} elseif ($tag instanceof Generic) {
245+
if ($tag->getName() === self::TODO_TAG_NAME) {
246+
$this->todos[] = $tag;
247+
unset($this->tags[$i]);
248+
} elseif ($tag->getName() === self::PHPSTAN_TYPE_ANNOTATION_NAME) {
249+
$tagData = explode(' ', trim($tag->getDescription()), 2);
250+
$phpStanType = new PseudoTypeDoc(
251+
PseudoTypeDoc::TYPE_PHPSTAN,
252+
$this,
253+
trim($tagData[0]),
254+
$typeResolver->resolve(trim($tagData[1]), $this->phpDocContext)
255+
);
256+
$fqsen = $fqsenResolver->resolve($phpStanType->name, $this->phpDocContext);
257+
$this->phpStanTypes[(string) $fqsen] = $phpStanType;
258+
unset($this->tags[$i]);
259+
} elseif ($tag->getName() === self::PSALM_TYPE_ANNOTATION_NAME) {
260+
$tagData = explode('=', trim($tag->getDescription()), 2);
261+
$psalmType = new PseudoTypeDoc(
262+
PseudoTypeDoc::TYPE_PSALM,
263+
$this,
264+
trim($tagData[0]),
265+
$typeResolver->resolve(trim($tagData[1]), $this->phpDocContext)
266+
);
267+
$fqsen = $fqsenResolver->resolve($psalmType->name, $this->phpDocContext);
268+
$this->psalmTypes[(string) $fqsen] = $psalmType;
269+
unset($this->tags[$i]);
270+
} elseif ($tag->getName() === self::PHPSTAN_IMPORT_TYPE_ANNOTATION_NAME) {
271+
$tagData = explode(' from ', trim($tag->getDescription()), 2);
272+
$phpStanTypeImport = new PseudoTypeImportDoc(
273+
PseudoTypeImportDoc::TYPE_PHPSTAN,
274+
trim($tagData[0]),
275+
$fqsenResolver->resolve(trim($tagData[1]), $this->phpDocContext)
276+
);
277+
$fqsen = $fqsenResolver->resolve($phpStanTypeImport->typeName, $this->phpDocContext);
278+
$this->phpStanTypeImports[(string) $fqsen] = $phpStanTypeImport;
279+
unset($this->tags[$i]);
280+
} elseif ($tag->getName() === self::PSALM_IMPORT_TYPE_ANNOTATION_NAME) {
281+
$tagData = explode(' from ', trim($tag->getDescription()), 2);
282+
$psalmTypeImport = new PseudoTypeImportDoc(
283+
PseudoTypeImportDoc::TYPE_PSALM,
284+
trim($tagData[0]),
285+
$fqsenResolver->resolve(trim($tagData[1]), $this->phpDocContext)
286+
);
287+
$fqsen = $fqsenResolver->resolve($psalmTypeImport->typeName, $this->phpDocContext);
288+
$this->psalmTypeImports[(string) $fqsen] = $psalmTypeImport;
289+
unset($this->tags[$i]);
290+
}
198291
} elseif ($tag instanceof InvalidTag && $context !== null) {
199292
$exception = $tag->getException();
200293
$message = 'Invalid tag: ' . $tag->render() . '.';

models/ClassDoc.php

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77

88
namespace yii\apidoc\models;
99

10+
use phpDocumentor\Reflection\Php\Class_;
11+
1012
/**
1113
* Represents API documentation information for a `class`.
1214
*
@@ -91,7 +93,9 @@ public function getNativeEvents()
9193
}
9294

9395
/**
94-
* @inheritdoc
96+
* @param Class_|null $reflector
97+
* @param Context|null $context
98+
* @param array $config
9599
*/
96100
public function __construct($reflector = null, $context = null, $config = [])
97101
{
@@ -118,11 +122,11 @@ public function __construct($reflector = null, $context = null, $config = [])
118122
foreach ($reflector->getConstants() as $constantReflector) {
119123
$docBlock = $constantReflector->getDocBlock();
120124
if ($docBlock !== null && count($docBlock->getTagsByName('event')) > 0) {
121-
$event = new EventDoc($constantReflector, null, [], $docBlock);
125+
$event = new EventDoc($this, $constantReflector, null, [], $docBlock);
122126
$event->definedBy = $this->name;
123127
$this->events[$event->name] = $event;
124128
} else {
125-
$constant = new ConstDoc($constantReflector);
129+
$constant = new ConstDoc($this, $constantReflector);
126130
$constant->definedBy = $this->name;
127131
$this->constants[$constant->name] = $constant;
128132
}

models/ConstDoc.php

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@
1818
*/
1919
class ConstDoc extends BaseDoc
2020
{
21+
/**
22+
* @var string|null
23+
*/
2124
public $definedBy;
2225
/**
2326
* @var string|null
@@ -26,14 +29,15 @@ class ConstDoc extends BaseDoc
2629

2730

2831
/**
32+
* @param ClassDoc|TraitDoc $parent
2933
* @param Constant|null $reflector
3034
* @param Context|null $context
3135
* @param array $config
3236
* @param DocBlock|null $docBlock
3337
*/
34-
public function __construct($reflector = null, $context = null, $config = [], $docBlock = null)
38+
public function __construct($parent, $reflector = null, $context = null, $config = [], $docBlock = null)
3539
{
36-
parent::__construct($reflector, $context, $config);
40+
parent::__construct($parent, $reflector, $context, $config);
3741

3842
if ($reflector === null) {
3943
return;

0 commit comments

Comments
 (0)