Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 8 additions & 9 deletions .claude/skills/verify-return-value-unwrapping/SKILL.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
---
name: verify-return-value-unwrapping
description: Exhaustively check, up to a bound, that the value of a resolver method, public field or typed Map reaches graphql-java fully unwrapped. Futures are awaited, generic wrappers are transformed, Optional and DataFetcherResult are handled, and list order, nulls and failures are kept, for every return type nested up to 3 containers deep. Use it before pushing any change to ReturnValueUnwrapper, TypeClassMatcher's unwrapping, GenericTypeResolver.resolve, the data fetchers in MethodFieldResolver, PropertyFieldResolver or MapFieldResolver, or the GenericWrapper defaults, and whenever a bug report says graphql-java got a CompletableFuture, Optional or wrapper object as a field value.
description: Exhaustively check, up to a bound, that the value of a resolver method, public field or typed Map reaches graphql-java fully unwrapped. Futures are awaited, generic wrappers are transformed, Optional and DataFetcherResult are handled, and list order, nulls and failures are kept, for every return type nested up to 3 containers deep. Use it before pushing any change to TypeShape (the type walk, ContainerKind, the generic wrapper rules or the Unwrapper), the data fetchers in MethodFieldResolver, PropertyFieldResolver or MapFieldResolver, or the GenericWrapper defaults, and whenever a bug report says graphql-java got a CompletableFuture, Optional or wrapper object as a field value.
---

# Verify return value unwrapping
Expand Down Expand Up @@ -73,7 +73,7 @@ as `-Dunwrapping.args`, and prints the report. On a failure it also prints the p
|---|---|
| 0 | `PASS`, or `PASS apart from the known findings above` |
| 1 | `FAIL`: a return type that isn't a known finding breaks the property. `NEW`: a known finding matches more accepted return types than its line records |
| 2 | `MODEL ERROR`: the checkout or the check didn't compile, or the build rejected a return type the check must exercise, so a pass would prove nothing |
| 2 | `MODEL ERROR`: the checkout or the check didn't compile, the build rejected a return type the check must exercise, or a `ContainerKind` in `TypeShape.kt` has no `Container` in `Check.java`, so a pass would prove nothing |
| 3 | `STALE`: a known finding matches a return type that no longer fails, or fewer accepted return types than its line records |

Options: `--depth N`, `--budget N`, `--property-budget N`, `--show N` (counterexamples and stale return types printed, default 15), and `--rejected`, which lists
Expand Down Expand Up @@ -157,25 +157,24 @@ or delete it when none fail, so that a regression shows up as `FAIL`.

## Coverage and sync

Last confirmed to describe the code at commit `af9ba4ad` (agent-friendly, 2026-11-04). It covers:
Last confirmed to describe the code at commit `6d41328d` (2026-11-05). It covers:

| File | What |
|---|---|
| `src/main/kotlin/graphql/kickstart/tools/resolver/ReturnValueUnwrapper.kt` | all of it |
| `src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt` | `PotentialMatch.unwrap`, `Unwrapped` |
| `src/main/kotlin/graphql/kickstart/tools/GenericTypeResolver.kt` | `resolve`, `resolveTypeVariables` |
| `src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt` | `createDataFetcher` (`valueClass`, the unwrapper), `returnType`, `returnValueMatch`, `LightMethodFieldResolverDataFetcher` |
| `src/main/kotlin/graphql/kickstart/tools/resolver/TypeShape.kt` | all of it: the walk, `ContainerKind`, `GenericWrappers`, `resolveTypeVariables`, the `Unwrapper` and its steps |
| `src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt` | `createDataFetcher` (the `TypeShape.Unwrapper`), `returnShape`, `LightMethodFieldResolverDataFetcher` |
| `src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt` | `GenericWrapper` and the default wrappers in `build()` |
| `src/main/kotlin/graphql/kickstart/tools/resolver/PropertyFieldResolver.kt` | all of it |
| `src/main/kotlin/graphql/kickstart/tools/resolver/MapFieldResolver.kt` | all of it, for Map subclasses with a typed value |

To see whether a change needs another look:

```bash
git log --oneline af9ba4ad..HEAD -- src/main/kotlin/graphql/kickstart/tools/resolver/ReturnValueUnwrapper.kt src/main/kotlin/graphql/kickstart/tools/TypeClassMatcher.kt src/main/kotlin/graphql/kickstart/tools/GenericTypeResolver.kt src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt src/main/kotlin/graphql/kickstart/tools/resolver/PropertyFieldResolver.kt src/main/kotlin/graphql/kickstart/tools/resolver/MapFieldResolver.kt
git log --oneline 6d41328d..HEAD -- src/main/kotlin/graphql/kickstart/tools/resolver/TypeShape.kt src/main/kotlin/graphql/kickstart/tools/resolver/MethodFieldResolver.kt src/main/kotlin/graphql/kickstart/tools/SchemaParserOptions.kt src/main/kotlin/graphql/kickstart/tools/resolver/PropertyFieldResolver.kt src/main/kotlin/graphql/kickstart/tools/resolver/MapFieldResolver.kt
```

