Skip to content

Add RootResolverSignatureRector to auto-fix root resolver signatures#2779

Open
spawnia wants to merge 42 commits into
masterfrom
root-resolver-signature-rector-rule
Open

Add RootResolverSignatureRector to auto-fix root resolver signatures#2779
spawnia wants to merge 42 commits into
masterfrom
root-resolver-signature-rector-rule

Conversation

@spawnia

@spawnia spawnia commented Jul 10, 2026

Copy link
Copy Markdown
Collaborator

Summary

Adds a configurable Rector rule (RootResolverSignatureRector) that normalizes root resolver method signatures to match the canonical ($root, array $args, GraphQLContext $context, ResolveInfo $resolveInfo) pattern. Supports custom parameter names, custom context class, and PHP version-aware typing (mixed for 8.0+, null for 8.2+).

Single-param handling:

  • (array $args) — prepends root: (null $_, array $args). This is the only single-param case that triggers prepending.
  • All other single-param cases (null $_, mixed $root, untyped $whatever) — strips the param entirely: (). These are useless root params or wrong signatures that static analysis will surface after removal.

Known limitation: the rule assumes every __invoke class directly under the configured resolver namespaces is a root resolver. Non-root-resolver classes (e.g. nested field resolvers receiving a parent model as first argument) should be moved out of those directories or added to the Rector skip list. See docs/master/testing/rector.md.

CI configuration

  • Remove rector/rector from PHPStan CI job (dependency conflicts with phpstan)
  • Exclude src/Rector and tests/Unit/Rector from PHPStan paths (rector unavailable after removal)
  • Bump larastan/larastan to ^3.10 (compatible with phpstan ^2.2 that rector requires)
  • Pin PHP 8.2 target in Rector test configs (fixtures expect null $root)
  • Keep rector/rector in PHPUnit job (needed for Rector rule tests)

Key files

  • src/Rector/RootResolverSignatureRector.php — the Rector rule
  • src/Schema/RootType.php — added non-empty validation for namespaces, reindex to guarantee list
  • tests/Unit/Rector/RootResolverSignatureRector/ — test suite
  • tests/Unit/Schema/RootTypeTest.php — RootType::namespaces() tests
  • .github/workflows/validate.yml — CI matrix (modified)
  • docs/master/testing/rector.md — documentation (moved from api-reference/resolvers.md)

spawnia added 15 commits July 10, 2026 10:35
The rule calls RootType::namespaces() which requires config() to be
available. Instead of silently falling back to defaults when config() is
unavailable, fail loudly as the spec requires. The test bootstrap loads
the Larastan bootstrap and registers the Lighthouse config.

🤖 Generated with Claude Code
Follow the project convention of protected over private for extensibility.
Also remove redundant $objectType->equals() check (isSuperTypeOf covers it).

🤖 Generated with Claude Code
🤖 Generated with Claude Code
When a resolver had multiple params with the first typed array (e.g.
__invoke(array $root, array $args)), or a single untyped param, the rule
would incorrectly prepend a new $root, creating duplicate variable names.

Now only considers root as missing when there is exactly one param that
is either untyped or typed array.

🤖 Generated with Claude Code
Assert the value is an array (satisfies PHPStan level-8 type narrowing)
and throw InvalidConfigurationException for non-string, non-null elements.

🤖 Generated with Claude Code
Instead of silently doing nothing when config() has no Lighthouse
namespaces loaded, throw a RuntimeException guiding the user to add
the required bootstrapFiles entry.

🤖 Generated with Claude Code
- Move fixParamType(0, ...) into else branch (no-op after prependRootParam)
- Split isset() && fix() into nested ifs (one-thing-per-line)
- Replace ensureMinParams while-loop with direct ensureArgsParam
- Extract fixObjectParam from duplicate fixContextParam/fixResolveInfoParam

🤖 Generated with Claude Code
Cover wrong ResolveInfo type and untyped context/resolveInfo params.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new Rector rule to automatically fix Lighthouse root resolver __invoke method signatures so they match Lighthouse’s positional calling convention and avoid runtime TypeErrors. It also centralizes resolver namespace lookup in RootType and documents the new behavior and tooling.

Changes:

  • Add RootResolverSignatureRector to detect/fix root resolver __invoke parameter order, presence, types, and (optionally) names.
  • Extract RootType::namespaces() and reuse it from FieldValue.
  • Add unit tests/fixtures plus user docs and changelog entry.

Reviewed changes

