-
-
Notifications
You must be signed in to change notification settings - Fork 469
Add RootResolverSignatureRector to auto-fix root resolver signatures #2779
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 15 commits
01508dd
3683814
be9a69c
e6f85c1
89bcd0b
1110380
96d6bfb
9d88837
13435be
6d7dacd
2ac1ac8
7acf822
1840f90
03a4ef9
ebab0a4
d9f02dd
4db7570
5c95630
046cee7
332fee5
585638b
be1c194
837450d
a657821
20d9943
ed8b242
c21ea81
ced5f40
5b695b5
6c85faf
db79118
4934a86
c633c64
3d1eaa5
f14e4ba
b6d2dec
60a42e1
3597bd9
9cfe2e1
d368c88
82e6e14
3ab761e
9bba73b
dacceb4
37fe9e8
5d21444
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,6 +7,7 @@ | |
| .vscode | ||
| phpunit.xml | ||
| .gitpod.yml | ||
| docs/superpowers | ||
|
|
||
| # Generated files | ||
| .phpunit.result.cache | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,291 @@ | ||
| <?php declare(strict_types=1); | ||
|
|
||
| namespace Nuwave\Lighthouse\Rector; | ||
|
|
||
| use Nuwave\Lighthouse\Schema\RootType; | ||
| use PhpParser\Node; | ||
| use PhpParser\Node\Expr\Variable; | ||
| use PhpParser\Node\Identifier; | ||
| use PhpParser\Node\Name\FullyQualified; | ||
| use PhpParser\Node\Param; | ||
| use PhpParser\Node\Stmt\Class_; | ||
| use PhpParser\Node\Stmt\ClassMethod; | ||
| use PHPStan\Type\ObjectType; | ||
| use Rector\Contract\Rector\ConfigurableRectorInterface; | ||
| use Rector\Exception\Configuration\InvalidConfigurationException; | ||
| use Rector\Php\PhpVersionProvider; | ||
| use Rector\Rector\AbstractRector; | ||
| use Rector\ValueObject\PhpVersion; | ||
| use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample; | ||
| use Symplify\RuleDocGenerator\ValueObject\RuleDefinition; | ||
|
|
||
| class RootResolverSignatureRector extends AbstractRector implements ConfigurableRectorInterface | ||
|
Check failure on line 22 in src/Rector/RootResolverSignatureRector.php
|
||
| { | ||
| /** @var array<int, string|null> */ | ||
| protected array $paramNames = []; | ||
|
|
||
| public function __construct( | ||
| protected PhpVersionProvider $phpVersionProvider, | ||
|
Check failure on line 28 in src/Rector/RootResolverSignatureRector.php
|
||
| ) {} | ||
|
|
||
| public function getRuleDefinition(): RuleDefinition | ||
|
Check failure on line 31 in src/Rector/RootResolverSignatureRector.php
|
||
| { | ||
| return new RuleDefinition('Fix root resolver __invoke signatures to match Lighthouse calling convention', [ | ||
|
Check failure on line 33 in src/Rector/RootResolverSignatureRector.php
|
||
| new CodeSample( | ||
|
Check failure on line 34 in src/Rector/RootResolverSignatureRector.php
|
||
| <<<'CODE_SAMPLE' | ||
| namespace App\GraphQL\Queries; | ||
|
|
||
| class Users | ||
| { | ||
| public function __invoke(array $args) | ||
| { | ||
| return []; | ||
| } | ||
| } | ||
| CODE_SAMPLE, | ||
| <<<'CODE_SAMPLE' | ||
| namespace App\GraphQL\Queries; | ||
|
|
||
| class Users | ||
| { | ||
| public function __invoke(null $root, array $args) | ||
|
spawnia marked this conversation as resolved.
Outdated
|
||
| { | ||
| return []; | ||
| } | ||
| } | ||
| CODE_SAMPLE, | ||
| ), | ||
|
spawnia marked this conversation as resolved.
|
||
| ]); | ||
| } | ||
|
|
||
| /** @return array<class-string<Node>> */ | ||
| public function getNodeTypes(): array | ||
| { | ||
| return [Class_::class]; | ||
| } | ||
|
|
||
| /** @param array<string, mixed> $configuration */ | ||
| public function configure(array $configuration): void | ||
| { | ||
| $paramNames = $configuration['paramNames'] ?? []; | ||
| assert(is_array($paramNames), 'paramNames must be an array.'); | ||
|
|
||
|
spawnia marked this conversation as resolved.
|
||
| if (count($paramNames) > 4) { | ||
| throw new InvalidConfigurationException('paramNames must have at most 4 elements.'); | ||
|
Check failure on line 74 in src/Rector/RootResolverSignatureRector.php
|
||
| } | ||
|
|
||
| foreach ($paramNames as $name) { | ||
| if ($name !== null && ! is_string($name)) { | ||
| throw new InvalidConfigurationException('Each paramNames element must be a string or null.'); | ||
|
Check failure on line 79 in src/Rector/RootResolverSignatureRector.php
|
||
| } | ||
| } | ||
|
|
||
| $this->paramNames = $paramNames; | ||
| } | ||
|
spawnia marked this conversation as resolved.
|
||
|
|
||
| /** @param Class_ $node */ | ||
| public function refactor(Node $node): ?Node | ||
| { | ||
| if (! $this->isRootResolver($node)) { | ||
| return null; | ||
| } | ||
|
|
||
| $invokeMethod = $node->getMethod('__invoke'); | ||
| if (! $invokeMethod instanceof ClassMethod) { | ||
| return null; | ||
| } | ||
|
|
||
| if ($invokeMethod->params === []) { | ||
| return null; | ||
| } | ||
|
|
||
| $changed = false; | ||
|
|
||
| if ($this->isMissingRootParam($invokeMethod)) { | ||
| $this->prependRootParam($invokeMethod); | ||
| $changed = true; | ||
| } elseif ($this->fixParamType($invokeMethod, 0, $this->rootTypeIdentifier())) { | ||
| $changed = true; | ||
| } | ||
|
|
||
| if ($this->ensureArgsParam($invokeMethod)) { | ||
| $changed = true; | ||
| } | ||
|
|
||
| if ($this->fixParamType($invokeMethod, 1, new Identifier('array'))) { | ||
| $changed = true; | ||
| } | ||
|
|
||
| if (isset($invokeMethod->params[2]) && $this->fixObjectParam($invokeMethod, 2, \Nuwave\Lighthouse\Support\Contracts\GraphQLContext::class)) { | ||
|
spawnia marked this conversation as resolved.
Outdated
|
||
| $changed = true; | ||
| } | ||
|
|
||
| if (isset($invokeMethod->params[3]) && $this->fixObjectParam($invokeMethod, 3, \Nuwave\Lighthouse\Execution\ResolveInfo::class)) { | ||
| $changed = true; | ||
| } | ||
|
|
||
| if ($this->normalizeNames($invokeMethod)) { | ||
| $changed = true; | ||
| } | ||
|
|
||
| if (! $changed) { | ||
| return null; | ||
| } | ||
|
|
||
| return $node; | ||
| } | ||
|
|
||
| protected function isRootResolver(Class_ $node): bool | ||
| { | ||
| $fqcn = $node->namespacedName?->toString(); | ||
| if ($fqcn === null) { | ||
| return false; | ||
| } | ||
|
|
||
| foreach ($this->resolverNamespaces() as $namespace) { | ||
| if ($this->isDirectChildOfNamespace($fqcn, $namespace)) { | ||
| return true; | ||
| } | ||
| } | ||
|
|
||
| return false; | ||
| } | ||
|
|
||
| /** | ||
| * Subscriptions are excluded — their resolver convention differs (they use subscriber classes, not __invoke). | ||
| * | ||
| * @return non-empty-list<string> | ||
| */ | ||
| protected function resolverNamespaces(): array | ||
| { | ||
| $namespaces = [ | ||
| ...RootType::namespaces(RootType::QUERY), | ||
| ...RootType::namespaces(RootType::MUTATION), | ||
| ]; | ||
|
spawnia marked this conversation as resolved.
|
||
|
|
||
| if ($namespaces === []) { | ||
| throw new \RuntimeException('Lighthouse resolver namespaces are empty. Ensure your Rector config includes bootstrapFiles with the Larastan bootstrap and Lighthouse config loaded.'); | ||
|
spawnia marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| return $namespaces; | ||
| } | ||
|
spawnia marked this conversation as resolved.
|
||
|
|
||
| protected function isDirectChildOfNamespace(string $fqcn, string $namespace): bool | ||
| { | ||
| return str_starts_with($fqcn, $namespace . '\\') | ||
| && ! str_contains(substr($fqcn, strlen($namespace) + 1), '\\'); | ||
| } | ||
|
|
||
| protected function isMissingRootParam(ClassMethod $method): bool | ||
| { | ||
| if (count($method->params) !== 1) { | ||
| return false; | ||
| } | ||
|
|
||
| $firstParam = $method->params[0]; | ||
|
|
||
| return $firstParam->type === null | ||
| || ($firstParam->type instanceof Identifier && $firstParam->type->name === 'array'); | ||
|
spawnia marked this conversation as resolved.
Outdated
|
||
| } | ||
|
|
||
| protected function prependRootParam(ClassMethod $method): void | ||
| { | ||
| $rootParam = new Param( | ||
| new Variable('root'), | ||
| null, | ||
| $this->rootTypeIdentifier(), | ||
| ); | ||
|
|
||
| array_unshift($method->params, $rootParam); | ||
| } | ||
|
|
||
| protected function rootTypeIdentifier(): Identifier | ||
| { | ||
| if ($this->phpVersionProvider->isAtLeastPhpVersion(PhpVersion::PHP_82)) { | ||
| return new Identifier('null'); | ||
| } | ||
|
|
||
| return new Identifier('mixed'); | ||
| } | ||
|
|
||
| protected function fixParamType(ClassMethod $method, int $index, Identifier $expectedType): bool | ||
| { | ||
| if (! isset($method->params[$index])) { | ||
| return false; | ||
| } | ||
|
|
||
| $param = $method->params[$index]; | ||
| $currentType = $param->type; | ||
|
|
||
| if ($currentType instanceof Identifier && $currentType->name === $expectedType->name) { | ||
|
spawnia marked this conversation as resolved.
Outdated
|
||
| return false; | ||
| } | ||
|
|
||
| $param->type = $expectedType; | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| protected function ensureArgsParam(ClassMethod $method): bool | ||
| { | ||
| if (count($method->params) >= 2) { | ||
| return false; | ||
| } | ||
|
|
||
| $method->params[] = new Param(new Variable('args'), null, new Identifier('array')); | ||
|
spawnia marked this conversation as resolved.
Outdated
|
||
|
|
||
| return true; | ||
| } | ||
|
|
||
| protected function fixObjectParam(ClassMethod $method, int $index, string $expectedClass): bool | ||
| { | ||
| $param = $method->params[$index]; | ||
| $currentType = $param->type; | ||
|
|
||
| if ($currentType instanceof FullyQualified || $currentType instanceof Node\Name) { | ||
|
spawnia marked this conversation as resolved.
Outdated
|
||
| $objectType = new ObjectType($currentType->toString()); | ||
| $expectedType = new ObjectType($expectedClass); | ||
|
|
||
| if ($expectedType->isSuperTypeOf($objectType)->yes()) { | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| $param->type = new FullyQualified($expectedClass); | ||
|
|
||
| return true; | ||
| } | ||
|
|
||
| protected function normalizeNames(ClassMethod $method): bool | ||
| { | ||
| if ($this->paramNames === []) { | ||
| return false; | ||
| } | ||
|
|
||
| $changed = false; | ||
|
|
||
| foreach ($this->paramNames as $index => $name) { | ||
| if ($name === null) { | ||
| continue; | ||
| } | ||
|
|
||
| if (! isset($method->params[$index])) { | ||
| continue; | ||
| } | ||
|
|
||
| $param = $method->params[$index]; | ||
| if (! $param->var instanceof Variable) { | ||
| continue; | ||
| } | ||
|
|
||
| if ($param->var->name === $name) { | ||
| continue; | ||
| } | ||
|
|
||
| $param->var = new Variable($name); | ||
|
spawnia marked this conversation as resolved.
|
||
| $changed = true; | ||
| } | ||
|
|
||
| return $changed; | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.