diff --git a/php-packages/phpstan/.gitignore b/php-packages/phpstan/.gitignore new file mode 100644 index 000000000..30e01fbd2 --- /dev/null +++ b/php-packages/phpstan/.gitignore @@ -0,0 +1,11 @@ +/vendor +composer.lock +composer.phar +.DS_Store +Thumbs.db +tests/.phpunit.result.cache +/tests/integration/tmp +.vagrant +.idea/* +.vscode +js/coverage-ts diff --git a/php-packages/phpstan/README.md b/php-packages/phpstan/README.md new file mode 100644 index 000000000..d342592df --- /dev/null +++ b/php-packages/phpstan/README.md @@ -0,0 +1 @@ +php-stubs diff --git a/php-packages/phpstan/composer.json b/php-packages/phpstan/composer.json new file mode 100644 index 000000000..ead4261b8 --- /dev/null +++ b/php-packages/phpstan/composer.json @@ -0,0 +1,25 @@ +{ + "name": "flarum/phpstan", + "description": "Flarum PHPStan extension", + "minimum-stability": "stable", + "license": "MIT", + "require": { + "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.x-dev" + } + } +} diff --git a/php-packages/phpstan/extension.neon b/php-packages/phpstan/extension.neon new file mode 100644 index 000000000..7ff796157 --- /dev/null +++ b/php-packages/phpstan/extension.neon @@ -0,0 +1,291 @@ +includes: + - vendor/phpstan/phpstan-php-parser/extension.neon +parameters: + stubFiles: + - stubs/Illuminate/Enumerable.stub + - stubs/Illuminate/Database/EloquentBuilder.stub + - stubs/Illuminate/Collection.stub + - stubs/Illuminate/Database/EloquentCollection.stub + - stubs/Illuminate/Database/Factory.stub + - stubs/Illuminate/Database/Model.stub + - stubs/Illuminate/Database/Gate.stub + - stubs/Illuminate/Database/Relation.stub + - stubs/Illuminate/Database/BelongsTo.stub + - stubs/Illuminate/Database/BelongsToMany.stub + - stubs/Illuminate/Database/HasOneOrMany.stub + - stubs/Illuminate/Database/HasMany.stub + - stubs/Illuminate/Database/HasOne.stub + - stubs/Illuminate/Database/HasOneThrough.stub + - stubs/Illuminate/Database/HasManyThrough.stub + - stubs/Illuminate/Database/MorphTo.stub + - stubs/Illuminate/Database/MorphToMany.stub + - stubs/Illuminate/Database/MorphMany.stub + - stubs/Illuminate/Database/MorphOne.stub + - stubs/Illuminate/Database/MorphOneOrMany.stub + - stubs/Illuminate/HigherOrderProxies.stub + - stubs/Illuminate/Database/QueryBuilder.stub + - stubs/Illuminate/EnumeratesValues.stub + - stubs/Contracts/Support.stub + universalObjectCratesClasses: + - Illuminate\Http\Request + mixinExcludeClasses: + - Eloquent + earlyTerminatingFunctionCalls: + - abort + - dd + 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 diff --git a/php-packages/phpstan/src/Concerns/HasContainer.php b/php-packages/phpstan/src/Concerns/HasContainer.php new file mode 100644 index 000000000..00ded5a1d --- /dev/null +++ b/php-packages/phpstan/src/Concerns/HasContainer.php @@ -0,0 +1,65 @@ +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; + } +} diff --git a/php-packages/phpstan/src/Concerns/LoadsAuthModel.php b/php-packages/phpstan/src/Concerns/LoadsAuthModel.php new file mode 100644 index 000000000..3e1401e3c --- /dev/null +++ b/php-packages/phpstan/src/Concerns/LoadsAuthModel.php @@ -0,0 +1,24 @@ +get('auth.defaults.guard'))) || + ! ($provider = $config->get('auth.guards.'.$guard.'.provider')) || + ! ($authModel = $config->get('auth.providers.'.$provider.'.model')) + ) { + return null; + } + + return $authModel; + } +} diff --git a/php-packages/phpstan/src/Contracts/Methods/PassableContract.php b/php-packages/phpstan/src/Contracts/Methods/PassableContract.php new file mode 100644 index 000000000..3ff5a9ab0 --- /dev/null +++ b/php-packages/phpstan/src/Contracts/Methods/PassableContract.php @@ -0,0 +1,97 @@ + + * + * 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; +} diff --git a/php-packages/phpstan/src/Contracts/Methods/Pipes/PipeContract.php b/php-packages/phpstan/src/Contracts/Methods/Pipes/PipeContract.php new file mode 100644 index 000000000..3685721c8 --- /dev/null +++ b/php-packages/phpstan/src/Contracts/Methods/Pipes/PipeContract.php @@ -0,0 +1,21 @@ + + * + * 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; +} diff --git a/php-packages/phpstan/src/Contracts/Types/Pipes/PipeContract.php b/php-packages/phpstan/src/Contracts/Types/Pipes/PipeContract.php new file mode 100644 index 000000000..f0b13aa2a --- /dev/null +++ b/php-packages/phpstan/src/Contracts/Types/Pipes/PipeContract.php @@ -0,0 +1,21 @@ +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()); + } +} diff --git a/php-packages/phpstan/src/Methods/EloquentBuilderForwardsCallsExtension.php b/php-packages/phpstan/src/Methods/EloquentBuilderForwardsCallsExtension.php new file mode 100644 index 000000000..b7067e78d --- /dev/null +++ b/php-packages/phpstan/src/Methods/EloquentBuilderForwardsCallsExtension.php @@ -0,0 +1,152 @@ + */ + 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` + return new EloquentBuilderMethodReflection( + $methodName, $classReflection, + $ref, + $parametersAcceptor->getParameters(), + new GenericObjectType($classReflection->getName(), [$modelType]), + $parametersAcceptor->isVariadic() + ); + } +} diff --git a/php-packages/phpstan/src/Methods/Extension.php b/php-packages/phpstan/src/Methods/Extension.php new file mode 100644 index 000000000..271a385a2 --- /dev/null +++ b/php-packages/phpstan/src/Methods/Extension.php @@ -0,0 +1,57 @@ +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()]; + } +} diff --git a/php-packages/phpstan/src/Methods/HigherOrderCollectionProxyExtension.php b/php-packages/phpstan/src/Methods/HigherOrderCollectionProxyExtension.php new file mode 100644 index 000000000..cd04ac4a7 --- /dev/null +++ b/php-packages/phpstan/src/Methods/HigherOrderCollectionProxyExtension.php @@ -0,0 +1,143 @@ +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(); + } + }; + } +} diff --git a/php-packages/phpstan/src/Methods/HigherOrderTapProxyExtension.php b/php-packages/phpstan/src/Methods/HigherOrderTapProxyExtension.php new file mode 100644 index 000000000..93c6c4800 --- /dev/null +++ b/php-packages/phpstan/src/Methods/HigherOrderTapProxyExtension.php @@ -0,0 +1,49 @@ +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()); + } +} diff --git a/php-packages/phpstan/src/Methods/Kernel.php b/php-packages/phpstan/src/Methods/Kernel.php new file mode 100644 index 000000000..bc7c0dd5a --- /dev/null +++ b/php-packages/phpstan/src/Methods/Kernel.php @@ -0,0 +1,72 @@ +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; + } +} diff --git a/php-packages/phpstan/src/Methods/Macro.php b/php-packages/phpstan/src/Methods/Macro.php new file mode 100644 index 000000000..e0dca2802 --- /dev/null +++ b/php-packages/phpstan/src/Methods/Macro.php @@ -0,0 +1,259 @@ + 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(); + } +} diff --git a/php-packages/phpstan/src/Methods/ModelFactoryMethodsClassReflectionExtension.php b/php-packages/phpstan/src/Methods/ModelFactoryMethodsClassReflectionExtension.php new file mode 100644 index 000000000..89d458298 --- /dev/null +++ b/php-packages/phpstan/src/Methods/ModelFactoryMethodsClassReflectionExtension.php @@ -0,0 +1,160 @@ +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(); + } + }; + } +} diff --git a/php-packages/phpstan/src/Methods/ModelForwardsCallsExtension.php b/php-packages/phpstan/src/Methods/ModelForwardsCallsExtension.php new file mode 100644 index 000000000..84c554069 --- /dev/null +++ b/php-packages/phpstan/src/Methods/ModelForwardsCallsExtension.php @@ -0,0 +1,212 @@ + */ + 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; + } +} diff --git a/php-packages/phpstan/src/Methods/ModelTypeHelper.php b/php-packages/phpstan/src/Methods/ModelTypeHelper.php new file mode 100644 index 000000000..4c527f778 --- /dev/null +++ b/php-packages/phpstan/src/Methods/ModelTypeHelper.php @@ -0,0 +1,31 @@ +getClassName() === Model::class) { + return new ObjectType($modelClass); + } + + return $traverse($type); + }); + } +} diff --git a/php-packages/phpstan/src/Methods/Passable.php b/php-packages/phpstan/src/Methods/Passable.php new file mode 100644 index 000000000..735784d4d --- /dev/null +++ b/php-packages/phpstan/src/Methods/Passable.php @@ -0,0 +1,217 @@ +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; + } +} diff --git a/php-packages/phpstan/src/Methods/Pipes/Auths.php b/php-packages/phpstan/src/Methods/Pipes/Auths.php new file mode 100644 index 000000000..4dfa86e23 --- /dev/null +++ b/php-packages/phpstan/src/Methods/Pipes/Auths.php @@ -0,0 +1,61 @@ +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); + } + } +} diff --git a/php-packages/phpstan/src/Methods/Pipes/Contracts.php b/php-packages/phpstan/src/Methods/Pipes/Contracts.php new file mode 100644 index 000000000..b985dd184 --- /dev/null +++ b/php-packages/phpstan/src/Methods/Pipes/Contracts.php @@ -0,0 +1,60 @@ +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 []; + } +} diff --git a/php-packages/phpstan/src/Methods/Pipes/Facades.php b/php-packages/phpstan/src/Methods/Pipes/Facades.php new file mode 100644 index 000000000..826cd1269 --- /dev/null +++ b/php-packages/phpstan/src/Methods/Pipes/Facades.php @@ -0,0 +1,59 @@ +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); + } +} diff --git a/php-packages/phpstan/src/Methods/Pipes/Macros.php b/php-packages/phpstan/src/Methods/Pipes/Macros.php new file mode 100644 index 000000000..5f93eff45 --- /dev/null +++ b/php-packages/phpstan/src/Methods/Pipes/Macros.php @@ -0,0 +1,96 @@ +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); + } + } +} diff --git a/php-packages/phpstan/src/Methods/Pipes/Managers.php b/php-packages/phpstan/src/Methods/Pipes/Managers.php new file mode 100644 index 000000000..dfd41b449 --- /dev/null +++ b/php-packages/phpstan/src/Methods/Pipes/Managers.php @@ -0,0 +1,56 @@ +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); + } + } +} diff --git a/php-packages/phpstan/src/Methods/Pipes/SelfClass.php b/php-packages/phpstan/src/Methods/Pipes/SelfClass.php new file mode 100644 index 000000000..330fda735 --- /dev/null +++ b/php-packages/phpstan/src/Methods/Pipes/SelfClass.php @@ -0,0 +1,28 @@ +getClassReflection() + ->getName(); + + if (! $passable->searchOn($className)) { + $next($passable); + } + } +} diff --git a/php-packages/phpstan/src/Methods/RelationForwardsCallsExtension.php b/php-packages/phpstan/src/Methods/RelationForwardsCallsExtension.php new file mode 100644 index 000000000..9e7725916 --- /dev/null +++ b/php-packages/phpstan/src/Methods/RelationForwardsCallsExtension.php @@ -0,0 +1,139 @@ + */ + 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() + ); + } +} diff --git a/php-packages/phpstan/src/Methods/StorageMethodsClassReflectionExtension.php b/php-packages/phpstan/src/Methods/StorageMethodsClassReflectionExtension.php new file mode 100644 index 000000000..f1a2f2fd5 --- /dev/null +++ b/php-packages/phpstan/src/Methods/StorageMethodsClassReflectionExtension.php @@ -0,0 +1,60 @@ +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()) + ); + } +} diff --git a/php-packages/phpstan/src/Properties/HigherOrderCollectionProxyPropertyExtension.php b/php-packages/phpstan/src/Properties/HigherOrderCollectionProxyPropertyExtension.php new file mode 100644 index 000000000..87a791928 --- /dev/null +++ b/php-packages/phpstan/src/Properties/HigherOrderCollectionProxyPropertyExtension.php @@ -0,0 +1,118 @@ +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(); + } + }; + } +} diff --git a/php-packages/phpstan/src/Properties/MigrationHelper.php b/php-packages/phpstan/src/Properties/MigrationHelper.php new file mode 100644 index 000000000..7a59229d8 --- /dev/null +++ b/php-packages/phpstan/src/Properties/MigrationHelper.php @@ -0,0 +1,95 @@ +parser = $parser; + $this->databaseMigrationPath = $databaseMigrationPath; + $this->fileHelper = $fileHelper; + } + + /** + * @return array + */ + 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; + } + } +} diff --git a/php-packages/phpstan/src/Properties/ModelAccessorExtension.php b/php-packages/phpstan/src/Properties/ModelAccessorExtension.php new file mode 100644 index 000000000..a2ee2d4f0 --- /dev/null +++ b/php-packages/phpstan/src/Properties/ModelAccessorExtension.php @@ -0,0 +1,39 @@ +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() + ); + } +} diff --git a/php-packages/phpstan/src/Properties/ModelProperty.php b/php-packages/phpstan/src/Properties/ModelProperty.php new file mode 100644 index 000000000..28625a507 --- /dev/null +++ b/php-packages/phpstan/src/Properties/ModelProperty.php @@ -0,0 +1,98 @@ +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(); + } +} diff --git a/php-packages/phpstan/src/Properties/ModelPropertyExtension.php b/php-packages/phpstan/src/Properties/ModelPropertyExtension.php new file mode 100644 index 000000000..8923b49b6 --- /dev/null +++ b/php-packages/phpstan/src/Properties/ModelPropertyExtension.php @@ -0,0 +1,266 @@ + */ + 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 + */ + 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'; + 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; + } + } +} diff --git a/php-packages/phpstan/src/Properties/ModelRelationsExtension.php b/php-packages/phpstan/src/Properties/ModelRelationsExtension.php new file mode 100644 index 000000000..f38e09ea4 --- /dev/null +++ b/php-packages/phpstan/src/Properties/ModelRelationsExtension.php @@ -0,0 +1,116 @@ +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); + } +} diff --git a/php-packages/phpstan/src/Properties/ReflectionTypeContainer.php b/php-packages/phpstan/src/Properties/ReflectionTypeContainer.php new file mode 100644 index 000000000..c2965694b --- /dev/null +++ b/php-packages/phpstan/src/Properties/ReflectionTypeContainer.php @@ -0,0 +1,60 @@ +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; + } +} diff --git a/php-packages/phpstan/src/Properties/SchemaAggregator.php b/php-packages/phpstan/src/Properties/SchemaAggregator.php new file mode 100644 index 000000000..fa3099d47 --- /dev/null +++ b/php-packages/phpstan/src/Properties/SchemaAggregator.php @@ -0,0 +1,432 @@ + */ + public $tables = []; + + /** + * @param array $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 $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; + } +} diff --git a/php-packages/phpstan/src/Properties/SchemaColumn.php b/php-packages/phpstan/src/Properties/SchemaColumn.php new file mode 100644 index 000000000..18c5c4c2e --- /dev/null +++ b/php-packages/phpstan/src/Properties/SchemaColumn.php @@ -0,0 +1,45 @@ + */ + 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; + } +} diff --git a/php-packages/phpstan/src/Properties/SchemaTable.php b/php-packages/phpstan/src/Properties/SchemaTable.php new file mode 100644 index 000000000..05631ba14 --- /dev/null +++ b/php-packages/phpstan/src/Properties/SchemaTable.php @@ -0,0 +1,47 @@ +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]); + } +} diff --git a/php-packages/phpstan/src/Reflection/AnnotationScopeMethodParameterReflection.php b/php-packages/phpstan/src/Reflection/AnnotationScopeMethodParameterReflection.php new file mode 100644 index 000000000..9f3420a05 --- /dev/null +++ b/php-packages/phpstan/src/Reflection/AnnotationScopeMethodParameterReflection.php @@ -0,0 +1,70 @@ +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; + } +} diff --git a/php-packages/phpstan/src/Reflection/AnnotationScopeMethodReflection.php b/php-packages/phpstan/src/Reflection/AnnotationScopeMethodReflection.php new file mode 100644 index 000000000..ada3b4f9e --- /dev/null +++ b/php-packages/phpstan/src/Reflection/AnnotationScopeMethodReflection.php @@ -0,0 +1,133 @@ +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; + } +} diff --git a/php-packages/phpstan/src/Reflection/DynamicWhereParameterReflection.php b/php-packages/phpstan/src/Reflection/DynamicWhereParameterReflection.php new file mode 100644 index 000000000..cb8cfd11c --- /dev/null +++ b/php-packages/phpstan/src/Reflection/DynamicWhereParameterReflection.php @@ -0,0 +1,43 @@ +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; + } +} diff --git a/php-packages/phpstan/src/Reflection/ModelScopeMethodReflection.php b/php-packages/phpstan/src/Reflection/ModelScopeMethodReflection.php new file mode 100644 index 000000000..769a40aff --- /dev/null +++ b/php-packages/phpstan/src/Reflection/ModelScopeMethodReflection.php @@ -0,0 +1,120 @@ +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(); + } +} diff --git a/php-packages/phpstan/src/Reflection/ReflectionHelper.php b/php-packages/phpstan/src/Reflection/ReflectionHelper.php new file mode 100644 index 000000000..2442d8ffb --- /dev/null +++ b/php-packages/phpstan/src/Reflection/ReflectionHelper.php @@ -0,0 +1,28 @@ +getPropertyTags())) { + return true; + } + + foreach ($classReflection->getAncestors() as $ancestor) { + if (array_key_exists($propertyName, $ancestor->getPropertyTags())) { + return true; + } + } + + return false; + } +} diff --git a/php-packages/phpstan/src/Reflection/StaticMethodReflection.php b/php-packages/phpstan/src/Reflection/StaticMethodReflection.php new file mode 100644 index 000000000..bce18eca1 --- /dev/null +++ b/php-packages/phpstan/src/Reflection/StaticMethodReflection.php @@ -0,0 +1,92 @@ +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(); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/BuilderModelFindExtension.php b/php-packages/phpstan/src/ReturnTypes/BuilderModelFindExtension.php new file mode 100644 index 000000000..b1903b428 --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/BuilderModelFindExtension.php @@ -0,0 +1,112 @@ +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) + ); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/CollectionFilterDynamicReturnTypeExtension.php b/php-packages/phpstan/src/ReturnTypes/CollectionFilterDynamicReturnTypeExtension.php new file mode 100644 index 000000000..269ccca31 --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/CollectionFilterDynamicReturnTypeExtension.php @@ -0,0 +1,112 @@ +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([], [])]); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/CollectionMakeDynamicStaticMethodReturnTypeExtension.php b/php-packages/phpstan/src/ReturnTypes/CollectionMakeDynamicStaticMethodReturnTypeExtension.php new file mode 100644 index 000000000..ecb3ed90f --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/CollectionMakeDynamicStaticMethodReturnTypeExtension.php @@ -0,0 +1,51 @@ +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); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/ContainerArrayAccessDynamicMethodReturnTypeExtension.php b/php-packages/phpstan/src/ReturnTypes/ContainerArrayAccessDynamicMethodReturnTypeExtension.php new file mode 100644 index 000000000..eabee76fc --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/ContainerArrayAccessDynamicMethodReturnTypeExtension.php @@ -0,0 +1,73 @@ +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); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/EloquentBuilderExtension.php b/php-packages/phpstan/src/ReturnTypes/EloquentBuilderExtension.php new file mode 100644 index 000000000..02b9be0cd --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/EloquentBuilderExtension.php @@ -0,0 +1,86 @@ +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; + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/Helpers/AppExtension.php b/php-packages/phpstan/src/ReturnTypes/Helpers/AppExtension.php new file mode 100644 index 000000000..5960e73e5 --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/Helpers/AppExtension.php @@ -0,0 +1,65 @@ +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(); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/Helpers/CollectExtension.php b/php-packages/phpstan/src/ReturnTypes/Helpers/CollectExtension.php new file mode 100644 index 000000000..d5c7398d5 --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/Helpers/CollectExtension.php @@ -0,0 +1,45 @@ +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); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/Helpers/TapExtension.php b/php-packages/phpstan/src/ReturnTypes/Helpers/TapExtension.php new file mode 100644 index 000000000..74d23628d --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/Helpers/TapExtension.php @@ -0,0 +1,49 @@ +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(); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/Helpers/TransExtension.php b/php-packages/phpstan/src/ReturnTypes/Helpers/TransExtension.php new file mode 100644 index 000000000..310e3e628 --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/Helpers/TransExtension.php @@ -0,0 +1,40 @@ +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(); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/Helpers/ValidatorExtension.php b/php-packages/phpstan/src/ReturnTypes/Helpers/ValidatorExtension.php new file mode 100644 index 000000000..26630c7cf --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/Helpers/ValidatorExtension.php @@ -0,0 +1,40 @@ +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), + ]); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/Helpers/ValueExtension.php b/php-packages/phpstan/src/ReturnTypes/Helpers/ValueExtension.php new file mode 100644 index 000000000..0766391a9 --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/Helpers/ValueExtension.php @@ -0,0 +1,56 @@ +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); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/HigherOrderTapProxyExtension.php b/php-packages/phpstan/src/ReturnTypes/HigherOrderTapProxyExtension.php new file mode 100644 index 000000000..e90f1e00a --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/HigherOrderTapProxyExtension.php @@ -0,0 +1,56 @@ +getType($methodCall->var); + if ($type instanceof GenericObjectType) { + $types = $type->getTypes(); + if (count($types) === 1 && $types[0] instanceof ObjectType) { + return $types[0]; + } + } + + return new MixedType(); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/ModelExtension.php b/php-packages/phpstan/src/ReturnTypes/ModelExtension.php new file mode 100644 index 000000000..f039ab2fd --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/ModelExtension.php @@ -0,0 +1,104 @@ +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; + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/ModelFactoryDynamicStaticMethodReturnTypeExtension.php b/php-packages/phpstan/src/ReturnTypes/ModelFactoryDynamicStaticMethodReturnTypeExtension.php new file mode 100644 index 000000000..a16440eef --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/ModelFactoryDynamicStaticMethodReturnTypeExtension.php @@ -0,0 +1,57 @@ +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'); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/ModelFindExtension.php b/php-packages/phpstan/src/ReturnTypes/ModelFindExtension.php new file mode 100644 index 000000000..a50a8b319 --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/ModelFindExtension.php @@ -0,0 +1,108 @@ +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) + ); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/RelationCollectionExtension.php b/php-packages/phpstan/src/ReturnTypes/RelationCollectionExtension.php new file mode 100644 index 000000000..9a5b8b2c4 --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/RelationCollectionExtension.php @@ -0,0 +1,86 @@ +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; + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/RelationFindExtension.php b/php-packages/phpstan/src/ReturnTypes/RelationFindExtension.php new file mode 100644 index 000000000..63ea62e37 --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/RelationFindExtension.php @@ -0,0 +1,99 @@ +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; + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/RequestExtension.php b/php-packages/phpstan/src/ReturnTypes/RequestExtension.php new file mode 100644 index 000000000..451581855 --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/RequestExtension.php @@ -0,0 +1,61 @@ +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)); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/StorageDynamicStaticMethodReturnTypeExtension.php b/php-packages/phpstan/src/ReturnTypes/StorageDynamicStaticMethodReturnTypeExtension.php new file mode 100644 index 000000000..15685119b --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/StorageDynamicStaticMethodReturnTypeExtension.php @@ -0,0 +1,38 @@ +getName() === 'disk'; + } + + public function getTypeFromStaticMethodCall( + MethodReflection $methodReflection, + StaticCall $methodCall, + Scope $scope + ): Type { + return new ObjectType(FilesystemAdapter::class); + } +} diff --git a/php-packages/phpstan/src/ReturnTypes/TestCaseExtension.php b/php-packages/phpstan/src/ReturnTypes/TestCaseExtension.php new file mode 100644 index 000000000..908fd2c90 --- /dev/null +++ b/php-packages/phpstan/src/ReturnTypes/TestCaseExtension.php @@ -0,0 +1,53 @@ +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); + } +} diff --git a/php-packages/phpstan/src/Support/CollectionHelper.php b/php-packages/phpstan/src/Support/CollectionHelper.php new file mode 100644 index 000000000..8fd6fced7 --- /dev/null +++ b/php-packages/phpstan/src/Support/CollectionHelper.php @@ -0,0 +1,99 @@ +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); + } +} diff --git a/php-packages/phpstan/src/Support/HigherOrderCollectionProxyHelper.php b/php-packages/phpstan/src/Support/HigherOrderCollectionProxyHelper.php new file mode 100644 index 000000000..da4827667 --- /dev/null +++ b/php-packages/phpstan/src/Support/HigherOrderCollectionProxyHelper.php @@ -0,0 +1,134 @@ +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; + } +} diff --git a/php-packages/phpstan/src/Types/AbortIfFunctionTypeSpecifyingExtension.php b/php-packages/phpstan/src/Types/AbortIfFunctionTypeSpecifyingExtension.php new file mode 100644 index 000000000..ec16776a6 --- /dev/null +++ b/php-packages/phpstan/src/Types/AbortIfFunctionTypeSpecifyingExtension.php @@ -0,0 +1,60 @@ +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; + } +} diff --git a/php-packages/phpstan/src/Types/GenericEloquentBuilderTypeNodeResolverExtension.php b/php-packages/phpstan/src/Types/GenericEloquentBuilderTypeNodeResolverExtension.php new file mode 100644 index 000000000..155926eab --- /dev/null +++ b/php-packages/phpstan/src/Types/GenericEloquentBuilderTypeNodeResolverExtension.php @@ -0,0 +1,56 @@ +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), + ]); + } +} diff --git a/php-packages/phpstan/src/Types/GenericEloquentCollectionTypeNodeResolverExtension.php b/php-packages/phpstan/src/Types/GenericEloquentCollectionTypeNodeResolverExtension.php new file mode 100644 index 000000000..538db67f1 --- /dev/null +++ b/php-packages/phpstan/src/Types/GenericEloquentCollectionTypeNodeResolverExtension.php @@ -0,0 +1,84 @@ + $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, + ]); + } +} diff --git a/php-packages/phpstan/src/Types/ModelProperty/GenericModelPropertyType.php b/php-packages/phpstan/src/Types/ModelProperty/GenericModelPropertyType.php new file mode 100644 index 000000000..2cb95300e --- /dev/null +++ b/php-packages/phpstan/src/Types/ModelProperty/GenericModelPropertyType.php @@ -0,0 +1,119 @@ +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']); + } +} diff --git a/php-packages/phpstan/src/Types/ModelProperty/ModelPropertyType.php b/php-packages/phpstan/src/Types/ModelProperty/ModelPropertyType.php new file mode 100644 index 000000000..a6b973193 --- /dev/null +++ b/php-packages/phpstan/src/Types/ModelProperty/ModelPropertyType.php @@ -0,0 +1,20 @@ +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; + } +} diff --git a/php-packages/phpstan/src/Types/ModelRelationsDynamicMethodReturnTypeExtension.php b/php-packages/phpstan/src/Types/ModelRelationsDynamicMethodReturnTypeExtension.php new file mode 100644 index 000000000..cec32766f --- /dev/null +++ b/php-packages/phpstan/src/Types/ModelRelationsDynamicMethodReturnTypeExtension.php @@ -0,0 +1,108 @@ +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)]); + } +} diff --git a/php-packages/phpstan/src/Types/Passable.php b/php-packages/phpstan/src/Types/Passable.php new file mode 100644 index 000000000..b9807333d --- /dev/null +++ b/php-packages/phpstan/src/Types/Passable.php @@ -0,0 +1,45 @@ +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; + } +} diff --git a/php-packages/phpstan/src/Types/RelationDynamicMethodReturnTypeExtension.php b/php-packages/phpstan/src/Types/RelationDynamicMethodReturnTypeExtension.php new file mode 100644 index 000000000..c41c619cd --- /dev/null +++ b/php-packages/phpstan/src/Types/RelationDynamicMethodReturnTypeExtension.php @@ -0,0 +1,65 @@ +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())]); + } +} diff --git a/php-packages/phpstan/src/Types/RelationParserHelper.php b/php-packages/phpstan/src/Types/RelationParserHelper.php new file mode 100644 index 000000000..8e6910712 --- /dev/null +++ b/php-packages/phpstan/src/Types/RelationParserHelper.php @@ -0,0 +1,132 @@ +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); + } +} diff --git a/php-packages/phpstan/src/Types/ViewStringType.php b/php-packages/phpstan/src/Types/ViewStringType.php new file mode 100644 index 000000000..985d6c11a --- /dev/null +++ b/php-packages/phpstan/src/Types/ViewStringType.php @@ -0,0 +1,80 @@ +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(); + } +} diff --git a/php-packages/phpstan/src/Types/ViewStringTypeNodeResolverExtension.php b/php-packages/phpstan/src/Types/ViewStringTypeNodeResolverExtension.php new file mode 100644 index 000000000..8883e6d27 --- /dev/null +++ b/php-packages/phpstan/src/Types/ViewStringTypeNodeResolverExtension.php @@ -0,0 +1,26 @@ +__toString() === 'view-string') { + return new ViewStringType(); + } + + return null; + } +} diff --git a/php-packages/phpstan/stubs/Contracts/Container.stub b/php-packages/phpstan/stubs/Contracts/Container.stub new file mode 100644 index 000000000..78ed9c632 --- /dev/null +++ b/php-packages/phpstan/stubs/Contracts/Container.stub @@ -0,0 +1,16 @@ + */ +interface Container extends \ArrayAccess +{ + +} + +namespace Illuminate\Contracts\Foundation; + +interface Application extends \Illuminate\Contracts\Container\Container +{ + +} diff --git a/php-packages/phpstan/stubs/Contracts/Pagination.stub b/php-packages/phpstan/stubs/Contracts/Pagination.stub new file mode 100644 index 000000000..f23beab12 --- /dev/null +++ b/php-packages/phpstan/stubs/Contracts/Pagination.stub @@ -0,0 +1,17 @@ + + */ + public function toArray(); +} + +interface Jsonable +{} diff --git a/php-packages/phpstan/stubs/Flarum/User.stub b/php-packages/phpstan/stubs/Flarum/User.stub new file mode 100644 index 000000000..4ce9aecb5 --- /dev/null +++ b/php-packages/phpstan/stubs/Flarum/User.stub @@ -0,0 +1,31 @@ + + * @implements Enumerable + */ +class Collection implements \ArrayAccess, Enumerable +{ + /** + * @param callable|null $callback + * @param mixed $default + * @return TValue|null + */ + public function first(callable $callback = null, $default = null){} + + /** + * @param callable|null $callback + * @param mixed $default + * @return TValue|null + */ + public function last(callable $callback = null, $default = null){} + + + /** + * @param mixed $key + * @param mixed $default + * @return TValue|null + */ + public function get($key, $default = null) {} + + /** + * @return TValue|null + */ + public function pop() {} + + /** + * @param mixed $key + * @param mixed $default + * @return TValue|null + */ + public function pull($key, $default = null) {} + + /** + * @param mixed $value + * @param bool $strict + * @return TKey|false + */ + public function search($value, $strict = false) {} + + /** + * @return TValue|null + */ + public function shift() {} + + /** + * @param callable(TValue, TKey): (void|bool) $callable + * @return static + */ + public function each($callable) {} + + /** + * @template TReturn + * @param callable(TValue, TKey): TReturn $callable + * @return static + */ + public function map($callable) {} + + /** + * Run a grouping map over the items. + * + * The callback should return an associative array with a single key/value pair. + * + * @template TMapToGroupsKey of array-key + * @template TMapToGroupsValue + * + * @param callable(TValue, TKey): array $callback + * @return static> + */ + public function mapToGroups(callable $callback) {} + + /** + * @param array|string|callable(TValue, TKey): mixed $groupBy + * @param bool $preserveKeys + * @return static> + */ + public function groupBy($groupBy, $preserveKeys = false); + + /** + * @template TClass + * @param class-string $class + * @return static + */ + public function mapInto($class); + + /** + * @template TReturn + * @param callable(TValue, TKey): (array|\Illuminate\Support\Enumerable) $callback + * @return static + */ + public function flatMap(callable $callback) {} + + /** + * @template TReturn + * @param callable(TValue ...$values): TReturn $callback + * @return static + */ + public function mapSpread(callable $callback) {} + + /** + * @param int $number + * @param null|callable(int, int): mixed $callback + * @return static + */ + public static function times($number, callable $callback = null) {} + + /** + * @param string|array $value + * @param string|null $key + * @return static + */ + public function pluck($value, $key = null) {} + + /** + * @return TValue + */ + public function pop() {} + + /** + * Push one or more items onto the end of the collection. + * + * @param TValue ...$values + * @return static + */ + public function push(...$values) {} + + /** + * Put an item in the collection by key. + * + * @param TKey $key + * @param TValue $value + * @return static + */ + public function put($key, $value) {} +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/BelongsTo.stub b/php-packages/phpstan/stubs/Illuminate/Database/BelongsTo.stub new file mode 100644 index 000000000..a5d474080 --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/BelongsTo.stub @@ -0,0 +1,27 @@ + + */ +class BelongsTo extends Relation +{ + /** @phpstan-return TChildModel */ + public function associate(); + + /** @phpstan-return TChildModel */ + public function dissociate(); + + /** @phpstan-return TChildModel */ + public function getChild(); + + /** + * Get the results of the relationship. + * + * @phpstan-return ?TRelatedModel + */ + public function getResults(); +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/BelongsToMany.stub b/php-packages/phpstan/stubs/Illuminate/Database/BelongsToMany.stub new file mode 100644 index 000000000..22484d0ba --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/BelongsToMany.stub @@ -0,0 +1,112 @@ + + */ +class BelongsToMany extends Relation +{ + /** + * Find a related model by its primary key or return new instance of the related model. + * + * @param mixed $id + * @param array $columns + * @return \Illuminate\Support\Collection|TRelatedModel + */ + public function findOrNew($id, $columns = ['*']); + + /** + * Get the first related model record matching the attributes or instantiate it. + * + * @param array $attributes + * @return TRelatedModel + */ + public function firstOrNew(array $attributes); + + /** + * Get the first related record matching the attributes or create it. + * + * @param array $attributes + * @param array $joining + * @param bool $touch + * @return TRelatedModel + */ + public function firstOrCreate(array $attributes, array $joining = [], $touch = true); + + /** + * Create or update a related record matching the attributes, and fill it with values. + * + * @param array $attributes + * @param array $values + * @param array $joining + * @param bool $touch + * @return TRelatedModel + */ + public function updateOrCreate(array $attributes, array $values = [], array $joining = [], $touch = true); + + /** + * Find a related model by its primary key. + * + * @param mixed $id + * @param array $columns + * @return TRelatedModel|\Illuminate\Database\Eloquent\Collection|null + */ + public function find($id, $columns = ['*']); + + /** + * Find multiple related models by their primary keys. + * + * @param \Illuminate\Contracts\Support\Arrayable|int[] $ids + * @param array $columns + * @return \Illuminate\Database\Eloquent\Collection + */ + public function findMany($ids, $columns = ['*']); + + /** + * Find a related model by its primary key or throw an exception. + * + * @param mixed $id + * @param array $columns + * @return TRelatedModel|\Illuminate\Database\Eloquent\Collection + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + */ + public function findOrFail($id, $columns = ['*']); + + /** + * Execute the query and get the first result. + * + * @param array $columns + * @return TRelatedModel|null + */ + public function first($columns = ['*']); + + /** + * Execute the query and get the first result or throw an exception. + * + * @param array $columns + * @return TRelatedModel + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + */ + public function firstOrFail($columns = ['*']); + + /** + * Create a new instance of the related model. + * + * @param array, mixed> $attributes + * @param mixed[] $joining + * @param bool $touch + * @return TRelatedModel + */ + public function create(array $attributes = [], array $joining = [], $touch = true); + + /** + * Get the results of the relationship. + * + * @phpstan-return \Illuminate\Database\Eloquent\Collection + */ + public function getResults(); +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/EloquentBuilder.stub b/php-packages/phpstan/stubs/Illuminate/Database/EloquentBuilder.stub new file mode 100644 index 000000000..039dd8e3c --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/EloquentBuilder.stub @@ -0,0 +1,454 @@ + $orWhere + */ +class Builder +{ + /** + * Create and return an un-saved model instance. + * + * @phpstan-param array, mixed> $attributes + * @phpstan-return TModelClass + */ + public function make(array $attributes = []); + + /** + * Register a new global scope. + * + * @param string $identifier + * @param \Illuminate\Database\Eloquent\Scope|\Closure $scope + * @return static + */ + public function withGlobalScope($identifier, $scope); + + /** + * Remove a registered global scope. + * + * @param \Illuminate\Database\Eloquent\Scope|string $scope + * @return static + */ + public function withoutGlobalScope($scope); + + /** @phpstan-return TModelClass */ + public function getModel(); + + /** + * @phpstan-param array, mixed> $attributes + * @phpstan-return TModelClass + */ + public function create(array $attributes = []); + + /** + * Create a collection of models from plain arrays. + * + * @param array $items + * @phpstan-return \Illuminate\Database\Eloquent\Collection + */ + public function hydrate(array $items); + + /** + * Create a collection of models from a raw query. + * + * @param string $query + * @param array $bindings + * @phpstan-return \Illuminate\Database\Eloquent\Collection + */ + public function fromQuery($query, $bindings = []); + + /** + * Find a model by its primary key. + * + * @param mixed $id + * @param array|'*')>|model-property|'*' $columns + * @phpstan-return TModelClass|\Illuminate\Database\Eloquent\Collection|null + */ + public function find($id, $columns = ['*']); + + /** + * Find multiple models by their primary keys. + * + * @param \Illuminate\Contracts\Support\Arrayable|array $ids + * @param array|'*')>|model-property|'*' $columns + * @phpstan-return \Illuminate\Database\Eloquent\Collection + */ + public function findMany($ids, $columns = ['*']); + + /** + * Find a model by its primary key or throw an exception. + * + * @param mixed $id + * @param array|'*')>|model-property|'*' $columns + * @phpstan-return TModelClass|\Illuminate\Database\Eloquent\Collection + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + */ + public function findOrFail($id, $columns = ['*']); + + /** + * Find a model by its primary key or return fresh model instance. + * + * @param mixed $id + * @param array|'*')>|model-property|'*' $columns + * @phpstan-return TModelClass + */ + public function findOrNew($id, $columns = ['*']); + + /** + * Execute the query and get the first result. + * + * @param array|int, mixed>|string $columns + * @return TModelClass|null + */ + public function first($columns = ['*']); + + /** + * Get the first record matching the attributes or instantiate it. + * + * @param array, mixed> $attributes + * @param array, mixed> $values + * @phpstan-return TModelClass + */ + public function firstOrNew(array $attributes = [], array $values = []); + + /** + * Get the first record matching the attributes or create it. + * + * @param array, mixed> $attributes + * @param array, mixed> $values + * @phpstan-return TModelClass + */ + public function firstOrCreate(array $attributes, array $values = []); + + /** + * Create or update a record matching the attributes, and fill it with values. + * + * @param array, mixed> $attributes + * @param array, mixed> $values + * @phpstan-return TModelClass + */ + public function updateOrCreate(array $attributes, array $values = []); + + /** + * @param array, mixed> $attributes + * @phpstan-return TModelClass + */ + public function forceCreate(array $attributes); + + /** + * @param array, mixed> $values + * @return int + */ + public function update(array $values); + + /** + * Execute the query and get the first result or throw an exception. + * + * @param array|'*')>|model-property|'*' $columns + * @phpstan-return TModelClass + * + * @throws \Illuminate\Database\Eloquent\ModelNotFoundException + */ + public function firstOrFail($columns = ['*']); + + /** + * Execute the query and get the first result if it's the sole matching record. + * + * @param array|'*')>|model-property|'*' $columns + * @phpstan-return TModelClass + */ + public function sole($columns = ['*']); + + /** + * Execute the query and get the first result or call a callback. + * + * @param \Closure|array|'*')> $columns + * @param \Closure|null $callback + * @phpstan-return TModelClass|mixed + */ + public function firstOr($columns = ['*'], \Closure $callback = null); + + /** + * Add a basic where clause to the query. + * + * @param \Closure|model-property|array|int, mixed>|\Illuminate\Database\Query\Expression $column + * @param mixed $operator + * @param mixed $value + * @param string $boolean + * @return static + */ + public function where($column, $operator = null, $value = null, $boolean = 'and'); + + /** + * Add an "or where" clause to the query. + * + * @param \Closure|model-property|array|int, mixed>|\Illuminate\Database\Query\Expression $column + * @param mixed $operator + * @param mixed $value + * @return static + */ + public function orWhere($column, $operator = null, $value = null); + + /** + * Add a relationship count / exists condition to the query. + * + * @template TRelatedModel of Model + * @param \Illuminate\Database\Eloquent\Relations\Relation|string $relation + * @param string $operator + * @param int $count + * @param string $boolean + * @param \Closure|null $callback + * @return static + * + * @throws \RuntimeException + */ + public function has($relation, $operator = '>=', $count = 1, $boolean = 'and', \Closure $callback = null); + + /** + * Add a relationship count / exists condition to the query with an "or". + * + * @param string $relation + * @param string $operator + * @param int $count + * @return static + */ + public function orHas($relation, $operator = '>=', $count = 1); + + /** + * Add a relationship count / exists condition to the query. + * + * @param string $relation + * @param string $boolean + * @param \Closure|null $callback + * @return static + */ + public function doesntHave($relation, $boolean = 'and', \Closure $callback = null); + + /** + * Add a relationship count / exists condition to the query with an "or". + * + * @param string $relation + * @return static + */ + public function orDoesntHave($relation); + + /** + * Add a relationship count / exists condition to the query with where clauses. + * + * @param string $relation + * @param \Closure|null $callback + * @param string $operator + * @param int $count + * @return static + */ + public function whereHas($relation, \Closure $callback = null, $operator = '>=', $count = 1); + + /** + * Add a relationship count / exists condition to the query with where clauses and an "or". + * + * @param string $relation + * @param \Closure|null $callback + * @param string $operator + * @param int $count + * @return static + */ + public function orWhereHas($relation, \Closure $callback = null, $operator = '>=', $count = 1); + + /** + * Add a polymorphic relationship count / exists condition to the query. + * + * @template TRelatedModel of Model + * @template TChildModel of Model + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation + * @param string|array $types + * @param string $operator + * @param int $count + * @param string $boolean + * @param \Closure|null $callback + * @return static + */ + public function hasMorph($relation, $types, $operator = '>=', $count = 1, $boolean = 'and', \Closure $callback = null); + + /** + * Add a polymorphic relationship count / exists condition to the query with an "or". + * + * @template TRelatedModel of Model + * @template TChildModel of Model + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation + * @param string|array $types + * @param string $operator + * @param int $count + * @return static + */ + public function orHasMorph($relation, $types, $operator = '>=', $count = 1); + + /** + * Add a polymorphic relationship count / exists condition to the query. + * + * @template TRelatedModel of Model + * @template TChildModel of Model + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation + * @param string|array $types + * @param string $boolean + * @param \Closure|null $callback + * @return static + */ + public function doesntHaveMorph($relation, $types, $boolean = 'and', \Closure $callback = null); + + /** + * Add a polymorphic relationship count / exists condition to the query with an "or". + * + * @template TRelatedModel of Model + * @template TChildModel of Model + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation + * @param string|array $types + * @return static + */ + public function orDoesntHaveMorph($relation, $types); + + /** + * Add a polymorphic relationship count / exists condition to the query with where clauses. + * + * @template TRelatedModel of Model + * @template TChildModel of Model + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation + * @param string|array $types + * @param \Closure|null $callback + * @param string $operator + * @param int $count + * @return static + */ + public function whereHasMorph($relation, $types, \Closure $callback = null, $operator = '>=', $count = 1); + + /** + * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or". + * + * @template TRelatedModel of Model + * @template TChildModel of Model + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation + * @param string|array $types + * @param \Closure|null $callback + * @param string $operator + * @param int $count + * @return static + */ + public function orWhereHasMorph($relation, $types, \Closure $callback = null, $operator = '>=', $count = 1); + + /** + * Add a polymorphic relationship count / exists condition to the query with where clauses. + * + * @template TRelatedModel of Model + * @template TChildModel of Model + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation + * @param string|array $types + * @param \Closure|null $callback + * @return static + */ + public function whereDoesntHaveMorph($relation, $types, \Closure $callback = null); + + /** + * Add a polymorphic relationship count / exists condition to the query with where clauses and an "or". + * + * @template TRelatedModel of Model + * @template TChildModel of Model + * @param \Illuminate\Database\Eloquent\Relations\MorphTo|string $relation + * @param string|array $types + * @param \Closure|null $callback + * @return static + */ + public function orWhereDoesntHaveMorph($relation, $types, \Closure $callback = null); + + /** + * Merge the where constraints from another query to the current query. + * + * @param \Illuminate\Database\Eloquent\Builder $from + * @return static + */ + public function mergeConstraintsFrom(\Illuminate\Database\Eloquent\Builder $from); + + /** + * Add a relationship count / exists condition to the query with where clauses and an "or". + * + * @param string $relation + * @param \Closure|null $callback + * @return static + */ + public function orWhereDoesntHave($relation, \Closure $callback = null); + + /** + * Add a relationship count / exists condition to the query with where clauses. + * + * @param string $relation + * @param \Closure|null $callback + * @return static + */ + public function whereDoesntHave($relation, \Closure $callback = null); + + /** + * Add a basic where clause to the query, and return the first result. + * + * @param \Closure|model-property|array|int, mixed>|\Illuminate\Database\Query\Expression $column + * @param mixed $operator + * @param mixed $value + * @param string $boolean + * @phpstan-return TModelClass|null + */ + public function firstWhere($column, $operator = null, $value = null, $boolean = 'and'); + + /** + * Execute the query as a "select" statement. + * + * @param array|'*')>|model-property|'*' $columns + * @phpstan-return \Illuminate\Database\Eloquent\Collection + */ + public function get($columns = ['*']); + + /** + * Get the hydrated models without eager loading. + * + * @param array|'*')>|model-property|'*' $columns + * @phpstan-return TModelClass[] + */ + public function getModels($columns = ['*']); + + /** + * Get a single column's value from the first result of a query. + * + * @param model-property|\Illuminate\Database\Query\Expression $column + * @return mixed + */ + public function value($column); + + /** + * Apply the callback's query changes if the given "value" is true. + * + * @param mixed $value + * @param callable($this, mixed): (void|Builder) $callback + * @param callable($this, mixed): (null|Builder)|null $default + * @return mixed|$this + */ + public function when($value, $callback, $default = null); + + /** + * Apply the callback's query changes if the given "value" is false. + * + * @param mixed $value + * @param callable($this, mixed): (void|Builder) $callback + * @param callable($this, mixed): (null|Builder)|null $default + * @return mixed|$this + */ + public function unless($value, $callback, $default = null); +} + +class Scope {} + +/** + * @method static \Illuminate\Database\Eloquent\Builder withTrashed(bool $withTrashed = true) + * @method static \Illuminate\Database\Eloquent\Builder onlyTrashed() + * @method static \Illuminate\Database\Eloquent\Builder withoutTrashed() + * @method static bool restore() + */ +trait SoftDeletes {} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/EloquentCollection.stub b/php-packages/phpstan/stubs/Illuminate/Database/EloquentCollection.stub new file mode 100644 index 000000000..02da6d683 --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/EloquentCollection.stub @@ -0,0 +1,43 @@ + + */ +class Collection extends \Illuminate\Support\Collection +{ + + /** @phpstan-use EnumeratesValues */ + use EnumeratesValues; + + /** + * @param mixed $key + * @param mixed $default + * @phpstan-return TValue|null + */ + public function find($key, $default = null) {} + + /** + * @template TReturn + * @param callable(TValue, int): TReturn $callable + * @return static|\Illuminate\Support\Collection + */ + public function map($callable) {} + + /** + * @param callable(TValue, int): mixed $callback + * @return \Illuminate\Support\Collection + */ + public function flatMap(callable $callback) {} + + /** + * @template TReturn + * @param callable(TValue ...$values): TReturn $callback + * @return static|\Illuminate\Support\Collection + */ + public function mapSpread(callable $callback) {} +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/Factory.stub b/php-packages/phpstan/stubs/Illuminate/Database/Factory.stub new file mode 100644 index 000000000..dc6add1a0 --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/Factory.stub @@ -0,0 +1,81 @@ + + */ + protected $model; + + /** + * Get a new factory instance for the given attributes. + * + * @param callable|array, mixed> $attributes + * @return static + */ + public static function new($attributes = []) {} + + /** + * Create a single model and persist it to the database. + * + * @param array, mixed> $attributes + * @return TModel + */ + public function createOne($attributes = []) {} + + /** + * Create a collection of models and persist them to the database. + * + * @param iterable, mixed>> $records + * @return \Illuminate\Database\Eloquent\Collection + */ + public function createMany(iterable $records) {} + + /** + * Create a collection of models and persist them to the database. + * + * @param array, mixed> $attributes + * @param \Illuminate\Database\Eloquent\Model|null $parent + * @return \Illuminate\Database\Eloquent\Collection|TModel + */ + public function create($attributes = [], ?\Illuminate\Database\Eloquent\Model $parent = null) {} + + /** + * Make a single instance of the model. + * + * @param callable|array, mixed> $attributes + * @return TModel + */ + public function makeOne($attributes = []) {} + + /** + * Create a collection of models. + * + * @param array, mixed> $attributes + * @param \Illuminate\Database\Eloquent\Model|null $parent + * @return \Illuminate\Database\Eloquent\Collection|TModel + */ + public function make($attributes = [], ?\Illuminate\Database\Eloquent\Model $parent = null) {} + + /** + * Make an instance of the model with the given attributes. + * + * @param \Illuminate\Database\Eloquent\Model|null $parent + * @return TModel + */ + protected function makeInstance(?\Illuminate\Database\Eloquent\Model $parent) {} + + /** + * Define the model's default state. + * + * @return array, mixed> + */ + abstract public function definition(); +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/Gate.stub b/php-packages/phpstan/stubs/Illuminate/Database/Gate.stub new file mode 100644 index 000000000..ed57dd695 --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/Gate.stub @@ -0,0 +1,31 @@ + + */ +class HasMany extends HasOneOrMany +{ + /** + * Get the results of the relationship. + * + * @phpstan-return \Illuminate\Database\Eloquent\Collection + */ + public function getResults(); +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/HasManyThrough.stub b/php-packages/phpstan/stubs/Illuminate/Database/HasManyThrough.stub new file mode 100644 index 000000000..e4537e015 --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/HasManyThrough.stub @@ -0,0 +1,17 @@ + + */ +class HasManyThrough extends Relation +{ + /** + * Get the results of the relationship. + * + * @phpstan-return \Illuminate\Database\Eloquent\Collection + */ + public function getResults(); +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/HasOne.stub b/php-packages/phpstan/stubs/Illuminate/Database/HasOne.stub new file mode 100644 index 000000000..336dd2d6d --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/HasOne.stub @@ -0,0 +1,17 @@ + + */ +class HasOne extends HasOneOrMany +{ + /** + * Get the results of the relationship. + * + * @phpstan-return ?TRelatedModel + */ + public function getResults(); +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/HasOneOrMany.stub b/php-packages/phpstan/stubs/Illuminate/Database/HasOneOrMany.stub new file mode 100644 index 000000000..75c04a1f9 --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/HasOneOrMany.stub @@ -0,0 +1,86 @@ + + */ +abstract class HasOneOrMany extends Relation +{ + /** + * Create a new has one or many relationship instance. + * + * @param \Illuminate\Database\Eloquent\Builder $query + * @param TRelatedModel $parent + * @param non-empty-string $foreignKey + * @param non-empty-string $localKey + * @return void + */ + public function __construct(\Illuminate\Database\Eloquent\Builder $query, \Illuminate\Database\Eloquent\Model $parent, $foreignKey, $localKey); + + /** + * @param array, mixed> $attributes + * @phpstan-return TRelatedModel + */ + public function make(array $attributes = []); + + /** + * Find a model by its primary key or return new instance of the related model. + * + * @param mixed $id + * @param array $columns + * @return \Illuminate\Support\Collection|TRelatedModel + */ + public function findOrNew($id, $columns = ['*']); + + /** + * Get the first related model record matching the attributes or instantiate it. + * + * @param array, mixed> $attributes + * @param array $values + * @return TRelatedModel + */ + public function firstOrNew(array $attributes, array $values = []); + + /** + * Get the first related record matching the attributes or create it. + * + * @param array, mixed> $attributes + * @param array $values + * @return TRelatedModel + */ + public function firstOrCreate(array $attributes, array $values = []); + + /** + * Create or update a related record matching the attributes, and fill it with values. + * + * @param array, mixed> $attributes + * @param array $values + * @return TRelatedModel + */ + public function updateOrCreate(array $attributes, array $values = []); + + /** + * Attach a model instance to the parent model. + * + * @param \Illuminate\Database\Eloquent\Model $model + * @return TRelatedModel|false + */ + public function save(\Illuminate\Database\Eloquent\Model $model); + + /** + * @phpstan-param array, mixed> $attributes + * + * @phpstan-return TRelatedModel + */ + public function create(array $attributes = []); + + /** + * Create a Collection of new instances of the related model. + * + * @param iterable $records + * @return \Illuminate\Database\Eloquent\Collection + */ + public function createMany(iterable $records); +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/HasOneThrough.stub b/php-packages/phpstan/stubs/Illuminate/Database/HasOneThrough.stub new file mode 100644 index 000000000..222878f9e --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/HasOneThrough.stub @@ -0,0 +1,24 @@ + + */ +class HasOneThrough extends HasManyThrough +{ + /** + * @param array, mixed> $attributes + * + * @phpstan-return TRelatedModel + */ + public function create(array $attributes = []); + + /** + * Get the results of the relationship. + * + * @phpstan-return \Illuminate\Database\Eloquent\Collection + */ + public function getResults(); +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/Model.stub b/php-packages/phpstan/stubs/Illuminate/Database/Model.stub new file mode 100644 index 000000000..a71b3a027 --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/Model.stub @@ -0,0 +1,28 @@ + + */ +abstract class Model implements \JsonSerializable, \ArrayAccess +{ + /** + * Update the model in the database. + * + * @param array, mixed> $attributes + * @param array $options + * @return bool + */ + public function update(array $attributes = [], array $options = []); + + /** + * Begin querying a model with eager loading. + * + * @param non-empty-string|array $relations + * @return \Illuminate\Database\Eloquent\Builder + */ + public static function with($relations); +} + +class ModelNotFoundException extends \RuntimeException {} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/MorphMany.stub b/php-packages/phpstan/stubs/Illuminate/Database/MorphMany.stub new file mode 100644 index 000000000..4543315c0 --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/MorphMany.stub @@ -0,0 +1,24 @@ + + */ +class MorphMany extends MorphOneOrMany +{ + /** + * @param array, mixed> $attributes + * + * @phpstan-return TRelatedModel + */ + public function create(array $attributes = []); + + /** + * Get the results of the relationship. + * + * @phpstan-return \Illuminate\Database\Eloquent\Collection + */ + public function getResults(); +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/MorphOne.stub b/php-packages/phpstan/stubs/Illuminate/Database/MorphOne.stub new file mode 100644 index 000000000..bf5e437fa --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/MorphOne.stub @@ -0,0 +1,17 @@ + + */ +class MorphOne extends MorphOneOrMany +{ + /** + * Get the results of the relationship. + * + * @phpstan-return ?TRelatedModel + */ + public function getResults(); +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/MorphOneOrMany.stub b/php-packages/phpstan/stubs/Illuminate/Database/MorphOneOrMany.stub new file mode 100644 index 000000000..c3b775b79 --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/MorphOneOrMany.stub @@ -0,0 +1,17 @@ + + */ +abstract class MorphOneOrMany extends HasOneOrMany +{ + /** + * @param array, mixed> $attributes + * + * @phpstan-return TRelatedModel + */ + public function create(array $attributes = []); +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/MorphTo.stub b/php-packages/phpstan/stubs/Illuminate/Database/MorphTo.stub new file mode 100644 index 000000000..2904af2ce --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/MorphTo.stub @@ -0,0 +1,11 @@ + + */ +class MorphTo extends BelongsTo +{} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/MorphToMany.stub b/php-packages/phpstan/stubs/Illuminate/Database/MorphToMany.stub new file mode 100644 index 000000000..65d0ec38c --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/MorphToMany.stub @@ -0,0 +1,11 @@ + + */ +class MorphToMany extends BelongsToMany +{ +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/QueryBuilder.stub b/php-packages/phpstan/stubs/Illuminate/Database/QueryBuilder.stub new file mode 100644 index 000000000..5641034e4 --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/QueryBuilder.stub @@ -0,0 +1,1440 @@ +|mixed $columns + * @return $this + */ + public function select($columns = ['*']) + {} + + /** + * Add a subselect expression to the query. + * + * @param \Closure|$this|non-empty-string $query + * @param non-empty-string $as + * @return $this + * + * @throws \InvalidArgumentException + */ + public function selectSub($query, $as) + {} + + /** + * Add a new "raw" select expression to the query. + * + * @param non-empty-string $expression + * @param array $bindings + * @return $this + */ + public function selectRaw($expression, array $bindings = []) + {} + + /** + * Makes "from" fetch from a subquery. + * + * @param \Closure|\Illuminate\Database\Query\Builder|non-empty-string $query + * @param non-empty-string $as + * @return $this + * + * @throws \InvalidArgumentException + */ + public function fromSub($query, $as) + {} + + /** + * Add a raw from clause to the query. + * + * @param non-empty-string $expression + * @param mixed $bindings + * @return $this + */ + public function fromRaw($expression, $bindings = []) + {} + + /** + * Creates a subquery and parse it. + * + * @param \Closure|\Illuminate\Database\Query\Builder|non-empty-string $query + * @return array + */ + protected function createSub($query) + {} + + /** + * Parse the subquery into SQL and bindings. + * + * @param mixed $query + * @return array + * + * @throws \InvalidArgumentException + */ + protected function parseSub($query) + {} + + /** + * Add a new select column to the query. + * + * @param array|mixed $column + * @return $this + */ + public function addSelect($column) + {} + + /** + * Force the query to only return distinct results. + * + * @return $this + */ + public function distinct() + {} + + /** + * Set the table which the query is targeting. + * + * @param \Closure|\Illuminate\Database\Query\Builder|non-empty-string $table + * @param non-empty-string|null $as + * @return $this + */ + public function from($table, $as = null) + {} + + /** + * Add a join clause to the query. + * + * @param non-empty-string $table + * @param \Closure|non-empty-string $first + * @param non-empty-string|null $operator + * @param non-empty-string|null $second + * @param non-empty-string $type + * @param bool $where + * @return $this + */ + public function join($table, $first, $operator = null, $second = null, $type = 'inner', $where = false) + {} + + /** + * Add a "join where" clause to the query. + * + * @param non-empty-string $table + * @param \Closure|non-empty-string $first + * @param non-empty-string $operator + * @param non-empty-string $second + * @param non-empty-string $type + * @return $this + */ + public function joinWhere($table, $first, $operator, $second, $type = 'inner') + {} + + /** + * Add a subquery join clause to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|non-empty-string $query + * @param non-empty-string $as + * @param \Closure|non-empty-string $first + * @param non-empty-string|null $operator + * @param non-empty-string|null $second + * @param non-empty-string $type + * @param bool $where + * @return $this + * + * @throws \InvalidArgumentException + */ + public function joinSub($query, $as, $first, $operator = null, $second = null, $type = 'inner', $where = false) + {} + + /** + * Add a left join to the query. + * + * @param non-empty-string $table + * @param \Closure|non-empty-string $first + * @param non-empty-string|null $operator + * @param non-empty-string|null $second + * @return $this + */ + public function leftJoin($table, $first, $operator = null, $second = null) + {} + + /** + * Add a "join where" clause to the query. + * + * @param non-empty-string $table + * @param \Closure|non-empty-string $first + * @param non-empty-string $operator + * @param non-empty-string $second + * @return $this + */ + public function leftJoinWhere($table, $first, $operator, $second) + {} + + /** + * Add a subquery left join to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|non-empty-string $query + * @param non-empty-string $as + * @param \Closure|non-empty-string $first + * @param non-empty-string|null $operator + * @param non-empty-string|null $second + * @return $this + */ + public function leftJoinSub($query, $as, $first, $operator = null, $second = null) + {} + + /** + * Add a right join to the query. + * + * @param non-empty-string $table + * @param \Closure|non-empty-string $first + * @param non-empty-string|null $operator + * @param non-empty-string|null $second + * @return $this + */ + public function rightJoin($table, $first, $operator = null, $second = null) + {} + + /** + * Add a "right join where" clause to the query. + * + * @param non-empty-string $table + * @param \Closure|non-empty-string $first + * @param non-empty-string $operator + * @param non-empty-string $second + * @return $this + */ + public function rightJoinWhere($table, $first, $operator, $second) + {} + + /** + * Add a subquery right join to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|non-empty-string $query + * @param non-empty-string $as + * @param \Closure|non-empty-string $first + * @param non-empty-string|null $operator + * @param non-empty-string|null $second + * @return $this + */ + public function rightJoinSub($query, $as, $first, $operator = null, $second = null) + {} + + /** + * Add a "cross join" clause to the query. + * + * @param non-empty-string $table + * @param \Closure|string|null $first + * @param non-empty-string|null $operator + * @param non-empty-string|null $second + * @return $this + */ + public function crossJoin($table, $first = null, $operator = null, $second = null) + {} + + /** + * Add a basic where clause to the query. + * + * @param \Closure|string|array $column + * @param mixed $operator + * @param mixed $value + * @param non-empty-string $boolean + * @return $this + */ + public function where($column, $operator = null, $value = null, $boolean = 'and') + {} + + /** + * Add an array of where clauses to the query. + * + * @param array $column + * @param non-empty-string $boolean + * @param non-empty-string $method + * @return $this + */ + protected function addArrayOfWheres($column, $boolean, $method = 'where') + {} + + /** + * Prepare the value and operator for a where clause. + * + * @param non-empty-string $value + * @param non-empty-string $operator + * @param bool $useDefault + * @return array + * + * @throws \InvalidArgumentException + */ + public function prepareValueAndOperator($value, $operator, $useDefault = false) + {} + + /** + * Determine if the given operator and value combination is legal. + * + * Prevents using Null values with invalid operators. + * + * @param non-empty-string $operator + * @param mixed $value + * @return bool + */ + protected function invalidOperatorAndValue($operator, $value) + {} + + /** + * Determine if the given operator is supported. + * + * @param non-empty-string $operator + * @return bool + */ + protected function invalidOperator($operator) + {} + + /** + * Add an "or where" clause to the query. + * + * @param \Closure|array $column + * @param mixed $operator + * @param mixed $value + * @return $this + */ + public function orWhere($column, $operator = null, $value = null) + {} + + /** + * Add a "where" clause comparing two columns to the query. + * + * @param array $first + * @param non-empty-string|null $operator + * @param non-empty-string|null $second + * @param non-empty-string|null $boolean + * @return $this + */ + public function whereColumn($first, $operator = null, $second = null, $boolean = 'and') + {} + + /** + * Add an "or where" clause comparing two columns to the query. + * + * @param non-empty-string|array $first + * @param non-empty-string|null $operator + * @param non-empty-string|null $second + * @return $this + */ + public function orWhereColumn($first, $operator = null, $second = null) + {} + + /** + * Add a raw where clause to the query. + * + * @param non-empty-string $sql + * @param mixed $bindings + * @param non-empty-string $boolean + * @return $this + */ + public function whereRaw($sql, $bindings = [], $boolean = 'and') + {} + + /** + * Add a raw or where clause to the query. + * + * @param non-empty-string $sql + * @param mixed $bindings + * @return $this + */ + public function orWhereRaw($sql, $bindings = []) + {} + + /** + * Add a "where in" clause to the query. + * + * @param non-empty-string $column + * @param mixed $values + * @param non-empty-string $boolean + * @param bool $not + * @return $this + */ + public function whereIn($column, $values, $boolean = 'and', $not = false) + {} + + /** + * Add an "or where in" clause to the query. + * + * @param non-empty-string $column + * @param mixed $values + * @return $this + */ + public function orWhereIn($column, $values) + {} + + /** + * Add a "where not in" clause to the query. + * + * @param non-empty-string $column + * @param mixed $values + * @param non-empty-string $boolean + * @return $this + */ + public function whereNotIn($column, $values, $boolean = 'and') + {} + + /** + * Add an "or where not in" clause to the query. + * + * @param non-empty-string $column + * @param mixed $values + * @return $this + */ + public function orWhereNotIn($column, $values) + {} + + /** + * Add a "where in raw" clause for integer values to the query. + * + * @param non-empty-string $column + * @param \Illuminate\Contracts\Support\Arrayable|array $values + * @param non-empty-string $boolean + * @param bool $not + * @return $this + */ + public function whereIntegerInRaw($column, $values, $boolean = 'and', $not = false) + {} + + /** + * Add an "or where in raw" clause for integer values to the query. + * + * @param non-empty-string $column + * @param \Illuminate\Contracts\Support\Arrayable|array $values + * @return $this + */ + public function orWhereIntegerInRaw($column, $values) + {} + + /** + * Add a "where not in raw" clause for integer values to the query. + * + * @param non-empty-string $column + * @param \Illuminate\Contracts\Support\Arrayable|array $values + * @param non-empty-string $boolean + * @return $this + */ + public function whereIntegerNotInRaw($column, $values, $boolean = 'and') + {} + + /** + * Add an "or where not in raw" clause for integer values to the query. + * + * @param non-empty-string $column + * @param \Illuminate\Contracts\Support\Arrayable|array $values + * @return $this + */ + public function orWhereIntegerNotInRaw($column, $values) + {} + + /** + * Add a "where null" clause to the query. + * + * @param non-empty-string|array $columns + * @param non-empty-string $boolean + * @param bool $not + * @return $this + */ + public function whereNull($columns, $boolean = 'and', $not = false) + {} + + /** + * Add an "or where null" clause to the query. + * + * @param non-empty-string $column + * @return $this + */ + public function orWhereNull($column) + {} + + /** + * Add a "where not null" clause to the query. + * + * @param non-empty-string|array $columns + * @param non-empty-string $boolean + * @return $this + */ + public function whereNotNull($columns, $boolean = 'and') + {} + + /** + * Add a where between statement to the query. + * + * @param non-empty-string $column + * @param array $values + * @param non-empty-string $boolean + * @param bool $not + * @return $this + */ + public function whereBetween($column, array $values, $boolean = 'and', $not = false) + {} + + /** + * Add an or where between statement to the query. + * + * @param non-empty-string $column + * @param array $values + * @return $this + */ + public function orWhereBetween($column, array $values) + {} + + /** + * Add a where not between statement to the query. + * + * @param non-empty-string $column + * @param array $values + * @param non-empty-string $boolean + * @return $this + */ + public function whereNotBetween($column, array $values, $boolean = 'and') + {} + + /** + * Add an or where not between statement to the query. + * + * @param non-empty-string $column + * @param array $values + * @return $this + */ + public function orWhereNotBetween($column, array $values) + {} + + /** + * Add an "or where not null" clause to the query. + * + * @param non-empty-string $column + * @return $this + */ + public function orWhereNotNull($column) + {} + + /** + * Add a "where date" statement to the query. + * + * @param non-empty-string $column + * @param non-empty-string $operator + * @param \DateTimeInterface|string|null $value + * @param non-empty-string $boolean + * @return $this + */ + public function whereDate($column, $operator, $value = null, $boolean = 'and') + {} + + /** + * Add an "or where date" statement to the query. + * + * @param non-empty-string $column + * @param non-empty-string $operator + * @param \DateTimeInterface|string|null $value + * @return $this + */ + public function orWhereDate($column, $operator, $value = null) + {} + + /** + * Add a "where time" statement to the query. + * + * @param non-empty-string $column + * @param non-empty-string $operator + * @param \DateTimeInterface|string|null $value + * @param non-empty-string $boolean + * @return $this + */ + public function whereTime($column, $operator, $value = null, $boolean = 'and') + {} + + /** + * Add an "or where time" statement to the query. + * + * @param non-empty-string $column + * @param non-empty-string $operator + * @param \DateTimeInterface|string|null $value + * @return $this + */ + public function orWhereTime($column, $operator, $value = null) + {} + + /** + * Add a "where day" statement to the query. + * + * @param non-empty-string $column + * @param non-empty-string $operator + * @param \DateTimeInterface|string|null $value + * @param non-empty-string $boolean + * @return $this + */ + public function whereDay($column, $operator, $value = null, $boolean = 'and') + {} + + /** + * Add an "or where day" statement to the query. + * + * @param non-empty-string $column + * @param non-empty-string $operator + * @param \DateTimeInterface|string|null $value + * @return $this + */ + public function orWhereDay($column, $operator, $value = null) + {} + + /** + * Add a "where month" statement to the query. + * + * @param non-empty-string $column + * @param non-empty-string $operator + * @param \DateTimeInterface|string|null $value + * @param non-empty-string $boolean + * @return $this + */ + public function whereMonth($column, $operator, $value = null, $boolean = 'and') + {} + + /** + * Add an "or where month" statement to the query. + * + * @param non-empty-string $column + * @param non-empty-string $operator + * @param \DateTimeInterface|string|null $value + * @return $this + */ + public function orWhereMonth($column, $operator, $value = null) + {} + + /** + * Add a "where year" statement to the query. + * + * @param non-empty-string $column + * @param non-empty-string $operator + * @param \DateTimeInterface|string|int|null $value + * @param non-empty-string $boolean + * @return $this + */ + public function whereYear($column, $operator, $value = null, $boolean = 'and') + {} + + /** + * Add an "or where year" statement to the query. + * + * @param non-empty-string $column + * @param non-empty-string $operator + * @param \DateTimeInterface|string|int|null $value + * @return $this + */ + public function orWhereYear($column, $operator, $value = null) + {} + + /** + * Add a date based (year, month, day, time) statement to the query. + * + * @param non-empty-string $type + * @param non-empty-string $column + * @param non-empty-string $operator + * @param mixed $value + * @param non-empty-string $boolean + * @return $this + */ + protected function addDateBasedWhere($type, $column, $operator, $value, $boolean = 'and') + {} + + /** + * Add a nested where statement to the query. + * + * @param \Closure $callback + * @param non-empty-string $boolean + * @return $this + */ + public function whereNested(\Closure $callback, $boolean = 'and') + {} + + /** + * Create a new query instance for nested where condition. + * + * @return $this + */ + public function forNestedWhere() + {} + + /** + * Add another query builder as a nested where to the query builder. + * + * @param $this $query + * @param non-empty-string $boolean + * @return $this + */ + public function addNestedWhereQuery($query, $boolean = 'and') + {} + + /** + * Add a full sub-select to the query. + * + * @param non-empty-string $column + * @param non-empty-string $operator + * @param \Closure $callback + * @param non-empty-string $boolean + * @return $this + */ + protected function whereSub($column, $operator, \Closure $callback, $boolean) + {} + + /** + * Add an exists clause to the query. + * + * @param \Closure $callback + * @param non-empty-string $boolean + * @param bool $not + * @return $this + */ + public function whereExists(\Closure $callback, $boolean = 'and', $not = false) + {} + + /** + * Add an or exists clause to the query. + * + * @param \Closure $callback + * @param bool $not + * @return $this + */ + public function orWhereExists(\Closure $callback, $not = false) + {} + + /** + * Add a where not exists clause to the query. + * + * @param \Closure $callback + * @param non-empty-string $boolean + * @return $this + */ + public function whereNotExists(\Closure $callback, $boolean = 'and') + {} + + /** + * Add a where not exists clause to the query. + * + * @param \Closure $callback + * @return $this + */ + public function orWhereNotExists(\Closure $callback) + {} + + /** + * Add an exists clause to the query. + * + * @param \Illuminate\Database\Query\Builder $query + * @param non-empty-string $boolean + * @param bool $not + * @return $this + */ + public function addWhereExistsQuery(self $query, $boolean = 'and', $not = false) + {} + + /** + * Adds a where condition using row values. + * + * @param array $columns + * @param non-empty-string $operator + * @param array $values + * @param non-empty-string $boolean + * @return $this + * + * @throws \InvalidArgumentException + */ + public function whereRowValues($columns, $operator, $values, $boolean = 'and') + {} + + /** + * Adds a or where condition using row values. + * + * @param array $columns + * @param non-empty-string $operator + * @param array $values + * @return $this + */ + public function orWhereRowValues($columns, $operator, $values) + {} + + /** + * Add a "where JSON contains" clause to the query. + * + * @param non-empty-string $column + * @param mixed $value + * @param non-empty-string $boolean + * @param bool $not + * @return $this + */ + public function whereJsonContains($column, $value, $boolean = 'and', $not = false) + {} + + /** + * Add a "or where JSON contains" clause to the query. + * + * @param non-empty-string $column + * @param mixed $value + * @return $this + */ + public function orWhereJsonContains($column, $value) + {} + + /** + * Add a "where JSON not contains" clause to the query. + * + * @param non-empty-string $column + * @param mixed $value + * @param non-empty-string $boolean + * @return $this + */ + public function whereJsonDoesntContain($column, $value, $boolean = 'and') + {} + + /** + * Add a "or where JSON not contains" clause to the query. + * + * @param non-empty-string $column + * @param mixed $value + * @return $this + */ + public function orWhereJsonDoesntContain($column, $value) + {} + + /** + * Add a "where JSON length" clause to the query. + * + * @param non-empty-string $column + * @param mixed $operator + * @param mixed $value + * @param non-empty-string $boolean + * @return $this + */ + public function whereJsonLength($column, $operator, $value = null, $boolean = 'and') + {} + + /** + * Add a "or where JSON length" clause to the query. + * + * @param non-empty-string $column + * @param mixed $operator + * @param mixed $value + * @return $this + */ + public function orWhereJsonLength($column, $operator, $value = null) + {} + + /** + * Handles dynamic "where" clauses to the query. + * + * @param non-empty-string $method + * @param array $parameters + * @return $this + */ + public function dynamicWhere($method, $parameters) + {} + + /** + * Add a single dynamic where clause statement to the query. + * + * @param non-empty-string $segment + * @param non-empty-string $connector + * @param array $parameters + * @param int $index + * @return void + */ + protected function addDynamic($segment, $connector, $parameters, $index) + {} + + /** + * Add a "group by" clause to the query. + * + * @param array|non-empty-string ...$groups + * @return $this + */ + public function groupBy(...$groups) + {} + + /** + * Add a raw groupBy clause to the query. + * + * @param non-empty-string $sql + * @param array $bindings + * @return $this + */ + public function groupByRaw($sql, array $bindings = []) + {} + + /** + * Add a "having" clause to the query. + * + * @param non-empty-string $column + * @param non-empty-string|null $operator + * @param non-empty-string|null $value + * @param non-empty-string $boolean + * @return $this + */ + public function having($column, $operator = null, $value = null, $boolean = 'and') + {} + + /** + * Add a "or having" clause to the query. + * + * @param non-empty-string $column + * @param non-empty-string|null $operator + * @param non-empty-string|null $value + * @return $this + */ + public function orHaving($column, $operator = null, $value = null) + {} + + /** + * Add a "having between " clause to the query. + * + * @param non-empty-string $column + * @param array $values + * @param non-empty-string $boolean + * @param bool $not + * @return $this + */ + public function havingBetween($column, array $values, $boolean = 'and', $not = false) + {} + + /** + * Add a raw having clause to the query. + * + * @param non-empty-string $sql + * @param array $bindings + * @param non-empty-string $boolean + * @return $this + */ + public function havingRaw($sql, array $bindings = [], $boolean = 'and') + {} + + /** + * Add a raw or having clause to the query. + * + * @param non-empty-string $sql + * @param array $bindings + * @return $this + */ + public function orHavingRaw($sql, array $bindings = []) + {} + + /** + * Add an "order by" clause to the query. + * + * @param \Closure|\Illuminate\Database\Query\Builder|\Illuminate\Database\Query\Expression|non-empty-string $column + * @param non-empty-string $direction + * @return $this + * + * @throws \InvalidArgumentException + */ + public function orderBy($column, $direction = 'asc') + {} + + /** + * Add a descending "order by" clause to the query. + * + * @param non-empty-string $column + * @return $this + */ + public function orderByDesc($column) + {} + + /** + * Add an "order by" clause for a timestamp to the query. + * + * @param non-empty-string|\Illuminate\Database\Query\Expression $column + * @return $this + */ + public function latest($column = 'created_at') + {} + + /** + * Add an "order by" clause for a timestamp to the query. + * + * @param non-empty-string|\Illuminate\Database\Query\Expression $column + * @return $this + */ + public function oldest($column = 'created_at') + {} + + /** + * Put the query's results in random order. + * + * @param non-empty-string $seed + * @return $this + */ + public function inRandomOrder($seed = '') + {} + + /** + * Add a raw "order by" clause to the query. + * + * @param non-empty-string $sql + * @param array $bindings + * @return $this + */ + public function orderByRaw($sql, $bindings = []) + {} + + /** + * Alias to set the "offset" value of the query. + * + * @param int $value + * @return $this + */ + public function skip($value) + {} + + /** + * Set the "offset" value of the query. + * + * @param int $value + * @return $this + */ + public function offset($value) + {} + + /** + * Alias to set the "limit" value of the query. + * + * @param int $value + * @return $this + */ + public function take($value) + {} + + /** + * Set the "limit" value of the query. + * + * @param int $value + * @return $this + */ + public function limit($value) + {} + + /** + * Set the limit and offset for a given page. + * + * @param int $page + * @param int $perPage + * @return $this + */ + public function forPage($page, $perPage = 15) + {} + + /** + * Constrain the query to the previous "page" of results before a given ID. + * + * @param int $perPage + * @param int|null $lastId + * @param non-empty-string $column + * @return $this + */ + public function forPageBeforeId($perPage = 15, $lastId = 0, $column = 'id') + {} + + /** + * Constrain the query to the next "page" of results after a given ID. + * + * @param int $perPage + * @param int|null $lastId + * @param non-empty-string $column + * @return $this + */ + public function forPageAfterId($perPage = 15, $lastId = 0, $column = 'id') + {} + + /** + * Remove all existing orders and optionally add a new order. + * + * @param non-empty-string|null $column + * @param non-empty-string $direction + * @return $this + */ + public function reorder($column = null, $direction = 'asc') + {} + + /** + * Get an array with all orders with a given column removed. + * + * @param non-empty-string $column + * @return array + */ + protected function removeExistingOrdersFor($column) + {} + + /** + * Add a union statement to the query. + * + * @param \Illuminate\Database\Query\Builder|\Closure $query + * @param bool $all + * @return $this + */ + public function union($query, $all = false) + {} + + /** + * Add a union all statement to the query. + * + * @param \Illuminate\Database\Query\Builder|\Closure $query + * @return $this + */ + public function unionAll($query) + {} + + /** + * Lock the selected rows in the table. + * + * @param non-empty-string|bool $value + * @return $this + */ + public function lock($value = true) + {} + + /** + * Lock the selected rows in the table for updating. + * + * @return static + */ + public function lockForUpdate() + {} + + /** + * Share lock the selected rows in the table. + * + * @return $this + */ + public function sharedLock() + {} + + /** + * Execute a query for a single record by ID. + * + * @param int|non-empty-string $id + * @param array $columns + * @return mixed|static + */ + public function find($id, $columns = ['*']) + {} + + /** + * Throw an exception if the query doesn't have an orderBy clause. + * + * @return void + * + * @throws \RuntimeException + */ + protected function enforceOrderBy() + {} + + /** + * Get an array with the values of a given column. + * + * @param non-empty-string|\Illuminate\Database\Query\Expression $column + * @param non-empty-string|null $key + * @return \Illuminate\Support\Collection + */ + public function pluck($column, $key = null) + {} + + /** + * Concatenate values of a given column as a string. + * + * @param non-empty-string $column + * @param non-empty-string $glue + * @return string + */ + public function implode($column, $glue = '') + {} + + /** + * Determine if any rows exist for the current query. + * + * @return bool + */ + public function exists() + {} + + /** + * Determine if no rows exist for the current query. + * + * @return bool + */ + public function doesntExist() + {} + + /** + * Execute the given callback if no rows exist for the current query. + * + * @param \Closure $callback + * @return mixed + */ + public function existsOr(\Closure $callback) + {} + + /** + * Execute the given callback if rows exist for the current query. + * + * @param \Closure $callback + * @return mixed + */ + public function doesntExistOr(\Closure $callback) + {} + + /** + * Retrieve the "count" result of the query. + * + * @param non-empty-string $columns + * @return int + */ + public function count($columns = '*') + {} + + /** + * Retrieve the minimum value of a given column. + * + * @param non-empty-string $column + * @return mixed + */ + public function min($column) + {} + + /** + * Retrieve the maximum value of a given column. + * + * @param non-empty-string $column + * @return mixed + */ + public function max($column) + {} + + /** + * Retrieve the sum of the values of a given column. + * + * @param non-empty-string $column + * @return mixed + */ + public function sum($column) + {} + + /** + * Retrieve the average of the values of a given column. + * + * @param non-empty-string $column + * @return mixed + */ + public function avg($column) + {} + + /** + * Alias for the "avg" method. + * + * @param non-empty-string $column + * @return mixed + */ + public function average($column) + {} + + /** + * Execute an aggregate function on the database. + * + * @param non-empty-string $function + * @param array $columns + * @return mixed + */ + public function aggregate($function, $columns = ['*']) + {} + + /** + * Execute a numeric aggregate function on the database. + * + * @param non-empty-string $function + * @param array $columns + * @return float|int + */ + public function numericAggregate($function, $columns = ['*']) + {} + + /** + * Set the aggregate property without running the query. + * + * @param non-empty-string $function + * @param array $columns + * @return $this + */ + protected function setAggregate($function, $columns) + {} + + /** + * Execute the given callback while selecting the given columns. + * + * After running the callback, the columns are reset to the original value. + * + * @param array $columns + * @param callable $callback + * @return mixed + */ + protected function onceWithColumns($columns, $callback) + {} + + /** + * Insert a new record into the database. + * + * @param array $values + * @return bool + */ + public function insert(array $values) + {} + + /** + * Insert a new record into the database while ignoring errors. + * + * @param array $values + * @return int + */ + public function insertOrIgnore(array $values) + {} + + /** + * Insert a new record and get the value of the primary key. + * + * @param array $values + * @param non-empty-string|null $sequence + * @return int + */ + public function insertGetId(array $values, $sequence = null) + {} + + /** + * Insert new records into the table using a subquery. + * + * @param array $columns + * @param \Closure|\Illuminate\Database\Query\Builder|non-empty-string $query + * @return int + */ + public function insertUsing(array $columns, $query) + {} + + /** + * Update a record in the database. + * + * @param array $values + * @return int + */ + public function update(array $values) + {} + + /** + * Insert or update a record matching the attributes, and fill it with values. + * + * @param array $attributes + * @param array $values + * @return bool + */ + public function updateOrInsert(array $attributes, array $values = []) + {} + + /** + * Increment a column's value by a given amount. + * + * @param non-empty-string|\Illuminate\Database\Query\Expression $column + * @param float|int $amount + * @param array $extra + * @return int + * + * @throws \InvalidArgumentException + */ + public function increment($column, $amount = 1, array $extra = []) + {} + + /** + * Decrement a column's value by a given amount. + * + * @param non-empty-string|\Illuminate\Database\Query\Expression $column + * @param float|int $amount + * @param array $extra + * @return int + * + * @throws \InvalidArgumentException + */ + public function decrement($column, $amount = 1, array $extra = []) + {} + + /** + * Delete a record from the database. + * + * @param mixed $id + * @return int + */ + public function delete($id = null) + {} + + /** + * Run a truncate statement on the table. + * + * @return void + */ + public function truncate() + {} + + /** + * Get a new instance of the query builder. + * + * @return $this + */ + public function newQuery() + {} + + /** + * Get the current query value bindings in a flattened array. + * + * @return array + */ + public function getBindings() + {} + + /** + * Get the raw array of bindings. + * + * @return array + */ + public function getRawBindings() + {} + + /** + * Set the bindings on the query builder. + * + * @param array $bindings + * @param non-empty-string $type + * @return $this + * + * @throws \InvalidArgumentException + */ + public function setBindings(array $bindings, $type = 'where') + {} + + /** + * Add a binding to the query. + * + * @param mixed $value + * @param non-empty-string $type + * @return $this + * + * @throws \InvalidArgumentException + */ + public function addBinding($value, $type = 'where') + {} + + /** + * Merge an array of bindings into our bindings. + * + * @param \Illuminate\Database\Query\Builder $query + * @return $this + */ + public function mergeBindings(self $query) + {} +} diff --git a/php-packages/phpstan/stubs/Illuminate/Database/Relation.stub b/php-packages/phpstan/stubs/Illuminate/Database/Relation.stub new file mode 100644 index 000000000..dff906308 --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Database/Relation.stub @@ -0,0 +1,24 @@ + $columns + * @phpstan-return \Illuminate\Database\Eloquent\Collection + */ + public function get($columns = ['*']); + + + /** + * @param array, mixed> $attributes + * @phpstan-return TRelatedModel + */ + public function make(array $attributes = []); +} diff --git a/php-packages/phpstan/stubs/Illuminate/Enumerable.stub b/php-packages/phpstan/stubs/Illuminate/Enumerable.stub new file mode 100644 index 000000000..7fd2c1ea5 --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Enumerable.stub @@ -0,0 +1,58 @@ + + */ +interface Enumerable extends \Countable, \IteratorAggregate, \JsonSerializable +{ + /** + * @param string|callable(TValue, TKey): bool $key + * @param mixed $operator + * @param mixed $value + * @return static + */ + public function partition($key, $operator = null, $value = null); + + /** + * @param string|callable(TValue, TKey): mixed $keyBy + * @return static + */ + public function keyBy($keyBy); + + /** + * @param callable(TValue, TKey): array $callback + * @return static + */ + public function mapWithKeys(callable $callback); + + /** + * @param callable(TValue, TKey): array $callback + * @return static> + */ + public function mapToDictionary(callable $callback); + + /** + * @param string|callable(TValue, TKey): bool $key + * @param mixed $operator + * @param mixed $value + * @return bool + */ + public function every($key, $operator = null, $value = null); + + /** + * @param int $size + * @return static + */ + public function chunk($size); + + /** + * @param callable(static): void $callable + * @return static + */ + public function tap($callable); +} diff --git a/php-packages/phpstan/stubs/Illuminate/EnumeratesValues.stub b/php-packages/phpstan/stubs/Illuminate/EnumeratesValues.stub new file mode 100644 index 000000000..6cc58f849 --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/EnumeratesValues.stub @@ -0,0 +1,36 @@ + $average + * @property-read HigherOrderCollectionProxy<'avg', TValue> $avg + * @property-read HigherOrderCollectionProxy<'contains', TValue> $contains + * @property-read HigherOrderCollectionProxy<'each', TValue> $each + * @property-read HigherOrderCollectionProxy<'every', TValue> $every + * @property-read HigherOrderCollectionProxy<'filter', TValue> $filter + * @property-read HigherOrderCollectionProxy<'first', TValue> $first + * @property-read HigherOrderCollectionProxy<'flatMap', TValue> $flatMap + * @property-read HigherOrderCollectionProxy<'groupBy', TValue> $groupBy + * @property-read HigherOrderCollectionProxy<'keyBy', TValue> $keyBy + * @property-read HigherOrderCollectionProxy<'map', TValue> $map + * @property-read HigherOrderCollectionProxy<'max', TValue> $max + * @property-read HigherOrderCollectionProxy<'min', TValue> $min + * @property-read HigherOrderCollectionProxy<'partition', TValue> $partition + * @property-read HigherOrderCollectionProxy<'reject', TValue> $reject + * @property-read HigherOrderCollectionProxy<'some', TValue> $some + * @property-read HigherOrderCollectionProxy<'sortBy', TValue> $sortBy + * @property-read HigherOrderCollectionProxy<'sortByDesc', TValue> $sortByDesc + * @property-read HigherOrderCollectionProxy<'skipUntil', TValue> $skipUntil + * @property-read HigherOrderCollectionProxy<'skipWhile', TValue> $skipWhile + * @property-read HigherOrderCollectionProxy<'sum', TValue> $sum + * @property-read HigherOrderCollectionProxy<'takeUntil', TValue> $takeUntil + * @property-read HigherOrderCollectionProxy<'takeWhile', TValue> $takeWhile + * @property-read HigherOrderCollectionProxy<'unique', TValue> $unique + * @property-read HigherOrderCollectionProxy<'until', TValue> $until + */ +trait EnumeratesValues +{} diff --git a/php-packages/phpstan/stubs/Illuminate/Facades.stub b/php-packages/phpstan/stubs/Illuminate/Facades.stub new file mode 100644 index 000000000..30f2089af --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Facades.stub @@ -0,0 +1,30 @@ + $data + * @param array $mergeData + * @return mixed + */ +function view($view = null, $data = [], $mergeData = []) +{ +} diff --git a/php-packages/phpstan/stubs/Illuminate/HigherOrderProxies.stub b/php-packages/phpstan/stubs/Illuminate/HigherOrderProxies.stub new file mode 100644 index 000000000..b3cf0f30a --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/HigherOrderProxies.stub @@ -0,0 +1,24 @@ + $data + * @return $this + */ + public function markdown($view, array $data = []) + {} + + /** + * @param view-string $view + * @param array $data + * @return $this + */ + public function view($view, array $data = []) + {} +} diff --git a/php-packages/phpstan/stubs/Illuminate/Pagination.stub b/php-packages/phpstan/stubs/Illuminate/Pagination.stub new file mode 100644 index 000000000..8c57cd079 --- /dev/null +++ b/php-packages/phpstan/stubs/Illuminate/Pagination.stub @@ -0,0 +1,23 @@ + + * @implements \IteratorAggregate + */ +class Paginator extends AbstractPaginator implements \Illuminate\Contracts\Support\Arrayable, \ArrayAccess, \Countable, \IteratorAggregate, \Illuminate\Contracts\Support\Jsonable, \JsonSerializable, \Illuminate\Contracts\Pagination\Paginator +{} + +/** + * @implements \ArrayAccess + * @implements \IteratorAggregate + */ +class LengthAwarePaginator extends AbstractPaginator implements \Illuminate\Contracts\Support\Arrayable, \ArrayAccess, \Countable, \IteratorAggregate, \Illuminate\Contracts\Support\Jsonable, \JsonSerializable, \Illuminate\Contracts\Pagination\LengthAwarePaginator +{}