Copilot reviewed 31 out of 32 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
tests/Unit/Rector/RootResolverSignatureRector/Source/CustomContext.php Test-only custom context implementing GraphQLContext.
tests/Unit/Rector/RootResolverSignatureRector/RootResolverSignatureRectorWithNamesTest.php Test runner for fixtures with name normalization config.
tests/Unit/Rector/RootResolverSignatureRector/RootResolverSignatureRectorTest.php Main rule test runner.
tests/Unit/Rector/RootResolverSignatureRector/RootResolverSignatureRectorPartialNamesTest.php Test runner for partial name normalization config.
tests/Unit/Rector/RootResolverSignatureRector/RootResolverSignatureRectorConfigTest.php Tests for invalid configuration rejection.
tests/Unit/Rector/RootResolverSignatureRector/FixtureWithNames/normalize_names.php.inc Fixture validating parameter name normalization behavior.
tests/Unit/Rector/RootResolverSignatureRector/FixturePartialNames/normalize_partial.php.inc Fixture validating partial parameter renaming.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/wrong_second_param.php.inc Fixture for correcting $args type.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/wrong_root_type.php.inc Fixture for correcting $root type.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/wrong_resolve_info_type.php.inc Fixture for correcting ResolveInfo type.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/wrong_context_type.php.inc Fixture for correcting GraphQLContext type.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/untyped_first_param.php.inc Fixture for prepending missing $root.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/untyped_context_and_resolve_info.php.inc Fixture for typing $context/$resolveInfo.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/skip_zero_params.php.inc Fixture ensuring 0-param __invoke is skipped.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/skip_subdirectory.php.inc Fixture ensuring nested namespaces are skipped.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/skip_non_resolver_namespace.php.inc Fixture ensuring non-resolver namespaces are skipped.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/skip_custom_context.php.inc Fixture ensuring custom context types are not rewritten.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/skip_correct_null.php.inc Fixture ensuring already-correct signature is skipped.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/single_wrong_root_param.php.inc Fixture for single-param root fix + args insertion.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/narrow_mixed_root.php.inc Fixture for narrowing mixed $root to null on PHP 8.2+.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/missing_root_param.php.inc Fixture for detecting missing $root when first param looks like $args.
tests/Unit/Rector/RootResolverSignatureRector/Fixture/array_root_with_args.php.inc Fixture for correcting invalid $root types.
tests/Unit/Rector/RootResolverSignatureRector/config/configured_rule.php Rector test config enabling rule (and importNames()).
tests/Unit/Rector/RootResolverSignatureRector/config/configured_rule_with_names.php Rector test config enabling rule with paramNames.
tests/Unit/Rector/RootResolverSignatureRector/config/configured_rule_partial_names.php Rector test config enabling partial paramNames.
tests/Unit/Rector/RootResolverSignatureRector/bootstrap.php Test bootstrap to load Larastan + Lighthouse config.
src/Schema/Values/FieldValue.php Switch to shared RootType::namespaces() for root parent namespaces.
src/Schema/RootType.php Add RootType::namespaces() helper to resolve configured namespaces.
src/Rector/RootResolverSignatureRector.php New Rector rule implementing signature/type/name corrections.
docs/master/api-reference/resolvers.md Document root resolver convention + Rector rule usage/config.
CHANGELOG.md Note new Rector rule in Unreleased section.
.gitignore Ignore docs/superpowers.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Rector/RootResolverSignatureRector.php
Comment thread src/Rector/RootResolverSignatureRector.php
Comment thread docs/master/api-reference/resolvers.md Outdated
Comment thread CHANGELOG.md Outdated
spawnia added 12 commits July 10, 2026 13:23
Use Rector's VariableRenamer to update all references to the old
parameter name within the method body, not just the signature.

#2779 (comment)

🤖 Generated with Claude Code
The standalone null type requires PHP 8.2+, but the package supports ^8.
Use mixed as the safe default and note that PHP 8.2+ can use null.

#2779 (comment)

🤖 Generated with Claude Code
Assertions can be disabled at runtime. Use a proper exception to
guarantee validation regardless of PHP assert settings.

#2779 (comment)

🤖 Generated with Claude Code
🤖 Generated with Claude Code
PHPUnit 9 (Laravel 9) does not support PHP 8 attributes for data
providers. Dual-annotating with both PHPDoc and attributes ensures
tests run across the full CI matrix.

🤖 Generated with Claude Code
AbstractRectorTestCase sets up error/exception handlers that are not
removed, triggering PHPUnit's risky test detection. Since configure()
does not use constructor dependencies, use newInstanceWithoutConstructor
to sidestep mocking final classes.

