Skip to content

Commit 28f0362

Browse files
authored
Merge pull request #11 from cego/niza/add-analysis-for-spatie-laravel-data
Niza/add analysis for spatie laravel data
2 parents fc1d8cf + dcaed5c commit 28f0362

28 files changed

+1665
-3
lines changed

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
*.cache
22
/.idea/
33
/vendor/
4-
composer.lock
4+
composer.lock
5+
.phpstan

.php-cs-fixer.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
<?php
2+
3+
use Cego\CegoFixer;
4+
5+
$finder = PhpCsFixer\Finder::create()
6+
->in(__DIR__ . '/src')
7+
->in(__DIR__ . '/test');
8+
9+
return CegoFixer::applyRules($finder, [
10+
'ternary_to_null_coalescing' => true,
11+
]);

composer.json

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,24 @@
1111
"email": "niza@cego.dk"
1212
}
1313
],
14+
"autoload": {
15+
"psr-4": {
16+
"Cego\\phpstan\\": "src/"
17+
}
18+
},
19+
"autoload-dev": {
20+
"psr-4": {
21+
"Test\\": "test/"
22+
}
23+
},
1424
"require": {
15-
"nunomaduro/larastan": "^2.1",
16-
"phpstan/phpstan": "^1.4"
25+
"php": "^8.1",
26+
"phpstan/phpstan": "^1.4",
27+
"nunomaduro/larastan": "^2.4",
28+
"spatie/laravel-data": "^3.0"
29+
},
30+
"require-dev": {
31+
"phpunit/phpunit": "^10.0",
32+
"cego/php-cs-fixer": "^1.0"
1733
}
1834
}

extension.neon

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,25 @@
11
includes:
22
- ./../../nunomaduro/larastan/extension.neon
33

4+
services:
5+
-
6+
class: Cego\phpstan\SpatieLaravelData\Collectors\ConstructorCollector
7+
tags:
8+
- phpstan.collector
9+
10+
-
11+
class: Cego\phpstan\SpatieLaravelData\Collectors\FromCollector
12+
tags:
13+
- phpstan.collector
14+
15+
-
16+
class: Cego\phpstan\SpatieLaravelData\Collectors\CastCollector
17+
tags:
18+
- phpstan.collector
19+
20+
rules:
21+
- Cego\phpstan\SpatieLaravelData\Rules\ValidTypeRule
22+
423
parameters:
524
level: 8
625
reportUnmatchedIgnoredErrors: false
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
namespace Cego\phpstan\SpatieLaravelData\Collectors;
4+
5+
use PhpParser\Node;
6+
use Illuminate\Support\Str;
7+
use PHPStan\Analyser\Scope;
8+
use PHPStan\Type\VerbosityLevel;
9+
use PHPStan\Collectors\Collector;
10+
use Illuminate\Support\Collection;
11+
use Spatie\LaravelData\Casts\Cast;
12+
use PhpParser\Node\Expr\StaticCall;
13+
use PHPStan\Node\InClassMethodNode;
14+
use PHPStan\ShouldNotHappenException;
15+
use Cego\phpstan\TypeSystem\UnionType;
16+
use Spatie\LaravelData\Casts\Uncastable;
17+
use PHPStan\Reflection\ParametersAcceptorSelector;
18+
19+
/**
20+
* @implements Collector<InClassMethodNode, array<string, array<int, string>>
21+
*/
22+
class CastCollector implements Collector
23+
{
24+
/**
25+
* Returns the node type, this collector operates on
26+
*
27+
* @phpstan-return class-string<InClassMethodNode>
28+
*/
29+
public function getNodeType(): string
30+
{
31+
return InClassMethodNode::class;
32+
}
33+
34+
/**
35+
* Process the nodes and stores value in the collector instance
36+
*
37+
* @phpstan-param StaticCall $node
38+
*
39+
* @throws ShouldNotHappenException
40+
*
41+
* @return string|null Collected data
42+
*/
43+
public function processNode(Node $node, Scope $scope): ?string
44+
{
45+
// Skip wrong nodes
46+
if ( ! $node instanceof InClassMethodNode) {
47+
return null;
48+
}
49+
50+
// Skip wrong methods
51+
if ($this->isNotCastMethod($node)) {
52+
return null;
53+
}
54+
55+
$variant = ParametersAcceptorSelector::selectSingle($node->getMethodReflection()->getVariants());
56+
$returnType = $variant->getReturnType();
57+
58+
return Str::of($returnType->describe(VerbosityLevel::typeOnly()))
59+
// Get individual union types
60+
->explode('|')
61+
// Get individual intersection types
62+
->map(fn (string $type) => Str::of($type)->explode('&'))
63+
// For each intersection type (which might be an intersection of 1 item)
64+
// Only keep cast information for classes / interfaces
65+
->map(function (Collection $intersectionTypes) {
66+
$classTypes = $intersectionTypes
67+
// We only care about classes / interfaces
68+
->filter(fn (string $type) => class_exists($type) || interface_exists($type))
69+
// We do not care for the uncastable class
70+
->reject(fn (string $type) => is_a($type, Uncastable::class, true));
71+
72+
// We only support intersection types of explicit classes / interfaces.
73+
if ($intersectionTypes->count() !== $classTypes->count()) {
74+
return [];
75+
}
76+
77+
return $classTypes->all();
78+
})
79+
// Remove any intersection types we have deemed unfit
80+
->reject(fn (array $collection) => empty($collection))
81+
->pipe(UnionType::fromRaw(...))
82+
->toString();
83+
}
84+
85+
/**
86+
* Returns true if the given node is not the cast method of a Cast class
87+
*
88+
* @param InClassMethodNode $node
89+
*
90+
* @return bool
91+
*/
92+
private function isNotCastMethod(InClassMethodNode $node): bool
93+
{
94+
return $node->getMethodReflection()->getName() !== 'cast'
95+
|| ! $node->getMethodReflection()->getDeclaringClass()->implementsInterface(Cast::class);
96+
}
97+
}
Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
<?php
2+
3+
namespace Cego\phpstan\SpatieLaravelData\Collectors;
4+
5+
use PhpParser\Node;
6+
use RuntimeException;
7+
use PhpParser\Node\Name;
8+
use PhpParser\Node\Param;
9+
use PHPStan\Analyser\Scope;
10+
use Spatie\LaravelData\Data;
11+
use PhpParser\Node\Identifier;
12+
use PhpParser\Node\ComplexType;
13+
use PHPStan\Collectors\Collector;
14+
use PHPStan\Node\InClassMethodNode;
15+
use Cego\phpstan\TypeSystem\UnionType;
16+
use PHPStan\Reflection\ClassReflection;
17+
use Cego\phpstan\SpatieLaravelData\Data\Constructor;
18+
use Cego\phpstan\SpatieLaravelData\Data\KeyTypePair;
19+
20+
/**
21+
* @implements Collector<InClassMethodNode, array<string, array<string, array<int, string>>>
22+
*/
23+
class ConstructorCollector implements Collector
24+
{
25+
/**
26+
* Returns the node type, this collector operates on
27+
*
28+
* @phpstan-return class-string<InClassMethodNode>
29+
*/
30+
public function getNodeType(): string
31+
{
32+
return InClassMethodNode::class;
33+
}
34+
35+
/**
36+
* Process the nodes and stores value in the collector instance
37+
*
38+
* @phpstan-param InClassMethodNode $node
39+
*
40+
* @return string|null Collected data
41+
*/
42+
public function processNode(Node $node, Scope $scope): ?string
43+
{
44+
if ( ! $node instanceof InClassMethodNode) {
45+
return null;
46+
}
47+
48+
if ($this->isNotSpatieLaravelDataConstructor($node)) {
49+
return null;
50+
}
51+
52+
return serialize(new Constructor(
53+
$node->getMethodReflection()->getDeclaringClass()->getName(),
54+
collect($node->getOriginalNode()->getParams())->map($this->getParameterTypes(...))->all()
55+
));
56+
}
57+
58+
/**
59+
* Returns a key-value mapping of the parameter name and its allowed types
60+
*
61+
* @param Param $parameter
62+
*
63+
* @return KeyTypePair
64+
*/
65+
private function getParameterTypes(Param $parameter): KeyTypePair
66+
{
67+
return new KeyTypePair(
68+
$this->getParameterName($parameter),
69+
UnionType::fromRaw($this->parseType($parameter->type)),
70+
);
71+
}
72+
73+
/**
74+
* @param null|Identifier|Name|ComplexType $type
75+
*
76+
* @return array<int, array<int, string>>
77+
*/
78+
private function parseType($type): array
79+
{
80+
// If no type is defined, then return mixed.
81+
if ($type === null) {
82+
return [['mixed']];
83+
}
84+
85+
// Simple type (int, string, bool)
86+
if ($type instanceof Identifier) {
87+
return [[$type->name]];
88+
}
89+
90+
// Class types
91+
if ($type instanceof Name) {
92+
// We do not support special type checking (self, parent, static)
93+
// since we are unlikely to use this feature,
94+
// and implementing it is currently not straight forward.
95+
if ($type->isSpecialClassName()) {
96+
return [['mixed']];
97+
}
98+
99+
return [[$type->toCodeString()]];
100+
}
101+
102+
// Complex types
103+
if ($type instanceof Node\ComplexType) {
104+
if ($type instanceof Node\NullableType) {
105+
return [
106+
...$this->parseType($type->type),
107+
['null'],
108+
];
109+
}
110+
111+
if ($type instanceof Node\UnionType) {
112+
return collect($type->types)
113+
->map(fn ($unionType) => $this->parseType($unionType))
114+
->flatten(1)
115+
->all();
116+
}
117+
118+
if ($type instanceof Node\IntersectionType) {
119+
return [
120+
collect($type->types)
121+
->map(fn ($intersectionType) => $this->parseType($intersectionType))
122+
->flatten(2)
123+
->all(),
124+
];
125+
}
126+
}
127+
128+
return [['mixed']];
129+
}
130+
131+
/**
132+
* Returns the name of the given parameter
133+
*
134+
* @param Param $parameter
135+
*
136+
* @return string
137+
*/
138+
private function getParameterName(Param $parameter): string
139+
{
140+
if ( ! is_string($parameter->var->name)) {
141+
throw new RuntimeException('A constructor property name cannot be an expression');
142+
}
143+
144+
return $parameter->var->name;
145+
}
146+
147+
/**
148+
* Returns true if the given node is not a laravel data constructor
149+
*
150+
* @param InClassMethodNode $node
151+
*
152+
* @return bool
153+
*/
154+
private function isNotSpatieLaravelDataConstructor(InClassMethodNode $node): bool
155+
{
156+
return $this->isNotConstructor($node)
157+
|| $this->isNotSpatieLaravelDataClass($node->getMethodReflection()->getDeclaringClass());
158+
}
159+
160+
/**
161+
* Returns true if the given node is not a constructor class
162+
*
163+
* @param InClassMethodNode $node
164+
*
165+
* @return bool
166+
*/
167+
private function isNotConstructor(InClassMethodNode $node): bool
168+
{
169+
return $node->getMethodReflection()->getName() !== '__construct';
170+
}
171+
172+
/**
173+
* Returns true if the given class is not a laravel data class
174+
*
175+
* @param ClassReflection $class
176+
*
177+
* @return bool
178+
*/
179+
private function isNotSpatieLaravelDataClass(ClassReflection $class): bool
180+
{
181+
return ! in_array(Data::class, $class->getParentClassesNames(), true);
182+
}
183+
}

0 commit comments

Comments
 (0)