Skip to content

Commit

Permalink
feat(phpstan): foundation for usage in extensions (#3666)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
SychO9 authored Jan 15, 2023
1 parent 6f7843b commit f98654a
Show file tree
Hide file tree
Showing 11 changed files with 889 additions and 0 deletions.
24 changes: 24 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,27 @@ parameters:
- stubs/Illuminate/Contracts/Filesystem/Factory.stub
- stubs/Illuminate/Contracts/Filesystem/Cloud.stub
- stubs/Illuminate/Contracts/Filesystem/Filesystem.stub

services:
-
class: Flarum\PHPStan\Relations\ModelRelationsExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
- phpstan.broker.propertiesClassReflectionExtension
-
class: Flarum\PHPStan\Attributes\ModelDateAttributesExtension
tags:
- phpstan.broker.propertiesClassReflectionExtension
-
class: Flarum\PHPStan\Attributes\ModelCastAttributeExtension
tags:
- phpstan.broker.propertiesClassReflectionExtension
-
class: Flarum\PHPStan\Extender\FilesProvider
arguments:
- %paths%
-
class: Flarum\PHPStan\Extender\Resolver
arguments:
- @Flarum\PHPStan\Extender\FilesProvider
- @defaultAnalysisParser
94 changes: 94 additions & 0 deletions src/Attributes/AttributeProperty.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\PHPStan\Attributes;

use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;

class AttributeProperty implements PropertyReflection
{
/** @var ClassReflection */
private $classReflection;
/** @var Type */
private $type;

public function __construct(ClassReflection $classReflection, Type $type)
{
$this->classReflection = $classReflection;
$this->type = $type;
}

public function getDeclaringClass(): ClassReflection
{
return $this->classReflection;
}

public function isStatic(): bool
{
return false;
}

public function isPrivate(): bool
{
return false;
}

public function isPublic(): bool
{
return true;
}

public function getDocComment(): ?string
{
return null;
}

public function getReadableType(): Type
{
return $this->type;
}

public function getWritableType(): Type
{
return $this->getReadableType();
}

public function canChangeTypeAfterAssignment(): bool
{
return false;
}

public function isReadable(): bool
{
return true;
}

public function isWritable(): bool
{
return true;
}

public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}

public function getDeprecatedDescription(): ?string
{
return null;
}

public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
}
87 changes: 87 additions & 0 deletions src/Attributes/ModelCastAttributeExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\PHPStan\Attributes;

use Carbon\Carbon;
use Flarum\PHPStan\Extender\MethodCall;
use Flarum\PHPStan\Extender\Resolver;
use PHPStan\PhpDoc\TypeStringResolver;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\PropertiesClassReflectionExtension;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\UnionType;

class ModelCastAttributeExtension implements PropertiesClassReflectionExtension
{
/** @var Resolver */
private $extendersResolver;
/** @var TypeStringResolver */
private $typeStringResolver;

public function __construct(Resolver $extendersResolver, TypeStringResolver $typeStringResolver)
{
$this->extendersResolver = $extendersResolver;
$this->typeStringResolver = $typeStringResolver;
}

public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
return $this->findCastAttributeMethod($classReflection, $propertyName) !== null;
}

public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
{
return $this->resolveCastAttributeProperty($this->findCastAttributeMethod($classReflection, $propertyName), $classReflection);
}

private function findCastAttributeMethod(ClassReflection $classReflection, string $propertyName): ?MethodCall
{
foreach ($this->extendersResolver->getExtenders() as $extender) {
if (! $extender->isExtender('Model')) {
continue;
}

foreach (array_merge([$classReflection->getName()], $classReflection->getParentClassesNames()) as $className) {
if ($className === 'Flarum\Database\AbstractModel') {
break;
}

if ($extender->extends($className)) {
if ($methodCalls = $extender->findMethodCalls('cast')) {
foreach ($methodCalls as $methodCall) {
if ($methodCall->arguments[0]->value === $propertyName) {
return $methodCall;
}
}
}
}
}
}

return null;
}

