Pushing latest stuff

This commit is contained in:
Matthew Kilgore 2021-12-28 20:45:22 -05:00
parent 05aa62f70c
commit 853926ce0b
80 changed files with 7103 additions and 16105 deletions

View File

@ -7,11 +7,19 @@
"phpstan/phpstan-php-parser": "^1.0",
"phpstan/phpstan": "^1.2"
},
"autoload": {
"psr-4": {
"Flarum\\PHPStan\\": "src/"
}
},
"extra": {
"phpstan": {
"includes": [
"extension.neon"
]
},
"branch-alias": {
"dev-master": "1.0.x-dev"
}
}
}

View File

@ -36,3 +36,256 @@ parameters:
excludePaths:
- *.blade.php
checkGenericClassInNonGenericObjectType: false
checkModelProperties: false
databaseMigrationsPath: []
parametersSchema:
databaseMigrationsPath: listOf(string())
checkModelProperties: bool()
services:
-
class: Flarum\PHPStan\Methods\RelationForwardsCallsExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
-
class: Flarum\PHPStan\Methods\ModelForwardsCallsExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
-
class: Flarum\PHPStan\Methods\EloquentBuilderForwardsCallsExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
-
class: Flarum\PHPStan\Methods\HigherOrderTapProxyExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
-
class: Flarum\PHPStan\Methods\HigherOrderCollectionProxyExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
-
class: Flarum\PHPStan\Methods\StorageMethodsClassReflectionExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
-
class: Flarum\PHPStan\Methods\Extension
tags:
- phpstan.broker.methodsClassReflectionExtension
-
class: Flarum\PHPStan\Methods\ModelFactoryMethodsClassReflectionExtension
tags:
- phpstan.broker.methodsClassReflectionExtension
-
class: Flarum\PHPStan\Properties\ModelAccessorExtension
tags:
- phpstan.broker.propertiesClassReflectionExtension
-
class: Flarum\PHPStan\Properties\ModelPropertyExtension
tags:
- phpstan.broker.propertiesClassReflectionExtension
-
class: Flarum\PHPStan\Properties\HigherOrderCollectionProxyPropertyExtension
tags:
- phpstan.broker.propertiesClassReflectionExtension
-
class: Flarum\PHPStan\Types\RelationDynamicMethodReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: Flarum\PHPStan\Types\ModelRelationsDynamicMethodReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\HigherOrderTapProxyExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\ContainerArrayAccessDynamicMethodReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
arguments:
className: Illuminate\Contracts\Container\Container
-
class: Flarum\PHPStan\ReturnTypes\ContainerArrayAccessDynamicMethodReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
arguments:
className: Illuminate\Container\Container
-
class: Flarum\PHPStan\ReturnTypes\ContainerArrayAccessDynamicMethodReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
arguments:
className: Illuminate\Foundation\Application
-
class: Flarum\PHPStan\ReturnTypes\ContainerArrayAccessDynamicMethodReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
arguments:
className: Illuminate\Contracts\Foundation\Application
-
class: Flarum\PHPStan\Properties\ModelRelationsExtension
tags:
- phpstan.broker.propertiesClassReflectionExtension
-
class: Flarum\PHPStan\ReturnTypes\ModelFactoryDynamicStaticMethodReturnTypeExtension
tags:
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\ModelExtension
tags:
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\RequestExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\EloquentBuilderExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\RelationFindExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\RelationCollectionExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\ModelFindExtension
tags:
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\BuilderModelFindExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\TestCaseExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\CollectionMakeDynamicStaticMethodReturnTypeExtension
tags:
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
class: Flarum\PHPStan\Support\CollectionHelper
-
class: Flarum\PHPStan\ReturnTypes\Helpers\CollectExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\Helpers\TransExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\Helpers\ValidatorExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\CollectionFilterDynamicReturnTypeExtension
tags:
- phpstan.broker.dynamicMethodReturnTypeExtension
-
class: Flarum\PHPStan\Types\AbortIfFunctionTypeSpecifyingExtension
tags:
- phpstan.typeSpecifier.functionTypeSpecifyingExtension
arguments:
methodName: 'abort'
negate: false
-
class: Flarum\PHPStan\Types\AbortIfFunctionTypeSpecifyingExtension
tags:
- phpstan.typeSpecifier.functionTypeSpecifyingExtension
arguments:
methodName: 'abort'
negate: true
-
class: Flarum\PHPStan\Types\AbortIfFunctionTypeSpecifyingExtension
tags:
- phpstan.typeSpecifier.functionTypeSpecifyingExtension
arguments:
methodName: throw
negate: false
-
class: Flarum\PHPStan\Types\AbortIfFunctionTypeSpecifyingExtension
tags:
- phpstan.typeSpecifier.functionTypeSpecifyingExtension
arguments:
methodName: throw
negate: true
-
class: Flarum\PHPStan\ReturnTypes\Helpers\AppExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\Helpers\ValueExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\Helpers\TapExtension
tags:
- phpstan.broker.dynamicFunctionReturnTypeExtension
-
class: Flarum\PHPStan\ReturnTypes\StorageDynamicStaticMethodReturnTypeExtension
tags:
- phpstan.broker.dynamicStaticMethodReturnTypeExtension
-
class: Flarum\PHPStan\Types\GenericEloquentCollectionTypeNodeResolverExtension
tags:
- phpstan.phpDoc.typeNodeResolverExtension
-
class: Flarum\PHPStan\Types\ViewStringTypeNodeResolverExtension
tags:
- phpstan.phpDoc.typeNodeResolverExtension
-
class: Flarum\PHPStan\Methods\BuilderHelper
arguments:
checkProperties: %checkModelProperties%
-
class: Flarum\PHPStan\Properties\MigrationHelper
arguments:
databaseMigrationPath: %databaseMigrationsPath%
parser: @currentPhpVersionSimpleDirectParser
-
class: Flarum\PHPStan\Types\RelationParserHelper
arguments:
parser: @currentPhpVersionSimpleDirectParser

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Concerns;
use Illuminate\Container\Container;
use Illuminate\Contracts\Container\BindingResolutionException;
use Illuminate\Contracts\Container\Container as ContainerContract;
use Psr\Container\NotFoundExceptionInterface;
use ReflectionException;
/**
* @internal
*/
trait HasContainer
{
/**
* @var ?\Illuminate\Contracts\Container\Container
*/
protected $container;
/**
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*/
public function setContainer(ContainerContract $container): void
{
$this->container = $container;
}
/**
* Returns the current broker.
*
* @return \Illuminate\Contracts\Container\Container
*/
public function getContainer(): ContainerContract
{
return $this->container ?? Container::getInstance();
}
/**
* Resolve the given type from the container.
*
* @param string $abstract
* @return mixed
*/
public function resolve(string $abstract)
{
$concrete = null;
try {
$concrete = $this->getContainer()
->make($abstract);
} catch (ReflectionException $exception) {
// ..
} catch (BindingResolutionException $exception) {
// ..
} catch (NotFoundExceptionInterface $exception) {
// ..
}
return $concrete;
}
}

View File

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Concerns;
use Illuminate\Config\Repository as ConfigRepository;
trait LoadsAuthModel
{
/** @phpstan-return class-string|null */
private function getAuthModel(ConfigRepository $config, ?string $guard = null): ?string
{
if (
($guard === null && ! ($guard = $config->get('auth.defaults.guard'))) ||
! ($provider = $config->get('auth.guards.'.$guard.'.provider')) ||
! ($authModel = $config->get('auth.providers.'.$provider.'.model'))
) {
return null;
}
return $authModel;
}
}

View File