🤖 Generated with Claude Code
Remove rector from PHPStan CI job (it's only needed for PHPUnit tests).
Rector's presence causes phpstan to resolve to incompatible versions:
- rector/rector 1.x pulls phpstan to versions where Composer\InstalledVersions
  class discovery breaks
- rector/rector 2.x requires phpstan ^2.2.2 which conflicts with larastan <3.10

Bump larastan minimum to ^3.10 (first version compatible with phpstan ^2.2).
Revert phpstan minimum to ^1.12.18 || ^2 since removing rector from the
PHPStan job eliminates the version conflict.

🤖 Generated with Claude Code
rector/rector is removed in the PHPStan CI job to avoid dependency
conflicts, so its classes are unavailable for static analysis.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 34 out of 35 changed files in this pull request and generated 4 comments.

Comment thread src/Rector/RootResolverSignatureRector.php Outdated
Comment thread src/Rector/RootResolverSignatureRector.php
Comment thread src/Rector/RootResolverSignatureRector.php Outdated
Comment thread src/Rector/RootResolverSignatureRector.php Outdated
Comment thread src/Rector/RootResolverSignatureRector.php Outdated
Comment thread src/Rector/RootResolverSignatureRector.php Outdated
Comment thread src/Rector/RootResolverSignatureRector.php Outdated
Comment thread src/Schema/RootType.php Outdated
Comment thread tests/Unit/Rector/RootResolverSignatureRector/bootstrap.php Outdated
spawnia added 3 commits July 15, 2026 11:24
Throw RuntimeException when configured namespaces are empty or contain
non-string values, return type is now non-empty-array<int, string>.

Remove redundant empty check in RootResolverSignatureRector since
RootType::namespaces() now handles this.

#2779 (comment)

🤖 Generated with Claude Code
- Use named args for CodeSample and Param constructors
- Break compound conditionals into multiple lines
- Extract variables for readability
- Use mixed in CodeSample (valid on all PHP ^8)
- Make test fixtures more realistic (no pre-existing imports)
- Use config() helper in bootstrap

#2779

🤖 Generated with Claude Code
Fixtures expect `null $root` which is only valid on PHP 8.2+.
Without pinning, tests would fail on PHP 8.0/8.1 where Rector
produces `mixed $root` instead.

#2779

🤖 Generated with Claude Code
@spawnia spawnia marked this pull request as ready for review July 15, 2026 10:20
@spawnia spawnia requested a review from Copilot July 15, 2026 10:20
Comment thread src/Rector/RootResolverSignatureRector.php Outdated
Comment thread src/Rector/RootResolverSignatureRector.php Outdated
Comment thread src/Schema/RootType.php Outdated

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 36 changed files in this pull request and generated 4 comments.

Comment thread src/Schema/RootType.php Outdated
Comment thread src/Rector/RootResolverSignatureRector.php
Comment thread src/Rector/RootResolverSignatureRector.php
Comment thread docs/master/api-reference/resolvers.md
spawnia and others added 4 commits July 15, 2026 12:57
Config may have string keys when defined as an associative array.

#2779

🤖 Generated with Claude Code
When a resolver intentionally declares only a root param (e.g. `null $_`
or `mixed $root`) with no args, the rule was incorrectly appending
`array \$args`. ensureArgsParam now only fires when the root param was
prepended by the rule itself (i.e. the resolver had array/untyped args
as its only param), not when the root param already existed and was
merely type-fixed.

Add skip_root_only_param.php.inc fixture to cover this case, and update
single_wrong_root_param.php.inc to expect only a type fix.

🤖 Generated with Claude Code
Single-param resolvers that don't match the canonical args signature
(array $args) are now stripped entirely rather than having an args
param incorrectly added. Only `(array $args)` triggers root prepending.

🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 37 out of 38 changed files in this pull request and generated 2 comments.

Comment thread docs/master/api-reference/resolvers.md Outdated
Comment thread src/Rector/RootResolverSignatureRector.php
spawnia added 6 commits July 16, 2026 13:41
Associative arrays from config would preserve string keys through
array spread, potentially causing collisions that drop namespaces.

#2779 (comment)

🤖 Generated with Claude Code
🤖 Generated with Claude Code

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 50 out of 51 changed files in this pull request and generated 6 comments.

Comment on lines +83 to +90
foreach ($paramNames as $name) {
if ($name !== null && ! is_string($name)) {
throw new InvalidConfigurationException('Each paramNames element must be a string or null.');
}
}

$this->paramNames = $paramNames;
}
Comment on lines +192 to +195
$type = $method->params[0]->type;

return $type instanceof Identifier && $type->name === 'array';
}
Comment on lines +205 to +207
if ($type instanceof Identifier && $type->name === 'array') {
return false;
}
Comment on lines +124 to +126
if ($rootWasPrepended && $this->ensureArgsParam($invokeMethod)) {
$changed = true;
}
public function __invoke(array $args): mixed {}

// After
public function __invoke(null $root, array $args): mixed {}
public function __invoke(User $root, array $args): mixed {}

// After
public function __invoke(null $root, array $args): mixed {}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants