Skip to content

Commit f98654a

Browse files
authored
feat(phpstan): foundation for usage in extensions (#3666)
* feat(phpstan): pick up extended model relations typings * feat(phpstan): pick up extended model date attributes * feat(core): introduce `castAttribute` extender Stops using `dates` as it's deprecated in laravel 8 * feat(phpstan): pick up extended model attributes through casts * fix: extenders not resolved when declared namespace * fix(phpstan): new model attributes are always nullable * chore(phpstan): add helpful cache clearing command * Apply fixes from StyleCI * chore: improve extend files provider logic * chore: rename `castAttribute` to just `cast` * chore: update phpstan package to detect `cast` method * Update framework/core/src/Extend/Model.php Signed-off-by: Sami Mazouz <[email protected]>
1 parent 6f7843b commit f98654a

11 files changed

+889
-0
lines changed

extension.neon

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,27 @@ parameters:
1515
- stubs/Illuminate/Contracts/Filesystem/Factory.stub
1616
- stubs/Illuminate/Contracts/Filesystem/Cloud.stub
1717
- stubs/Illuminate/Contracts/Filesystem/Filesystem.stub
18+
19+
services:
20+
-
21+
class: Flarum\PHPStan\Relations\ModelRelationsExtension
22+
tags:
23+
- phpstan.broker.methodsClassReflectionExtension
24+
- phpstan.broker.propertiesClassReflectionExtension
25+
-
26+
class: Flarum\PHPStan\Attributes\ModelDateAttributesExtension
27+
tags:
28+
- phpstan.broker.propertiesClassReflectionExtension
29+
-
30+
class: Flarum\PHPStan\Attributes\ModelCastAttributeExtension
31+
tags:
32+
- phpstan.broker.propertiesClassReflectionExtension
33+
-
34+
class: Flarum\PHPStan\Extender\FilesProvider
35+
arguments:
36+
- %paths%
37+
-
38+
class: Flarum\PHPStan\Extender\Resolver
39+
arguments:
40+
- @Flarum\PHPStan\Extender\FilesProvider
41+
- @defaultAnalysisParser
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Flarum.
5+
*
6+
* For detailed copyright and license information, please view the
7+
* LICENSE file that was distributed with this source code.
8+
*/
9+
10+
namespace Flarum\PHPStan\Attributes;
11+
12+
use PHPStan\Reflection\ClassReflection;
13+
use PHPStan\Reflection\PropertyReflection;
14+
use PHPStan\TrinaryLogic;
15+
use PHPStan\Type\Type;
16+
17+
class AttributeProperty implements PropertyReflection
18+
{
19+
/** @var ClassReflection */
20+
private $classReflection;
21+
/** @var Type */
22+
private $type;
23+
24+
public function __construct(ClassReflection $classReflection, Type $type)
25+
{
26+
$this->classReflection = $classReflection;
27+
$this->type = $type;
28+
}
29+
30+
public function getDeclaringClass(): ClassReflection
31+
{
32+
return $this->classReflection;
33+
}
34+
35+
public function isStatic(): bool
36+
{
37+
return false;
38+
}
39+
40+
public function isPrivate(): bool
41+
{
42+
return false;
43+
}
44+
45+
public function isPublic(): bool
46+
{
47+
return true;
48+
}
49+
50+
public function getDocComment(): ?string
51+
{
52+
return null;
53+
}
54+
55+
public function getReadableType(): Type
56+
{
57+
return $this->type;
58+
}
59+
60+
public function getWritableType(): Type
61+
{
62+
return $this->getReadableType();
63+
}
64+
65+
public function canChangeTypeAfterAssignment(): bool
66+
{
67+
return false;
68+
}
69+
70+
public function isReadable(): bool
71+
{
72+
return true;
73+
}
74+
75+
public function isWritable(): bool
76+
{
77+
return true;
78+
}
79+
80+
public function isDeprecated(): TrinaryLogic
81+
{
82+
return TrinaryLogic::createNo();
83+
}
84+
85+
public function getDeprecatedDescription(): ?string
86+
{
87+
return null;
88+
}
89+
90+
public function isInternal(): TrinaryLogic
91+
{
92+
return TrinaryLogic::createNo();
93+
}
94+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Flarum.
5+
*
6+
* For detailed copyright and license information, please view the
7+
* LICENSE file that was distributed with this source code.
8+
*/
9+
10+
namespace Flarum\PHPStan\Attributes;
11+
12+
use Carbon\Carbon;
13+
use Flarum\PHPStan\Extender\MethodCall;
14+
use Flarum\PHPStan\Extender\Resolver;
15+
use PHPStan\PhpDoc\TypeStringResolver;
16+
use PHPStan\Reflection\ClassReflection;
17+
use PHPStan\Reflection\PropertiesClassReflectionExtension;
18+
use PHPStan\Reflection\PropertyReflection;
19+
use PHPStan\Type\NullType;
20+
use PHPStan\Type\ObjectType;
21+
use PHPStan\Type\UnionType;
22+
23+
class ModelCastAttributeExtension implements PropertiesClassReflectionExtension
24+
{
25+
/** @var Resolver */
26+
private $extendersResolver;
27+
/** @var TypeStringResolver */
28+
private $typeStringResolver;
29+
30+
public function __construct(Resolver $extendersResolver, TypeStringResolver $typeStringResolver)
31+
{
32+
$this->extendersResolver = $extendersResolver;
33+
$this->typeStringResolver = $typeStringResolver;
34+
}
35+
36+
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
37+
{
38+
return $this->findCastAttributeMethod($classReflection, $propertyName) !== null;
39+
}
40+
41+
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
42+
{
43+
return $this->resolveCastAttributeProperty($this->findCastAttributeMethod($classReflection, $propertyName), $classReflection);
44+
}
45+
46+
private function findCastAttributeMethod(ClassReflection $classReflection, string $propertyName): ?MethodCall
47+
{
48+
foreach ($this->extendersResolver->getExtenders() as $extender) {
49+
if (! $extender->isExtender('Model')) {
50+
continue;
51+
}
52+
53+
foreach (array_merge([$classReflection->getName()], $classReflection->getParentClassesNames()) as $className) {
54+
if ($className === 'Flarum\Database\AbstractModel') {
55+
break;
56+
}
57+
58+
if ($extender->extends($className)) {
59+
if ($methodCalls = $extender->findMethodCalls('cast')) {
60+
foreach ($methodCalls as $methodCall) {
61+
if ($methodCall->arguments[0]->value === $propertyName) {
62+
return $methodCall;
63+
}
64+
}
65+
}
66+
}
67+
}
68+
}
69+
70+
return null;
71+
}
72+
73+
private function resolveCastAttributeProperty(MethodCall $methodCall, ClassReflection $classReflection): PropertyReflection
74+
{
75+
$typeName = $methodCall->arguments[1]->value;
76+
$type = $this->typeStringResolver->resolve("$typeName|null");
77+
78+
if (str_contains($typeName, 'date') || $typeName === 'timestamp') {
79+
$type = new UnionType([
80+
new ObjectType(Carbon::class),
81+
new NullType(),
82+
]);
83+
}
84+
85+
return new AttributeProperty($classReflection, $type);
86+
}
87+
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Flarum.
5+
*
6+
* For detailed copyright and license information, please view the
7+
* LICENSE file that was distributed with this source code.
8+
*/
9+
10+
namespace Flarum\PHPStan\Attributes;
11+
12+
use Carbon\Carbon;
13+
use Flarum\PHPStan\Extender\MethodCall;
14+
use Flarum\PHPStan\Extender\Resolver;
15+
use PHPStan\Reflection\ClassReflection;
16+
use PHPStan\Reflection\PropertiesClassReflectionExtension;
17+
use PHPStan\Reflection\PropertyReflection;
18+
use PHPStan\Type\NullType;
19+
use PHPStan\Type\ObjectType;
20+
use PHPStan\Type\UnionType;
21+
22+
class ModelDateAttributesExtension implements PropertiesClassReflectionExtension
23+
{
24+
/** @var Resolver */
25+
private $extendersResolver;
26+
27+
public function __construct(Resolver $extendersResolver)
28+
{
29+
$this->extendersResolver = $extendersResolver;
30+
}
31+
32+
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
33+
{
34+
return $this->findDateAttributeMethod($classReflection, $propertyName) !== null;
35+
}
36+
37+
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
38+
{
39+
return $this->resolveDateAttributeProperty($this->findDateAttributeMethod($classReflection, $propertyName), $classReflection);
40+
}
41+
42+
private function findDateAttributeMethod(ClassReflection $classReflection, string $propertyName): ?MethodCall
43+
{
44+
foreach ($this->extendersResolver->getExtenders() as $extender) {
45+
if (! $extender->isExtender('Model')) {
46+
continue;
47+
}
48+
49+
foreach (array_merge([$classReflection->getName()], $classReflection->getParentClassesNames()) as $className) {
50+
if ($className === 'Flarum\Database\AbstractModel') {
51+
break;
52+
}
53+
54+
if ($extender->extends($className)) {
55+
if ($methodCalls = $extender->findMethodCalls('dateAttribute')) {
56+
foreach ($methodCalls as $methodCall) {
57+
if ($methodCall->arguments[0]->value === $propertyName) {
58+
return $methodCall;
59+
}
60+
}
61+
}
62+
}
63+
}
64+
}
65+
66+
return null;
67+
}
68+
69+
private function resolveDateAttributeProperty(MethodCall $methodCall, ClassReflection $classReflection): PropertyReflection
70+
{
71+
return new AttributeProperty($classReflection, new UnionType([
72+
new ObjectType(Carbon::class),
73+
new NullType(),
74+
]));
75+
}
76+
}

src/Extender/Extender.php

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
<?php
2+
3+
/*
4+
* This file is part of Flarum.
5+
*
6+
* For detailed copyright and license information, please view the
7+
* LICENSE file that was distributed with this source code.
8+
*/
9+
10+
namespace Flarum\PHPStan\Extender;
11+
12+
use PhpParser\Node\Expr;
13+
use PhpParser\Node\Scalar;
14+
15+
class Extender
16+
{
17+
/** @var string */
18+
public $qualifiedClassName;
19+
/** @var Expr[] */
20+
public $constructorArguments;
21+
/** @var MethodCall[] */
22+
public $methodCalls;
23+
24+
public function __construct(string $qualifiedClassName, array $constructorArguments = [], array $methodCalls = [])
25+
{
26+
$this->qualifiedClassName = $qualifiedClassName;
27+
$this->constructorArguments = $constructorArguments;
28+
$this->methodCalls = $methodCalls;
29+
}
30+
31+
public function isExtender(string $className): bool
32+
{
33+
return $this->qualifiedClassName === "Flarum\\Extend\\$className";
34+
}
35+
36+
public function extends(...$args): bool
37+
{
38+
foreach ($this->constructorArguments as $index => $constructorArgument) {
39+
$string = null;
40+
41+
switch (get_class($constructorArgument)) {
42+
case Expr\ClassConstFetch::class:
43+
$string = $constructorArgument->class->toString();
44+
break;
45+
case Scalar\String_::class:
46+
$string = $constructorArgument->value;
47+
break;
48+
default:
49+
$string = $constructorArgument;
50+
}
51+
52+
if ($string !== $args[$index]) {
53+
return false;
54+
}
55+
}
56+
57+
return true;
58+
}
59+
60+
/** @return MethodCall[] */
61+
public function findMethodCalls(string ...$methods): array
62+
{
63+
$methodCalls = [];
64+
65+
foreach ($this->methodCalls as $methodCall) {
66+
if (in_array($methodCall->methodName, $methods)) {
67+
$methodCalls[] = $methodCall;
68+
}
69+
}
70+
71+
return $methodCalls;
72+
}
73+
}

0 commit comments

Comments
 (0)