Skip to content

Walk each declared type once for matching and unwrapping - #952

Merged
oryan-block merged 4 commits into
agent-friendlyfrom
agent-friendly-p10-type-shape
Nov 6, 2026
Merged

oryan-block merged 4 commits into
agent-friendlyfrom
agent-friendly-p10-type-shape

Conversation

@oryan-block

@oryan-block oryan-block commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Why

Agents working in graphql-java-tools keep repeating the same mistakes, and the repo should change so a change that looks right from one file is right for the whole repo. Resolver return values are one of the worst spots for that. Three places walked the same declared type: TypeClassMatcher.PotentialMatch.unwrap for the class the schema build binds, GenericTypeResolver.resolve (called again from MethodFieldResolver for the value class) and ReturnValueUnwrapper for what the data fetcher unwraps. So a fix to how a return type is unwrapped had to land in more than one file. E.g. d60c8520 (a DataFetcherResult inside an Optional) had to change both TypeClassMatcher and ReturnValueUnwrapper.

Checklist

  • Pull requests follows the contribution guide
  • New or modified functionality is covered by tests

Description

New resolver/TypeShape.kt walks a return or parameter type against its GraphQL type once and keeps both results: the leaf TypeClassMatcher binds and the layers TypeShape.Unwrapper unwraps a method's value through. GenericTypeResolver.kt, resolver/ReturnValueUnwrapper.kt and PotentialMatch / Unwrapping / Unwrapped are gone.

  • DataFetcherResult and Optional are one private enum the walk iterates. Each entry has an abstract placement rule and value step, so a new container doesn't compile without both
  • a new entry also fails ReturnValueUnwrappingTest with a MODEL ERROR until the bounded check has a Container for it. Before, an entry with a stubbed step passed every test, since Check.java keeps its own container list
  • both generic wrapper rules live in one class, side by side: exact declared type at schema build, most specific instance at fetch. They differ on purpose and stay that way
  • the subscription Publisher view is taken inside TypeShape, next to the rule that leaves a subscription's events alone
  • the value class (what a top-level Future is checked against) comes from the same walk instead of a second resolve. It's still only computed for methods, since an untyped Map's value type is a Classmate ResolvedType that TypeUtils.getRawType throws on
  • resolveTypeVariables stays a separate top-level function, since argument conversion needs wrappers, wildcards and primitives kept. The raw class and assignability helpers became plain TypeUtils calls in the three files that used them
  • scanForMatches returns a lazy Sequence, so each shape is still walked only when the scanner gets to it, like PotentialMatch.unwrapping was. Otherwise a schema with two errors could report a different one first
  • property and Map fields match through TypeShape and still return their raw values. The next PR routes them through the unwrapper and deletes the known findings
  • the verify-return-value-unwrapping skill named the deleted classes in its trigger, coverage table and sync command. It now points at TypeShape.kt, so a change there triggers it and shows up in its staleness check
  • the AGENTS.md rule table names TypeShape for "a resolver method's return value is unwrapped by one rule"
  • merged agent-friendly after Merge each SDL type with its extensions once #951: TypeClassMatcher now takes MergedDefinitions and matches the TypeShape leaf against MergedDefinitions.resolve. TypeShape never reads SDL definitions, so DefinitionsBoundaryTest passes over it unchanged

Two spots still work out type facts on their own and are left for the resolver plan PR: subscription Publisher detection in FieldResolverScanner.kt (resolverMethodReturnsPublisher) and the argument type checks in MethodFieldResolver.kt (the Optional check and isConcreteScalarType).

No release note, nothing changes for library users.

Testing:

  • mise exec java@temurin-17.0.21+9 -- mvn -B -ntp clean verify passes at this head, 480 tests (this PR adds no test methods, the 10 new ones are DefinitionsBoundaryTest and TypeKindRecipeTest from Merge each SDL type with its extensions once #951), and japicmp prints No changes. against 15.0.0
  • mvn -B -ntp clean verify on JDK 25 passes at this head too
  • the bounded check prints the same five known findings with the same counts (448, 896, 234, 1,254, 18), and the same 5,655 accepted and 1,365 rejected return types and 226,277 executions as on agent-friendly (run on both). check.sh --rejected lists the same rejected return types with the same build errors on both
  • ResolverCompatibilityTest, CompatTest, DefinitionsBoundaryTest and TypeKindRecipeTest pass unchanged. NestedGenericWrapperTest and TypeClassMatcherTest only swap PotentialMatch for TypeShape
  • the verify-tools probe gives the same schema, results, warnings and errors at agent-friendly and this head for all 40 scenarios. Only stack frames differ, e.g. optional-results-nested's build error is now thrown from TypeShape instead of TypeClassMatcher$PotentialMatch, with the same text word for word
  • a scratch ContainerKind entry with a placement rule but no step doesn't compile (Enum entry 'ContainerKind.SCRATCH' does not implement abstract member). One with a stubbed step fails ReturnValueUnwrappingTest in under a second with MODEL ERROR: TypeShape.kt has a ContainerKind for these classes, but no Container in Check.java builds them and its class, and check.sh exits 2
  • rg -n "fun resolve\(" src/main prints SourceResolver.resolve in FieldResolver.kt and MergedDefinitions.resolve, which looks a type definition up by name. Both are unrelated (the plan expected nothing outside TypeShape.kt)
  • the first version computed the value class for every return type and ResolverCompatibilityTest caught it: untyped Map fields stopped building (unknown type: java.lang.Object), the build rejected 85 more return types and three known findings went STALE

oryan-block and others added 4 commits November 5, 2026 17:41
TypeShape walks a resolver's return or parameter type against its
GraphQL type once and keeps both results: the leaf the dictionary binds
and the layers a method's value is unwrapped through. It replaces
GenericTypeResolver.resolve, TypeClassMatcher.PotentialMatch and
ReturnValueUnwrapper, which each walked or recomputed the same type.

DataFetcherResult and Optional are one enum whose placement rule and
value step are abstract, so a new container can't skip either. Both
generic-wrapper rules (exact declared type at build, most specific
instance at fetch) sit in one class. A subscription's Publisher view is
taken inside TypeShape, next to the rule that leaves its events alone.

Property and Map fields still match through TypeShape and return their
raw values, as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Rename stripWrappers to matchedType, outermostType to unwrappedRootType
and misplaced to placementError, and drop the comments the names now
cover. The rule table names TypeShape as what keeps a method's return
value matched and unwrapped by one rule.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new ContainerKind with a stubbed value step compiled and passed every
test, because Check.java keeps its own list of containers. The test now
passes TypeShape's container types to the check, which stops with a
MODEL ERROR naming each one Check.java has no Container for.

The verify-return-value-unwrapping skill named ReturnValueUnwrapper,
PotentialMatch and GenericTypeResolver in its trigger, coverage table
and sync command. It now names TypeShape, so a change there triggers
the skill and shows up in its staleness check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
TypeClassMatcher takes MergedDefinitions and matches a TypeShape leaf.
@sonarqubecloud

sonarqubecloud Bot commented Nov 5, 2026

Copy link
Copy Markdown

@oryan-block
oryan-block merged commit 7ba7db9 into agent-friendly Nov 6, 2026
5 checks passed
@oryan-block
oryan-block deleted the agent-friendly-p10-type-shape branch November 6, 2026 00:40
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.

1 participant