1
0
mirror of https://github.com/flarum/core.git synced 2025-07-27 19:50:20 +02:00

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

@@ -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())
);
}
}