Skip to content

Commit 698f3cc

Browse files
committed
TypeNodeResolverExtension documentation
1 parent 891e283 commit 698f3cc

5 files changed

Lines changed: 154 additions & 0 deletions

File tree

website/src/developing-extensions/allowed-subtypes.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,9 @@ When you implement this extension, it has a couple of effects:
4141
* Smarter type inference when subtracting types from each other
4242
* Error reporting when a disallowed class implements the restricted interface/extends a restricted parent class
4343

44+
An example
45+
----------------
46+
4447
Let's say you have a class `Foo` and you want only `Bar` and `Baz` to be its child classes:
4548

4649
```php
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
---
2+
title: Custom PHPDoc Types
3+
---
4+
5+
PHPStan lets you override how it converts [PHPDoc Type AST coming from its phpdoc-parser library](https://apiref.phpstan.org/1.9.x/PHPStan.PhpDocParser.Ast.Type.TypeNode.html) into [its type system representation](/developing-extensions/type-system). This can be used to introduce custom utility types - a popular feature known [from other languages like TypeScript](https://www.typescriptlang.org/docs/handbook/utility-types.html).
6+
7+
The implementation is all about applying the [core concepts](/developing-extensions/core-concepts) like [the type system](/developing-extensions/type-system) so check out that guide first and then continue here.
8+
9+
The conversion is done by a class called [`TypeNodeResolver`](https://apiref.phpstan.org/1.9.x/PHPStan.PhpDoc.TypeNodeResolver.html). That's why the interface to implement in this extension type is called [`TypeNodeResolverExtension`](https://apiref.phpstan.org/1.9.x/PHPStan.PhpDoc.TypeNodeResolverExtension.html):
10+
11+
```php
12+
namespace PHPStan\PhpDoc;
13+
14+
use PHPStan\Analyser\NameScope;
15+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
16+
use PHPStan\Type\Type;
17+
18+
interface TypeNodeResolverExtension
19+
{
20+
21+
public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type;
22+
23+
}
24+
```
25+
26+
The implementation needs to be registered in your [configuration file](/config-reference):
27+
28+
```yaml
29+
services:
30+
-
31+
class: MyApp\PHPStan\MyTypeNodeResolverExtension
32+
tags:
33+
- phpstan.phpDoc.typeNodeResolverExtension
34+
```
35+
36+
TypeNodeResolverExtension cannot have [`TypeNodeResolver`](https://apiref.phpstan.org/1.9.x/PHPStan.PhpDoc.TypeNodeResolver.html) injected in the constructor due to circular reference issue, but the extensions can implement [`TypeNodeResolverAwareExtension`](https://apiref.phpstan.org/1.9.x/PHPStan.PhpDoc.TypeNodeResolverAwareExtension.html) interface to obtain `TypeNodeResolver` via a setter.
37+
38+
An example
39+
----------------
40+
41+
Let's say we want to implement the [`Pick` utility type](https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys) from TypeScript. It will allow us to achieve the code in the following example:
42+
43+
```php
44+
/**
45+
* @phpstan-type Address array{name: string, surname: string, street: string, city: string, country: Country}
46+
*/
47+
class Foo
48+
{
49+
50+
/**
51+
* @param Pick<Address, 'name' | 'surname'> $personalDetails
52+
*/
53+
public function doFoo(array $personalDetails): void
54+
{
55+
\PHPStan\dumpType($personalDetails); // array{name: string, surname: string}
56+
}
57+
58+
}
59+
```
60+
61+
This is how we'd be able to achieve that in our own `TypeNodeResolverExtension`:
62+
63+
```php
64+
namespace MyApp\PHPStan;
65+
66+
use PHPStan\Analyser\NameScope;
67+
use PHPStan\PhpDoc\TypeNodeResolver;
68+
use PHPStan\PhpDoc\TypeNodeResolverAwareExtension;
69+
use PHPStan\PhpDoc\TypeNodeResolverExtension;
70+
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
71+
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
72+
use PHPStan\Type\Constant\ConstantArrayTypeBuilder;
73+
use PHPStan\Type\Type;
74+
use PHPStan\Type\TypeCombinator;
75+
76+
class MyTypeNodeResolverExtension implements TypeNodeResolverExtension, TypeNodeResolverAwareExtension
77+
{
78+
79+
private TypeNodeResolver $typeNodeResolver;
80+
81+
public function setTypeNodeResolver(TypeNodeResolver $typeNodeResolver): void
82+
{
83+
$this->typeNodeResolver = $typeNodeResolver;
84+
}
85+
86+
public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type
87+
{
88+
if (!$typeNode instanceof GenericTypeNode) {
89+
// returning null means this extension is not interested in this node
90+
return null;
91+
}
92+
93+
$typeName = $typeNode->type;
94+
if ($typeName->name !== 'Pick') {
95+
return null;
96+
}
97+
98+
$arguments = $typeNode->genericTypes;
99+
if (count($arguments) !== 2) {
100+
return null;
101+
}
102+
103+
$arrayType = $this->typeNodeResolver->resolve($arguments[0], $nameScope);
104+
$keysType = $this->typeNodeResolver->resolve($arguments[1], $nameScope);
105+
106+
$constantArrays = $arrayType->getConstantArrays();
107+
if (count($constantArrays) === 0) {
108+
return null;
109+
}
110+
111+
$newTypes = [];
112+
foreach ($constantArrays as $constantArray) {
113+
$newTypeBuilder = ConstantArrayTypeBuilder::createEmpty();
114+
foreach ($constantArray->getKeyTypes() as $i => $keyType) {
115+
if (!$keysType->isSuperTypeOf($keyType)->yes()) {
116+
// eliminate keys that aren't in the Pick type
117+
continue;
118+
}
119+
120+
$valueType = $constantArray->getValueTypes()[$i];
121+
$newTypeBuilder->setOffsetValueType(
122+
$keyType,
123+
$valueType,
124+
$constantArray->isOptionalKey($i),
125+
);
126+
}
127+
128+
$newTypes[] = $newTypeBuilder->getArray();
129+
}
130+
131+
return TypeCombinator::union(...$newTypes);
132+
}
133+
134+
}
135+
```
136+
137+
One example of TypeNodeResolverExtension usage is in the [phpstan-phpunit](https://github.com/phpstan/phpstan-phpunit) extension. Before [intersection types](https://phpstan.org/blog/union-types-vs-intersection-types) picked up the pace and were largely unknown to PHP community, developers often written `Foo|MockObject` when they actually meant `Foo&MockObject`. So [the extension actually fixed it for them](https://github.com/phpstan/phpstan-phpunit/blob/1.1.x/src/PhpDoc/PHPUnit/MockObjectTypeNodeResolverExtension.php) and made PHPStan interpret `Foo|MockObject` as an intersection type.

website/src/developing-extensions/developing-extensions.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,10 @@
7575
"title": "Type-Specifying Extensions",
7676
"link": "/developing-extensions/type-specifying-extensions"
7777
},
78+
{
79+
"title": "Custom PHPDoc Types",
80+
"link": "/developing-extensions/custom-phpdoc-types"
81+
},
7882
{
7983
"title": "Always-Read and Written Properties",
8084
"link": "/developing-extensions/always-read-written-properties"

website/src/developing-extensions/dynamic-throw-type-extensions.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,9 @@ interface DynamicMethodThrowTypeExtension
3131
}
3232
```
3333

34+
An example
35+
----------------
36+
3437
Let's say you have a method with an implementation that looks like this:
3538

3639
```php

website/src/developing-extensions/extension-types.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,13 @@ These extensions allow you to specify types of expressions based on certain type
5353

5454
[Learn more »](/developing-extensions/type-specifying-extensions)
5555

56+
Custom PHPDoc Types
57+
-------------------
58+
59+
PHPStan lets you override how it converts PHPDoc AST coming from its phpdoc-parser library into its typesystem representation. This can be used to introduce custom utility types.
60+
61+
[Learn more »](/developing-extensions/custom-phpdoc-types)
62+
5663
Always-read and written properties
5764
-------------------
5865

0 commit comments

Comments
 (0)