@ -0,0 +1,97 @@
<?php
/*
* This file is part of PhpStorm.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\PHPStan\Contracts\Methods;
use Illuminate\Contracts\Container\Container as ContainerContract;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\Php\PhpMethodReflectionFactory;
use PHPStan\Reflection\ReflectionProvider;
/**
* @internal
*/
interface PassableContract
{
/**
* @param \Illuminate\Contracts\Container\Container $container
* @return void
*/
public function setContainer(ContainerContract $container): void;
/**
* @return \PHPStan\Reflection\ClassReflection
*/
public function getClassReflection(): ClassReflection;
/**
* @param \PHPStan\Reflection\ClassReflection $classReflection
* @return PassableContract
*/
public function setClassReflection(ClassReflection $classReflection): PassableContract;
/**
* @return string
*/
public function getMethodName(): string;
/**
* @return bool
*/
public function hasFound(): bool;
/**
* @param string $class
* @return bool
*/
public function searchOn(string $class): bool;
/**
* @return \PHPStan\Reflection\MethodReflection
*
* @throws \LogicException
*/
public function getMethodReflection(): MethodReflection;
/**
* @param \PHPStan\Reflection\MethodReflection $methodReflection
*/
public function setMethodReflection(MethodReflection $methodReflection): void;
/**
* Declares that the provided method can be called statically.
*
* @param bool $staticAllowed
* @return void
*/
public function setStaticAllowed(bool $staticAllowed): void;
/**
* Returns whether the method can be called statically.
*
* @return bool
*/
public function isStaticAllowed(): bool;
/**
* @param class-string $class
* @param bool $staticAllowed
* @return bool
*/
public function sendToPipeline(string $class, $staticAllowed = false): bool;
public function getReflectionProvider(): ReflectionProvider;
/**
* @return \PHPStan\Reflection\Php\PhpMethodReflectionFactory
*/
public function getMethodReflectionFactory(): PhpMethodReflectionFactory;
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Contracts\Methods\Pipes;
use Closure;
use Flarum\PHPStan\Contracts\Methods\PassableContract;
/**
* @internal
*/
interface PipeContract
{
/**
* @param \Flarum\PHPStan\Contracts\Methods\PassableContract $passable
* @param \Closure $next
* @return void
*/
public function handle(PassableContract $passable, Closure $next): void;
}

View File

@ -0,0 +1,30 @@
<?php
/*
* This file is part of PhpStorm.
*
* (c) Nuno Maduro <enunomaduro@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Flarum\PHPStan\Contracts\Types;
use PHPStan\Type\Type;
/**
* @internal
*/
interface PassableContract
{
/**
* @return \PHPStan\Type\Type
*/
public function getType(): Type;
/**
* @param \PHPStan\Type\Type $type
* @return void
*/
public function setType(Type $type): void;
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Contracts\Types\Pipes;
use Closure;
use Flarum\PHPStan\Contracts\Types\PassableContract;
/**
* @internal
*/
interface PipeContract
{
/**
* @param \Flarum\PHPStan\Contracts\Types\PassableContract $passable
* @param \Closure $next
* @return void
*/
public function handle(PassableContract $passable, Closure $next): void;
}

View File

@ -0,0 +1,231 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Str;
use Flarum\PHPStan\Reflection\AnnotationScopeMethodParameterReflection;
use Flarum\PHPStan\Reflection\AnnotationScopeMethodReflection;
use Flarum\PHPStan\Reflection\DynamicWhereParameterReflection;
use Flarum\PHPStan\Reflection\EloquentBuilderMethodReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MissingMethodFromReflectionException;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\VerbosityLevel;
class BuilderHelper
{
/** @var string[] */
public const MODEL_RETRIEVAL_METHODS = ['first', 'find', 'findMany', 'findOrFail', 'firstOrFail', 'sole'];
/** @var string[] */
public const MODEL_CREATION_METHODS = ['make', 'create', 'forceCreate', 'findOrNew', 'firstOrNew', 'updateOrCreate', 'firstOrCreate'];
/**
* The methods that should be returned from query builder.
*
* @var string[]
*/
public $passthru = [
'average', 'avg',
'count',
'dd', 'dump',
'doesntExist', 'exists',
'getBindings', 'getConnection', 'getGrammar',
'insert', 'insertGetId', 'insertOrIgnore', 'insertUsing',
'max', 'min',
'raw',
'sum',
'toSql',
];
/** @var ReflectionProvider */
private $reflectionProvider;
/** @var bool */
private $checkProperties;
public function __construct(ReflectionProvider $reflectionProvider, bool $checkProperties)
{
$this->reflectionProvider = $reflectionProvider;
$this->checkProperties = $checkProperties;
}
public function dynamicWhere(
string $methodName,
Type $returnObject
): ?EloquentBuilderMethodReflection {
if (! Str::startsWith($methodName, 'where')) {
return null;
}
if ($returnObject instanceof GenericObjectType && $this->checkProperties) {
$returnClassReflection = $returnObject->getClassReflection();
if ($returnClassReflection !== null) {
$modelType = $returnClassReflection->getActiveTemplateTypeMap()->getType('TModelClass');
if ($modelType === null) {
$modelType = $returnClassReflection->getActiveTemplateTypeMap()->getType('TRelatedModel');
}
if ($modelType !== null) {
$finder = substr($methodName, 5);
$segments = preg_split(
'/(And|Or)(?=[A-Z])/', $finder, -1, PREG_SPLIT_DELIM_CAPTURE
);
if ($segments !== false) {
$trinaryLogic = TrinaryLogic::createYes();
foreach ($segments as $segment) {
if ($segment !== 'And' && $segment !== 'Or') {
$trinaryLogic = $trinaryLogic->and($modelType->hasProperty(Str::snake($segment)));
}
}
if (! $trinaryLogic->yes()) {
return null;
}
}
}
}
}
$classReflection = $this->reflectionProvider->getClass(QueryBuilder::class);
$methodReflection = $classReflection->getNativeMethod('dynamicWhere');
return new EloquentBuilderMethodReflection(
$methodName,
$classReflection,
$methodReflection,
[new DynamicWhereParameterReflection],
$returnObject,
true
);
}
/**
* This method mimics the `EloquentBuilder::__call` method.
* Does not handle the case where $methodName exists in `EloquentBuilder`,
* that should be checked by caller before calling this method.
*
* @param ClassReflection $eloquentBuilder Can be `EloquentBuilder` or a custom builder extending it.
* @param string $methodName
* @param ClassReflection $model
* @return MethodReflection|null
*
* @throws MissingMethodFromReflectionException
* @throws ShouldNotHappenException
*/
public function searchOnEloquentBuilder(ClassReflection $eloquentBuilder, string $methodName, ClassReflection $model): ?MethodReflection
{
// Check for local query scopes
if (array_key_exists('scope'.ucfirst($methodName), $model->getMethodTags())) {
$methodTag = $model->getMethodTags()['scope'.ucfirst($methodName)];
$parameters = [];
foreach ($methodTag->getParameters() as $parameterName => $parameterTag) {
$parameters[] = new AnnotationScopeMethodParameterReflection($parameterName, $parameterTag->getType(), $parameterTag->passedByReference(), $parameterTag->isOptional(), $parameterTag->isVariadic(), $parameterTag->getDefaultValue());
}
// We shift the parameters,
// because first parameter is the Builder
array_shift($parameters);
return new EloquentBuilderMethodReflection(
'scope'.ucfirst($methodName),
$model,
new AnnotationScopeMethodReflection('scope'.ucfirst($methodName), $model, $methodTag->getReturnType(), $parameters, $methodTag->isStatic(), false),
$parameters,
$methodTag->getReturnType()
);
}
if ($model->hasNativeMethod('scope'.ucfirst($methodName))) {
$methodReflection = $model->getNativeMethod('scope'.ucfirst($methodName));
$parametersAcceptor = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants());
$parameters = $parametersAcceptor->getParameters();
// We shift the parameters,
// because first parameter is the Builder
array_shift($parameters);
$returnType = $parametersAcceptor->getReturnType();
return new EloquentBuilderMethodReflection(
'scope'.ucfirst($methodName),
$methodReflection->getDeclaringClass(),
$methodReflection,
$parameters,
$returnType,
$parametersAcceptor->isVariadic()
);
}
$queryBuilderReflection = $this->reflectionProvider->getClass(QueryBuilder::class);
if (in_array($methodName, $this->passthru, true)) {
return $queryBuilderReflection->getNativeMethod($methodName);
}
if ($queryBuilderReflection->hasNativeMethod($methodName)) {
return $queryBuilderReflection->getNativeMethod($methodName);
}
return $this->dynamicWhere($methodName, new GenericObjectType($eloquentBuilder->getName(), [new ObjectType($model->getName())]));
}
/**
* @param string $modelClassName
* @return string
*
* @throws MissingMethodFromReflectionException
* @throws ShouldNotHappenException
*/
public function determineBuilderName(string $modelClassName): string
{
$method = $this->reflectionProvider->getClass($modelClassName)->getNativeMethod('newEloquentBuilder');
$returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType();
if (in_array(EloquentBuilder::class, $returnType->getReferencedClasses(), true)) {
return EloquentBuilder::class;
}
if ($returnType instanceof ObjectType) {
return $returnType->getClassName();
}
return $returnType->describe(VerbosityLevel::value());
}
/**
* @throws MissingMethodFromReflectionException
* @throws ShouldNotHappenException
*/
public function determineCollectionClassName(string $modelClassName): string
{
$newCollectionMethod = $this->reflectionProvider->getClass($modelClassName)->getNativeMethod('newCollection');
$returnType = ParametersAcceptorSelector::selectSingle($newCollectionMethod->getVariants())->getReturnType();
if ($returnType instanceof ObjectType) {
return $returnType->getClassName();
}
return $returnType->describe(VerbosityLevel::value());
}
}

View File

@ -0,0 +1,152 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\SoftDeletes;
use Flarum\PHPStan\Reflection\EloquentBuilderMethodReflection;
use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Reflection\MissingMethodFromReflectionException;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateMixedType;
use PHPStan\Type\Generic\TemplateObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeWithClassName;
final class EloquentBuilderForwardsCallsExtension implements MethodsClassReflectionExtension
{
/** @var array<string, MethodReflection> */
private $cache = [];
/** @var BuilderHelper */
private $builderHelper;
/** @var ReflectionProvider */
private $reflectionProvider;
public function __construct(BuilderHelper $builderHelper, ReflectionProvider $reflectionProvider)
{
$this->builderHelper = $builderHelper;
$this->reflectionProvider = $reflectionProvider;
}
/**
* @throws ShouldNotHappenException
* @throws MissingMethodFromReflectionException
*/
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
if (array_key_exists($classReflection->getCacheKey().'-'.$methodName, $this->cache)) {
return true;
}
$methodReflection = $this->findMethod($classReflection, $methodName);
if ($methodReflection !== null && $classReflection->isGeneric()) {
$this->cache[$classReflection->getCacheKey().'-'.$methodName] = $methodReflection;
return true;
}
return false;
}
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
{
return $this->cache[$classReflection->getCacheKey().'-'.$methodName];
}
/**
* @throws MissingMethodFromReflectionException
* @throws ShouldNotHappenException
*/
private function findMethod(ClassReflection $classReflection, string $methodName): ?MethodReflection
{
if ($classReflection->getName() !== EloquentBuilder::class && ! $classReflection->isSubclassOf(EloquentBuilder::class)) {
return null;
}
/** @var Type|TemplateMixedType|null $modelType */
$modelType = $classReflection->getActiveTemplateTypeMap()->getType('TModelClass');
// Generic type is not specified
if ($modelType === null) {
return null;
}
if ($modelType instanceof TemplateObjectType) {
$modelType = $modelType->getBound();
if ($modelType->equals(new ObjectType(Model::class))) {
return null;
}
}
if ($modelType instanceof TypeWithClassName) {
$modelReflection = $modelType->getClassReflection();
} else {
$modelReflection = $this->reflectionProvider->getClass(Model::class);
}
if ($modelReflection === null) {
return null;
}
$ref = $this->builderHelper->searchOnEloquentBuilder($classReflection, $methodName, $modelReflection);
if ($ref === null) {
// Special case for `SoftDeletes` trait
if (
in_array($methodName, ['withTrashed', 'onlyTrashed', 'withoutTrashed'], true) &&
in_array(SoftDeletes::class, array_keys($modelReflection->getTraits(true)))
) {
$ref = $this->reflectionProvider->getClass(SoftDeletes::class)->getMethod($methodName, new OutOfClassScope());
return new EloquentBuilderMethodReflection(
$methodName,
$classReflection,
$ref,
ParametersAcceptorSelector::selectSingle($ref->getVariants())->getParameters(),
new GenericObjectType($classReflection->getName(), [$modelType]),
ParametersAcceptorSelector::selectSingle($ref->getVariants())->isVariadic()
);
}
return null;
}
$parametersAcceptor = ParametersAcceptorSelector::selectSingle($ref->getVariants());
if (in_array($methodName, $this->builderHelper->passthru, true)) {
$returnType = $parametersAcceptor->getReturnType();
return new EloquentBuilderMethodReflection(
$methodName, $classReflection,
$ref,
$parametersAcceptor->getParameters(),
$returnType,
$parametersAcceptor->isVariadic()
);
}
// Returning custom reflection
// to ensure return type is always `EloquentBuilder<Model>`
return new EloquentBuilderMethodReflection(
$methodName, $classReflection,
$ref,
$parametersAcceptor->getParameters(),
new GenericObjectType($classReflection->getName(), [$modelType]),
$parametersAcceptor->isVariadic()
);
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods;
use Illuminate\Database\Eloquent\Model;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Reflection\Php\PhpMethodReflectionFactory;
use PHPStan\Reflection\ReflectionProvider;
/**
* @internal
*/
final class Extension implements MethodsClassReflectionExtension
{
/**
* @var Kernel
*/
private $kernel;
/** @var MethodReflection[] */
private $methodReflections = [];
public function __construct(PhpMethodReflectionFactory $methodReflectionFactory, ReflectionProvider $reflectionProvider, Kernel $kernel = null)
{
$this->kernel = $kernel ?? new Kernel($methodReflectionFactory, $reflectionProvider);
}
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
if ($classReflection->getName() === Model::class) {
return false;
}
if (array_key_exists($methodName.'-'.$classReflection->getName(), $this->methodReflections)) {
return true;
}
$passable = $this->kernel->handle($classReflection, $methodName);
$found = $passable->hasFound();
if ($found) {
$this->methodReflections[$methodName.'-'.$classReflection->getName()] = $passable->getMethodReflection();
}
return $found;
}
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
{
return $this->methodReflections[$methodName.'-'.$classReflection->getName()];
}
}

View File

@ -0,0 +1,143 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods;
use Flarum\PHPStan\Support\HigherOrderCollectionProxyHelper;
use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\TrinaryLogic;
use PHPStan\Type;
final class HigherOrderCollectionProxyExtension implements MethodsClassReflectionExtension
{
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
return HigherOrderCollectionProxyHelper::hasPropertyOrMethod($classReflection, $methodName, 'method');
}
public function getMethod(
ClassReflection $classReflection,
string $methodName
): MethodReflection {
$activeTemplateTypeMap = $classReflection->getActiveTemplateTypeMap();
/** @var Type\Constant\ConstantStringType $methodType */
$methodType = $activeTemplateTypeMap->getType('T');
/** @var Type\ObjectType $valueType */
$valueType = $activeTemplateTypeMap->getType('TValue');
$modelMethodReflection = $valueType->getMethod($methodName, new OutOfClassScope());
$modelMethodReturnType = ParametersAcceptorSelector::selectSingle($modelMethodReflection->getVariants())->getReturnType();
$returnType = HigherOrderCollectionProxyHelper::determineReturnType($methodType->getValue(), $valueType, $modelMethodReturnType);
return new class($classReflection, $methodName, $modelMethodReflection, $returnType) implements MethodReflection
{
/** @var ClassReflection */
private $classReflection;
/** @var string */
private $methodName;
/** @var MethodReflection */
private $modelMethodReflection;
/** @var Type\Type */
private $returnType;
public function __construct(ClassReflection $classReflection, string $methodName, MethodReflection $modelMethodReflection, Type\Type $returnType)
{
$this->classReflection = $classReflection;
$this->methodName = $methodName;
$this->modelMethodReflection = $modelMethodReflection;
$this->returnType = $returnType;
}
public function getDeclaringClass(): \PHPStan\Reflection\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 getName(): string
{
return $this->methodName;
}
public function getPrototype(): \PHPStan\Reflection\ClassMemberReflection
{
return $this;
}
public function getVariants(): array
{
return [
new FunctionVariant(
ParametersAcceptorSelector::selectSingle($this->modelMethodReflection->getVariants())->getTemplateTypeMap(),
ParametersAcceptorSelector::selectSingle($this->modelMethodReflection->getVariants())->getResolvedTemplateTypeMap(),
ParametersAcceptorSelector::selectSingle($this->modelMethodReflection->getVariants())->getParameters(),
ParametersAcceptorSelector::selectSingle($this->modelMethodReflection->getVariants())->isVariadic(),
$this->returnType
),
];
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): ?string
{
return null;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): ?\PHPStan\Type\Type
{
return null;
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
};
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods;
use Illuminate\Support\HigherOrderTapProxy;
use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Type\ObjectType;
final class HigherOrderTapProxyExtension implements MethodsClassReflectionExtension
{
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
if ($classReflection->getName() !== HigherOrderTapProxy::class) {
return false;
}
$templateTypeMap = $classReflection->getActiveTemplateTypeMap();
$templateType = $templateTypeMap->getType('TClass');
if (! $templateType instanceof ObjectType) {
return false;
}
if ($templateType->getClassReflection() === null) {
return false;
}
return $templateType->hasMethod($methodName)->yes();
}
public function getMethod(
ClassReflection $classReflection,
string $methodName
): MethodReflection {
/** @var ObjectType $templateType */
$templateType = $classReflection->getActiveTemplateTypeMap()->getType('TClass');
/** @var ClassReflection $reflection */
$reflection = $templateType->getClassReflection();
return $reflection->getMethod($methodName, new OutOfClassScope());
}
}

View File

@ -0,0 +1,72 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods;
use Illuminate\Pipeline\Pipeline;
use Flarum\PHPStan\Concerns;
use Flarum\PHPStan\Contracts\Methods\PassableContract;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\Php\PhpMethodReflectionFactory;
use PHPStan\Reflection\ReflectionProvider;
/**
* @internal
*/
final class Kernel
{
use Concerns\HasContainer;
/**
* @var PhpMethodReflectionFactory
*/
private $methodReflectionFactory;
/**
* @var ReflectionProvider
*/
private $reflectionProvider;
/**
* Kernel constructor.
*
* @param PhpMethodReflectionFactory $methodReflectionFactory
*/
public function __construct(
PhpMethodReflectionFactory $methodReflectionFactory,
ReflectionProvider $reflectionProvider
) {
$this->methodReflectionFactory = $methodReflectionFactory;
$this->reflectionProvider = $reflectionProvider;
}
/**
* @param ClassReflection $classReflection
* @param string $methodName
* @return PassableContract
*/
public function handle(ClassReflection $classReflection, string $methodName): PassableContract
{
$pipeline = new Pipeline($this->getContainer());
$passable = new Passable($this->methodReflectionFactory, $this->reflectionProvider, $pipeline, $classReflection, $methodName);
$pipeline->send($passable)
->through(
[
Pipes\SelfClass::class,
Pipes\Macros::class,
Pipes\Contracts::class,
Pipes\Facades::class,
Pipes\Managers::class,
Pipes\Auths::class,
]
)
->then(
function ($method) {
}
);
return $passable;
}
}

View File

@ -0,0 +1,259 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods;
use function array_map;
use Closure;
use ErrorException;
use Illuminate\Validation\ValidationException;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\PassedByReference;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypehintHelper;
use ReflectionFunction;
use ReflectionParameter;
use ReflectionType;
use stdClass;
final class Macro implements MethodReflection
{
/**
* @var ClassReflection
*/
private $classReflection;
/**
* The method name.
*
* @var string
*/
private $methodName;
/**
* The reflection function.
*
* @var ReflectionFunction
*/
private $reflectionFunction;
/**
* The parameters.
*
* @var ReflectionParameter[]
*/
private $parameters;
/**
* The is static.
*
* @var bool
*/
private $isStatic = false;
/**
* Map of macro methods and thrown exception classes.
*
* @var string[]
*/
private $methodThrowTypeMap = [
'validate' => ValidationException::class,
'validateWithBag' => ValidationException::class,
];
public function __construct(ClassReflection $classReflection, string $methodName, ReflectionFunction $reflectionFunction)
{
$this->classReflection = $classReflection;
$this->methodName = $methodName;
$this->reflectionFunction = $reflectionFunction;
$this->parameters = $this->reflectionFunction->getParameters();
if ($this->reflectionFunction->isClosure()) {
try {
/** @var Closure $closure */
$closure = $this->reflectionFunction->getClosure();
Closure::bind($closure, new stdClass);
// The closure can be bound so it was not explicitly marked as static
} catch (ErrorException $e) {
// The closure was explicitly marked as static
$this->isStatic = true;
}
}
}
public function getDeclaringClass(): ClassReflection
{
return $this->classReflection;
}
public function isPrivate(): bool
{
return false;
}
public function isPublic(): bool
{
return true;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isStatic(): bool
{
return $this->isStatic;
}
/**
* Set the is static value.
*
* @param bool $isStatic
* @return void
*/
public function setIsStatic(bool $isStatic): void
{
$this->isStatic = $isStatic;
}
/**
* {@inheritdoc}
*/
public function getDocComment(): ?string
{
return $this->reflectionFunction->getDocComment() ?: null;
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return $this->methodName;
}
/** @return ParameterReflection[] */
public function getParameters(): array
{
return array_map(function (ReflectionParameter $reflection): ParameterReflection {
return new class($reflection) implements ParameterReflection
{
/**
* @var ReflectionParameter
*/
private $reflection;
public function __construct(ReflectionParameter $reflection)
{
$this->reflection = $reflection;
}
public function getName(): string
{
return $this->reflection->getName();
}
public function isOptional(): bool
{
return $this->reflection->isOptional();
}
public function getType(): Type
{
$type = $this->reflection->getType();
if ($type === null) {
return new MixedType();
}
return TypehintHelper::decideTypeFromReflection($this->reflection->getType());
}
public function passedByReference(): PassedByReference
{
return PassedByReference::createNo();
}
public function isVariadic(): bool
{
return $this->reflection->isVariadic();
}
public function getDefaultValue(): ?Type
{
return null;
}
};
}, $this->parameters);
}
/**
* Set the parameters value.
*
* @param ReflectionParameter[] $parameters
* @return void
*/
public function setParameters(array $parameters): void
{
$this->parameters = $parameters;
}
public function getReturnType(): ?ReflectionType
{
return $this->reflectionFunction->getReturnType();
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createFromBoolean($this->reflectionFunction->isDeprecated());
}
public function getPrototype(): ClassMemberReflection
{
return $this;
}
/**
* @inheritDoc
*/
public function getVariants(): array
{
return [
new FunctionVariant(TemplateTypeMap::createEmpty(), null, $this->getParameters(), $this->reflectionFunction->isVariadic(), TypehintHelper::decideTypeFromReflection($this->getReturnType())),
];
}
public function getDeprecatedDescription(): ?string
{
return null;
}
public function getThrowType(): ?Type
{
if (array_key_exists($this->methodName, $this->methodThrowTypeMap)) {
return new ObjectType($this->methodThrowTypeMap[$this->methodName]);
}
return null;
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
}

View File

@ -0,0 +1,160 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Str;
use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
class ModelFactoryMethodsClassReflectionExtension implements MethodsClassReflectionExtension
{
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
// Class only available on Laravel 8
if (! class_exists('\Illuminate\Database\Eloquent\Factories\Factory')) {
return false;
}
if (! $classReflection->isSubclassOf(Factory::class)) {
return false;
}
if (! Str::startsWith($methodName, ['for', 'has'])) {
return false;
}
$relationship = Str::camel(Str::substr($methodName, 3));
$parent = $classReflection->getParentClass();
if ($parent === null) {
return false;
}
$modelType = $parent->getActiveTemplateTypeMap()->getType('TModel');
if ($modelType === null) {
return false;
}
return $modelType->hasMethod($relationship)->yes();
}
public function getMethod(
ClassReflection $classReflection,
string $methodName
): MethodReflection {
return new class($classReflection, $methodName) implements MethodReflection
{
/** @var ClassReflection */
private $classReflection;
/** @var string */
private $methodName;
public function __construct(ClassReflection $classReflection, string $methodName)
{
$this->classReflection = $classReflection;
$this->methodName = $methodName;
}
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 getName(): string
{
return $this->methodName;
}
public function getPrototype(): ClassMemberReflection
{
return $this;
}
public function getVariants(): array
{
$returnType = new ObjectType($this->classReflection->getName());
$stateParameter = ParametersAcceptorSelector::selectSingle($this->classReflection->getMethod('state', new OutOfClassScope())->getVariants())->getParameters()[0];
$countParameter = ParametersAcceptorSelector::selectSingle($this->classReflection->getMethod('count', new OutOfClassScope())->getVariants())->getParameters()[0];
$variants = [
new FunctionVariant(TemplateTypeMap::createEmpty(), null, [], false, $returnType),
];
if (Str::startsWith($this->methodName, 'for')) {
$variants[] = new FunctionVariant(TemplateTypeMap::createEmpty(), null, [$stateParameter], false, $returnType);
} else {
$variants[] = new FunctionVariant(TemplateTypeMap::createEmpty(), null, [$countParameter], false, $returnType);
$variants[] = new FunctionVariant(TemplateTypeMap::createEmpty(), null, [$stateParameter], false, $returnType);
$variants[] = new FunctionVariant(TemplateTypeMap::createEmpty(), null, [$countParameter, $stateParameter], false, $returnType);
}
return $variants;
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): ?string
{
return null;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): ?Type
{
return null;
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
};
}
}

View File

@ -0,0 +1,212 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Flarum\PHPStan\Reflection\EloquentBuilderMethodReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Reflection\MissingMethodFromReflectionException;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\TypeWithClassName;
final class ModelForwardsCallsExtension implements MethodsClassReflectionExtension
{
/** @var BuilderHelper */
private $builderHelper;
/** @var ReflectionProvider */
private $reflectionProvider;
/** @var EloquentBuilderForwardsCallsExtension */
private $eloquentBuilderForwardsCallsExtension;
/** @var array<string, MethodReflection> */
private $cache = [];
public function __construct(BuilderHelper $builderHelper, ReflectionProvider $reflectionProvider, EloquentBuilderForwardsCallsExtension $eloquentBuilderForwardsCallsExtension)
{
$this->builderHelper = $builderHelper;
$this->reflectionProvider = $reflectionProvider;
$this->eloquentBuilderForwardsCallsExtension = $eloquentBuilderForwardsCallsExtension;
}
/**
* @throws MissingMethodFromReflectionException
* @throws ShouldNotHappenException
*/
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
if (array_key_exists($classReflection->getCacheKey().'-'.$methodName, $this->cache)) {
return true;
}
$methodReflection = $this->findMethod($classReflection, $methodName);
if ($methodReflection !== null) {
$this->cache[$classReflection->getCacheKey().'-'.$methodName] = $methodReflection;
return true;
}
return false;
}
/**
* @param ClassReflection $classReflection
* @param string $methodName
* @return MethodReflection
*/
public function getMethod(ClassReflection $classReflection, string $methodName): MethodReflection
{
return $this->cache[$classReflection->getCacheKey().'-'.$methodName];
}
/**
* @throws ShouldNotHappenException
* @throws MissingMethodFromReflectionException
*/
private function findMethod(ClassReflection $classReflection, string $methodName): ?MethodReflection
{
if ($classReflection->getName() !== Model::class && ! $classReflection->isSubclassOf(Model::class)) {
return null;
}
$builderName = $this->builderHelper->determineBuilderName($classReflection->getName());
if (in_array($methodName, ['increment', 'decrement'], true)) {
$methodReflection = $classReflection->getNativeMethod($methodName);
return new class($classReflection, $methodName, $methodReflection) implements MethodReflection
{
/** @var ClassReflection */
private $classReflection;
/** @var string */
private $methodName;
/** @var MethodReflection */
private $methodReflection;
public function __construct(ClassReflection $classReflection, string $methodName, MethodReflection $methodReflection)
{
$this->classReflection = $classReflection;
$this->methodName = $methodName;
$this->methodReflection = $methodReflection;
}
public function getDeclaringClass(): \PHPStan\Reflection\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 getName(): string
{
return $this->methodName;
}
public function getPrototype(): \PHPStan\Reflection\ClassMemberReflection
{
return $this;
}
public function getVariants(): array
{
return $this->methodReflection->getVariants();
}
public function isDeprecated(): \PHPStan\TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): ?string
{
return null;
}
public function isFinal(): \PHPStan\TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): \PHPStan\TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): ?\PHPStan\Type\Type
{
return null;
}
public function hasSideEffects(): \PHPStan\TrinaryLogic
{
return TrinaryLogic::createYes();
}
};
}
$builderReflection = $this->reflectionProvider->getClass($builderName)->withTypes([new ObjectType($classReflection->getName())]);
$genericBuilderAndModelType = new GenericObjectType($builderName, [new ObjectType($classReflection->getName())]);
if ($builderReflection->hasNativeMethod($methodName)) {
$reflection = $builderReflection->getNativeMethod($methodName);
$parametersAcceptor = ParametersAcceptorSelector::selectSingle($reflection->getVariants());
$returnType = TypeTraverser::map($parametersAcceptor->getReturnType(), static function (Type $type, callable $traverse) use ($genericBuilderAndModelType) {
if ($type instanceof TypeWithClassName && $type->getClassName() === Builder::class) {
return $genericBuilderAndModelType;
}
return $traverse($type);
});
return new EloquentBuilderMethodReflection(
$methodName, $classReflection,
$reflection,
$parametersAcceptor->getParameters(),
$returnType,
$parametersAcceptor->isVariadic()
);
}
if ($this->eloquentBuilderForwardsCallsExtension->hasMethod($builderReflection, $methodName)) {
return $this->eloquentBuilderForwardsCallsExtension->getMethod($builderReflection, $methodName);
}
return null;
}
}

