Repository navigation
Build one resolver plan per field and delete the resolver hierarchy - #955
Merged
Merged
Conversation
ResolverPlan.kt replaces FieldResolverScanner, FieldResolver and the method, property, Map and missing resolvers. Its nested Finder picks the member behind a field, and the plan reads that member's signature once: the argument types, the extra parameters, the source and the return shape. The scan, the data fetcher, the proxy and parameter checks and UnmappedInputFieldChecker read the plan instead of the Method. Member variants are private to the file and only expose a raw read. The plan builds every data fetcher and unwraps every value through TypeShape. The resolver kinds and the extra parameters are enums whose entries the finder and the binding iterate, and the subscription Publisher check and the argument Optional and concrete scalar checks move next to the type walk in TypeShape.kt. SourceResolver keeps its public interface in its own file. ResolverOwnershipTest keeps member reflection in ResolverPlan.kt, container dispatch in TypeShape.kt and TokenBuffer conversion in ArgumentConverter.kt. Messages, lookup order and laziness are unchanged.
… member lookup order Argument shapes are walked when the scan asks for them instead of being kept for the schema's lifetime, the method fetcher's slots are an array, and the source check captures only the search type. A test pins that a method wins over a field and a field over a Map key, and ResolverOwnershipTest keeps ArgumentConverter construction in ResolverPlan.kt.
The rows for deciding a method's signature once and for one argument conversion entry point now name ResolverPlan, ArgumentConverter and ResolverOwnershipTest. The bounded check skill names ResolverPlan.kt where it named the deleted resolver files.
The probe diff against trunk showed the missing-resolver and inaccessible-field WARNs moving from graphql.kickstart.tools.resolver.FieldResolverScanner to ResolverPlan$Finder, which would change what an app's logging config raises or silences. MissingFieldResolverTest now checks the logger names.
Apps call getWrappedFetchingObject by reflection from Instrumentation.instrumentDataFetcher, and some check a fetcher's class, so the fetchers keep their names and stay public in bytecode. Each member's fetcher is a ResolverDataFetcher, whose constructor takes the unwrapper, and the member's class decides its ResolverKind. The bounded check now fails with MODEL ERROR when a ResolverKind has no kind in Check.java, and the fixture tests name the fixture to add instead of offering an else branch.
…eflection gaps Exemptions now name the top-level classes each owning file declares and count nested and lambda classes through their enclosing class, so neither a file of the same name elsewhere nor a '$' in a class name inherits one. The ScanDictionary exception covers only the type reads findInputValueTypeInType makes. The reflection rule also covers MethodHandles, kotlin-reflect calls and return types, Field writes and primitive reads, and commons-lang3 field and method utilities, and the TokenBuffer rule covers updateValue and readValueAs. Every library data fetcher must be a ResolverDataFetcher.
…from its owning file Comment pass and /simplify: drop comments the types and tests already say, rename the Map flag to unwrapsUntypedValues, move the ScanDictionary exemption's reason into its description so a failure prints it, use ArchUnit's own not, and, or and isTopLevelClass, and fail when an OwningFile lists a class compiled from another file.
…rPlan.kt in the ownership test Any use of a KFunction or KProperty, kotlin-reflect's member lookups and JVM mappings, and the Field, Parameter and annotated type reads now fail outside ResolverPlan.kt. A ResolverDataFetcher subclass must be declared in ResolverPlan.kt, and the ScanDictionary exemption matches findInputValueTypeInType(String, Class) by signature, so an overload doesn't inherit it. AGENTS.md lists what the test still doesn't see.
|
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. The most common one: the scan and the data fetcher each worked out the same facts about a resolver's signature.
FieldResolverScannerpicked the method and checked its trailing parameters against a list, thenMethodFieldResolver.createDataFetcherre-read theMethodto work out the parameter types, which ones areOptional, which scalars skip conversion and what each trailing parameter binds to (with awheninstead of the list).SchemaClassScannerandUnmappedInputFieldCheckerread the parameter types a third and fourth time, each handling a missing one differently. Fixing a rule in one of them left the others on the old rule, and nothing failed.Checklist
Description
FieldResolverScanner,FieldResolver,MethodFieldResolver,PropertyFieldResolver,MapFieldResolverandMissingFieldResolverare gone.ResolverPlan.kthas oneResolverPlanper field, built byResolverPlan.Finder(the old scanner's lookup, moved). The plan reads the member once and gives the scan its type shapes, the parser its data fetcher, the proxy and "no parameter for its argument" checks what they need, andUnmappedInputFieldCheckerthe argument types and mapper. Nothing outside the file reflects on the member.ScanDictionary's references still hold theMethodfor error text and to check whether a return type came from aGraphQLResolver, but only read its declaring class.ResolverPlan.kt, each one decides itsResolverKind, and none of them builds a data fetcher.ResolverPlan.dataFetchergives every member aResolverDataFetcher, a sealed class whose constructor takes theTypeShape.Unwrapper. So aDataFetcher { }lambda insidefetcherOf, or a fetcher without an unwrapper, doesn't compile, and a fetcher that holds one but skips it fails the per-kind fixture testMethodFieldResolverDataFetcherandLightMethodFieldResolverDataFetcherfor methods,PropertyFieldResolverDataFetcherandMapFieldResolverDataFetcherfor fields and Maps. They'reinternalbut public in bytecode, like before, soinstanceofand class name checks still work, andgetWrappedFetchingObjecton the two method fetchers is reachable by reflection again (apps call it fromInstrumentation.instrumentDataFetcherto get to the resolver behind a field, Can't get the wrapped dataFetcher #164).WrappedFetchingObjectTestdoes that from another packageResolverKind.entries(method, field, Map key) with an exhaustivewhen, and the trailing parameters are anExtraParameterenum with an abstract binder. Adding either without the rest doesn't compile. The fixture tests inResolverCompatibilityTestiterate the entries and fail with the fixture to add, instead of a compile error that offers anelsebranch. A newResolverKindalso ends the bounded check inMODEL ERRORuntilCheck.javahas a kind for it, the same way a newContainerKinddoesResolverKind.MAP_VALUE.unwrapsUntypedValues = false, read by theUnwrapper, instead of a null unwrapperContinuation, instead of mapping a list of closures and copying the array againPublishercheck, the rawOptionalcheck and the List-only concrete scalar check move intoTypeShape.kt, next toasPublisherand the walk. Each keeps its own rule (erased type, raw class,Listonly), since merging them into the walk would change which shapes build and which arguments get convertedSourceResolverstays public and unchanged, in its own file. japicmp printsNo changes.ResolverOwnershipTest(ArchUnit) fails on:ResolverPlan.kt:Method.invoke,Fieldreads and writes (get,getInt,set, ...),setAccessible, signature reads (generic and raw parameter types,Field.getType,Parametertypes, annotated types),MethodHandleslookups and calls, kotlin-reflect's member lookups (members,memberProperties,declaredMemberFunctions, ...),callSuspendand JVM mappings (anykotlin.reflect.fullorkotlin.reflect.jvmcall), a value typed as aKFunction,KPropertyor one of their getters (sof(t)on aKFunction1andgetter.invoke(t)fail too), and commons-lang3FieldUtils/MethodUtilsreads and invokes. The one exception is the type reads inScanDictionary.findInputValueTypeInType(String, Class), matched by its full signature so an overload doesn't get it, which still guesses input field types from gettersDataFetcherthat isn't aResolverDataFetcher, and aResolverDataFetcherdeclared outsideResolverPlan.kt(Kotlin lets a sealed class have subclasses in other files of the package)DataFetcherResulthandling outsideTypeShape.kt, TokenBuffer conversion (now includingupdateValueandreadValueAs) outsideArgumentConverter.kt, and anArgumentConverterbuilt outsideResolverPlan.ktThe exemptions name the exact top-level classes each owning file declares, and nested and lambda classes count through their enclosing class. So another file called
ResolverPlan.ktin the same package, or a class namedArgumentConverter$Something, doesn't get an exemption. A test fails when a listed class isn't compiled from that file, or the file declares a class the list misses.Two rounds of bypass hunting showed a bytecode check can't be airtight, so this is where I stopped. What it doesn't see:
is CompletableFuture,CompletionStageorOptionalchecks outsideTypeShape.kt. On purpose, they're ordinary JDK types main code uses for its own workClass.forNamelookups. ArchUnit can't see a class named by a string, and the proxy handlers use it for optional dependenciesMethod.getReturnType.TypeShapereads it on a generic wrapper's transformer, so banning it needs an exemption for main codeKFunctionheld asAnyand called through a cast, like(f as KFunction1<Any, Any?>)(t). ArchUnit doesn't count a cast as a dependency. Getting theKFunctionin the first place needs a kotlin-reflect call the test does catchPropertyDataFetcher.fetching,java.beans.ExpressionandStatementDataFetcher { }lambda outsideResolverPlan.kt. It compiles to invokedynamic, so there's no class to check. The per-kind fixture tests and the bounded check catch a value it leaves wrapped, but only for fetchersResolverPlanbuildsAGENTS.md's ownership row lists the same gaps.Kept on purpose, so nothing a user sees changes:
MethodFieldResolver{method=...}andPropertyFieldResolver{property=...}, now pinned by a test. 137FieldResolverErrorrows in the compat corpus still matchgraphql.kickstart.tools.resolver.FieldResolverScanner, since apps may silence them by that name.MissingFieldResolverTestchecks the logger namesObjectMapperprovider runs at parse time, once per method fieldDataFetchingEnvironmentOne small difference: calling
get(environment)on a field or Map fetcher with an environment that has nofieldDefinitionused to throw an NPE from Kotlin's null check on a parameter the fetcher never reads. Now all four fetchers take it as nullable, like the method fetchers already did. graphql-java always sets it, so only hand-built environments see this. I kept it lenient rather than adding the check back.AGENTS.md's rows for deciding a method's signature once and for one conversion entry point name what enforces them, and theverify-return-value-unwrappingskill names the fetchers and its sync commit.Testing:
mise exec [email protected]+9 -- mvn -B -ntp clean verifypasses at this head, 493 tests, japicmpNo changes..mvn -B -ntp clean verifyon JDK 25 passes tooPASSwith the same counts asagent-friendly: 7,020 return types, 1,365 rejected, 5,655 accepted, 226,277 executions,pinnedCoverageunchanged0f235257and atagent-friendly: same exit codes, and schemas, results, warnings and build errors identical apart from stack frames. The last rounds only touched the test andAGENTS.mdinvoke,getter.invoke,callSuspend/callSuspendBy,KParametertypes,Field.getType,Parameterand annotated types, afindInputValueTypeInTypeoverload, aResolverDataFetchersubclass in another file). Each failure names the line and the fix. The ones that get past it are the list above. A clean head passesfetcherOf, a sealed fetcher that skips its unwrapper fails the fixture test, a scratchResolverKindends the bounded check inMODEL ERROR, and a property fetcher that drops the unwrapping fails the per-kind testWrappedProbe(Java, unnamed module) gets theHolderback from both method fetchers again, under the old class namesUnwrapper.unwrapcall that returns it as is