芝麻web文件管理V1.00
编辑当前文件:/home/sditechnicalteam/www/vendor/nunomaduro/larastan/src/Rules/NoUnnecessaryCollectionCallRule.php
count() * could be simplified to: * User::count() * * In addition, this code: * User::whereStatus('active')->get()->pluck('id') * could be simplified to: * User::whereStatus('active')->pluck('id') * * @implements Rule
*/ class NoUnnecessaryCollectionCallRule implements Rule { /** * The method names that can be applied on a Collection, but should be applied on a Builder. * * @var string[] */ protected const RISKY_METHODS = [ 'count', 'isempty', 'isnotempty', 'last', 'pop', 'skip', 'slice', 'take', // Eloquent collection 'diff', 'except', 'find', 'modelkeys', 'only', ]; /** * The method names that can be applied on a Collection, but should in some cases be applied * on a builder depending on what parameter is passed to the method. * * @var string[] */ protected const RISKY_PARAM_METHODS = [ 'average', 'avg', 'firstwhere', 'max', 'min', 'pluck', 'sum', 'where', 'wherestrict', ]; /** @var string[] The method names that should be checked by this rule - can be configured by the user. */ protected $shouldHandle; /** * @var \PHPStan\Reflection\ReflectionProvider */ protected $reflectionProvider; /** * @var \NunoMaduro\Larastan\Properties\ModelPropertyExtension */ protected $propertyExtension; /** * NoRedundantCollectionCallRule constructor. * * @param ReflectionProvider $reflectionProvider * @param ModelPropertyExtension $propertyExtension * @param string[] $onlyMethods * @param string[] $excludeMethods */ public function __construct( ReflectionProvider $reflectionProvider, ModelPropertyExtension $propertyExtension, array $onlyMethods, array $excludeMethods ) { $this->reflectionProvider = $reflectionProvider; $this->propertyExtension = $propertyExtension; $allMethods = array_merge( static::RISKY_METHODS, static::RISKY_PARAM_METHODS, [ 'first', 'contains', 'containsstrict', ] ); if (! empty($onlyMethods)) { $this->shouldHandle = array_map(function (string $methodName): string { return Str::lower($methodName); }, $onlyMethods); } else { $this->shouldHandle = $allMethods; } if (! empty($excludeMethods)) { $this->shouldHandle = array_diff($this->shouldHandle, array_map(function (string $methodName): string { return Str::lower($methodName); }, $excludeMethods)); } } /** * @return string */ public function getNodeType(): string { return MethodCall::class; } /** * @param Node $node * @param Scope $scope * @return string[] */ public function processNode(Node $node, Scope $scope): array { /** @var \PhpParser\Node\Expr\MethodCall $node */ if (! $node->name instanceof Identifier) { return []; } /** @var \PhpParser\Node\Identifier $name */ $name = $node->name; if (! $this->isCalledOnCollection($node->var, $scope)) { // Method was not called on a collection, so no errors. return []; } $previousCall = $node->var; if (! $this->callIsQuery($previousCall, $scope)) { // Previous call wasn't on a Builder, so no errors. return []; } /** @var Node\Expr\MethodCall|Node\Expr\StaticCall $previousCall */ if (! ($previousCall->name instanceof Identifier)) { // Previous call was made dynamically e.g. User::query()->{$method}() // Can't really analyze it in this scenario so no errors. return []; } if (! in_array($name->toLowerString(), $this->shouldHandle, true)) { return []; } if ($name->toLowerString() === 'first') { if (count($node->args) === 0) { // 'first', also accepts a closure as an argument. return [$this->formatError($name->toString())]; } } elseif ($this->isRiskyMethod($name)) { return [$this->formatError($name->toString())]; } elseif ($this->isRiskyParamMethod($name)) { if (count($node->args) === 0) { // Calling e.g. DB::table()->pluck($columnName)-sum() // We have to check whether $columnName is actually a database column // and not an alias for some computed attribute if ($previousCall->name->name === 'pluck' && $this->firstArgIsDatabaseColumn($previousCall, $scope)) { return [$this->formatError($name->toString())]; } return []; } if ($this->firstArgIsDatabaseColumn($node, $scope)) { return [$this->formatError($name->toString())]; } } elseif (in_array($name->toLowerString(), ['contains', 'containsstrict'], true)) { // 'contains' can also be called with Model instances or keys as its first argument /** @var \PhpParser\Node\Arg[] $args */ $args = $node->args; if (count($args) === 1 && ! ($args[0]->value instanceof Node\Expr\Closure)) { return [$this->formatError($name->toString())]; } if ($this->firstArgIsDatabaseColumn($node, $scope)) { return [$this->formatError($name->toString())]; } } return []; } /** * Determines whether the first argument is a string and references a database column. * * @param Node\Expr\StaticCall|MethodCall $node * @param Scope $scope * @return bool */ protected function firstArgIsDatabaseColumn($node, Scope $scope): bool { /** @var \PhpParser\Node\Arg[] $args */ $args = $node->args; if (count($args) === 0 || ! ($args[0]->value instanceof Node\Scalar\String_)) { return false; } if ($node instanceof Node\Expr\StaticCall) { /** @var Node\Name $class */ $class = $node->class; $modelReflection = $this->reflectionProvider->getClass($class->toCodeString()); /** @var \PhpParser\Node\Scalar\String_ $firstArg */ $firstArg = $args[0]->value; return $this->propertyExtension->hasProperty($modelReflection, $firstArg->value); } $iterableType = $scope->getType($node->var)->getIterableValueType(); if ($iterableType instanceof MixedType) { $previous_call = $node->var; if ($previous_call instanceof MethodCall) { $query_builder_type = $scope->getType($previous_call->var); if ((new ObjectType(QueryBuilder::class))->isSuperTypeOf($query_builder_type)->yes()) { // We encountered a DB query such as DB::table(..)->get()->max('id') // We assume max('id') could have been retrieved without calling get(). return true; } } return false; } /** @var \PHPStan\Type\ObjectType $iterableType */ if ((new ObjectType(Model::class))->isSuperTypeOf($iterableType)->yes()) { $modelReflection = $this->reflectionProvider->getClass($iterableType->getClassName()); /** @var \PhpParser\Node\Scalar\String_ $firstArg */ $firstArg = $args[0]->value; return $this->propertyExtension->hasProperty($modelReflection, $firstArg->value); } return false; } /** * Returns whether the method call is a call on a builder instance. * * @param Node\Expr $call * @param Scope $scope * @return bool */ protected function callIsQuery(Node\Expr $call, Scope $scope): bool { if ($call instanceof MethodCall) { $calledOn = $scope->getType($call->var); return $this->isBuilder($calledOn); } if ($call instanceof Node\Expr\StaticCall) { /** @var Node\Name $class */ $class = $call->class; if ($class instanceof Node\Name) { $modelClassName = $class->toCodeString(); return (new ObjectType(Model::class))->isSuperTypeOf(new ObjectType($modelClassName))->yes() && $call->name instanceof Identifier && in_array($call->name->toLowerString(), ['get', 'all', 'pluck'], true); } } return false; } /** * Returns whether the method is one of the risky methods. * * @param Identifier $name * @return bool */ protected function isRiskyMethod(Identifier $name): bool { return in_array($name->toLowerString(), self::RISKY_METHODS, true); } /** * Returns whether the method might be a risky method depending on the parameters passed. * * @param Identifier $name * @return bool */ protected function isRiskyParamMethod(Identifier $name): bool { return in_array($name->toLowerString(), self::RISKY_PARAM_METHODS, true); } /** * Returns whether its argument is some builder instance. * * @param Type $type * @return bool */ protected function isBuilder(Type $type): bool { return (new ObjectType(EloquentBuilder::class))->isSuperTypeOf($type)->yes() || (new ObjectType(QueryBuilder::class))->isSuperTypeOf($type)->yes() || (new ObjectType(Relation::class))->isSuperTypeOf($type)->yes(); } /** * Returns whether the Expr was not called on a Collection instance. * * @param Node\Expr $expr * @param Scope $scope * @return bool */ protected function isCalledOnCollection(Node\Expr $expr, Scope $scope): bool { $calledOnType = $scope->getType($expr); return (new ObjectType(Collection::class))->isSuperTypeOf($calledOnType)->yes(); } /** * Formats the error. * * @param string $method_name * @return string */ protected function formatError(string $method_name): string { return "Called '{$method_name}' on Laravel collection, but could have been retrieved as a query."; } }