Repository navigation
Walk each declared type once for matching and unwrapping - #952
Merged
Merged
Conversation
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.
|
2 tasks done
This file contains hidden or 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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



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.unwrapfor the class the schema build binds,GenericTypeResolver.resolve(called again fromMethodFieldResolverfor the value class) andReturnValueUnwrapperfor 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(aDataFetcherResultinside anOptional) had to change bothTypeClassMatcherandReturnValueUnwrapper.Checklist
Description
New
resolver/TypeShape.ktwalks a return or parameter type against its GraphQL type once and keeps both results: the leafTypeClassMatcherbinds and the layersTypeShape.Unwrapperunwraps a method's value through.GenericTypeResolver.kt,resolver/ReturnValueUnwrapper.ktandPotentialMatch/Unwrapping/Unwrappedare gone.DataFetcherResultandOptionalare one private enum the walk iterates. Each entry has an abstract placement rule and value step, so a new container doesn't compile without bothReturnValueUnwrappingTestwith aMODEL ERRORuntil the bounded check has aContainerfor it. Before, an entry with a stubbedsteppassed every test, sinceCheck.javakeeps its own container listPublisherview is taken insideTypeShape, next to the rule that leaves a subscription's events aloneFutureis checked against) comes from the same walk instead of a secondresolve. It's still only computed for methods, since an untyped Map's value type is a ClassmateResolvedTypethatTypeUtils.getRawTypethrows onresolveTypeVariablesstays a separate top-level function, since argument conversion needs wrappers, wildcards and primitives kept. The raw class and assignability helpers became plainTypeUtilscalls in the three files that used themscanForMatchesreturns a lazySequence, so each shape is still walked only when the scanner gets to it, likePotentialMatch.unwrappingwas. Otherwise a schema with two errors could report a different one firstTypeShapeand still return their raw values. The next PR routes them through the unwrapper and deletes the known findingsverify-return-value-unwrappingskill named the deleted classes in its trigger, coverage table and sync command. It now points atTypeShape.kt, so a change there triggers it and shows up in its staleness checkAGENTS.mdrule table namesTypeShapefor "a resolver method's return value is unwrapped by one rule"agent-friendlyafter Merge each SDL type with its extensions once #951:TypeClassMatchernow takesMergedDefinitionsand matches theTypeShapeleaf againstMergedDefinitions.resolve.TypeShapenever reads SDL definitions, soDefinitionsBoundaryTestpasses over it unchangedTwo spots still work out type facts on their own and are left for the resolver plan PR: subscription
Publisherdetection inFieldResolverScanner.kt(resolverMethodReturnsPublisher) and the argument type checks inMethodFieldResolver.kt(theOptionalcheck andisConcreteScalarType).No release note, nothing changes for library users.
Testing:
mise exec java@temurin-17.0.21+9 -- mvn -B -ntp clean verifypasses at this head, 480 tests (this PR adds no test methods, the 10 new ones areDefinitionsBoundaryTestandTypeKindRecipeTestfrom Merge each SDL type with its extensions once #951), and japicmp printsNo changes.against 15.0.0mvn -B -ntp clean verifyon JDK 25 passes at this head tooagent-friendly(run on both).check.sh --rejectedlists the same rejected return types with the same build errors on bothResolverCompatibilityTest,CompatTest,DefinitionsBoundaryTestandTypeKindRecipeTestpass unchanged.NestedGenericWrapperTestandTypeClassMatcherTestonly swapPotentialMatchforTypeShapeagent-friendlyand this head for all 40 scenarios. Only stack frames differ, e.g.optional-results-nested's build error is now thrown fromTypeShapeinstead ofTypeClassMatcher$PotentialMatch, with the same text word for wordContainerKindentry with a placement rule but no step doesn't compile (Enum entry 'ContainerKind.SCRATCH' does not implement abstract member). One with a stubbedstepfailsReturnValueUnwrappingTestin under a second withMODEL ERROR: TypeShape.kt has a ContainerKind for these classes, but no Container in Check.java builds themand its class, andcheck.shexits 2rg -n "fun resolve\(" src/mainprintsSourceResolver.resolveinFieldResolver.ktandMergedDefinitions.resolve, which looks a type definition up by name. Both are unrelated (the plan expected nothing outsideTypeShape.kt)ResolverCompatibilityTestcaught it: untyped Map fields stopped building (unknown type: java.lang.Object), the build rejected 85 more return types and three known findings wentSTALE