Skip to content

Commit 2b9aa88

Browse files
committed
Merge @show-type from klimick/psalm-show-type
1 parent a6c7292 commit 2b9aa88

File tree

3 files changed

+244
-0
lines changed

3 files changed

+244
-0
lines changed

src/Toolkit/Hook/ShowTypeHook.php

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Fp\PsalmToolkit\Toolkit\Hook;
6+
7+
use PhpParser\Node;
8+
use Psalm\Type\Union;
9+
use Psalm\CodeLocation;
10+
use Psalm\Issue\Trace;
11+
use Psalm\IssueBuffer;
12+
use Fp\PsalmToolkit\Toolkit\ShowTypePrettier;
13+
use Psalm\Plugin\EventHandler\AfterExpressionAnalysisInterface;
14+
use Psalm\Plugin\EventHandler\AfterStatementAnalysisInterface;
15+
use Psalm\Plugin\EventHandler\Event\AfterExpressionAnalysisEvent;
16+
use Psalm\Plugin\EventHandler\Event\AfterStatementAnalysisEvent;
17+
18+
final class ShowTypeHook implements AfterExpressionAnalysisInterface, AfterStatementAnalysisInterface
19+
{
20+
public static function afterExpressionAnalysis(AfterExpressionAnalysisEvent $event): ?bool
21+
{
22+
$node = $event->getExpr();
23+
24+
if (self::hasShowComment($node) && $node instanceof Node\Expr\Assign) {
25+
$source = $event->getStatementsSource();
26+
$provider = $source->getNodeTypeProvider();
27+
28+
self::show($provider->getType($node->expr), new CodeLocation($source, $node));
29+
}
30+
31+
return null;
32+
}
33+
34+
public static function afterStatementAnalysis(AfterStatementAnalysisEvent $event): ?bool
35+
{
36+
$node = $event->getStmt();
37+
38+
if (self::hasShowComment($node) && $node instanceof Node\Stmt\Return_) {
39+
$source = $event->getStatementsSource();
40+
$provider = $source->getNodeTypeProvider();
41+
42+
self::show($provider->getType($node), new CodeLocation($source, $node));
43+
}
44+
45+
return null;
46+
}
47+
48+
private static function hasShowComment(Node $node): bool
49+
{
50+
$doc = $node->getDocComment();
51+
52+
return null !== $doc && str_contains($doc->getText(), '@show-type');
53+
}
54+
55+
private static function show(?Union $type, CodeLocation $location): void
56+
{
57+
IssueBuffer::accepts(
58+
null === $type
59+
? new Trace('Unable to determine type', $location)
60+
: new Trace(ShowTypePrettier::pretty($type), $location)
61+
);
62+
}
63+
}

src/Toolkit/Plugin.php

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
use Fp\PsalmToolkit\Toolkit\Hook\IntersectionReturnTypeProvider;
99
use Fp\PsalmToolkit\Toolkit\Hook\OptionalReturnTypeProvider;
1010
use Fp\PsalmToolkit\Toolkit\Hook\ShapeReturnTypeProvider;
11+
use Fp\PsalmToolkit\Toolkit\Hook\ShowTypeHook;
1112
use Fp\PsalmToolkit\Toolkit\Hook\TestCaseAnalysis;
1213
use Psalm\Internal\Analyzer\ProjectAnalyzer;
1314
use Psalm\Plugin\PluginEntryPointInterface;
@@ -34,5 +35,6 @@ public function __invoke(RegistrationInterface $registration, ?SimpleXMLElement
3435
$register(GenericObjectReturnTypeProvider::class);
3536
$register(OptionalReturnTypeProvider::class);
3637
$register(TestCaseAnalysis::class);
38+
$register(ShowTypeHook::class);
3739
}
3840
}