View File

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods;
use Illuminate\Database\Eloquent\Model;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\StaticType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeTraverser;
use PHPStan\Type\TypeWithClassName;
final class ModelTypeHelper
{
public static function replaceStaticTypeWithModel(Type $type, string $modelClass): Type
{
return TypeTraverser::map($type, static function (Type $type, callable $traverse) use ($modelClass): Type {
if ($type instanceof ObjectWithoutClassType || $type instanceof StaticType) {
return new ObjectType($modelClass);
}
if ($type instanceof TypeWithClassName && $type->getClassName() === Model::class) {
return new ObjectType($modelClass);
}
return $traverse($type);
});
}
}

View File

@ -0,0 +1,217 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods;
use Illuminate\Contracts\Pipeline\Pipeline;
use LogicException;
use Mockery;
use Flarum\PHPStan\Concerns;
use Flarum\PHPStan\Contracts\Methods\PassableContract;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\Php\PhpMethodReflection;
use PHPStan\Reflection\Php\PhpMethodReflectionFactory;
use PHPStan\Reflection\ReflectionProvider;
/**
* @internal
*/
final class Passable implements PassableContract
{
use Concerns\HasContainer;
/**
* @var \PHPStan\Reflection\Php\PhpMethodReflectionFactory
*/
private $methodReflectionFactory;
/**
* @var ReflectionProvider
*/
private $reflectionProvider;
/**
* @var \Illuminate\Contracts\Pipeline\Pipeline
*/
private $pipeline;
/**
* @var \PHPStan\Reflection\ClassReflection
*/
private $classReflection;
/**
* @var string
*/
private $methodName;
/**
* @var \PHPStan\Reflection\MethodReflection|null
*/
private $methodReflection;
/**
* @var bool
*/
private $staticAllowed = false;
/**
* Method constructor.
*
* @param \PHPStan\Reflection\Php\PhpMethodReflectionFactory $methodReflectionFactory
* @param ReflectionProvider $reflectionProvider
* @param \Illuminate\Contracts\Pipeline\Pipeline $pipeline
* @param \PHPStan\Reflection\ClassReflection $classReflection
* @param string $methodName
*/
public function __construct(
PhpMethodReflectionFactory $methodReflectionFactory,
ReflectionProvider $reflectionProvider,
Pipeline $pipeline,
ClassReflection $classReflection,
string $methodName
) {
$this->methodReflectionFactory = $methodReflectionFactory;
$this->reflectionProvider = $reflectionProvider;
$this->pipeline = $pipeline;
$this->classReflection = $classReflection;
$this->methodName = $methodName;
}
/**
* {@inheritdoc}
*/
public function getClassReflection(): ClassReflection
{
return $this->classReflection;
}
/**
* {@inheritdoc}
*/
public function setClassReflection(ClassReflection $classReflection): PassableContract
{
$this->classReflection = $classReflection;
return $this;
}
/**
* {@inheritdoc}
*/
public function getMethodName(): string
{
return $this->methodName;
}
/**
* {@inheritdoc}
*/
public function hasFound(): bool
{
return $this->methodReflection !== null;
}
/**
* {@inheritdoc}
*/
public function searchOn(string $class): bool
{
$classReflection = $this->reflectionProvider->getClass($class);
$found = $classReflection->hasNativeMethod($this->methodName);
if ($found) {
$this->setMethodReflection($classReflection->getNativeMethod($this->methodName));
}
return $found;
}
/**
* {@inheritdoc}
*/
public function getMethodReflection(): MethodReflection
{
if ($this->methodReflection === null) {
throw new LogicException("MethodReflection doesn't exist");
}
return $this->methodReflection;
}
/**
* {@inheritdoc}
*/
public function setMethodReflection(MethodReflection $methodReflection): void
{
$this->methodReflection = $methodReflection;
}
/**
* {@inheritdoc}
*/
public function setStaticAllowed(bool $staticAllowed): void
{
$this->staticAllowed = $staticAllowed;
}
/**
* {@inheritdoc}
*/
public function isStaticAllowed(): bool
{
return $this->staticAllowed;
}
/**
* {@inheritdoc}
*/
public function sendToPipeline(string $class, $staticAllowed = false): bool
{
$classReflection = $this->reflectionProvider->getClass($class);
$this->setStaticAllowed($this->staticAllowed ?: $staticAllowed);
$originalClassReflection = $this->classReflection;
$this->pipeline->send($this->setClassReflection($classReflection))
->then(
function (PassableContract $passable) use ($originalClassReflection) {
if ($passable->hasFound()) {
$this->setMethodReflection($passable->getMethodReflection());
$this->setStaticAllowed($passable->isStaticAllowed());
}
$this->setClassReflection($originalClassReflection);
}
);
if ($result = $this->hasFound()) {
$methodReflection = $this->getMethodReflection();
if (get_class($methodReflection) === PhpMethodReflection::class) {
$methodReflection = Mockery::mock($methodReflection);
$methodReflection->shouldReceive('isStatic')
->andReturn($this->isStaticAllowed());
}
$this->setMethodReflection($methodReflection);
}
return $result;
}
public function getReflectionProvider(): ReflectionProvider
{
return $this->reflectionProvider;
}
/**
* {@inheritdoc}
*/
public function getMethodReflectionFactory(): PhpMethodReflectionFactory
{
return $this->methodReflectionFactory;
}
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods\Pipes;
use Closure;
use Illuminate\Contracts\Auth\Access\Authorizable;
use Illuminate\Contracts\Auth\Authenticatable;
use Illuminate\Contracts\Auth\CanResetPassword;
use function in_array;
use Flarum\PHPStan\Concerns;
use Flarum\PHPStan\Contracts\Methods\PassableContract;
use Flarum\PHPStan\Contracts\Methods\Pipes\PipeContract;
/**
* @internal
*/
final class Auths implements PipeContract
{
use Concerns\HasContainer;
use Concerns\LoadsAuthModel;
/**
* @var string[]
*/
private $classes = [
Authenticatable::class,
CanResetPassword::class,
Authorizable::class,
];
/**
* {@inheritdoc}
*/
public function handle(PassableContract $passable, Closure $next): void
{
$classReflectionName = $passable->getClassReflection()
->getName();
$found = false;
$config = $this->resolve('config');
if ($config !== null && in_array($classReflectionName, $this->classes, true)) {
$authModel = $this->getAuthModel($config);
if ($authModel !== null) {
$found = $passable->sendToPipeline($authModel);
}
} elseif ($classReflectionName === \Illuminate\Contracts\Auth\Factory::class || $classReflectionName === \Illuminate\Auth\AuthManager::class) {
$found = $passable->sendToPipeline(
\Illuminate\Contracts\Auth\Guard::class
);
}
if (! $found) {
$next($passable);
}
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods\Pipes;
use Closure;
use function get_class;
use Illuminate\Support\Str;
use Flarum\PHPStan\Concerns;
use Flarum\PHPStan\Contracts\Methods\PassableContract;
use Flarum\PHPStan\Contracts\Methods\Pipes\PipeContract;
use PHPStan\Reflection\ClassReflection;
/**
* @internal
*/
final class Contracts implements PipeContract
{
use Concerns\HasContainer;
/**
* {@inheritdoc}
*/
public function handle(PassableContract $passable, Closure $next): void
{
$found = false;
foreach ($this->concretes($passable->getClassReflection()) as $concrete) {
if ($found = $passable->sendToPipeline($concrete)) {
break;
}
}
if (! $found) {
$next($passable);
}
}
/**
* @param \PHPStan\Reflection\ClassReflection $classReflection
* @return class-string[]
*/
private function concretes(ClassReflection $classReflection): array
{
if ($classReflection->isInterface() && Str::startsWith($classReflection->getName(), 'Illuminate\Contracts')) {
$concrete = $this->resolve($classReflection->getName());
if ($concrete !== null) {
$class = get_class($concrete);
if ($class) {
return [$class];
}
}
}
return [];
}
}

View File

@ -0,0 +1,59 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods\Pipes;
use Closure;
use Illuminate\Support\Facades\Facade;
use Illuminate\Support\Str;
use Flarum\PHPStan\Contracts\Methods\PassableContract;
use Flarum\PHPStan\Contracts\Methods\Pipes\PipeContract;
/**
* @internal
*/
final class Facades implements PipeContract
{
/**
* {@inheritdoc}
*/
public function handle(PassableContract $passable, Closure $next): void
{
$classReflection = $passable->getClassReflection();
$found = false;
if ($classReflection->isSubclassOf(Facade::class)) {
$facadeClass = $classReflection->getName();
if ($concrete = $facadeClass::getFacadeRoot()) {
$class = get_class($concrete);
if ($class) {
$found = $passable->sendToPipeline($class, true);
}
}
if (! $found && Str::startsWith($passable->getMethodName(), 'assert')) {
$fakeFacadeClass = $this->getFake($facadeClass);
if ($passable->getReflectionProvider()->hasClass($fakeFacadeClass)) {
assert(class_exists($fakeFacadeClass));
$found = $passable->sendToPipeline($fakeFacadeClass, true);
}
}
}
if (! $found) {
$next($passable);
}
}
private function getFake(string $facade): string
{
$shortClassName = substr($facade, strrpos($facade, '\\') + 1);
return sprintf('\\Illuminate\\Support\\Testing\\Fakes\\%sFake', $shortClassName);
}
}

View File

@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods\Pipes;
use Carbon\Traits\Macro as CarbonMacro;
use Closure;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Support\Str;
use Illuminate\Support\Traits\Macroable;
use Flarum\PHPStan\Concerns;
use Flarum\PHPStan\Contracts\Methods\PassableContract;
use Flarum\PHPStan\Contracts\Methods\Pipes\PipeContract;
use Flarum\PHPStan\Methods\Macro;
use PHPStan\Reflection\ClassReflection;
/**
* @internal
*/
final class Macros implements PipeContract
{
use Concerns\HasContainer;
private function hasIndirectTraitUse(ClassReflection $class, string $traitName): bool
{
foreach ($class->getTraits() as $trait) {
if ($this->hasIndirectTraitUse($trait, $traitName)) {
return true;
}
}
return $class->hasTraitUse($traitName);
}
/**
* {@inheritdoc}
*/
public function handle(PassableContract $passable, Closure $next): void
{
$classReflection = $passable->getClassReflection();
/** @var class-string $className */
$className = null;
$found = false;
$macroTraitProperty = null;
if ($classReflection->isInterface() && Str::startsWith($classReflection->getName(), 'Illuminate\Contracts')) {
/** @var object|null $concrete */
$concrete = $this->resolve($classReflection->getName());
if ($concrete !== null) {
$className = get_class($concrete);
if ($className && $passable->getReflectionProvider()
->getClass($className)
->hasTraitUse(Macroable::class)) {
$macroTraitProperty = 'macros';
}
}
} elseif ($classReflection->hasTraitUse(Macroable::class) || $classReflection->getName() === Builder::class) {
$className = $classReflection->getName();
$macroTraitProperty = 'macros';
} elseif ($this->hasIndirectTraitUse($classReflection, CarbonMacro::class)) {
$className = $classReflection->getName();
$macroTraitProperty = 'globalMacros';
}
if ($className !== null && $macroTraitProperty) {
$classReflection = $passable->getReflectionProvider()->getClass($className);
$refObject = new \ReflectionClass($className);
$refProperty = $refObject->getProperty($macroTraitProperty);
$refProperty->setAccessible(true);
$found = $className === Builder::class
? $className::hasGlobalMacro($passable->getMethodName())
: $className::hasMacro($passable->getMethodName());
if ($found) {
$reflectionFunction = new \ReflectionFunction($refProperty->getValue()[$passable->getMethodName()]);
$methodReflection = new Macro(
$classReflection, $passable->getMethodName(), $reflectionFunction
);
$methodReflection->setIsStatic(true);
$passable->setMethodReflection($methodReflection);
}
}
if (! $found) {
$next($passable);
}
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods\Pipes;
use Closure;
use Illuminate\Support\Manager;
use InvalidArgumentException;
use Flarum\PHPStan\Concerns;
use Flarum\PHPStan\Contracts\Methods\PassableContract;
use Flarum\PHPStan\Contracts\Methods\Pipes\PipeContract;
/**
* @internal
*/
final class Managers implements PipeContract
{
use Concerns\HasContainer;
/**
* {@inheritdoc}
*/
public function handle(PassableContract $passable, Closure $next): void
{
$classReflection = $passable->getClassReflection();
$found = false;
if ($classReflection->isSubclassOf(Manager::class)) {
$driver = null;
$concrete = $this->resolve(
$classReflection->getName()
);
try {
$driver = $concrete->driver();
} catch (InvalidArgumentException $exception) {
// ..
}
if ($driver !== null) {
$class = get_class($driver);
if ($class) {
$found = $passable->sendToPipeline($class);
}
}
}
if (! $found) {
$next($passable);
}
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods\Pipes;
use Closure;
use Flarum\PHPStan\Contracts\Methods\PassableContract;
use Flarum\PHPStan\Contracts\Methods\Pipes\PipeContract;
/**
* @internal
*/
final class SelfClass implements PipeContract
{
/**
* {@inheritdoc}
*/
public function handle(PassableContract $passable, Closure $next): void
{
$className = $passable->getClassReflection()
->getName();
if (! $passable->searchOn($className)) {
$next($passable);
}
}
}

View File

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Relation;
use Flarum\PHPStan\Reflection\EloquentBuilderMethodReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Reflection\MissingMethodFromReflectionException;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateMixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeWithClassName;
final class RelationForwardsCallsExtension implements MethodsClassReflectionExtension
{
/** @var BuilderHelper */
private $builderHelper;
/** @var array<string, MethodReflection> */
private $cache = [];
/** @var ReflectionProvider */
private $reflectionProvider;
/** @var EloquentBuilderForwardsCallsExtension */
private $eloquentBuilderForwardsCallsExtension;
public function __construct(BuilderHelper $builderHelper, ReflectionProvider $reflectionProvider, EloquentBuilderForwardsCallsExtension $eloquentBuilderForwardsCallsExtension)
{
$this->builderHelper = $builderHelper;
$this->reflectionProvider = $reflectionProvider;
$this->eloquentBuilderForwardsCallsExtension = $eloquentBuilderForwardsCallsExtension;
}
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
if (array_key_exists($classReflection->getCacheKey().'-'.$methodName, $this->cache)) {
return true;
}
$methodReflection = $this->findMethod($classReflection, $methodName);
if ($methodReflection !== null) {
$this->cache[$classReflection->getCacheKey().'-'.$methodName] = $methodReflection;
return true;
}
return false;
}
public function getMethod(
ClassReflection $classReflection,
string $methodName
): MethodReflection {
return $this->cache[$classReflection->getCacheKey().'-'.$methodName];
}
/**
* @throws MissingMethodFromReflectionException
* @throws ShouldNotHappenException
*/
private function findMethod(ClassReflection $classReflection, string $methodName): ?MethodReflection
{
if (! $classReflection->isSubclassOf(Relation::class)) {
return null;
}
/** @var Type|TemplateMixedType|null $relatedModel */
$relatedModel = $classReflection->getActiveTemplateTypeMap()->getType('TRelatedModel');
if ($relatedModel === null) {
return null;
}
if ($relatedModel instanceof TypeWithClassName) {
$modelReflection = $relatedModel->getClassReflection();
} else {
$modelReflection = $this->reflectionProvider->getClass(Model::class);
}
if ($modelReflection === null) {
return null;
}
$builderName = $this->builderHelper->determineBuilderName($modelReflection->getName());
$builderReflection = $this->reflectionProvider->getClass($builderName)->withTypes([$relatedModel]);
if ($builderReflection->hasNativeMethod($methodName)) {
$reflection = $builderReflection->getNativeMethod($methodName);
} elseif ($this->eloquentBuilderForwardsCallsExtension->hasMethod($builderReflection, $methodName)) {
$reflection = $this->eloquentBuilderForwardsCallsExtension->getMethod($builderReflection, $methodName);
} else {
return null;
}
$parametersAcceptor = ParametersAcceptorSelector::selectSingle($reflection->getVariants());
$returnType = $parametersAcceptor->getReturnType();
$types = [$relatedModel];
// BelongsTo relation needs second generic type
if ($classReflection->getName() === BelongsTo::class) {
$childType = $classReflection->getActiveTemplateTypeMap()->getType('TChildModel');
if ($childType !== null) {
$types[] = $childType;
}
}
if ((new ObjectType(Builder::class))->isSuperTypeOf($returnType)->yes()) {
return new EloquentBuilderMethodReflection(
$methodName, $classReflection,
$reflection, $parametersAcceptor->getParameters(),
new GenericObjectType($classReflection->getName(), $types),
$parametersAcceptor->isVariadic()
);
}
return new EloquentBuilderMethodReflection(
$methodName, $classReflection,
$reflection, $parametersAcceptor->getParameters(),
$returnType,
$parametersAcceptor->isVariadic()
);
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Methods;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Filesystem\FilesystemManager;
use Illuminate\Support\Facades\Storage;
use Flarum\PHPStan\Reflection\StaticMethodReflection;
use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\MethodsClassReflectionExtension;
use PHPStan\Reflection\ReflectionProvider;
class StorageMethodsClassReflectionExtension implements MethodsClassReflectionExtension
{
/**
* @var ReflectionProvider
*/
private $reflectionProvider;
public function __construct(ReflectionProvider $reflectionProvider)
{
$this->reflectionProvider = $reflectionProvider;
}
public function hasMethod(ClassReflection $classReflection, string $methodName): bool
{
if ($classReflection->getName() !== Storage::class) {
return false;
}
if ($this->reflectionProvider->getClass(FilesystemManager::class)->hasMethod($methodName)) {
return true;
}
if ($this->reflectionProvider->getClass(FilesystemAdapter::class)->hasMethod($methodName)) {
return true;
}
return false;
}
public function getMethod(
ClassReflection $classReflection,
string $methodName
): MethodReflection {
if ($this->reflectionProvider->getClass(FilesystemManager::class)->hasMethod($methodName)) {
return new StaticMethodReflection(
$this->reflectionProvider->getClass(FilesystemManager::class)->getMethod($methodName, new OutOfClassScope())
);
}
return new StaticMethodReflection(
$this->reflectionProvider->getClass(FilesystemAdapter::class)->getMethod($methodName, new OutOfClassScope())
);
}
}

View File

@ -0,0 +1,118 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Properties;
use Flarum\PHPStan\Support\HigherOrderCollectionProxyHelper;
use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\PropertiesClassReflectionExtension;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type;
final class HigherOrderCollectionProxyPropertyExtension implements PropertiesClassReflectionExtension
{
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
return HigherOrderCollectionProxyHelper::hasPropertyOrMethod($classReflection, $propertyName, 'property');
}
public function getProperty(
ClassReflection $classReflection,
string $propertyName
): PropertyReflection {
$activeTemplateTypeMap = $classReflection->getActiveTemplateTypeMap();
/** @var Type\Constant\ConstantStringType $methodType */
$methodType = $activeTemplateTypeMap->getType('T');
/** @var Type\ObjectType $modelType */
$modelType = $activeTemplateTypeMap->getType('TValue');
$propertyType = $modelType->getProperty($propertyName, new OutOfClassScope())->getReadableType();
$returnType = HigherOrderCollectionProxyHelper::determineReturnType($methodType->getValue(), $modelType, $propertyType);
return new class($classReflection, $returnType) implements PropertyReflection
{
/** @var ClassReflection */
private $classReflection;
/** @var Type\Type */
private $returnType;
public function __construct(ClassReflection $classReflection, Type\Type $returnType)
{
$this->classReflection = $classReflection;
$this->returnType = $returnType;
}
public function getDeclaringClass(): \PHPStan\Reflection\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\Type
{
return $this->returnType;
}
public function getWritableType(): Type\Type
{
return $this->returnType;
}
public function canChangeTypeAfterAssignment(): bool
{
return false;
}
public function isReadable(): bool
{
return true;
}
public function isWritable(): bool
{
return false;
}
public function isDeprecated(): \PHPStan\TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): ?string
{
return null;
}
public function isInternal(): \PHPStan\TrinaryLogic
{
return TrinaryLogic::createNo();
}
};
}
}

View File

@ -0,0 +1,95 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Properties;
use PHPStan\File\FileHelper;
use PHPStan\Parser\Parser;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
use RegexIterator;
use SplFileInfo;
class MigrationHelper
{
/** @var Parser */
private $parser;
/** @var string[] */
private $databaseMigrationPath;
/** @var FileHelper */
private $fileHelper;
/**
* @param string[] $databaseMigrationPath
*/
public function __construct(Parser $parser, array $databaseMigrationPath, FileHelper $fileHelper)
{
$this->parser = $parser;
$this->databaseMigrationPath = $databaseMigrationPath;
$this->fileHelper = $fileHelper;
}
/**
* @return array<string, SchemaTable>
*/
public function initializeTables(): array
{
if (empty($this->databaseMigrationPath)) {
$this->databaseMigrationPath = [database_path('migrations')];
}
$schemaAggregator = new SchemaAggregator();
$filesArray = $this->getMigrationFiles();
if (empty($filesArray)) {
return [];
}
ksort($filesArray);
$this->requireFiles($filesArray);
foreach ($filesArray as $file) {
$schemaAggregator->addStatements($this->parser->parseFile($file->getPathname()));
}
return $schemaAggregator->tables;
}
/**
* @return SplFileInfo[]
*/
private function getMigrationFiles(): array
{
/** @var SplFileInfo[] $migrationFiles */
$migrationFiles = [];
foreach ($this->databaseMigrationPath as $additionalPath) {
$absolutePath = $this->fileHelper->absolutizePath($additionalPath);
if (is_dir($absolutePath)) {
$migrationFiles += iterator_to_array(
new RegexIterator(
new RecursiveIteratorIterator(new RecursiveDirectoryIterator($absolutePath)),
'/\.php$/i'
)
);
}
}
return $migrationFiles;
}
/**
* @param SplFileInfo[] $files
*/
private function requireFiles(array $files): void
{
foreach ($files as $file) {
require_once $file;
}
}
}

View File

@ -0,0 +1,39 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Properties;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\PropertiesClassReflectionExtension;
use PHPStan\Reflection\PropertyReflection;
/**
* @internal
*/
final class ModelAccessorExtension implements PropertiesClassReflectionExtension
{
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
if (! $classReflection->isSubclassOf(Model::class)) {
return false;
}
return $classReflection->hasNativeMethod('get'.Str::studly($propertyName).'Attribute');
}
public function getProperty(
ClassReflection $classReflection,
string $propertyName
): PropertyReflection {
$method = $classReflection->getNativeMethod('get'.Str::studly($propertyName).'Attribute');
return new ModelProperty(
$classReflection,
$method->getVariants()[0]->getReturnType(),
$method->getVariants()[0]->getReturnType()
);
}
}

View File

@ -0,0 +1,98 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Properties;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;
class ModelProperty implements PropertyReflection
{
/** @var ClassReflection */
private $declaringClass;
/** @var Type */
private $readableType;
/** @var Type */
private $writableType;
/** @var bool */
private $writeable;
public function __construct(ClassReflection $declaringClass, Type $readableType, Type $writableType, bool $writeable = true)
{
$this->declaringClass = $declaringClass;
$this->readableType = $readableType;
$this->writableType = $writableType;
$this->writeable = $writeable;
}
public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}
public function isStatic(): bool
{
return false;
}
public function isPrivate(): bool
{
return false;
}
public function isPublic(): bool
{
return true;
}
public function isReadable(): bool
{
return true;
}
public function isWritable(): bool
{
return $this->writeable;
}
public function getDocComment(): ?string
{
return null;
}
public function getReadableType(): Type
{
return $this->readableType;
}
public function getWritableType(): Type
{
return $this->writableType;
}
public function canChangeTypeAfterAssignment(): bool
{
return false;
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): ?string
{
return null;
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
}

View File

@ -0,0 +1,266 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Properties;
use ArrayObject;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Str;
use Flarum\PHPStan\Reflection\ReflectionHelper;
use PHPStan\PhpDoc\TypeStringResolver;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\PropertiesClassReflectionExtension;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\IntegerType;
/**
* @internal
*/
final class ModelPropertyExtension implements PropertiesClassReflectionExtension
{
/** @var array<string, SchemaTable> */
private $tables = [];
/** @var TypeStringResolver */
private $stringResolver;
/** @var string */
private $dateClass;
/** @var MigrationHelper */
private $migrationHelper;
public function __construct(TypeStringResolver $stringResolver, MigrationHelper $migrationHelper)
{
$this->stringResolver = $stringResolver;
$this->migrationHelper = $migrationHelper;
}
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
if (! $classReflection->isSubclassOf(Model::class)) {
return false;
}
if ($classReflection->isAbstract()) {
return false;
}
if ($classReflection->hasNativeMethod('get'.Str::studly($propertyName).'Attribute')) {
return false;
}
if (ReflectionHelper::hasPropertyTag($classReflection, $propertyName)) {
return false;
}
if (count($this->tables) === 0) {
$this->tables = $this->migrationHelper->initializeTables();
}
if ($propertyName === 'id') {
return true;
}
$modelName = $classReflection->getNativeReflection()->getName();
try {
$reflect = new \ReflectionClass($modelName);
/** @var Model $modelInstance */
$modelInstance = $reflect->newInstanceWithoutConstructor();
$tableName = $modelInstance->getTable();
} catch (\ReflectionException $e) {
return false;
}
if (! array_key_exists($tableName, $this->tables)) {
return false;
}
if (! array_key_exists($propertyName, $this->tables[$tableName]->columns)) {
return false;
}
$this->castPropertiesType($modelInstance);
$column = $this->tables[$tableName]->columns[$propertyName];
[$readableType, $writableType] = $this->getReadableAndWritableTypes($column, $modelInstance);
$column->readableType = $readableType;
$column->writeableType = $writableType;
$this->tables[$tableName]->columns[$propertyName] = $column;
return true;
}
public function getProperty(
ClassReflection $classReflection,
string $propertyName
): PropertyReflection {
$modelName = $classReflection->getNativeReflection()->getName();
try {
$reflect = new \ReflectionClass($modelName);
/** @var Model $modelInstance */
$modelInstance = $reflect->newInstanceWithoutConstructor();
$tableName = $modelInstance->getTable();
} catch (\ReflectionException $e) {
// `hasProperty` should return false if there was a reflection exception.
// so this should never happen
throw new ShouldNotHappenException();
}
if (
(! array_key_exists($tableName, $this->tables)
|| ! array_key_exists($propertyName, $this->tables[$tableName]->columns)
)
&& $propertyName === 'id'
) {
return new ModelProperty(
$classReflection,
new IntegerType(),
new IntegerType()
);
}
$column = $this->tables[$tableName]->columns[$propertyName];
return new ModelProperty(
$classReflection,
$this->stringResolver->resolve($column->readableType),
$this->stringResolver->resolve($column->writeableType)
);
}
private function getDateClass(): string
{
if (! $this->dateClass) {
$this->dateClass = class_exists(\Illuminate\Support\Facades\Date::class)
? '\\'.get_class(\Illuminate\Support\Facades\Date::now())
: '\Illuminate\Support\Carbon';
$this->dateClass .= '|\Carbon\Carbon';
}
return $this->dateClass;
}
/**
* @param SchemaColumn $column
* @param Model $modelInstance
* @return string[]
* @phpstan-return array<int, string>
*/
private function getReadableAndWritableTypes(SchemaColumn $column, Model $modelInstance): array
{
$readableType = $column->readableType;
$writableType = $column->writeableType;
if (in_array($column->name, $modelInstance->getDates(), true)) {
return [$this->getDateClass().($column->nullable ? '|null' : ''), $this->getDateClass().'|string'.($column->nullable ? '|null' : '')];
}
switch ($column->readableType) {
case 'string':
case 'int':
case 'float':
$readableType = $writableType = $column->readableType.($column->nullable ? '|null' : '');
break;
case 'boolean':
case 'bool':
switch ((string) config('database.default')) {
case 'sqlite':
case 'mysql':
$writableType = '0|1|bool';
$readableType = 'bool';
break;
default:
$readableType = $writableType = 'bool';
break;
}
break;
case 'enum':
if (! $column->options) {
$readableType = $writableType = 'string';
} else {
$readableType = $writableType = '\''.implode('\'|\'', $column->options).'\'';
}
break;
default:
break;
}
return [$readableType, $writableType];
}
private function castPropertiesType(Model $modelInstance): void
{
$casts = $modelInstance->getCasts();
foreach ($casts as $name => $type) {
if (! array_key_exists($name, $this->tables[$modelInstance->getTable()]->columns)) {
continue;
}
switch ($type) {
case 'boolean':
case 'bool':
$realType = 'boolean';
break;
case 'string':
$realType = 'string';
break;
case 'array':
case 'json':
$realType = 'array';
break;
case 'object':
$realType = 'object';
break;
case 'int':
case 'integer':
case 'timestamp':
$realType = 'integer';
break;
case 'real':
case 'double':
case 'float':
$realType = 'float';
break;
case 'date':
case 'datetime':
$realType = $this->getDateClass();
break;
case 'collection':
$realType = '\Illuminate\Support\Collection';
break;
case 'Illuminate\Database\Eloquent\Casts\AsArrayObject':
$realType = ArrayObject::class;
break;
case 'Illuminate\Database\Eloquent\Casts\AsCollection':
$realType = '\Illuminate\Support\Collection<mixed, mixed>';
break;
default:
$realType = class_exists($type) ? ('\\'.$type) : 'mixed';
break;
}
if ($this->tables[$modelInstance->getTable()]->columns[$name]->nullable) {
$realType .= '|null';
}
$this->tables[$modelInstance->getTable()]->columns[$name]->readableType = $realType;
$this->tables[$modelInstance->getTable()]->columns[$name]->writeableType = $realType;
}
}
}

View File

@ -0,0 +1,116 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Properties;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Str;
use Flarum\PHPStan\Concerns;
use Flarum\PHPStan\Methods\BuilderHelper;
use Flarum\PHPStan\Reflection\ReflectionHelper;
use Flarum\PHPStan\Types\RelationParserHelper;
use PHPStan\Analyser\OutOfClassScope;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\PropertiesClassReflectionExtension;
use PHPStan\Reflection\PropertyReflection;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\MixedType;
use PHPStan\Type\NeverType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\UnionType;
/**
* @internal
*/
final class ModelRelationsExtension implements PropertiesClassReflectionExtension
{
use Concerns\HasContainer;
/** @var RelationParserHelper */
private $relationParserHelper;
/** @var BuilderHelper */
private $builderHelper;
public function __construct(
RelationParserHelper $relationParserHelper,
BuilderHelper $builderHelper)
{
$this->relationParserHelper = $relationParserHelper;
$this->builderHelper = $builderHelper;
}
public function hasProperty(ClassReflection $classReflection, string $propertyName): bool
{
if (! $classReflection->isSubclassOf(Model::class)) {
return false;
}
if (ReflectionHelper::hasPropertyTag($classReflection, $propertyName)) {
return false;
}
$hasNativeMethod = $classReflection->hasNativeMethod($propertyName);
if (! $hasNativeMethod) {
return false;
}
$returnType = ParametersAcceptorSelector::selectSingle($classReflection->getNativeMethod($propertyName)->getVariants())->getReturnType();
if (! (new ObjectType(Relation::class))->isSuperTypeOf($returnType)->yes()) {
return false;
}
return true;
}
public function getProperty(ClassReflection $classReflection, string $propertyName): PropertyReflection
{
$method = $classReflection->getMethod($propertyName, new OutOfClassScope());
/** @var ObjectType $returnType */
$returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType();
if ($returnType instanceof GenericObjectType) {
/** @var ObjectType $relatedModelType */
$relatedModelType = $returnType->getTypes()[0];
$relatedModelClassName = $relatedModelType->getClassName();
} else {
$relatedModelClassName = $this
->relationParserHelper
->findRelatedModelInRelationMethod($method);
}
if ($relatedModelClassName === null) {
$relatedModelClassName = Model::class;
}
$relatedModel = new ObjectType($relatedModelClassName);
$collectionClass = $this->builderHelper->determineCollectionClassName($relatedModelClassName);
if (Str::contains($returnType->getClassName(), 'Many')) {
return new ModelProperty(
$classReflection,
new GenericObjectType($collectionClass, [$relatedModel]),
new NeverType(), false
);
}
if (Str::endsWith($returnType->getClassName(), 'MorphTo')) {
return new ModelProperty($classReflection, new UnionType([
new ObjectType(Model::class),
new MixedType(),
]), new NeverType(), false);
}
return new ModelProperty($classReflection, new UnionType([
$relatedModel,
new NullType(),
]), new NeverType(), false);
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Properties;
use ReflectionNamedType;
/**
* @internal
*/
final class ReflectionTypeContainer extends ReflectionNamedType
{
/**
* @var string
*/
private $type;
/**
* ReflectionTypeContainer constructor.
*
* @param string $type
*/
public function __construct(string $type)
{
$this->type = $type;
}
/**
* {@inheritdoc}
*/
public function allowsNull(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function isBuiltin(): bool
{
return false;
}
/**
* {@inheritdoc}
*/
public function __toString(): string
{
return $this->getName();
}
/**
* {@inheritdoc}
*/
public function getName(): string
{
return $this->type;
}
}

View File

@ -0,0 +1,432 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Properties;
use Illuminate\Support\Str;
use PhpParser;
use PhpParser\NodeFinder;
/**
* @see https://github.com/psalm/laravel-psalm-plugin/blob/master/src/SchemaAggregator.php
*/
final class SchemaAggregator
{
/** @var array<string, SchemaTable> */
public $tables = [];
/**
* @param array<int, PhpParser\Node\Stmt> $stmts
*/
public function addStatements(array $stmts): void
{
$nodeFinder = new NodeFinder();
/** @var PhpParser\Node\Stmt\Class_[] $classes */
$classes = $nodeFinder->findInstanceOf($stmts, PhpParser\Node\Stmt\Class_::class);
foreach ($classes as $stmt) {
$this->addClassStatements($stmt->stmts);
}
}
/**
* @param array<int, PhpParser\Node\Stmt> $stmts
*/
private function addClassStatements(array $stmts): void
{
foreach ($stmts as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\ClassMethod
&& $stmt->name->name !== 'down'
&& $stmt->stmts
) {
$this->addUpMethodStatements($stmt->stmts);
}
}
}
/**
* @param PhpParser\Node\Stmt[] $stmts
*/
private function addUpMethodStatements(array $stmts): void
{
$nodeFinder = new NodeFinder();
$methods = $nodeFinder->findInstanceOf($stmts, PhpParser\Node\Stmt\Expression::class);
foreach ($methods as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\Expression
&& $stmt->expr instanceof PhpParser\Node\Expr\StaticCall
&& ($stmt->expr->class instanceof PhpParser\Node\Name)
&& $stmt->expr->name instanceof PhpParser\Node\Identifier
&& ($stmt->expr->class->toCodeString() === '\Illuminate\Support\Facades\Schema' || $stmt->expr->class->toCodeString() === '\Schema')
) {
switch ($stmt->expr->name->name) {
case 'create':
$this->alterTable($stmt->expr, true);
break;
case 'table':
$this->alterTable($stmt->expr, false);
break;
case 'drop':
case 'dropIfExists':
$this->dropTable($stmt->expr);
break;
case 'rename':
$this->renameTable($stmt->expr);
}
}
}
}
private function alterTable(PhpParser\Node\Expr\StaticCall $call, bool $creating): void
{
if (! isset($call->args[0])
|| ! $call->getArgs()[0]->value instanceof PhpParser\Node\Scalar\String_
) {
return;
}
$tableName = $call->getArgs()[0]->value->value;
if ($creating) {
$this->tables[$tableName] = new SchemaTable($tableName);
}
if (! isset($call->args[1])
|| ! $call->getArgs()[1]->value instanceof PhpParser\Node\Expr\Closure
|| count($call->getArgs()[1]->value->params) < 1
|| ($call->getArgs()[1]->value->params[0]->type instanceof PhpParser\Node\Name
&& $call->getArgs()[1]->value->params[0]->type->toCodeString()
!== '\\Illuminate\Database\Schema\Blueprint')
) {
return;
}
$updateClosure = $call->getArgs()[1]->value;
if ($call->getArgs()[1]->value->params[0]->var instanceof PhpParser\Node\Expr\Variable
&& is_string($call->getArgs()[1]->value->params[0]->var->name)
) {
$argName = $call->getArgs()[1]->value->params[0]->var->name;
$this->processColumnUpdates($tableName, $argName, $updateClosure->stmts);
}
}
/**
* @param string $tableName
* @param string $argName
* @param PhpParser\Node\Stmt[] $stmts
*
* @throws \Exception
*/
private function processColumnUpdates(string $tableName, string $argName, array $stmts): void
{
if (! isset($this->tables[$tableName])) {
return;
}
$table = $this->tables[$tableName];
foreach ($stmts as $stmt) {
if ($stmt instanceof PhpParser\Node\Stmt\Expression
&& $stmt->expr instanceof PhpParser\Node\Expr\MethodCall
&& $stmt->expr->name instanceof PhpParser\Node\Identifier
) {
$rootVar = $stmt->expr;
$firstMethodCall = $rootVar;
$nullable = false;
while ($rootVar instanceof PhpParser\Node\Expr\MethodCall) {
if ($rootVar->name instanceof PhpParser\Node\Identifier
&& $rootVar->name->name === 'nullable'
) {
$nullable = true;
}
$firstMethodCall = $rootVar;
$rootVar = $rootVar->var;
}
if ($rootVar instanceof PhpParser\Node\Expr\Variable
&& $rootVar->name === $argName
&& $firstMethodCall->name instanceof PhpParser\Node\Identifier
) {
$firstArg = $firstMethodCall->getArgs()[0]->value ?? null;
$secondArg = $firstMethodCall->getArgs()[1]->value ?? null;
if ($firstMethodCall->name->name === 'foreignIdFor') {
if ($firstArg instanceof PhpParser\Node\Expr\ClassConstFetch
&& $firstArg->class instanceof PhpParser\Node\Name
) {
$modelClass = $firstArg->class->toCodeString();
} elseif ($firstArg instanceof PhpParser\Node\Scalar\String_) {
$modelClass = $firstArg->value;
} else {
continue;
}
$columnName = Str::snake(class_basename($modelClass)).'_id';
if ($secondArg instanceof PhpParser\Node\Scalar\String_) {
$columnName = $secondArg->value;
}
$table->setColumn(new SchemaColumn($columnName, 'int', $nullable));
continue;
}
if (! $firstArg instanceof PhpParser\Node\Scalar\String_) {
if ($firstMethodCall->name->name === 'timestamps'
|| $firstMethodCall->name->name === 'timestampsTz'
|| $firstMethodCall->name->name === 'nullableTimestamps'
|| $firstMethodCall->name->name === 'nullableTimestampsTz'
|| $firstMethodCall->name->name === 'rememberToken'
) {
switch (strtolower($firstMethodCall->name->name)) {
case 'droptimestamps':
case 'droptimestampstz':
$table->dropColumn('created_at');
$table->dropColumn('updated_at');
break;
case 'remembertoken':
$table->setColumn(new SchemaColumn('remember_token', 'string', $nullable));
break;
case 'dropremembertoken':
$table->dropColumn('remember_token');
break;
case 'timestamps':
case 'timestampstz':
case 'nullabletimestamps':
$table->setColumn(new SchemaColumn('created_at', 'string', true));
$table->setColumn(new SchemaColumn('updated_at', 'string', true));
break;
}
continue;
}
if ($firstMethodCall->name->name === 'softDeletes'
|| $firstMethodCall->name->name === 'softDeletesTz'
|| $firstMethodCall->name->name === 'dropSoftDeletes'
|| $firstMethodCall->name->name === 'dropSoftDeletesTz'
) {
$columnName = 'deleted_at';
} else {
continue;
}
} else {
$columnName = $firstArg->value;
}
$secondArgArray = null;
if ($secondArg instanceof PhpParser\Node\Expr\Array_) {
$secondArgArray = [];
foreach ($secondArg->items as $array_item) {
if ($array_item !== null && $array_item->value instanceof PhpParser\Node\Scalar\String_) {
$secondArgArray[] = $array_item->value->value;
}
}
}
switch (strtolower($firstMethodCall->name->name)) {
case 'biginteger':
case 'increments':
case 'integer':
case 'integerincrements':
case 'mediumincrements':
case 'mediuminteger':
case 'smallincrements':
case 'smallinteger':
case 'tinyincrements':
case 'tinyinteger':
case 'unsignedbiginteger':
case 'unsignedinteger':
case 'unsignedmediuminteger':
case 'unsignedsmallinteger':
case 'unsignedtinyinteger':
case 'bigincrements':
$table->setColumn(new SchemaColumn($columnName, 'int', $nullable));
break;
case 'char':
case 'datetimetz':
case 'date':
case 'datetime':
case 'ipaddress':
case 'json':
case 'jsonb':
case 'linestring':
case 'longtext':
case 'macaddress':
case 'mediumtext':
case 'multilinestring':
case 'string':
case 'text':
case 'time':
case 'timestamp':
case 'uuid':
case 'binary':
$table->setColumn(new SchemaColumn($columnName, 'string', $nullable));
break;
case 'boolean':
$table->setColumn(new SchemaColumn($columnName, 'bool', $nullable));
break;
case 'geometry':
case 'geometrycollection':
case 'multipoint':
case 'multipolygon':
case 'multipolygonz':
case 'point':
case 'polygon':
case 'computed':
$table->setColumn(new SchemaColumn($columnName, 'mixed', $nullable));
break;
case 'double':
case 'float':
case 'unsigneddecimal':
case 'decimal':
$table->setColumn(new SchemaColumn($columnName, 'float', $nullable));
break;
case 'after':
if ($secondArg instanceof PhpParser\Node\Expr\Closure
&& $secondArg->params[0]->var instanceof PhpParser\Node\Expr\Variable
&& ! ($secondArg->params[0]->var->name instanceof PhpParser\Node\Expr)) {
$argName = $secondArg->params[0]->var->name;
$this->processColumnUpdates($tableName, $argName, $secondArg->stmts);
}
break;
case 'dropcolumn':
case 'dropifexists':
case 'dropsoftdeletes':
case 'dropsoftdeletestz':
case 'removecolumn':
case 'drop':
$table->dropColumn($columnName);
break;
case 'dropforeign':
case 'dropindex':
case 'dropprimary':
case 'dropunique':
case 'foreign':
case 'index':
case 'primary':
case 'renameindex':
case 'spatialIndex':
case 'unique':
case 'dropspatialindex':
break;
case 'dropmorphs':
$table->dropColumn($columnName.'_type');
$table->dropColumn($columnName.'_id');
break;
case 'enum':
$table->setColumn(new SchemaColumn($columnName, 'enum', $nullable, $secondArgArray));
break;
case 'morphs':
$table->setColumn(new SchemaColumn($columnName.'_type', 'string', $nullable));
$table->setColumn(new SchemaColumn($columnName.'_id', 'int', $nullable));
break;
case 'nullablemorphs':
$table->setColumn(new SchemaColumn($columnName.'_type', 'string', true));
$table->setColumn(new SchemaColumn($columnName.'_id', 'int', true));
break;
case 'nullableuuidmorphs':
$table->setColumn(new SchemaColumn($columnName.'_type', 'string', true));
$table->setColumn(new SchemaColumn($columnName.'_id', 'string', true));
break;
case 'rename':
case 'renamecolumn':
if ($secondArg instanceof PhpParser\Node\Scalar\String_) {
$table->renameColumn($columnName, $secondArg->value);
}
break;
case 'set':
$table->setColumn(new SchemaColumn($columnName, 'set', $nullable, $secondArgArray));
break;
case 'softdeletestz':
case 'timestamptz':
case 'timetz':
case 'year':
case 'softdeletes':
$table->setColumn(new SchemaColumn($columnName, 'string', true));
break;
case 'uuidmorphs':
$table->setColumn(new SchemaColumn($columnName.'_type', 'string', $nullable));
$table->setColumn(new SchemaColumn($columnName.'_id', 'string', $nullable));
break;
default:
// We know a property exists with a name, we just don't know its type.
$table->setColumn(new SchemaColumn($columnName, 'mixed', $nullable));
break;
}
}
}
}
}
private function dropTable(PhpParser\Node\Expr\StaticCall $call): void
{
if (! isset($call->args[0])
|| ! $call->getArgs()[0]->value instanceof PhpParser\Node\Scalar\String_
) {
return;
}
$tableName = $call->getArgs()[0]->value->value;
unset($this->tables[$tableName]);
}
private function renameTable(PhpParser\Node\Expr\StaticCall $call): void
{
if (! isset($call->args[0], $call->args[1])
|| ! $call->getArgs()[0]->value instanceof PhpParser\Node\Scalar\String_
|| ! $call->getArgs()[1]->value instanceof PhpParser\Node\Scalar\String_
) {
return;
}
$oldTableName = $call->getArgs()[0]->value->value;
$newTableName = $call->getArgs()[1]->value->value;
if (! isset($this->tables[$oldTableName])) {
return;
}
$table = $this->tables[$oldTableName];
unset($this->tables[$oldTableName]);
$table->name = $newTableName;
$this->tables[$newTableName] = $table;
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Properties;
/**
* @see https://github.com/psalm/laravel-psalm-plugin/blob/master/src/SchemaColumn.php
*/
final class SchemaColumn
{
/** @var string */
public $name;
/** @var string */
public $readableType;
/** @var string */
public $writeableType;
/** @var bool */
public $nullable;
/** @var ?array<int, string> */
public $options;
/**
* @param string $name
* @param string $readableType
* @param bool $nullable
* @param string[]|null $options
*/
public function __construct(
string $name,
string $readableType,
bool $nullable = false,
?array $options = null
) {
$this->name = $name;
$this->readableType = $readableType;
$this->writeableType = $readableType;
$this->nullable = $nullable;
$this->options = $options;
}
}

View File

@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Properties;
/**
* @see https://github.com/psalm/laravel-psalm-plugin/blob/master/src/SchemaTable.php
*/
final class SchemaTable
{
/** @var string */
public $name;
/** @var SchemaColumn[] */
public $columns = [];
public function __construct(string $name)
{
$this->name = $name;
}
public function setColumn(SchemaColumn $column): void
{
$this->columns[$column->name] = $column;
}
public function renameColumn(string $oldName, string $newName): void
{
if (! isset($this->columns[$oldName])) {
return;
}
$oldColumn = $this->columns[$oldName];
unset($this->columns[$oldName]);
$oldColumn->name = $newName;
$this->columns[$newName] = $oldColumn;
}
public function dropColumn(string $columnName): void
{
unset($this->columns[$columnName]);
}
}

View File

@ -0,0 +1,70 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Reflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\PassedByReference;
use PHPStan\Type\Type;
final class AnnotationScopeMethodParameterReflection implements ParameterReflection
{
/** @var string */
private $name;
/** @var Type */
private $type;
/** @var PassedByReference */
private $passedByReference;
/** @var bool */
private $isOptional;
/** @var bool */
private $isVariadic;
/** @var Type|null */
private $defaultValue;
public function __construct(string $name, Type $type, PassedByReference $passedByReference, bool $isOptional, bool $isVariadic, ?Type $defaultValue)
{
$this->name = $name;
$this->type = $type;
$this->passedByReference = $passedByReference;
$this->isOptional = $isOptional;
$this->isVariadic = $isVariadic;
$this->defaultValue = $defaultValue;
}
public function getName(): string
{
return $this->name;
}
public function isOptional(): bool
{
return $this->isOptional;
}
public function getType(): Type
{
return $this->type;
}
public function passedByReference(): PassedByReference
{
return $this->passedByReference;
}
public function isVariadic(): bool
{
return $this->isVariadic;
}
public function getDefaultValue(): ?Type
{
return $this->defaultValue;
}
}

View File

@ -0,0 +1,133 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Reflection;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptor;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Type;
final class AnnotationScopeMethodReflection implements MethodReflection
{
/** @var string */
private $name;
/** @var ClassReflection */
private $declaringClass;
/** @var Type */
private $returnType;
/** @var bool */
private $isStatic;
/** @var AnnotationScopeMethodParameterReflection[] */
private $parameters;
/** @var bool */
private $isVariadic;
/** @var FunctionVariant[]|null */
private $variants = null;
/**
* @param string $name
* @param ClassReflection $declaringClass
* @param Type $returnType
* @param AnnotationScopeMethodParameterReflection[] $parameters
* @param bool $isStatic
* @param bool $isVariadic
*/
public function __construct(string $name, ClassReflection $declaringClass, Type $returnType, array $parameters, bool $isStatic, bool $isVariadic)
{
$this->name = $name;
$this->declaringClass = $declaringClass;
$this->returnType = $returnType;
$this->parameters = $parameters;
$this->isStatic = $isStatic;
$this->isVariadic = $isVariadic;
}
public function getDeclaringClass(): ClassReflection
{
return $this->declaringClass;
}
public function getPrototype(): ClassMemberReflection
{
return $this;
}
public function isStatic(): bool
{
return $this->isStatic;
}
public function isPrivate(): bool
{
return false;
}
public function isPublic(): bool
{
return true;
}
public function getName(): string
{
return $this->name;
}
/**
* @return ParametersAcceptor[]
*/
public function getVariants(): array
{
if ($this->variants === null) {
$this->variants = [new FunctionVariant(TemplateTypeMap::createEmpty(), null, $this->parameters, $this->isVariadic, $this->returnType)];
}
return $this->variants;
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): ?string
{
return null;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): ?Type
{
return null;
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
public function getDocComment(): ?string
{
return null;
}
}

View File

@ -0,0 +1,43 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Reflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\Reflection\PassedByReference;
use PHPStan\Type\MixedType;
use PHPStan\Type\Type;
class DynamicWhereParameterReflection implements ParameterReflection
{
public function getName(): string
{
return 'dynamic-where-parameter';
}
public function isOptional(): bool
{
return true;
}
public function getType(): Type
{
return new MixedType();
}
public function passedByReference(): PassedByReference
{
return PassedByReference::createNo();
}
public function isVariadic(): bool
{
return false;
}
public function getDefaultValue(): ?Type
{
return null;
}
}

View File

@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Reflection;
use Illuminate\Database\Eloquent\Builder;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParameterReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
final class EloquentBuilderMethodReflection implements MethodReflection
{
/**
* @var string
*/
private $methodName;
/**
* @var ClassReflection
*/
private $classReflection;
/** @var MethodReflection */
private $originalMethodReflection;
/**
* @var ParameterReflection[]
*/
private $parameters;
/**
* @var Type
*/
private $returnType;
/**
* @var bool
*/
private $isVariadic;
/**
* @param string $methodName
* @param ClassReflection $classReflection
* @param MethodReflection $originalMethodReflection
* @param ParameterReflection[] $parameters
* @param Type|null $returnType
* @param bool $isVariadic
*/
public function __construct(string $methodName, ClassReflection $classReflection, MethodReflection $originalMethodReflection, array $parameters, ?Type $returnType = null, bool $isVariadic = false)
{
$this->methodName = $methodName;
$this->classReflection = $classReflection;
$this->originalMethodReflection = $originalMethodReflection;
$this->parameters = $parameters;
$this->returnType = $returnType ?? new ObjectType(Builder::class);
$this->isVariadic = $isVariadic;
}
public function getDeclaringClass(): ClassReflection
{
return $this->classReflection;
}
public function isStatic(): bool
{
return true;
}
public function isPrivate(): bool
{
return false;
}
public function isPublic(): bool
{
return true;
}
public function getName(): string
{
return $this->methodName;
}
public function getPrototype(): ClassMemberReflection
{
return $this;
}
/**
* {@inheritdoc}
*/
public function getVariants(): array
{
return [
new FunctionVariant(
TemplateTypeMap::createEmpty(),
null,
$this->parameters,
$this->isVariadic,
$this->returnType
),
];
}
public function getDocComment(): ?string
{
return null;
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): ?string
{
return null;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): ?Type
{
return null;
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
/**
* @return MethodReflection
*/
public function getOriginalMethodReflection(): MethodReflection
{
return $this->originalMethodReflection;
}
}

View File

@ -0,0 +1,120 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Reflection;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
final class ModelScopeMethodReflection implements MethodReflection
{
/**
* @var string
*/
private $methodName;
/**
* @var ClassReflection
*/
private $classReflection;
/**
* @var ClassReflection
*/
private $relation;
public function __construct(string $methodName, ClassReflection $classReflection, ClassReflection $relation)
{
$this->methodName = $methodName;
$this->classReflection = $classReflection;
$this->relation = $relation;
}
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 getName(): string
{
return $this->methodName;
}
public function getPrototype(): ClassMemberReflection
{
return $this;
}
/**
* {@inheritdoc}
*/
public function getVariants(): array
{
return [
new FunctionVariant(
TemplateTypeMap::createEmpty(),
null,
[],
false,
new ObjectType($this->relation->getName())
),
];
}
public function getDocComment(): ?string
{
return null;
}
public function isDeprecated(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getDeprecatedDescription(): ?string
{
return null;
}
public function isFinal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function isInternal(): TrinaryLogic
{
return TrinaryLogic::createNo();
}
public function getThrowType(): ?Type
{
return null;
}
public function hasSideEffects(): TrinaryLogic
{
return TrinaryLogic::createMaybe();
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Reflection;
use PHPStan\Reflection\ClassReflection;
final class ReflectionHelper
{
/**
* Does the given class or any of its ancestors have an `@property*` annotation with the given name?
*/
public static function hasPropertyTag(ClassReflection $classReflection, string $propertyName): bool
{
if (array_key_exists($propertyName, $classReflection->getPropertyTags())) {
return true;
}
foreach ($classReflection->getAncestors() as $ancestor) {
if (array_key_exists($propertyName, $ancestor->getPropertyTags())) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Reflection;
use PHPStan\Reflection\ClassMemberReflection;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Reflection\MethodReflection;
use PHPStan\TrinaryLogic;
use PHPStan\Type\Type;
class StaticMethodReflection implements MethodReflection
{
/** @var MethodReflection */
private $methodReflection;
public function __construct(MethodReflection $methodReflection)
{
$this->methodReflection = $methodReflection;
}
public function getDeclaringClass(): ClassReflection
{
return $this->methodReflection->getDeclaringClass();
}
public function isStatic(): bool
{
return true;
}
public function isPrivate(): bool
{
return $this->methodReflection->isPrivate();
}
public function isPublic(): bool
{
return $this->methodReflection->isPublic();
}
public function getDocComment(): ?string
{
return $this->methodReflection->getDocComment();
}
public function getName(): string
{
return $this->methodReflection->getName();
}
public function getPrototype(): ClassMemberReflection
{
return $this->methodReflection->getPrototype();
}
public function getVariants(): array
{
return $this->methodReflection->getVariants();
}
public function isDeprecated(): TrinaryLogic
{
return $this->methodReflection->isDeprecated();
}
public function getDeprecatedDescription(): ?string
{
return $this->methodReflection->getDeprecatedDescription();
}
public function isFinal(): TrinaryLogic
{
return $this->methodReflection->isFinal();
}
public function isInternal(): TrinaryLogic
{
return $this->methodReflection->isInternal();
}
public function getThrowType(): ?Type
{
return $this->methodReflection->getThrowType();
}
public function hasSideEffects(): TrinaryLogic
{
return $this->methodReflection->hasSideEffects();
}
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Str;
use Flarum\PHPStan\Methods\BuilderHelper;
use Flarum\PHPStan\Methods\ModelTypeHelper;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\ArrayType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
/**
* @internal
*/
final class BuilderModelFindExtension implements DynamicMethodReturnTypeExtension
{
/** @var BuilderHelper */
private $builderHelper;
/** @var ReflectionProvider */
private $reflectionProvider;
public function __construct(ReflectionProvider $reflectionProvider, BuilderHelper $builderHelper)
{
$this->builderHelper = $builderHelper;
$this->reflectionProvider = $reflectionProvider;
}
/**
* {@inheritdoc}
*/
public function getClass(): string
{
return Builder::class;
}
/**
* {@inheritdoc}
*/
public function isMethodSupported(MethodReflection $methodReflection): bool
{
$methodName = $methodReflection->getName();
if (! Str::startsWith($methodName, 'find')) {
return false;
}
$model = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TModelClass');
if ($model === null || ! $model instanceof ObjectType) {
return false;
}
if (! $this->reflectionProvider->getClass(Builder::class)->hasNativeMethod($methodName) &&
! $this->reflectionProvider->getClass(QueryBuilder::class)->hasNativeMethod($methodName)) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type {
/** @var ObjectType $model */
$model = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TModelClass');
$returnType = $methodReflection->getVariants()[0]->getReturnType();
$argType = $scope->getType($methodCall->getArgs()[0]->value);
$returnType = ModelTypeHelper::replaceStaticTypeWithModel($returnType, $model->getClassName());
if ($argType->isIterable()->yes()) {
if (in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
$collectionClassName = $this->builderHelper->determineCollectionClassName($model->getClassName());
return new GenericObjectType($collectionClassName, [$model]);
}
return TypeCombinator::remove($returnType, $model);
}
if ($argType instanceof MixedType) {
return $returnType;
}
return TypeCombinator::remove(
TypeCombinator::remove(
$returnType,
new ArrayType(new MixedType(), $model)
),
new ObjectType(Collection::class)
);
}
}

View File

@ -0,0 +1,112 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Enumerable;
use PhpParser\Node\Expr\ArrowFunction;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Stmt\Return_;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\Constant\ConstantArrayType;
use PHPStan\Type\Constant\ConstantBooleanType;
use PHPStan\Type\Constant\ConstantFloatType;
use PHPStan\Type\Constant\ConstantIntegerType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\NullType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
class CollectionFilterDynamicReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return Enumerable::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'filter';
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type {
$calledOnType = $scope->getType($methodCall->var);
if (! $calledOnType instanceof \PHPStan\Type\Generic\GenericObjectType) {
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
}
$keyType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TKey');
$valueType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TValue');
if ($keyType === null || $valueType === null) {
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
}
if (count($methodCall->getArgs()) < 1) {
$falseyTypes = $this->getFalseyTypes();
$nonFalseyTypes = TypeCombinator::remove($valueType, $falseyTypes);
if ((new ObjectType(Collection::class))->isSuperTypeOf($calledOnType)->yes()) {
return new GenericObjectType($calledOnType->getClassName(), [$nonFalseyTypes]);
}
return new GenericObjectType($calledOnType->getClassName(), [$keyType, $nonFalseyTypes]);
}
$callbackArg = $methodCall->getArgs()[0]->value;
$var = null;
$expr = null;
if ($callbackArg instanceof Closure && count($callbackArg->stmts) === 1 && count($callbackArg->params) > 0) {
$statement = $callbackArg->stmts[0];
if ($statement instanceof Return_ && $statement->expr !== null) {
$var = $callbackArg->params[0]->var;
$expr = $statement->expr;
}
} elseif ($callbackArg instanceof ArrowFunction && count($callbackArg->params) > 0) {
$var = $callbackArg->params[0]->var;
$expr = $callbackArg->expr;
}
if ($var !== null && $expr !== null) {
if (! $var instanceof Variable || ! is_string($var->name)) {
throw new \PHPStan\ShouldNotHappenException();
}
$itemVariableName = $var->name;
// @phpstan-ignore-next-line
$scope = $scope->assignVariable($itemVariableName, $valueType);
$scope = $scope->filterByTruthyValue($expr);
$valueType = $scope->getVariableType($itemVariableName);
}
if ((new ObjectType(Collection::class))->isSuperTypeOf($calledOnType)->yes()) {
return new GenericObjectType($calledOnType->getClassName(), [$valueType]);
}
return new GenericObjectType($calledOnType->getClassName(), [$keyType, $valueType]);
}
private function getFalseyTypes(): UnionType
{
return new UnionType([new NullType(), new ConstantBooleanType(false), new ConstantIntegerType(0), new ConstantFloatType(0.0), new ConstantStringType(''), new ConstantStringType('0'), new ConstantArrayType([], [])]);
}
}

View File

@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes;
use Illuminate\Support\Collection;
use Flarum\PHPStan\Support\CollectionHelper;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\Type;
class CollectionMakeDynamicStaticMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{
/**
* @var CollectionHelper
*/
private $collectionHelper;
public function __construct(CollectionHelper $collectionHelper)
{
$this->collectionHelper = $collectionHelper;
}
public function getClass(): string
{
return Collection::class;
}
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'make';
}
public function getTypeFromStaticMethodCall(
MethodReflection $methodReflection,
StaticCall $methodCall,
Scope $scope
): Type {
if (count($methodCall->getArgs()) < 1) {
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
}
$valueType = $scope->getType($methodCall->getArgs()[0]->value);
return $this->collectionHelper->determineGenericCollectionTypeFromType($valueType);
}
}

View File

@ -0,0 +1,73 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes;
use Flarum\PHPStan\Concerns\HasContainer;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\ErrorType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
class ContainerArrayAccessDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
use HasContainer;
/**
* @var string
*/
private $className;
public function __construct(string $className)
{
$this->className = $className;
}
public function getClass(): string
{
return $this->className;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'offsetGet';
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type {
$args = $methodCall->getArgs();
if (count($args) === 0) {
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
}
$argType = $scope->getType($args[0]->value);
if (! $argType instanceof ConstantStringType) {
return ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
}
$resolvedValue = $this->resolve($argType->getValue());
if ($resolvedValue === null) {
return new ErrorType();
}
if (is_object($resolvedValue)) {
$class = get_class($resolvedValue);
return new ObjectType($class);
}
return $scope->getTypeFromValue($resolvedValue);
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Str;
use Flarum\PHPStan\Methods\BuilderHelper;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Generic\TemplateMixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
final class EloquentBuilderExtension implements DynamicMethodReturnTypeExtension
{
/** @var BuilderHelper */
private $builderHelper;
/** @var ReflectionProvider */
private $reflectionProvider;
public function __construct(ReflectionProvider $reflectionProvider, BuilderHelper $builderHelper)
{
$this->builderHelper = $builderHelper;
$this->reflectionProvider = $reflectionProvider;
}
public function getClass(): string
{
return EloquentBuilder::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
$builderReflection = $this->reflectionProvider->getClass(EloquentBuilder::class);
// Don't handle dynamic wheres
if (Str::startsWith($methodReflection->getName(), 'where') &&
! $builderReflection->hasNativeMethod($methodReflection->getName())
) {
return false;
}
if (Str::startsWith($methodReflection->getName(), 'find') &&
$builderReflection->hasNativeMethod($methodReflection->getName())
) {
return false;
}
$templateTypeMap = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap();
if (! $templateTypeMap->getType('TModelClass') instanceof ObjectType) {
return false;
}
return $builderReflection->hasNativeMethod($methodReflection->getName());
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type {
$returnType = ParametersAcceptorSelector::selectFromArgs($scope, $methodCall->getArgs(), $methodReflection->getVariants())->getReturnType();
$templateTypeMap = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap();
/** @var Type|ObjectType|TemplateMixedType $modelType */
$modelType = $templateTypeMap->getType('TModelClass');
if ($modelType instanceof ObjectType && in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
$collectionClassName = $this->builderHelper->determineCollectionClassName($modelType->getClassName());
return new GenericObjectType($collectionClassName, [$modelType]);
}
return $returnType;
}
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes\Helpers;
use Illuminate\Foundation\Application;
use Flarum\PHPStan\Concerns\HasContainer;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\ClassConstFetch;
use PhpParser\Node\Expr\FuncCall;
use PhpParser\Node\Name\FullyQualified;
use PhpParser\Node\Scalar\String_;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\ErrorType;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use Throwable;
class AppExtension implements DynamicFunctionReturnTypeExtension
{
use HasContainer;
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'app' || $functionReflection->getName() === 'resolve';
}
public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope
): Type {
if (count($functionCall->getArgs()) === 0) {
return new ObjectType(Application::class);
}
/** @var Expr $expr */
$expr = $functionCall->getArgs()[0]->value;
if ($expr instanceof String_) {
try {
/** @var object|null $resolved */
$resolved = $this->resolve($expr->value);
if ($resolved === null) {
return new ErrorType();
}
return new ObjectType(get_class($resolved));
} catch (Throwable $exception) {
return new ErrorType();
}
}
if ($expr instanceof ClassConstFetch && $expr->class instanceof FullyQualified) {
return new ObjectType($expr->class->toString());
}
return new NeverType();
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes\Helpers;
use Flarum\PHPStan\Support\CollectionHelper;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\Type;
final class CollectExtension implements DynamicFunctionReturnTypeExtension
{
/**
* @var CollectionHelper
*/
private $collectionHelper;
public function __construct(CollectionHelper $collectionHelper)
{
$this->collectionHelper = $collectionHelper;
}
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'collect';
}
public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope
): Type {
if (count($functionCall->getArgs()) < 1) {
return ParametersAcceptorSelector::selectSingle($functionReflection->getVariants())->getReturnType();
}
$valueType = $scope->getType($functionCall->getArgs()[0]->value);
return $this->collectionHelper->determineGenericCollectionTypeFromType($valueType);
}
}

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes\Helpers;
use Illuminate\Support\HigherOrderTapProxy;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\NeverType;
use PHPStan\Type\ThisType;
use PHPStan\Type\Type;
class TapExtension implements DynamicFunctionReturnTypeExtension
{
/**
* {@inheritdoc}
*/
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'tap';
}
/**
* {@inheritdoc}
*/
public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope
): Type {
if (count($functionCall->getArgs()) === 1) {
$type = $scope->getType($functionCall->getArgs()[0]->value);
return new GenericObjectType(HigherOrderTapProxy::class, [
$type instanceof ThisType ? $type->getStaticObjectType() : $type,
]);
}
if (count($functionCall->getArgs()) === 2) {
return $scope->getType($functionCall->getArgs()[0]->value);
}
return new NeverType();
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes\Helpers;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
class TransExtension implements DynamicFunctionReturnTypeExtension
{
/**
* {@inheritdoc}
*/
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'trans';
}
/**
* {@inheritdoc}
*/
public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope
): Type {
// No path provided, so it returns a Translator instance
if (count($functionCall->getArgs()) === 0) {
return new ObjectType(\Illuminate\Contracts\Translation\Translator::class);
}
return new MixedType();
}
}

View File

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes\Helpers;
use function count;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
/**
* @internal
*/
final class ValidatorExtension implements DynamicFunctionReturnTypeExtension
{
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'validator';
}
public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope
): Type {
if (count($functionCall->getArgs()) === 0) {
return new ObjectType(\Illuminate\Contracts\Validation\Factory::class);
}
return new IntersectionType([
new ObjectType(\Illuminate\Validation\Validator::class),
new ObjectType(\Illuminate\Contracts\Validation\Validator::class),
]);
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes\Helpers;
use function count;
use PhpParser\Node\Expr\Closure;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\DynamicFunctionReturnTypeExtension;
use PHPStan\Type\NeverType;
use PHPStan\Type\Type;
/**
* @internal
*/
final class ValueExtension implements DynamicFunctionReturnTypeExtension
{
/**
* {@inheritdoc}
*/
public function isFunctionSupported(FunctionReflection $functionReflection): bool
{
return $functionReflection->getName() === 'value';
}
/**
* {@inheritdoc}
*/
public function getTypeFromFunctionCall(
FunctionReflection $functionReflection,
FuncCall $functionCall,
Scope $scope
): Type {
if (count($functionCall->getArgs()) === 0) {
return new NeverType();
}
$arg = $functionCall->getArgs()[0]->value;
if ($arg instanceof Closure) {
$callbackType = $scope->getType($arg);
$callbackReturnType = ParametersAcceptorSelector::selectFromArgs(
$scope,
$functionCall->getArgs(),
$callbackType->getCallableParametersAcceptors($scope)
)->getReturnType();
return $callbackReturnType;
}
return $scope->getType($arg);
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes;
use Illuminate\Support\HigherOrderTapProxy;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
/**
* @internal
*/
final class HigherOrderTapProxyExtension implements DynamicMethodReturnTypeExtension
{
/**
* {@inheritdoc}
*/
public function getClass(): string
{
return HigherOrderTapProxy::class;
}
/**
* {@inheritdoc}
*/
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return true;
}
/**
* {@inheritdoc}
*/
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type {
$type = $scope->getType($methodCall->var);
if ($type instanceof GenericObjectType) {
$types = $type->getTypes();
if (count($types) === 1 && $types[0] instanceof ObjectType) {
return $types[0];
}
}
return new MixedType();
}
}

View File

@ -0,0 +1,104 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes;
use Illuminate\Database\Eloquent\Builder as EloquentBuilder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Flarum\PHPStan\Methods\BuilderHelper;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
/**
* @internal
*/
final class ModelExtension implements DynamicStaticMethodReturnTypeExtension
{
/** @var BuilderHelper */
private $builderHelper;
/**
* @param BuilderHelper $builderHelper
*/
public function __construct(BuilderHelper $builderHelper)
{
$this->builderHelper = $builderHelper;
}
/**
* {@inheritdoc}
*/
public function getClass(): string
{
return Model::class;
}
/**
* {@inheritdoc}
*/
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
$name = $methodReflection->getName();
if ($name === '__construct') {
return false;
}
if (in_array($name, ['get', 'hydrate', 'fromQuery'], true)) {
return true;
}
if (! $methodReflection->getDeclaringClass()->hasNativeMethod($name)) {
return false;
}
$method = $methodReflection->getDeclaringClass()->getNativeMethod($methodReflection->getName());
$returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType();
return (count(array_intersect([EloquentBuilder::class, QueryBuilder::class, Collection::class], $returnType->getReferencedClasses()))) > 0;
}
/**
* {@inheritdoc}
*/
public function getTypeFromStaticMethodCall(
MethodReflection $methodReflection,
StaticCall $methodCall,
Scope $scope
): Type {
$method = $methodReflection->getDeclaringClass()
->getMethod($methodReflection->getName(), $scope);
$returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType();
if ((count(array_intersect([EloquentBuilder::class, QueryBuilder::class], $returnType->getReferencedClasses())) > 0)
&& $methodCall->class instanceof \PhpParser\Node\Name
) {
$returnType = new GenericObjectType(
$this->builderHelper->determineBuilderName($scope->resolveName($methodCall->class)),
[new ObjectType($scope->resolveName($methodCall->class))]
);
}
if (
$methodCall->class instanceof \PhpParser\Node\Name
&& in_array(Collection::class, $returnType->getReferencedClasses(), true)
&& in_array($methodReflection->getName(), ['get', 'hydrate', 'fromQuery', 'all', 'findMany'], true)
) {
$collectionClassName = $this->builderHelper->determineCollectionClassName($scope->resolveName($methodCall->class));
return new GenericObjectType($collectionClassName, [new ObjectType($scope->resolveName($methodCall->class))]);
}
return $returnType;
}
}

View File

@ -0,0 +1,57 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes;
use Illuminate\Database\Eloquent\Model;
use PhpParser\Node\Expr\StaticCall;
use PhpParser\Node\Name;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\ErrorType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
final class ModelFactoryDynamicStaticMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{
public function getClass(): string
{
return Model::class;
}
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
if ($methodReflection->getName() !== 'factory') {
return false;
}
// Class only available on Laravel 8
if (! class_exists('\Illuminate\Database\Eloquent\Factories\Factory')) {
return false;
}
return true;
}
public function getTypeFromStaticMethodCall(
MethodReflection $methodReflection,
StaticCall $methodCall,
Scope $scope
): Type {
$class = $methodCall->class;
if (! $class instanceof Name) {
return new ErrorType();
}
$modelName = basename(str_replace('\\', '/', $class->toCodeString()));
if (! class_exists('Database\\Factories\\'.$modelName.'Factory')) {
return new ErrorType();
}
return new ObjectType('Database\\Factories\\'.$modelName.'Factory');
}
}

View File

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Str;
use Flarum\PHPStan\Methods\BuilderHelper;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\ArrayType;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\ErrorType;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
/**
* @internal
*/
final class ModelFindExtension implements DynamicStaticMethodReturnTypeExtension
{
/** @var BuilderHelper */
private $builderHelper;
/** @var ReflectionProvider */
private $reflectionProvider;
public function __construct(ReflectionProvider $reflectionProvider, BuilderHelper $builderHelper)
{
$this->builderHelper = $builderHelper;
$this->reflectionProvider = $reflectionProvider;
}
/**
* {@inheritdoc}
*/
public function getClass(): string
{
return Model::class;
}
/**
* {@inheritdoc}
*/
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
$methodName = $methodReflection->getName();
if (! Str::startsWith($methodName, 'find')) {
return false;
}
if (! $this->reflectionProvider->getClass(Builder::class)->hasNativeMethod($methodName) &&
! $this->reflectionProvider->getClass(QueryBuilder::class)->hasNativeMethod($methodName)) {
return false;
}
return true;
}
/**
* {@inheritdoc}
*/
public function getTypeFromStaticMethodCall(
MethodReflection $methodReflection,
StaticCall $methodCall,
Scope $scope
): Type {
if (count($methodCall->getArgs()) < 1) {
return new ErrorType();
}
$modelName = $methodReflection->getDeclaringClass()->getName();
$returnType = $methodReflection->getVariants()[0]->getReturnType();
$argType = $scope->getType($methodCall->getArgs()[0]->value);
if ($argType->isIterable()->yes()) {
if (in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
$collectionClassName = $this->builderHelper->determineCollectionClassName($modelName);
return new GenericObjectType($collectionClassName, [new ObjectType($modelName)]);
}
return TypeCombinator::remove($returnType, new ObjectType($modelName));
}
if ($argType instanceof MixedType) {
return $returnType;
}
return TypeCombinator::remove(
TypeCombinator::remove(
$returnType,
new ArrayType(new MixedType(), new ObjectType($modelName))
),
new ObjectType(Collection::class)
);
}
}

View File

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Support\Str;
use Flarum\PHPStan\Methods\BuilderHelper;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
/**
* @internal
*/
final class RelationCollectionExtension implements DynamicMethodReturnTypeExtension
{
/** @var BuilderHelper */
private $builderHelper;
public function __construct(BuilderHelper $builderHelper)
{
$this->builderHelper = $builderHelper;
}
/**
* {@inheritdoc}
*/
public function getClass(): string
{
return Relation::class;
}
/**
* {@inheritdoc}
*/
public function isMethodSupported(MethodReflection $methodReflection): bool
{
if (Str::startsWith($methodReflection->getName(), 'find')) {
return false;
}
$modelType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TRelatedModel');
if (! $modelType instanceof ObjectType) {
return false;
}
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
if (! in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
return false;
}
return $methodReflection->getDeclaringClass()->hasNativeMethod($methodReflection->getName());
}
/**
* {@inheritdoc}
*/
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type {
/** @var ObjectType $modelType */
$modelType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TRelatedModel');
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
if (in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
$collectionClassName = $this->builderHelper->determineCollectionClassName($modelType->getClassname());
return new GenericObjectType($collectionClassName, [$modelType]);
}
return $returnType;
}
}

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Relations\Relation;
use Illuminate\Database\Query\Builder as QueryBuilder;
use Illuminate\Support\Str;
use Flarum\PHPStan\Methods\BuilderHelper;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\ArrayType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
/**
* @internal
*/
final class RelationFindExtension implements DynamicMethodReturnTypeExtension
{
/** @var BuilderHelper */
private $builderHelper;
/** @var ReflectionProvider */
private $reflectionProvider;
public function __construct(ReflectionProvider $reflectionProvider, BuilderHelper $builderHelper)
{
$this->builderHelper = $builderHelper;
$this->reflectionProvider = $reflectionProvider;
}
/**
* {@inheritdoc}
*/
public function getClass(): string
{
return Relation::class;
}
/**
* {@inheritdoc}
*/
public function isMethodSupported(MethodReflection $methodReflection): bool
{
if (! Str::startsWith($methodReflection->getName(), 'find')) {
return false;
}
$modelType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TRelatedModel');
if (! $modelType instanceof ObjectType) {
return false;
}
return $methodReflection->getDeclaringClass()->hasNativeMethod($methodReflection->getName()) ||
$this->reflectionProvider->getClass(Builder::class)->hasNativeMethod($methodReflection->getName()) ||
$this->reflectionProvider->getClass(QueryBuilder::class)->hasNativeMethod($methodReflection->getName());
}
/**
* {@inheritdoc}
*/
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type {
/** @var ObjectType $modelType */
$modelType = $methodReflection->getDeclaringClass()->getActiveTemplateTypeMap()->getType('TRelatedModel');
$argType = $scope->getType($methodCall->getArgs()[0]->value);
$returnType = $methodReflection->getVariants()[0]->getReturnType();
if (in_array(Collection::class, $returnType->getReferencedClasses(), true)) {
if ($argType->isIterable()->yes()) {
$collectionClassName = $this->builderHelper->determineCollectionClassName($modelType->getClassname());
return new GenericObjectType($collectionClassName, [$modelType]);
}
$returnType = TypeCombinator::remove($returnType, new ObjectType(Collection::class));
return TypeCombinator::remove($returnType, new ArrayType(new MixedType(), $modelType));
}
return $returnType;
}
}

View File

@ -0,0 +1,61 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes;
use Illuminate\Http\Request;
use Illuminate\Http\UploadedFile;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\ArrayType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\IntegerType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
/**
* @internal
*/
final class RequestExtension implements DynamicMethodReturnTypeExtension
{
/**
* {@inheritdoc}
*/
public function getClass(): string
{
return Request::class;
}
/**
* {@inheritdoc}
*/
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'file';
}
/**
* {@inheritdoc}
*/
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type {
$uploadedFileType = new ObjectType(UploadedFile::class);
$uploadedFileArrayType = new ArrayType(new IntegerType(), $uploadedFileType);
if (count($methodCall->getArgs()) === 0) {
return new ArrayType(new IntegerType(), $uploadedFileType);
}
if (count($methodCall->getArgs()) === 1) {
return TypeCombinator::union($uploadedFileArrayType, TypeCombinator::addNull($uploadedFileType));
}
return TypeCombinator::union(TypeCombinator::union($uploadedFileArrayType, $uploadedFileType), $scope->getType($methodCall->getArgs()[1]->value));
}
}

View File

@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes;
use Illuminate\Filesystem\FilesystemAdapter;
use Illuminate\Support\Facades\Storage;
use Flarum\PHPStan\Concerns\HasContainer;
use PhpParser\Node\Expr\StaticCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\DynamicStaticMethodReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
class StorageDynamicStaticMethodReturnTypeExtension implements DynamicStaticMethodReturnTypeExtension
{
use HasContainer;
public function getClass(): string
{
return Storage::class;
}
public function isStaticMethodSupported(MethodReflection $methodReflection): bool
{
return $methodReflection->getName() === 'disk';
}
public function getTypeFromStaticMethodCall(
MethodReflection $methodReflection,
StaticCall $methodCall,
Scope $scope
): Type {
return new ObjectType(FilesystemAdapter::class);
}
}

View File

@ -0,0 +1,53 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\ReturnTypes;
use Illuminate\Foundation\Testing\TestCase;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
/**
* @internal
*/
final class TestCaseExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return TestCase::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return in_array($methodReflection->getName(), [
'mock',
'partialMock',
'spy',
], true);
}
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type {
$defaultReturnType = new ObjectType('Mockery\\MockInterface');
$classType = $scope->getType($methodCall->getArgs()[0]->value);
if (! $classType instanceof ConstantStringType) {
return $defaultReturnType;
}
$objectType = new ObjectType($classType->getValue());
return TypeCombinator::intersect($defaultReturnType, $objectType);
}
}

View File

@ -0,0 +1,99 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Support;
use Illuminate\Database\Eloquent\Collection as EloquentCollection;
use Illuminate\Support\Collection;
use Illuminate\Support\Enumerable;
use Iterator;
use IteratorAggregate;
use PHPStan\Type\GeneralizePrecision;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\IntegerType;
use PHPStan\Type\MixedType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\TypeUtils;
use PHPStan\Type\TypeWithClassName;
use Traversable;
final class CollectionHelper
{
public function determineGenericCollectionTypeFromType(Type $type): GenericObjectType
{
$keyType = TypeCombinator::union(new IntegerType(), new StringType());
if ($type instanceof TypeWithClassName) {
if ((new ObjectType(Enumerable::class))->isSuperTypeOf($type)->yes()) {
return $this->getTypeFromEloquentCollection($type);
}
if (
(new ObjectType(Traversable::class))->isSuperTypeOf($type)->yes() ||
(new ObjectType(IteratorAggregate::class))->isSuperTypeOf($type)->yes() ||
(new ObjectType(Iterator::class))->isSuperTypeOf($type)->yes()
) {
return $this->getTypeFromIterator($type);
}
}
if (! $type->isArray()->yes()) {
return new GenericObjectType(Collection::class, [$type->toArray()->getIterableKeyType(), $type->toArray()->getIterableValueType()]);
}
if ($type->isIterableAtLeastOnce()->no()) {
return new GenericObjectType(Collection::class, [$keyType, new MixedType()]);
}
return new GenericObjectType(Collection::class, [
TypeUtils::generalizeType($type->getIterableKeyType(), GeneralizePrecision::lessSpecific()),
TypeUtils::generalizeType($type->getIterableValueType(), GeneralizePrecision::lessSpecific()),
]);
}
private function getTypeFromEloquentCollection(TypeWithClassName $valueType): GenericObjectType
{
$keyType = TypeCombinator::union(new IntegerType(), new StringType());
$classReflection = $valueType->getClassReflection();
if ($classReflection === null) {
return new GenericObjectType(Collection::class, [$keyType, new MixedType()]);
}
$innerValueType = $classReflection->getActiveTemplateTypeMap()->getType('TValue');
if ($classReflection->getName() === EloquentCollection::class || $classReflection->isSubclassOf(EloquentCollection::class)) {
$keyType = new IntegerType();
}
if ($innerValueType !== null) {
return new GenericObjectType(Collection::class, [$keyType, $innerValueType]);
}
return new GenericObjectType(Collection::class, [$keyType, new MixedType()]);
}
private function getTypeFromIterator(TypeWithClassName $valueType): GenericObjectType
{
$keyType = TypeCombinator::union(new IntegerType(), new StringType());
$classReflection = $valueType->getClassReflection();
if ($classReflection === null) {
return new GenericObjectType(Collection::class, [$keyType, new MixedType()]);
}
$templateTypes = array_values($classReflection->getActiveTemplateTypeMap()->getTypes());
if (count($templateTypes) === 1) {
return new GenericObjectType(Collection::class, [$keyType, $templateTypes[0]]);
}
return new GenericObjectType(Collection::class, $templateTypes);
}
}

View File

@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Support;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Collection as SupportCollection;
use Illuminate\Support\HigherOrderCollectionProxy;
use PHPStan\Reflection\ClassReflection;
use PHPStan\Type;
class HigherOrderCollectionProxyHelper
{
/**
* @phpstan-param 'method'|'property' $propertyOrMethod
*/
public static function hasPropertyOrMethod(ClassReflection $classReflection, string $name, string $propertyOrMethod): bool
{
if ($classReflection->getName() !== HigherOrderCollectionProxy::class) {
return false;
}
$activeTemplateTypeMap = $classReflection->getActiveTemplateTypeMap();
if ($activeTemplateTypeMap->count() !== 2) {
return false;
}
$methodType = $activeTemplateTypeMap->getType('T');
$valueType = $activeTemplateTypeMap->getType('TValue');
if (($methodType === null) || ($valueType === null)) {
return false;
}
if (! $methodType instanceof Type\Constant\ConstantStringType) {
return false;
}
if (! $valueType->canCallMethods()->yes()) {
return false;
}
if ($propertyOrMethod === 'method') {
return $valueType->hasMethod($name)->yes();
}
return $valueType->hasProperty($name)->yes();
}
public static function determineReturnType(string $name, Type\Type $valueType, Type\Type $methodOrPropertyReturnType): Type\Type
{
if ((new Type\ObjectType(Model::class))->isSuperTypeOf($valueType)->yes()) {
$collectionType = Collection::class;
$types = [$valueType];
} else {
$collectionType = SupportCollection::class;
$types = [new Type\IntegerType(), $valueType];
}
switch ($name) {
case 'average':
case 'avg':
$returnType = new Type\FloatType();
break;
case 'contains':
case 'every':
case 'some':
$returnType = new Type\BooleanType();
break;
case 'each':
case 'filter':
case 'reject':
case 'skipUntil':
case 'skipWhile':
case 'sortBy':
case 'sortByDesc':
case 'takeUntil':
case 'takeWhile':
case 'unique':
$returnType = new Type\Generic\GenericObjectType($collectionType, $types);
break;
case 'keyBy':
if ($collectionType === SupportCollection::class) {
$returnType = new Type\Generic\GenericObjectType($collectionType, [$methodOrPropertyReturnType, $valueType]);
} else {
$returnType = new Type\Generic\GenericObjectType($collectionType, $types);
}
break;
case 'first':
$returnType = Type\TypeCombinator::addNull($valueType);
break;
case 'flatMap':
$returnType = new Type\Generic\GenericObjectType(SupportCollection::class, [new Type\IntegerType(), new Type\MixedType()]);
break;
case 'groupBy':
case 'partition':
$innerTypes = [
new Type\Generic\GenericObjectType($collectionType, $types),
];
if ($collectionType === SupportCollection::class) {
array_unshift($innerTypes, new Type\IntegerType());
}
$returnType = new Type\Generic\GenericObjectType($collectionType, $innerTypes);
break;
case 'map':
$returnType = new Type\Generic\GenericObjectType(SupportCollection::class, [
new Type\IntegerType(),
$methodOrPropertyReturnType,
]);
break;
case 'max':
case 'min':
$returnType = $methodOrPropertyReturnType;
break;
case 'sum':
if ($methodOrPropertyReturnType->accepts(new Type\IntegerType(), true)->yes()) {
$returnType = new Type\IntegerType();
} else {
$returnType = new Type\ErrorType();
}
break;
default:
$returnType = new Type\ErrorType();
break;
}
return $returnType;
}
}

View File

@ -0,0 +1,60 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Types;
use PhpParser\Node\Expr\FuncCall;
use PHPStan\Analyser\Scope;
use PHPStan\Analyser\SpecifiedTypes;
use PHPStan\Analyser\TypeSpecifier;
use PHPStan\Analyser\TypeSpecifierAwareExtension;
use PHPStan\Analyser\TypeSpecifierContext;
use PHPStan\Reflection\FunctionReflection;
use PHPStan\Type\FunctionTypeSpecifyingExtension;
final class AbortIfFunctionTypeSpecifyingExtension implements FunctionTypeSpecifyingExtension, TypeSpecifierAwareExtension
{
/** @var TypeSpecifier */
private $typeSpecifier;
/** @var bool */
protected $negate;
/** @var string */
protected $methodName;
public function __construct(bool $negate, string $methodName)
{
$this->negate = $negate;
$this->methodName = $methodName.'_'.($negate === false ? 'if' : 'unless');
}
public function isFunctionSupported(
FunctionReflection $functionReflection,
FuncCall $node,
TypeSpecifierContext $context
): bool {
return $functionReflection->getName() === $this->methodName && $context->null();
}
public function specifyTypes(
FunctionReflection $functionReflection,
FuncCall $node,
Scope $scope,
TypeSpecifierContext $context
): SpecifiedTypes {
if (count($node->args) < 2) {
return new SpecifiedTypes();
}
$context = $this->negate === false ? TypeSpecifierContext::createFalsey() : TypeSpecifierContext::createTruthy();
return $this->typeSpecifier->specifyTypesInCondition($scope, $node->getArgs()[0]->value, $context);
}
public function setTypeSpecifier(TypeSpecifier $typeSpecifier): void
{
$this->typeSpecifier = $typeSpecifier;
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Types;
use function count;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use PHPStan\Analyser\NameScope;
use PHPStan\PhpDoc\TypeNodeResolverExtension;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
class GenericEloquentBuilderTypeNodeResolverExtension implements TypeNodeResolverExtension
{
public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type
{
if (! $typeNode instanceof UnionTypeNode || count($typeNode->types) !== 2) {
return null;
}
$modelTypeNode = null;
$builderTypeNode = null;
foreach ($typeNode->types as $innerTypeNode) {
if ($innerTypeNode instanceof IdentifierTypeNode
&& is_subclass_of($nameScope->resolveStringName($innerTypeNode->name), Model::class)
) {
$modelTypeNode = $innerTypeNode;
continue;
}
if (
$innerTypeNode instanceof IdentifierTypeNode
&& ($nameScope->resolveStringName($innerTypeNode->name) === Builder::class || is_subclass_of($nameScope->resolveStringName($innerTypeNode->name), Builder::class))
) {
$builderTypeNode = $innerTypeNode;
}
}
if ($modelTypeNode === null || $builderTypeNode === null) {
return null;
}
$builderTypeName = $nameScope->resolveStringName($builderTypeNode->name);
$modelTypeName = $nameScope->resolveStringName($modelTypeNode->name);
return new GenericObjectType($builderTypeName, [
new ObjectType($modelTypeName),
]);
}
}

View File

@ -0,0 +1,84 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Types;
use function count;
use Illuminate\Database\Eloquent\Collection;
use PHPStan\Analyser\NameScope;
use PHPStan\PhpDoc\TypeNodeResolver;
use PHPStan\PhpDoc\TypeNodeResolverExtension;
use PHPStan\PhpDocParser\Ast\Type\ArrayTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\PhpDocParser\Ast\Type\UnionTypeNode;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\Type;
/**
* @see https://github.com/nunomaduro/larastan/issues/476
* @see https://gist.github.com/ondrejmirtes/56af016d0595788d5400b8dfb6520adc
*
* This extension interprets docblocks like:
*
* \Illuminate\Database\Eloquent\Collection|\App\Account[] $accounts
*
* and transforms them into:
*
* \Illuminate\Database\Eloquent\Collection<\App\Account> $accounts
*
* Now IDE's can benefit from auto-completion, and we can benefit from the correct type passed to the generic collection
*/
class GenericEloquentCollectionTypeNodeResolverExtension implements TypeNodeResolverExtension
{
/**
* @var TypeNodeResolver
*/
private $typeNodeResolver;
public function __construct(TypeNodeResolver $typeNodeResolver)
{
$this->typeNodeResolver = $typeNodeResolver;
}
public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type
{
if (! $typeNode instanceof UnionTypeNode || count($typeNode->types) !== 2) {
return null;
}
$arrayTypeNode = null;
$identifierTypeNode = null;
foreach ($typeNode->types as $innerTypeNode) {
if ($innerTypeNode instanceof ArrayTypeNode) {
$arrayTypeNode = $innerTypeNode;
continue;
}
if ($innerTypeNode instanceof IdentifierTypeNode) {
$identifierTypeNode = $innerTypeNode;
}
}
if ($arrayTypeNode === null || $identifierTypeNode === null) {
return null;
}
$identifierTypeName = $nameScope->resolveStringName($identifierTypeNode->name);
if ($identifierTypeName !== Collection::class) {
return null;
}
$innerArrayTypeNode = $arrayTypeNode->type;
if (! $innerArrayTypeNode instanceof IdentifierTypeNode) {
return null;
}
$resolvedInnerArrayType = $this->typeNodeResolver->resolve($innerArrayTypeNode, $nameScope);
return new GenericObjectType($identifierTypeName, [
$resolvedInnerArrayType,
]);
}
}

View File

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Types\ModelProperty;
use PHPStan\TrinaryLogic;
use PHPStan\Type\ClassStringType;
use PHPStan\Type\CompoundType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Generic\TemplateType;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\Generic\TemplateTypeVariance;
use PHPStan\Type\IntersectionType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\ObjectWithoutClassType;
use PHPStan\Type\Type;
use PHPStan\Type\TypeCombinator;
use PHPStan\Type\UnionType;
class GenericModelPropertyType extends ModelPropertyType
{
/** @var Type */
private $type;
public function __construct(Type $type)
{
parent::__construct();
$this->type = $type;
}
public function getReferencedClasses(): array
{
return $this->getGenericType()->getReferencedClasses();
}
public function getGenericType(): Type
{
return $this->type;
}
public function isSuperTypeOf(Type $type): TrinaryLogic
{
if ($type instanceof ConstantStringType) {
return $this->getGenericType()->hasProperty($type->getValue());
}
if ($type instanceof self) {
return TrinaryLogic::createYes();
}
if ($type instanceof parent) {
return TrinaryLogic::createMaybe();
}
if ($type instanceof CompoundType) {
return $type->isSubTypeOf($this);
}
return TrinaryLogic::createNo();
}
public function traverse(callable $cb): Type
{
$newType = $cb($this->getGenericType());
if ($newType === $this->getGenericType()) {
return $this;
}
return new self($newType);
}
public function inferTemplateTypes(Type $receivedType): TemplateTypeMap
{
if ($receivedType instanceof UnionType || $receivedType instanceof IntersectionType) {
return $receivedType->inferTemplateTypesOn($this);
}
if ($receivedType instanceof ConstantStringType) {
$typeToInfer = new ObjectType($receivedType->getValue());
} elseif ($receivedType instanceof self) {
$typeToInfer = $receivedType->type;
} elseif ($receivedType instanceof ClassStringType) {
$typeToInfer = $this->getGenericType();
if ($typeToInfer instanceof TemplateType) {
$typeToInfer = $typeToInfer->getBound();
}
$typeToInfer = TypeCombinator::intersect($typeToInfer, new ObjectWithoutClassType());
} else {
return TemplateTypeMap::createEmpty();
}
if (! $this->getGenericType()->isSuperTypeOf($typeToInfer)->no()) {
return $this->getGenericType()->inferTemplateTypes($typeToInfer);
}
return TemplateTypeMap::createEmpty();
}
public function getReferencedTemplateTypes(TemplateTypeVariance $positionVariance): array
{
$variance = $positionVariance->compose(TemplateTypeVariance::createCovariant());
return $this->getGenericType()->getReferencedTemplateTypes($variance);
}
/**
* @param mixed[] $properties
* @return Type
*/
public static function __set_state(array $properties): Type
{
return new self($properties['type']);
}
}

View File

@ -0,0 +1,20 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Types\ModelProperty;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
class ModelPropertyType extends StringType
{
/**
* @param mixed[] $properties
* @return Type
*/
public static function __set_state(array $properties): Type
{
return new self();
}
}

View File

@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Types\ModelProperty;
use Illuminate\Database\Eloquent\Model;
use PHPStan\Analyser\NameScope;
use PHPStan\PhpDoc\TypeNodeResolver;
use PHPStan\PhpDoc\TypeNodeResolverExtension;
use PHPStan\PhpDocParser\Ast\Type\GenericTypeNode;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\ErrorType;
use PHPStan\Type\NeverType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
/**
* Ensures a 'model-property' type in PHPDoc is recognised to be of type ModelPropertyType.
*/
class ModelPropertyTypeNodeResolverExtension implements TypeNodeResolverExtension
{
/** @var bool */
protected $active;
/** @var TypeNodeResolver */
protected $baseResolver;
public function __construct(TypeNodeResolver $baseResolver, bool $active)
{
$this->baseResolver = $baseResolver;
$this->active = $active;
}
public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type
{
if ($typeNode instanceof IdentifierTypeNode && $typeNode->name === 'model-property') {
return $this->active ? new ModelPropertyType() : new StringType();
}
if ($typeNode instanceof GenericTypeNode && $typeNode->type->name === 'model-property') {
if (! $this->active) {
return new StringType();
}
if (count($typeNode->genericTypes) !== 1) {
return new ErrorType();
}
$genericType = $this->baseResolver->resolve($typeNode->genericTypes[0], $nameScope);
if ((new ObjectType(Model::class))->isSuperTypeOf($genericType)->no()) {
return new ErrorType();
}
if ($genericType instanceof NeverType) {
return new ErrorType();
}
return new GenericModelPropertyType($genericType);
}
return null;
}
}

View File

@ -0,0 +1,108 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Types;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\Relation;
use Flarum\PHPStan\Concerns\HasContainer;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
class ModelRelationsDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
use HasContainer;
/** @var RelationParserHelper */
private $relationParserHelper;
public function __construct(RelationParserHelper $relationParserHelper)
{
$this->relationParserHelper = $relationParserHelper;
}
public function getClass(): string
{
return Model::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
$variants = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants());
$returnType = $variants->getReturnType();
if (! $returnType instanceof ObjectType) {
return false;
}
if (! (new ObjectType(Relation::class))->isSuperTypeOf($returnType)->yes()) {
return false;
}
if (! $methodReflection->getDeclaringClass()->hasNativeMethod($methodReflection->getName())) {
return false;
}
if (count($variants->getParameters()) !== 0) {
return false;
}
if (in_array($methodReflection->getName(), [
'hasOne', 'hasOneThrough', 'morphOne',
'belongsTo', 'morphTo',
'hasMany', 'hasManyThrough', 'morphMany',
'belongsToMany', 'morphToMany', 'morphedByMany',
], true)) {
return false;
}
$relatedModel = $this
->relationParserHelper
->findRelatedModelInRelationMethod($methodReflection);
return $relatedModel !== null;
}
/**
* @param MethodReflection $methodReflection
* @param MethodCall $methodCall
* @param Scope $scope
* @return Type
*
* @throws ShouldNotHappenException
*/
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type {
/** @var ObjectType $returnType */
$returnType = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants())->getReturnType();
/** @var string $relatedModelClassName */
$relatedModelClassName = $this
->relationParserHelper
->findRelatedModelInRelationMethod($methodReflection);
$classReflection = $methodReflection->getDeclaringClass();
if ($returnType->isInstanceOf(BelongsTo::class)->yes()) {
return new GenericObjectType($returnType->getClassName(), [
new ObjectType($relatedModelClassName),
new ObjectType($classReflection->getName()),
]);
}
return new GenericObjectType($returnType->getClassName(), [new ObjectType($relatedModelClassName)]);
}
}

View File

@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Types;
use Flarum\PHPStan\Contracts\Types\PassableContract;
use PHPStan\Type\Type;
/**
* @internal
*/
final class Passable implements PassableContract
{
/**
* @var \PHPStan\Type\Type
*/
private $type;
/**
* Passable constructor.
*
* @param \PHPStan\Type\Type $type
*/
public function __construct(Type $type)
{
$this->type = $type;
}
/**
* @return \PHPStan\Type\Type
*/
public function getType(): Type
{
return $this->type;
}
/**
* @param \PHPStan\Type\Type $type
*/
public function setType(Type $type): void
{
$this->type = $type;
}
}

View File

@ -0,0 +1,65 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Types;
use Illuminate\Database\Eloquent\Model;
use PhpParser\Node\Expr\MethodCall;
use PHPStan\Analyser\Scope;
use PHPStan\Reflection\FunctionVariant;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ParametersAcceptorSelector;
use PHPStan\ShouldNotHappenException;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\DynamicMethodReturnTypeExtension;
use PHPStan\Type\Generic\GenericObjectType;
use PHPStan\Type\ObjectType;
use PHPStan\Type\Type;
class RelationDynamicMethodReturnTypeExtension implements DynamicMethodReturnTypeExtension
{
public function getClass(): string
{
return Model::class;
}
public function isMethodSupported(MethodReflection $methodReflection): bool
{
return in_array($methodReflection->getName(), [
'hasOne', 'hasOneThrough', 'morphOne',
'belongsTo', 'morphTo',
'hasMany', 'hasManyThrough', 'morphMany',
'belongsToMany', 'morphToMany', 'morphedByMany',
], true);
}
/**
* @throws ShouldNotHappenException
*/
public function getTypeFromMethodCall(
MethodReflection $methodReflection,
MethodCall $methodCall,
Scope $scope
): Type {
/** @var FunctionVariant $functionVariant */
$functionVariant = ParametersAcceptorSelector::selectSingle($methodReflection->getVariants());
$returnType = $functionVariant->getReturnType();
if (count($methodCall->getArgs()) === 0) {
return $returnType;
}
$argType = $scope->getType($methodCall->getArgs()[0]->value);
if (! $argType instanceof ConstantStringType) {
return $returnType;
}
if (! $returnType instanceof ObjectType) {
return $returnType;
}
return new GenericObjectType($returnType->getClassName(), [new ObjectType($argType->getValue())]);
}
}

View File

@ -0,0 +1,132 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Types;
use PhpParser\Node;
use PhpParser\Node\Expr\MethodCall;
use PhpParser\NodeFinder;
use PHPStan\Analyser\ScopeContext;
use PHPStan\Analyser\ScopeFactory;
use PHPStan\Parser\Parser;
use PHPStan\Reflection\MethodReflection;
use PHPStan\Reflection\ReflectionProvider;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\Generic\GenericClassStringType;
use PHPStan\Type\Generic\TemplateTypeMap;
use PHPStan\Type\ObjectType;
class RelationParserHelper
{
/** @var Parser */
private $parser;
/** @var ScopeFactory */
private $scopeFactory;
/** @var ReflectionProvider */
private $reflectionProvider;
public function __construct(Parser $parser, ScopeFactory $scopeFactory, ReflectionProvider $reflectionProvider)
{
$this->parser = $parser;
$this->scopeFactory = $scopeFactory;
$this->reflectionProvider = $reflectionProvider;
}
public function findRelatedModelInRelationMethod(
MethodReflection $methodReflection
): ?string {
$fileName = $methodReflection
->getDeclaringClass()
->getNativeReflection()
->getMethod($methodReflection->getName())
->getFileName();
if ($fileName === false) {
return null;
}
$fileStmts = $this->parser->parseFile($fileName);
/** @var Node\Stmt\ClassMethod|null $relationMethod */
$relationMethod = $this->findMethod($methodReflection->getName(), $fileStmts);
if ($relationMethod === null) {
return null;
}
/** @var Node\Stmt\Return_|null $returnStmt */
$returnStmt = $this->findReturn($relationMethod);
if ($returnStmt === null || ! $returnStmt->expr instanceof MethodCall) {
return null;
}
$methodCall = $returnStmt->expr;
while ($methodCall->var instanceof MethodCall) {
$methodCall = $methodCall->var;
}
if (count($methodCall->getArgs()) < 1) {
return null;
}
$scope = $this->scopeFactory->create(
ScopeContext::create($fileName),
false,
[],
$methodReflection
);
$methodScope = $scope
->enterClass($methodReflection->getDeclaringClass())
->enterClassMethod($relationMethod, TemplateTypeMap::createEmpty(), [], null, null, null, false, false, false);
$argType = $methodScope->getType($methodCall->getArgs()[0]->value);
$returnClass = null;
if ($argType instanceof ConstantStringType) {
$returnClass = $argType->getValue();
}
if ($argType instanceof GenericClassStringType) {
$modelType = $argType->getGenericType();
if (! $modelType instanceof ObjectType) {
return null;
}
$returnClass = $modelType->getClassName();
}
if ($returnClass === null) {
return null;
}
return $this->reflectionProvider->hasClass($returnClass) ? $returnClass : null;
}
/**
* @param string $method
* @param mixed $statements
* @return Node|null
*/
private function findMethod(string $method, $statements): ?Node
{
return (new NodeFinder)->findFirst($statements, static function (Node $node) use ($method) {
return $node instanceof Node\Stmt\ClassMethod
&& $node->name->toString() === $method;
});
}
private function findReturn(Node\Stmt\ClassMethod $relationMethod): ?Node
{
/** @var Node[] $statements */
$statements = $relationMethod->stmts;
return (new NodeFinder)->findFirstInstanceOf($statements, Node\Stmt\Return_::class);
}
}

View File

@ -0,0 +1,80 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Types;
use PHPStan\TrinaryLogic;
use PHPStan\Type\CompoundType;
use PHPStan\Type\Constant\ConstantStringType;
use PHPStan\Type\StringType;
use PHPStan\Type\Type;
/**
* The custom 'view-string' type class. It's a subset of the string type. Every string that passes the
* view()->exists($string) test is a valid view-string type.
*/
class ViewStringType extends StringType
{
public function describe(\PHPStan\Type\VerbosityLevel $level): string
{
return 'view-string';
}
public function accepts(Type $type, bool $strictTypes): TrinaryLogic
{
if ($type instanceof CompoundType) {
return $type->isAcceptedBy($this, $strictTypes);
}
if ($type instanceof ConstantStringType) {
/** @var \Illuminate\View\Factory $view */
$view = view();
return TrinaryLogic::createFromBoolean($view->exists($type->getValue()));
}
if ($type instanceof self) {
return TrinaryLogic::createYes();
}
if ($type instanceof StringType) {
return TrinaryLogic::createMaybe();
}
return TrinaryLogic::createNo();
}
public function isSuperTypeOf(Type $type): TrinaryLogic
{
if ($type instanceof ConstantStringType) {
/** @var \Illuminate\View\Factory $view */
$view = view();
return TrinaryLogic::createFromBoolean($view->exists($type->getValue()));
}
if ($type instanceof self) {
return TrinaryLogic::createYes();
}
if ($type instanceof parent) {
return TrinaryLogic::createMaybe();
}
if ($type instanceof CompoundType) {
return $type->isSubTypeOf($this);
}
return TrinaryLogic::createNo();
}
/**
* @param mixed[] $properties
* @return Type
*/
public static function __set_state(array $properties): Type
{
return new self();
}
}

View File

@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
namespace Flarum\PHPStan\Types;
use PHPStan\Analyser\NameScope;
use PHPStan\PhpDoc\TypeNodeResolverExtension;
use PHPStan\PhpDocParser\Ast\Type\IdentifierTypeNode;
use PHPStan\PhpDocParser\Ast\Type\TypeNode;
use PHPStan\Type\Type;
/**
* Ensures a 'view-string' type in PHPDoc is recognised to be of type ViewStringType.
*/
class ViewStringTypeNodeResolverExtension implements TypeNodeResolverExtension
{
public function resolve(TypeNode $typeNode, NameScope $nameScope): ?Type
{
if ($typeNode instanceof IdentifierTypeNode && $typeNode->__toString() === 'view-string') {
return new ViewStringType();
}
return null;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +0,0 @@
<?php
namespace Illuminate\Redis\Connections;
/**
* @mixin \Redis
*/
abstract class Connection
{}