Created
May 7, 2022 08:02
-
-
Save Gummibeer/8bce2aa120563858167218fc68e03b1d to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
namespace App\PHPStan\Rules; | |
use Illuminate\Database\Eloquent\Builder as EloquentBuilder; | |
use Illuminate\Database\Eloquent\Model; | |
use Illuminate\Database\Query\Builder; | |
use Illuminate\Support\Facades\DB; | |
use PhpParser\Node; | |
use PhpParser\Node\Expr\MethodCall; | |
use PhpParser\Node\Expr\StaticCall; | |
use PhpParser\Node\Identifier; | |
use PhpParser\Node\Name\FullyQualified; | |
use PHPStan\Analyser\Scope; | |
use PHPStan\Reflection\ParametersAcceptorSelector; | |
use PHPStan\Reflection\Php\DummyParameter; | |
use PHPStan\Rules\Rule; | |
use PHPStan\Rules\RuleErrorBuilder; | |
use PHPStan\Type\ObjectType; | |
class QueryBuilderWhereOperatorRule implements Rule | |
{ | |
public function getNodeType(): string | |
{ | |
return MethodCall::class; | |
} | |
/** | |
* @param \PhpParser\Node\Expr\MethodCall $node | |
* @param \PHPStan\Analyser\Scope $scope | |
* @return array | |
*/ | |
public function processNode(Node $node, Scope $scope): array | |
{ | |
$name = $this->nodeName($node); | |
if($name === null || !str_starts_with($name, 'where')){ | |
return []; | |
} | |
$classNode = $node; | |
do { | |
$classNode = $classNode->var; | |
} while(property_exists($classNode, 'var') && !$classNode instanceof StaticCall); | |
if(!$classNode instanceof StaticCall) { | |
return []; | |
} | |
if (! $this->isCalledOnModel($classNode)) { | |
return []; | |
} | |
$staticName = $this->nodeName($classNode); | |
if($staticName === null) { | |
return []; | |
} | |
$class = $classNode->class; | |
if (!$class instanceof FullyQualified) { | |
return []; | |
} | |
$object = new ObjectType($class->toString()); | |
$method = $object->getMethod($staticName, $scope); | |
$returnType = ParametersAcceptorSelector::selectSingle($method->getVariants())->getReturnType(); | |
if(!$returnType instanceof ObjectType) { | |
return []; | |
} | |
if(!$this->isQueryBuilder($returnType)) { | |
return []; | |
} | |
$index = array_search( | |
'operator', | |
array_map( | |
fn(DummyParameter $parameter) => $parameter->getName(), | |
ParametersAcceptorSelector::selectSingle($returnType->getMethod($name, $scope)->getVariants())->getParameters() | |
) | |
); | |
if($index === false) { | |
return []; | |
} | |
$operator = $node->args[$index]->value; | |
if(!$operator instanceof Node\Scalar\String_) { | |
return [ | |
RuleErrorBuilder::message("Called '{$returnType->getClassName()}->{$name}()' with a non-string \$operator.") | |
->identifier('rules.queryBuilderWhereOperator') | |
->line($node->getLine()) | |
->file($scope->getFile()) | |
->build(), | |
]; | |
} | |
if(!in_array($operator->value, DB::connection()->query()->operators)) { | |
return [ | |
RuleErrorBuilder::message("Called '{$returnType->getClassName()}->{$name}()' without an allowed \$operator.") | |
->identifier('rules.queryBuilderWhereOperator') | |
->line($node->getLine()) | |
->file($scope->getFile()) | |
->build(), | |
]; | |
} | |
return []; | |
} | |
protected function nodeName(Node $node): ?string | |
{ | |
$name = $node?->name; | |
if (! $name instanceof Identifier) { | |
return null; | |
} | |
return $name->name; | |
} | |
protected function isCalledOnModel(StaticCall $call): bool | |
{ | |
$class = $call->class; | |
if ($class instanceof FullyQualified) { | |
$type = new ObjectType($class->toString()); | |
} else { | |
return false; | |
} | |
return (new ObjectType(Model::class)) | |
->isSuperTypeOf($type) | |
->yes(); | |
} | |
protected function isQueryBuilder(ObjectType $type): bool | |
{ | |
return (new ObjectType(Builder::class))->isSuperTypeOf($type)->yes() | |
|| (new ObjectType(EloquentBuilder::class))->isSuperTypeOf($type)->yes(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment