ID |
|
|---|---|
Status |
Backlog |
Bucket |
bugs |
Created |
2026-05-21 |
Updated |
2026-05-21 |
Plain-input field rejections attributed to consumer field, losing input-field source location
When a plain (non-@table) input type carries a broken @condition directive (parameter-binding mismatch, reflection failure, etc.) or an unresolvable column, the failure surfaces as an UnclassifiedField on the consuming field (e.g. Query.opptak), not on the input type’s offending field (e.g. OpptakFilterInput.opptaksNavn). The reported SourceLocation is the consuming field’s definition, so LSP fix-its, watch-mode formatters, and editor highlights point one indirection away from the actual broken directive the author needs to edit.
Symptom
Production schema (opptak-subgraph, same source as R211):
input OpptakFilterInput {
opptaksNavn: String @condition(
condition: {className: "no.sikt.fs.opptak.opptak.OpptakService", method: "opptakNavnSok"},
override: true
)
}
type Query { opptak(filter: OpptakFilterInput): [Opptak!]! }
When `opptakNavnSok’s parameter name doesn’t match the input field name, the build error is rendered as:
Field 'Query.opptak': argument 'filter': plain input type 'OpptakFilterInput': input field 'opptaksNavn' @condition: parameter 'navn' in method 'opptakNavnSok' is not a GraphQL argument and not a context key
The qualifiedName is Query.opptak and the location is Query.opptak’s definition line. The author has to read the prose to find the actual offending site; tooling that consumes `ValidationError.location can’t take them there.
Trace
The location is dropped early and the diagnostic is folded into prose at four successive layers:
-
InputFieldResolver.resolve(InputFieldResolver.java:60-97) iterates the input fields, collects column-miss failures (InputFieldResolution.Unresolved, which has noSourceLocation) andcondErrors(List<String>, also no location) and folds everything into oneResolution.Rejected(Rejection.structural("plain input type '<T>': …")). -
FieldBuilder.classifyArgument(FieldBuilder.java:1010-1015) wraps that intoArgumentRef.UnclassifiedArgon the consuming arg. -
projectFilters(FieldBuilder.java:1377-1380) re-prefixes with"argument '<n>': "and pushes onto the parent field’serrors. -
foldRejections(FieldBuilder.java:1277-1282) joins everything into a single structural rejection, andvalidateUnclassifiedField(GraphitronSchemaValidator.java:953-959) emits oneValidationErrorwhosequalifiedNameis the consuming field and whoselocationis the consuming field’s definition.
By the time the rejection reaches the validator, the offending @condition directive’s SourceLocation on OpptakFilterInput.opptaksNavn has been discarded twice (at the Unresolved record’s schema, and at the condErrors List<String> channel) and there’s nothing left to point at except the consumer.
Direction (sketch)
Three layers need touching:
-
InputFieldResolution.Unresolved(InputFieldResolution.java:20-24) grows aSourceLocationfield (the input field’s definition, or the@conditiondirective’s location when the failure is condition-side). -
The
condErrorschannel populated bybuildInputFieldConditionbecomesList<LocatedRejection>(or similar) rather thanList<String>; each entry carries its input field’s location. -
InputFieldResolver.Resolution.RejectedcarriesList<LocatedRejection>rather than a single foldedRejection.FieldBuilder.classifyArgumentandprojectFilterspropagate the list;UnclassifiedArg.rejectioneither becomes a list-carrying variant or grows a sibling located-items slot. -
validateUnclassifiedFieldfans located items out into oneValidationErrorper offending input field, each with its ownSourceLocation. The consuming field still gets oneValidationError(its existence asUnclassifiedFieldis itself a fact ; emission can’t proceed against it), but with a different message: "argument '<n>': input type '<T>' has unresolved fields ; see errors on that type’s fields" rather than the joined dump.
The change touches the rejection/diagnostic shape across three resolvers and the validator. Worth a design-fork pass with principles-architect before drafting Spec: the rejection sealed hierarchy is load-bearing for typed lifts (R205 AuthorError.UnknownName), and growing a list-carrying arm needs to interact cleanly with existing folding sites (e.g. foldRejections, Rejection.prefixedWith).
Scope notes
-
The fix benefits every plain-input failure mode (column-miss, condition reflection failure, parameter-binding mismatch), not just the R211 surface ; same broken attribution applies to all of them.
-
@tableinput types route throughTableInputTypeclassification at type-build time and already attribute to the input type viaUnclassifiedType; this item is plain-input-only. -
R211 (
condition-override-true-misleading-column-miss-message.md) is the narrower fix at the gate; it suppresses the column-miss noise line. R211 and R213 don’t interact: R211 shrinks the message at the consuming-field carrier, R213 moves the carrier (or splits it). R211 ships first.
References
-
graphitron/src/main/java/no/sikt/graphitron/rewrite/InputFieldResolution.java:20-24;Unresolvedrecord missing a location field. -
graphitron/src/main/java/no/sikt/graphitron/rewrite/InputFieldResolver.java:60-97; the folding site (Resolution.Rejected carries oneRejection, no per-field locations). -
graphitron/src/main/java/no/sikt/graphitron/rewrite/FieldBuilder.java:1377-1380;UnclassifiedArg→ parent field’serrorsre-prefix. -
graphitron/src/main/java/no/sikt/graphitron/rewrite/FieldBuilder.java:1277-1282;foldRejections, the final collapse to a single rejection. -
graphitron/src/main/java/no/sikt/graphitron/rewrite/GraphitronSchemaValidator.java:953-959;validateUnclassifiedField, the single-ValidationError emission point. -
R211 (
condition-override-true-misleading-column-miss-message.md) ; narrower companion fix, sameopptak-subgraphsource schema. -
R215 (
column-binding-at-classification-not-usage.md) ; the cascade-aware classification restructure. R215’s direction §2 (defer@tablecolumn-coverage to consumption) makes attribution natural: rejections fire at the field’s source location by construction. R213 may merge into R215 if both land together, or stay as the sibling that handles the plain-input override:false-on-non-binding-field case the classifier still rejects directly. -
Surfaced by alf’s production
opptak-subgraphschema; same investigation pass as R211.