Repository navigation
Merge each SDL type with its extensions once - #951
Merged
oryan-block merged 12 commits intoNov 5, 2026
Merged
Conversation
The scanner and the parser now see an SDL type only as a MergedType, built once in MergedDefinitions.kt. Each one merges its extensions' fields, interfaces, members, values and directives, base first, and keeps the definition and extensions themselves private. MergedType.prepare runs what every kind's builder needs: the repeated-declaration check, the name, definition and extensionDefinitions, the description and the SDL directives. The six create functions in SchemaParser only add members. withExtensions, extensionsOf, directivesOf and the TypeExtensions alias are gone, with all 18 calls of withExtensions. The lookups keep their shapes: byName is last-wins like before, baseObjects keeps every object definition, the specification scalars resolve first, and type errors search definitions before extensions. One helper in TypeClassDictionary owns "only input object types may share a class" for both the supplied dictionary and the scanned one, each with its own message. Co-Authored-By: Claude <noreply@anthropic.com>
Kotlin's internal is module-wide, so DefinitionsBoundaryTest checks the compiled classes with ArchUnit. Outside MergedDefinitions.kt no code may read the fields, interfaces, members, values, input fields or directives of a type definition, see a type extension class, or start a named-type builder. Only the directive wiring may transform a built type. TypeDefinitionFactory implementations such as the Relay factory get raw definitions by contract and stay allowed. Each violation names the method, the line and the MergedType member to use instead. It catches import aliases, qualified names, property syntax, Java files, generic receivers, with blocks, callable references, imported nested builders and typealiases. Reflection and getNamedChildren still get through. logback-test.xml keeps ArchUnit's class import at INFO. Co-Authored-By: Claude <noreply@anthropic.com>
TypeKindRecipeTest builds one type of each kind with a description, a directive on the definition and one on its extension, and checks the built description, definition, extension definitions and applied directives. It also checks that each kind with members rejects a member its extension repeats, with the exact error and source location. Its first test compares its cases with the leaves of MergedType, so a new kind without a case fails, after the compiler has already asked for it in every exhaustive when. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
…ding byName and baseObjects come from the merged types instead of a second pass over the definitions, and the type reference errors take a type's kind from MergedType.kind. A scalar keeps its provided description through newScalar, so prepare only sets one the SDL gives. The boundary test finds extension classes and named types by their classes instead of their names. Co-Authored-By: Claude <noreply@anthropic.com>
objectsWithRepeats, kindPhrase, declarationTextOf and findUsage's reference parameter say what the KDoc said, so the comments go. Co-Authored-By: Claude <noreply@anthropic.com>
The allowlist now names the exact classes in graphql.kickstart.tools, and only the Relay factory may read raw definitions. Reads through a DirectivesContainer or Node receiver, getChildren and getNamedChildren, SDL node walks and TypeDefinitionRegistry reads fail too, and each message names the MergedType member to use. TypeKindRecipeTest names the leaf with no case and what to add.
A project with this jar on its test classpath and no logging config of its own would pick it up.
|
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. For type extensions it's the same mistake every time. A type and its extensions travel separately, so every reader of a type's fields, members, values or interfaces has to remember
withExtensions(19 calls), and everycreate*inSchemaParserrepeats the same header by hand: extension directives, the repeat check,definition/extensionDefinitions, description and SDL directives. A reader or builder that forgets one still compiles and passes every test without extensions. #863 (the repeat check only covered some kinds) and #864 (scalars dropped their directives) are that mistake.Checklist
Description
Each SDL type is now merged with its extensions once, in
MergedDefinitions.kt, and nothing outside that file can read the raw definitions.MergedTypehas one leaf per kind (Object,Interface,Union,Enum,Input,Scalar). Each exposes its mergedfields,interfaces,membersorvalues, definition first, and keeps the definition and its extensions private. The scanner,ScanDictionary,SealedSubclassMatcher,TypeClassMatcher,TypeReferenceErrors,DictionaryTypeResolverand the parser takeMergedTypeinstead ofTypeDefinition.MergedType.prepareis the shared recipe: the repeat check, name,definition,extensionDefinitions, description and SDL directives. The sixcreate*functions only add members.withExtensions,extensionsOf,SdlDirectives.directivesOfand theTypeExtensionsalias are gone. No caller is left on the old path.byNameis last-wins,objectsWithRepeatskeeps every object definition, the specification scalars resolve first, and type errors search the reached definitions, then directive definitions, then extensions.TypeClassDictionary.requireOnlyInputTypesShareAClass. The supplied dictionary check and the scanned one both call it and keep their own messages.Kotlin's
internalis module-wide, soDefinitionsBoundaryTest(ArchUnit, test scope) checks the compiled classes. The allowed classes are named exactly (graphql.kickstart.tools.MergedDefinitionsandMergedType), so a second file or class with the same name somewhere else doesn't get a pass. Outside them, code can't:getChildren,getNamedChildren) of a type definition. That includes reads through a receiver typed as a supertype, likeDirectivesContainer<*>,Node<*>orDefinition<*>, since it may hold a type definition. OnlyRelayConnectionFactoryis allowed, because it gets raw definitions by contract. Any otherTypeDefinitionFactoryisn't.NodeTraverser, aNodeVisitororAstTransformer.TypeDefinitionRegistry(types(),objectTypeExtensions()and so on).DirectiveWiringHelpermaytransforma built type.A violation names the method, the line and what to use instead:
There's no public merged member for a read-only type directive check, since nothing needs one yet. So the message points at
MergedType.prepareand the built type'sappliedDirectivesinstead of adding one.TypeKindRecipeTestchecks each recipe step on every kind, and its first test compares its cases withMergedType's sealed leaves. So a seventh kind fails to compile in every exhaustivewhenfirst, then fails the test until it gets a case:ArchUnit logs at DEBUG, so
src/test/resources/logback-test.xmlsets it to INFO. The published tests jar leaves that file out, so a project with that jar on its test classpath and no logging config of its own doesn't pick it up.No release note, nothing changes for library users.
Scope
DirectivesContainer<*>helper fail the test even though they're fine. The message says to type the receiver asFieldDefinitioninstead. Nothing in the library reads them that way today.Blast Radius
Every schema build goes through the new path, so a mistake here would show up as a changed schema, error or WARN. The suite,
CompatTestand the probe diff below say nothing changed. Contributors get ArchUnit at test scope and about 1 s more inmvn verify.Verification
mvn -B -ntp clean verifyon JDK 17 at the head: 480 tests pass, includingCompatTestandResolverCompatibilityTest. The API check finds no incompatible change against 15.0.0 and the jar check passes. Same 480 on JDK 25.unzip -lon the tests jar lists nologback-test.xml.verify-toolsscenarios ran atagent-friendlyand at920275f5, with the exit codes, printed schemas, results, WARNs and build errors diffed. 31 build and 9 throw on both sides. The schemas, results, WARNs and error messages are identical. The only difference is in the 9 build errors' stack traces, where line numbers moved and the repeat check now throws fromMergedType.rejectRepeatedinstead ofSchemaParser.rejectRepeated. The commits since only touch tests,AGENTS.mdand the tests jar config.src/main, plus two more that read a type's directives through aDirectivesContainer<*>helper and its fields throughnamedChildren.getChildren(...). They cover import aliases, qualified names, property syntax, Java files, generic receivers,withblocks, callable and constructor references, typealiases,whenover the kinds, other packages,getChildren,NodeTraverser,TypeDefinitionRegistry, a secondMergedDefinitions.ktin another package and anotherTypeDefinitionFactory. All but reflection fail the test, with the line and a message naming the replacement. Before this round, 10 got through. Five more on top (a class namedMergedTypeelsewhere,graphql.util.Traverseroverchildren, aDefinition<*>receiver,types()on a registry, a field's directives throughDirectivesContainer<*>) fail too, and the same read through aFieldDefinitionreceiver passes.MergedTypeleaf fails to compile, and with thewhens filled in it failsTypeKindRecipeTestwith the message above. On98846412, droppingextensionDefinitionsfrom the enum failed it too.