src/Toolkit/ShowTypePrettier.php

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Fp\PsalmToolkit\Toolkit;
6+
7+
use Psalm\Type;
8+
use Psalm\Type\Union;
9+
use Psalm\Type\Atomic;
10+
11+
final class ShowTypePrettier
12+
{
13+
public static function pretty(Union $union): string
14+
{
15+
return "\n" . self::union($union) . "\n";
16+
}
17+
18+
private static function union(Union $union, int $level = 1): string
19+
{
20+
return implode(' | ', array_map(
21+
fn($atomic) => self::atomic($atomic, $level),
22+
$union->getAtomicTypes(),
23+
));
24+
}
25+
26+
private static function atomic(Atomic $atomic, int $level): string
27+
{
28+
return match (true) {
29+
$atomic instanceof Atomic\TList => self::list($atomic, $level),
30+
$atomic instanceof Atomic\TArray => self::array($atomic, $level),
31+
$atomic instanceof Atomic\TIterable => self::iterable($atomic, $level),
32+
$atomic instanceof Atomic\TClosure => self::callable($atomic, $level),
33+
$atomic instanceof Atomic\TCallable => self::callable($atomic, $level),
34+
$atomic instanceof Atomic\TClassString => self::classString($atomic, $level),
35+
$atomic instanceof Atomic\TLiteralClassString => self::literalClassString($atomic),
36+
$atomic instanceof Atomic\TNamedObject => self::namedObject($atomic, $level),
37+
$atomic instanceof Atomic\TKeyedArray => self::keyedArray($atomic, $level),
38+
$atomic instanceof Atomic\TTemplateParam => self::templateParam($atomic, $level),
39+
default => $atomic->getId(),
40+
};
41+
}
42+
43+
private static function iterable(Atomic\TIterable $atomic, int $level): string
44+
{
45+
$key = self::union($atomic->type_params[0], $level);
46+
$val = self::union($atomic->type_params[1], $level);
47+
48+
return "iterable<{$key}, {$val}>";
49+
}
50+
51+
private static function classString(Atomic\TClassString $atomic, int $level): string
52+
{
53+
return null !== $atomic->as_type
54+
? self::namedObject($atomic->as_type, $level) . '::class'
55+
: 'class-string';
56+
}
57+
58+
private static function literalClassString(Atomic\TLiteralClassString $atomic): string
59+
{
60+
return self::shortClassName($atomic->value) . '::class';
61+
}
62+
63+
private static function templateParam(Atomic\TTemplateParam $atomic, int $level): string
64+
{
65+
$shortClassName = self::shortClassName($atomic->defining_class);
66+
$as = self::union($atomic->as, $level);
67+
68+
return "from {$shortClassName} as {$as}";
69+
}
70+
71+
private static function array(Atomic\TArray $atomic, int $level): string
72+
{
73+
$key = self::union($atomic->type_params[0], $level);
74+
$val = self::union($atomic->type_params[1], $level);
75+
76+
return $atomic instanceof Atomic\TNonEmptyArray
77+
? "non-empty-array<{$key}, {$val}>"
78+
: "array<{$key}, {$val}>";
79+
}
80+
81+
private static function list(Atomic\TList $atomic, int $level): string
82+
{
83+
$type = self::union($atomic->type_param, $level);
84+
85+
return $atomic instanceof Atomic\TNonEmptyList
86+
? "non-empty-list<{$type}>"
87+
: "list<{$type}>";
88+
}
89+
90+
private static function callable(Atomic\TClosure|Atomic\TCallable $atomic, int $level): string
91+
{
92+
$return = self::union($atomic->return_type ?? Type::getVoid(), $level);
93+
94+
$params = implode(', ', array_map(
95+
function($param) use ($level) {
96+
$paramType = $param->type ?? Type::getMixed();
97+
$paramName = $param->by_ref ? "&\${$param->name}" : "\${$param->name}";
98+
$variadic = $param->is_variadic ? '...' : '';
99+
100+
return trim($variadic . self::union($paramType, $level) . " {$paramName}");
101+
},
102+
$atomic->params ?? [],
103+
));
104+
105+
$pure = $atomic->is_pure ? 'pure-' : '';
106+
107+
return $atomic instanceof Atomic\TClosure
108+
? "{$pure}Closure({$params}): {$return}"
109+
: "{$pure}callable({$params}): {$return}";
110+
}
111+
112+
private static function getGenerics(Atomic\TGenericObject $atomic, int $level): string
113+
{
114+
return implode(', ', array_map(
115+
fn(Union $param) => self::union($param, $level),
116+
$atomic->type_params,
117+
));
118+
}
119+
120+
private static function shortClassName(string $class): string
121+
{
122+
if (1 === preg_match('~(\\\\)?(?<short_class_name>\w+)$~', $class, $m)) {
123+
return $m['short_class_name'];
124+
}
125+
126+
return $class;
127+
}
128+
129+
private static function namedObject(Atomic\TNamedObject $atomic, int $level): string
130+
{
131+
$generics = $atomic instanceof Atomic\TGenericObject
132+
? self::getGenerics($atomic, $level)
133+
: null;
134+
135+
$shortClassName = self::shortClassName($atomic->value);
136+
$mainSide = null !== $generics ? "{$shortClassName}<{$generics}>" : $shortClassName;
137+
138+
$intersectionTypes = $atomic->getIntersectionTypes();
139+
140+
$intersectionSide = null !== $intersectionTypes
141+
? implode(', ', array_map(fn(Atomic $a) => self::atomic($a, $level), $intersectionTypes))
142+
: null;
143+
144+
return null !== $intersectionSide ? "{$mainSide} & {$intersectionSide}" : $mainSide;
145+
}
146+
147+
private static function keyedArray(Atomic\TKeyedArray $atomic, int $level): string
148+
{
149+
$tab = fn(int $l): string => str_repeat("\t", $l);
150+
151+
$openBracket = 'array{';
152+
$closeBracket = $level === 1 ? '}' : $tab($level - 1) . '}';
153+
$isList = self::isKeyedArrayList($atomic);
154+
155+
$shape = $isList
156+
? array_map(
157+
fn(Union $type) => self::union($type, $level + 1),
158+
$atomic->properties,
159+
)
160+
: array_map(
161+
fn(int|string $property, Union $type) => implode('', [
162+
$tab($level),
163+
$type->possibly_undefined ? "{$property}?: " : "{$property}: ",
164+
self::union($type, $level + 1),
165+
]),
166+
array_keys($atomic->properties),
167+
array_values($atomic->properties),
168+
);
169+
170+
return $isList
171+
? $openBracket . implode(", ", array_values($shape)) . $closeBracket
172+
: $openBracket . "\n" . implode(",\n", array_values($shape)) . ",\n" . $closeBracket;
173+
}
174+
175+
private static function isKeyedArrayList(Atomic\TKeyedArray $atomic): bool
176+
{
177+
return array_keys($atomic->properties) === range(0, count($atomic->properties) - 1);
178+
}
179+
}

0 commit comments

Comments
 (0)