Move the sync commit forward only after you've read those changes and confirmed that the containers, variants and
expected values in `Check.java` still describe them. For example, a new default wrapper or a new kind of unwrapping
step needs a new `Container` with its variants and its plain value in `build`. After an intended change, rerun the check,
step needs a new `Container` with its variants and its plain value in `build`. A new `ContainerKind` ends in
`MODEL ERROR` until it has one. After an intended change, rerun the check,
and break the new code on purpose once in a scratch worktree to see the check fail before you trust its pass.
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ happening. "docs only" means nothing does yet, so the rule depends on you readin

| Rule | Enforced by |
|---|---|
| A resolver method's return value is unwrapped by one rule that the schema build and the data fetcher share | docs only |
| A resolver method's return value is unwrapped by one rule that the schema build and the data fetcher share | `TypeShape`, whose one walk yields both the class the schema build matches and the layers the data fetcher unwraps. A container kind can't be added without its placement rule and its value step, since both are abstract on the enum, and `ReturnValueUnwrappingTest` fails until the bounded check builds values for it |
| A resolver method's parameter layout, type variables and source are decided once, at schema build | docs only |
| A change to how resolvers are found, bound or unwrapped keeps what a method, a property and a Map field return | `ResolverCompatibilityTest` and the bounded check in `ReturnValueUnwrappingTest`. Each lists exactly today's property and Map gaps as known findings, and fails when one is fixed until it's removed, or when a new failure would hide under one |
| One output has one builder. Don't add a second path that builds the same thing | docs only |
Expand Down
97 changes: 0 additions & 97 deletions src/main/kotlin/graphql/kickstart/tools/GenericTypeResolver.kt

This file was deleted.

6 changes: 4 additions & 2 deletions src/main/kotlin/graphql/kickstart/tools/ScanDictionary.kt
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package graphql.kickstart.tools

import graphql.kickstart.tools.resolver.TypeShape
import graphql.kickstart.tools.util.JavaType
import graphql.kickstart.tools.util.unwrap
import graphql.language.*
Expand Down Expand Up @@ -91,11 +92,12 @@ internal class ScanDictionary(
return@forEach
}

handleFoundType(typeClassMatcher.match(TypeClassMatcher.PotentialMatch.parameterType(
handleFoundType(typeClassMatcher.match(TypeShape.parameterType(
// a class from the dictionary is for the unwrapped type, not for its list or non-null wrapper
if (propertyType != null) inputValueDefinition.type else inputGraphQLType,
inputValueJavaType,
GenericTypeResolver(javaType ?: inputValueJavaType, options),
javaType ?: inputValueJavaType,
options,
InputObjectReference(inputValueDefinition)
)))
}
Expand Down
7 changes: 4 additions & 3 deletions src/main/kotlin/graphql/kickstart/tools/SchemaClassScanner.kt
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import graphql.language.*
import graphql.schema.idl.ScalarInfo
import graphql.schema.idl.TypeUtil
import org.apache.commons.lang3.ClassUtils
import org.apache.commons.lang3.reflect.TypeUtils
import org.slf4j.LoggerFactory
import java.lang.reflect.Field
import java.lang.reflect.Member
Expand Down Expand Up @@ -428,8 +429,8 @@ internal class SchemaClassScanner(

fieldResolversByType.getOrPut(type) { mutableMapOf() }[fieldResolver.field] = fieldResolver

fieldResolver.scanForMatches().forEach { potentialMatch ->
dictionary.handleFoundType(typeClassMatcher.match(potentialMatch))
fieldResolver.scanForMatches().forEach { shape ->
dictionary.handleFoundType(typeClassMatcher.match(shape))
}
requireParameterForEachArgument(type, fieldResolver)
}
Expand Down Expand Up @@ -471,7 +472,7 @@ internal class SchemaClassScanner(
}

fieldResolver.field.inputValueDefinitions.forEachIndexed { index, argument ->
val parameter = fieldResolver.getMethodParameterType(index)?.let(fieldResolver.typeResolver::getRawClass) ?: return@forEachIndexed
val parameter = fieldResolver.getMethodParameterType(index)?.let { TypeUtils.getRawType(it, fieldResolver.search.type) } ?: return@forEachIndexed
if (!fieldResolverScanner.isContextParameter(parameter, argument)) {
return@forEachIndexed
}
Expand Down
Loading
Loading