private function resolveCastAttributeProperty(MethodCall $methodCall, ClassReflection $classReflection): PropertyReflection
{
$typeName = $methodCall->arguments[1]->value;
$type = $this->typeStringResolver->resolve("$typeName|null");

if (str_contains($typeName, 'date') || $typeName === 'timestamp') {
$type = new UnionType([
new ObjectType(Carbon::class),
new NullType(),
]);
}

return new AttributeProperty($classReflection, $type);
}
}
76 changes: 76 additions & 0 deletions src/Attributes/ModelDateAttributesExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\PHPStan\Attributes;

use Carbon\Carbon;
use Flarum\PHPStan\Extender\MethodCall;
use Flarum\PHPStan\Extender\Resolver;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\PropertiesClassReflectionExtension;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\UnionType;

class ModelDateAttributesExtension implements PropertiesClassReflectionExtension
{
/** @var Resolver */
private $extendersResolver;

public function __construct(Resolver $extendersResolver)
{
$this->extendersResolver = $extendersResolver;
}

public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
return $this->findDateAttributeMethod($classReflection, $propertyName) !== null;
}

public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
{
return $this->resolveDateAttributeProperty($this->findDateAttributeMethod($classReflection, $propertyName), $classReflection);
}

private function findDateAttributeMethod(ClassReflection $classReflection, string $propertyName): ?MethodCall
{
foreach ($this->extendersResolver->getExtenders() as $extender) {
if (! $extender->isExtender('Model')) {
continue;
}

foreach (array_merge([$classReflection->getName()], $classReflection->getParentClassesNames()) as $className) {
if ($className === 'Flarum\Database\AbstractModel') {
break;
}

if ($extender->extends($className)) {
if ($methodCalls = $extender->findMethodCalls('dateAttribute')) {
foreach ($methodCalls as $methodCall) {
if ($methodCall->arguments[0]->value === $propertyName) {
return $methodCall;
}
}
}
}
}
}

return null;
}

private function resolveDateAttributeProperty(MethodCall $methodCall, ClassReflection $classReflection): PropertyReflection
{
return new AttributeProperty($classReflection, new UnionType([
new ObjectType(Carbon::class),
new NullType(),
]));
}
}
73 changes: 73 additions & 0 deletions src/Extender/Extender.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
<?php

/*
* This file is part of Flarum.
*
* For detailed copyright and license information, please view the
* LICENSE file that was distributed with this source code.
*/

namespace Flarum\PHPStan\Extender;

use PhpParser\Node\Expr;
use PhpParser\Node\Scalar;

class Extender
{
/** @var string */
public $qualifiedClassName;
/** @var Expr[] */
public $constructorArguments;
/** @var MethodCall[] */
public $methodCalls;

public function __construct(string $qualifiedClassName, array $constructorArguments = [], array $methodCalls = [])
{
$this->qualifiedClassName = $qualifiedClassName;
$this->constructorArguments = $constructorArguments;
$this->methodCalls = $methodCalls;
}

public function isExtender(string $className): bool
{
return $this->qualifiedClassName === "Flarum\\Extend\\$className";
}

public function extends(...$args): bool
{
foreach ($this->constructorArguments as $index => $constructorArgument) {
$string = null;

switch (get_class($constructorArgument)) {
case Expr\ClassConstFetch::class:
$string = $constructorArgument->class->toString();
break;
case Scalar\String_::class:
$string = $constructorArgument->value;
break;
default:
$string = $constructorArgument;
}

if ($string !== $args[$index]) {
return false;
}
}

return true;
}

/** @return MethodCall[] */
public function findMethodCalls(string ...$methods): array
{
$methodCalls = [];

foreach ($this->methodCalls as $methodCall) {
if (in_array($methodCall->methodName, $methods)) {
$methodCalls[] = $methodCall;
}
}

return $methodCalls;
}
}
Loading

0 comments on commit f98654a

Please sign in to comment.