• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

graphql-go / graphql / 1912

09 May 2026 10:10PM UTC coverage: 85.967% (-6.3%) from 92.284%
1912

Pull #740

circleci

Nthalk
perf: skip extension hooks when schema has no extensions

resolvePlannedField unconditionally called handleExtensionsResolveFieldDidStart,
which allocates a map[string]ResolveFieldFinishFunc + a closure on
every resolved field — even when the schema has zero extensions
registered. With ~1000 fields per request that's 2000 wasted allocs
per request on the common no-extensions case.

Gate the call (and the matching finish-handler invocation) behind
`len(eCtx.Schema.extensions) > 0`.

Wide-query bench (100 fields × 10 items):
  before: 1.15ms / 9043 allocs/op
  after:  0.99ms / 7040 allocs/op   (-14% time, -22% allocs)

Hot-loop with native variables:
  before: 1.44ms / 9888 allocs/op
  after:  1.30ms / 7884 allocs/op   (-10% time, -20% allocs)

Schemas with extensions registered take the same path as before.
Pull Request #740: Plan + PlanQuery + ExecutePlan + PlanCache (cacheable execution shape)

916 of 1070 new or added lines in 4 files covered. (85.61%)

97 existing lines in 1 file now uncovered.

8350 of 9713 relevant lines covered (85.97%)

1296.5 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

80.11
/plan_cache_normalize.go
1
package graphql
2

3
import (
4
        "fmt"
5
        "hash/fnv"
6
        "strconv"
7

8
        "github.com/graphql-go/graphql/language/ast"
9
)
10

11
// normalizeDocument walks the given operation in `doc`, replacing
12
// fully-literal field-argument values with synthetic variables. Two
13
// queries that differ only in literal values now collapse to one
14
// normalized form — so a PlanCache keyed on the normalized text can
15
// reuse a single *Plan across arbitrary literal variations.
16
//
17
// Returns:
18
//   - newDoc: the normalized document (a deep-clone of the operation
19
//     with rewritten arguments + appended VariableDefinitions). The
20
//     fragments in `doc` are preserved by reference; this first cut
21
//     does NOT recurse into fragment definitions, so literals there
22
//     remain in-text (still cached, just without de-duplication
23
//     across literal variants).
24
//   - synthArgs: the extracted literal values, keyed by synth
25
//     variable name. Callers merge this into the request's Args
26
//     before ExecutePlan.
27
//   - cacheKey: the printed normalized document (the canonical
28
//     cache identifier). Empty when normalization isn't applicable
29
//     (e.g. operationName not found).
30
//   - err: only set for document-level malformations; missing
31
//     literals to extract is a no-op return, not an error.
32
//
33
// Scope (first cut, intentional):
34
//   - Only field arguments are normalized. Directive arguments
35
//     (@skip(if: true), @deprecated(reason: "..."), etc.) stay as
36
//     literals — they're rare and rule-bound.
37
//   - Only fully-literal argument values are extracted as a single
38
//     synth variable. A value containing a variable somewhere
39
//     (e.g. {a: 1, b: $foo}) is left untouched.
40
//   - Abstract-typed sub-selections (Interface/Union returns) are
41
//     not recursed into — type inference for arg positions across
42
//     concrete-type branches is more bookkeeping than first-cut
43
//     warrants. Wide queries that shape parametric calls at the
44
//     top level get the win regardless.
45
//   - Fragment definitions stay as-is. Real-world parametric
46
//     queries usually carry their literals at the call site, not
47
//     in fragment definitions.
48
//
49
// Synth variable naming uses the prefix `__pcv` (plan-cache-var) to
50
// minimize collision with hand-authored variable names.
51
func normalizeDocument(schema *Schema, doc *ast.Document, operationName string) (*ast.Document, map[string]interface{}, string, error) {
35✔
52
        if schema == nil || doc == nil {
35✔
NEW
53
                return doc, nil, "", nil
×
NEW
54
        }
×
55

56
        var op *ast.OperationDefinition
35✔
57
        var foundOps int
35✔
58
        for _, def := range doc.Definitions {
78✔
59
                if d, ok := def.(*ast.OperationDefinition); ok {
82✔
60
                        foundOps++
39✔
61
                        if operationName == "" || (d.GetName() != nil && d.GetName().Value == operationName) {
78✔
62
                                op = d
39✔
63
                        }
39✔
64
                }
65
        }
66
        if op == nil {
35✔
NEW
67
                return doc, nil, "", nil
×
NEW
68
        }
×
69
        if foundOps > 1 && operationName == "" {
39✔
70
                return doc, nil, "", nil
4✔
71
        }
4✔
72

73
        rootType, err := getOperationRootType(*schema, op)
31✔
74
        if err != nil {
31✔
NEW
75
                return doc, nil, "", err
×
NEW
76
        }
×
77

78
        ctx := &normCtx{
31✔
79
                schema:    schema,
31✔
80
                synthArgs: map[string]interface{}{},
31✔
81
                newVarDefs: nil,
31✔
82
        }
31✔
83

31✔
84
        newOp := cloneOperation(op)
31✔
85
        ctx.normalizeSelectionSet(newOp.SelectionSet, rootType)
31✔
86

31✔
87
        if len(ctx.synthArgs) == 0 {
43✔
88
                // No literals to extract — return early. Fingerprint the
12✔
89
                // original doc (over the operation + reachable fragments)
12✔
90
                // for the cache key.
12✔
91
                return doc, nil, fingerprintDocument(doc, op, operationName), nil
12✔
92
        }
12✔
93

94
        // Append the new synth-variable definitions to the operation.
95
        newOp.VariableDefinitions = append(newOp.VariableDefinitions, ctx.newVarDefs...)
19✔
96

19✔
97
        // Build a new doc containing the normalized op + the rest of the
19✔
98
        // definitions (fragments) unchanged. Order preserved: op stays in
19✔
99
        // its original slot.
19✔
100
        newDefs := make([]ast.Node, 0, len(doc.Definitions))
19✔
101
        for _, def := range doc.Definitions {
38✔
102
                if def == op {
38✔
103
                        newDefs = append(newDefs, newOp)
19✔
104
                } else {
19✔
NEW
105
                        newDefs = append(newDefs, def)
×
NEW
106
                }
×
107
        }
108
        newDoc := &ast.Document{Kind: doc.Kind, Loc: doc.Loc, Definitions: newDefs}
19✔
109
        return newDoc, ctx.synthArgs, fingerprintDocument(newDoc, newOp, operationName), nil
19✔
110
}
111

112
// fingerprintDocument produces a canonical string identifying the
113
// normalized structural shape of `op` (and any fragments it spreads,
114
// recursively). Two queries with the same shape — but possibly
115
// different extracted literals — produce the same fingerprint, so
116
// they collapse to one PlanCache entry.
117
//
118
// We use a 64-bit FNV-1a hash so the cache key stays tiny regardless
119
// of query size; collisions over a 1024-entry cache are
120
// vanishingly improbable, and the schema-pointer guard inside the
121
// cache catches any cross-schema accident.
122
//
123
// The fingerprint captures: operation type, operation name, variable
124
// definitions, and the selection tree (field names, arg names, sub-
125
// selections, fragment spreads). It deliberately ignores literal
126
// values that survived normalization — those are encoded by their
127
// AST kind only — so two normalize-equivalent queries hash the
128
// same.
129
func fingerprintDocument(doc *ast.Document, op *ast.OperationDefinition, operationName string) string {
31✔
130
        h := fnv.New64a()
31✔
131
        w := fingerprintWriter{h: h, fragments: collectFragmentDefs(doc)}
31✔
132
        w.writeString("OP:")
31✔
133
        w.writeString(string(op.Operation))
31✔
134
        w.writeByte(0)
31✔
135
        w.writeString(operationName)
31✔
136
        w.writeByte(0)
31✔
137
        w.writeVariableDefs(op.VariableDefinitions)
31✔
138
        w.writeSelectionSet(op.SelectionSet)
31✔
139
        return strconv.FormatUint(h.Sum64(), 16)
31✔
140
}
31✔
141

142
func collectFragmentDefs(doc *ast.Document) map[string]*ast.FragmentDefinition {
31✔
143
        out := map[string]*ast.FragmentDefinition{}
31✔
144
        for _, def := range doc.Definitions {
66✔
145
                if fd, ok := def.(*ast.FragmentDefinition); ok && fd.Name != nil {
39✔
146
                        out[fd.Name.Value] = fd
4✔
147
                }
4✔
148
        }
149
        return out
31✔
150
}
151

152
// fingerprintWriter walks the AST and feeds canonical bytes into the
153
// hash. Separate from the normalizer's mutating walker because we
154
// need a different traversal: we follow fragment spreads here (so
155
// spread-reachable structure participates in the cache key), but we
156
// don't rewrite anything.
157
type fingerprintWriter struct {
158
        h         interface{ Write([]byte) (int, error) }
159
        fragments map[string]*ast.FragmentDefinition
160
        visited   map[string]bool
161
}
162

163
func (w *fingerprintWriter) writeString(s string) { _, _ = w.h.Write([]byte(s)) }
294✔
164
func (w *fingerprintWriter) writeByte(b byte)     { _, _ = w.h.Write([]byte{b}) }
450✔
165

166
func (w *fingerprintWriter) writeVariableDefs(defs []*ast.VariableDefinition) {
31✔
167
        w.writeString("VD(")
31✔
168
        for _, d := range defs {
56✔
169
                if d == nil || d.Variable == nil || d.Variable.Name == nil {
25✔
NEW
170
                        continue
×
171
                }
172
                w.writeString(d.Variable.Name.Value)
25✔
173
                w.writeByte(':')
25✔
174
                w.writeType(d.Type)
25✔
175
                w.writeByte(',')
25✔
176
        }
177
        w.writeByte(')')
31✔
178
}
179

180
func (w *fingerprintWriter) writeType(t ast.Type) {
31✔
181
        switch tt := t.(type) {
31✔
182
        case *ast.NonNull:
3✔
183
                w.writeType(tt.Type)
3✔
184
                w.writeByte('!')
3✔
185
        case *ast.List:
3✔
186
                w.writeByte('[')
3✔
187
                w.writeType(tt.Type)
3✔
188
                w.writeByte(']')
3✔
189
        case *ast.Named:
25✔
190
                if tt != nil && tt.Name != nil {
50✔
191
                        w.writeString(tt.Name.Value)
25✔
192
                }
25✔
193
        }
194
}
195

196
func (w *fingerprintWriter) writeSelectionSet(sel *ast.SelectionSet) {
81✔
197
        if sel == nil {
114✔
198
                return
33✔
199
        }
33✔
200
        w.writeByte('{')
48✔
201
        for _, isel := range sel.Selections {
100✔
202
                switch s := isel.(type) {
52✔
203
                case *ast.Field:
44✔
204
                        if s.Alias != nil {
44✔
NEW
205
                                w.writeString(s.Alias.Value)
×
NEW
206
                                w.writeByte(':')
×
NEW
207
                        }
×
208
                        if s.Name != nil {
88✔
209
                                w.writeString(s.Name.Value)
44✔
210
                        }
44✔
211
                        if len(s.Arguments) > 0 {
69✔
212
                                w.writeByte('(')
25✔
213
                                for _, a := range s.Arguments {
50✔
214
                                        if a == nil || a.Name == nil {
25✔
NEW
215
                                                continue
×
216
                                        }
217
                                        w.writeString(a.Name.Value)
25✔
218
                                        w.writeByte('=')
25✔
219
                                        w.writeValue(a.Value)
25✔
220
                                        w.writeByte(',')
25✔
221
                                }
222
                                w.writeByte(')')
25✔
223
                        }
224
                        w.writeSelectionSet(s.SelectionSet)
44✔
225
                        w.writeByte(';')
44✔
226
                case *ast.InlineFragment:
2✔
227
                        w.writeString("...")
2✔
228
                        if s.TypeCondition != nil && s.TypeCondition.Name != nil {
4✔
229
                                w.writeString(s.TypeCondition.Name.Value)
2✔
230
                        }
2✔
231
                        w.writeSelectionSet(s.SelectionSet)
2✔
232
                        w.writeByte(';')
2✔
233
                case *ast.FragmentSpread:
6✔
234
                        w.writeString("...")
6✔
235
                        if s.Name != nil {
12✔
236
                                w.writeString(s.Name.Value)
6✔
237
                                w.writeByte(';')
6✔
238
                                w.writeFragmentBody(s.Name.Value)
6✔
239
                        }
6✔
240
                }
241
        }
242
        w.writeByte('}')
48✔
243
}
244

245
func (w *fingerprintWriter) writeFragmentBody(name string) {
6✔
246
        if w.visited == nil {
10✔
247
                w.visited = map[string]bool{}
4✔
248
        }
4✔
249
        if w.visited[name] {
8✔
250
                return
2✔
251
        }
2✔
252
        w.visited[name] = true
4✔
253
        frag, ok := w.fragments[name]
4✔
254
        if !ok {
4✔
NEW
255
                return
×
NEW
256
        }
×
257
        w.writeByte('F')
4✔
258
        if frag.TypeCondition != nil && frag.TypeCondition.Name != nil {
8✔
259
                w.writeString(frag.TypeCondition.Name.Value)
4✔
260
        }
4✔
261
        w.writeSelectionSet(frag.SelectionSet)
4✔
262
}
263

264
// writeValue writes canonical bytes for an ast.Value. Variables are
265
// hashed by name (so synth var names from normalization participate
266
// in the key). Literals that survived normalization are hashed as
267
// their kind+content — two identical un-extractable literals map to
268
// the same fingerprint, two different ones don't.
269
func (w *fingerprintWriter) writeValue(v ast.Value) {
34✔
270
        switch n := v.(type) {
34✔
NEW
271
        case nil:
×
NEW
272
                w.writeByte('n')
×
273
        case *ast.Variable:
25✔
274
                w.writeByte('V')
25✔
275
                if n.Name != nil {
50✔
276
                        w.writeString(n.Name.Value)
25✔
277
                }
25✔
278
        case *ast.IntValue:
6✔
279
                w.writeByte('i')
6✔
280
                w.writeString(n.Value)
6✔
NEW
281
        case *ast.FloatValue:
×
NEW
282
                w.writeByte('f')
×
NEW
283
                w.writeString(n.Value)
×
NEW
284
        case *ast.StringValue:
×
NEW
285
                w.writeByte('s')
×
NEW
286
                w.writeString(n.Value)
×
NEW
287
        case *ast.BooleanValue:
×
NEW
288
                w.writeByte('b')
×
NEW
289
                if n.Value {
×
NEW
290
                        w.writeByte('1')
×
NEW
291
                } else {
×
NEW
292
                        w.writeByte('0')
×
NEW
293
                }
×
NEW
294
        case *ast.EnumValue:
×
NEW
295
                w.writeByte('e')
×
NEW
296
                w.writeString(n.Value)
×
297
        case *ast.ListValue:
3✔
298
                w.writeByte('[')
3✔
299
                for _, item := range n.Values {
12✔
300
                        w.writeValue(item)
9✔
301
                        w.writeByte(',')
9✔
302
                }
9✔
303
                w.writeByte(']')
3✔
NEW
304
        case *ast.ObjectValue:
×
NEW
305
                w.writeByte('{')
×
NEW
306
                for _, f := range n.Fields {
×
NEW
307
                        if f == nil || f.Name == nil {
×
NEW
308
                                continue
×
309
                        }
NEW
310
                        w.writeString(f.Name.Value)
×
NEW
311
                        w.writeByte('=')
×
NEW
312
                        w.writeValue(f.Value)
×
NEW
313
                        w.writeByte(',')
×
314
                }
NEW
315
                w.writeByte('}')
×
316
        }
317
}
318

319
// normCtx threads state across the recursive walk: schema for type
320
// lookups, synth counter, accumulated args + var defs.
321
type normCtx struct {
322
        schema     *Schema
323
        counter    int
324
        synthArgs  map[string]interface{}
325
        newVarDefs []*ast.VariableDefinition
326
}
327

328
func (c *normCtx) nextName() string {
19✔
329
        n := fmt.Sprintf("__pcv%d", c.counter)
19✔
330
        c.counter++
19✔
331
        return n
19✔
332
}
19✔
333

334
// normalizeSelectionSet walks selections under the given parent type.
335
// Inline fragments are followed (with type-condition awareness);
336
// fragment spreads are skipped (literals inside named fragments stay).
337
func (c *normCtx) normalizeSelectionSet(sel *ast.SelectionSet, parentType *Object) {
40✔
338
        if sel == nil {
40✔
NEW
339
                return
×
NEW
340
        }
×
341
        for _, isel := range sel.Selections {
82✔
342
                switch s := isel.(type) {
42✔
343
                case *ast.Field:
36✔
344
                        c.normalizeField(s, parentType)
36✔
NEW
345
                case *ast.InlineFragment:
×
NEW
346
                        frag := s
×
NEW
347
                        condType := parentType
×
NEW
348
                        if frag.TypeCondition != nil {
×
NEW
349
                                if t, err := typeFromAST(*c.schema, frag.TypeCondition); err == nil {
×
NEW
350
                                        if obj, ok := t.(*Object); ok {
×
NEW
351
                                                condType = obj
×
NEW
352
                                        }
×
353
                                }
354
                        }
NEW
355
                        c.normalizeSelectionSet(frag.SelectionSet, condType)
×
356
                case *ast.FragmentSpread:
6✔
357
                        // Skipped: see scope note in normalizeDocument.
358
                }
359
        }
360
}
361

362
// normalizeField rewrites a field's arguments in place (the field
363
// AST is already a clone of the original) and recurses into its
364
// sub-selection if the return type is a concrete Object.
365
func (c *normCtx) normalizeField(f *ast.Field, parentType *Object) {
36✔
366
        fieldName := ""
36✔
367
        if f.Name != nil {
72✔
368
                fieldName = f.Name.Value
36✔
369
        }
36✔
370
        fieldDef := getFieldDef(*c.schema, parentType, fieldName)
36✔
371
        if fieldDef == nil {
36✔
NEW
372
                return
×
NEW
373
        }
×
374
        if len(f.Arguments) > 0 {
61✔
375
                // Build an arg-name → argDef map for O(1) lookup.
25✔
376
                argDefByName := make(map[string]*Argument, len(fieldDef.Args))
25✔
377
                for _, ad := range fieldDef.Args {
50✔
378
                        argDefByName[ad.PrivateName] = ad
25✔
379
                }
25✔
380
                for _, arg := range f.Arguments {
50✔
381
                        if arg == nil || arg.Name == nil {
25✔
NEW
382
                                continue
×
383
                        }
384
                        ad := argDefByName[arg.Name.Value]
25✔
385
                        if ad == nil {
25✔
NEW
386
                                continue
×
387
                        }
388
                        if newVal, ok := c.tryExtract(arg.Value, ad.Type); ok {
44✔
389
                                arg.Value = newVal
19✔
390
                        }
19✔
391
                }
392
        }
393
        // Recurse into sub-selection on concrete object returns. List and
394
        // NonNull wrappers are unwrapped here.
395
        if f.SelectionSet != nil {
47✔
396
                t := unwrapToNamed(fieldDef.Type)
11✔
397
                if obj, ok := t.(*Object); ok {
20✔
398
                        c.normalizeSelectionSet(f.SelectionSet, obj)
9✔
399
                }
9✔
400
        }
401
}
402

403
// tryExtract attempts to replace the entire `value` AST with a synth
404
// variable. Returns the replacement *ast.Variable + true on success;
405
// returns the original value + false when extraction isn't safe
406
// (variable already present anywhere in the value).
407
func (c *normCtx) tryExtract(value ast.Value, expected Input) (ast.Value, bool) {
25✔
408
        if value == nil {
25✔
NEW
409
                return value, false
×
NEW
410
        }
×
411
        if _, isVar := value.(*ast.Variable); isVar {
28✔
412
                return value, false
3✔
413
        }
3✔
414
        if valueHasVariables(value) {
25✔
415
                return value, false
3✔
416
        }
3✔
417
        if expected == nil {
19✔
NEW
418
                return value, false
×
NEW
419
        }
×
420
        // Coerce literal once at extract time. We pass nil variableValues
421
        // because we already know the value tree contains no variables.
422
        coerced := valueFromAST(value, expected, nil)
19✔
423
        if coerced == nil {
19✔
NEW
424
                // valueFromAST returns nil for literals it can't coerce
×
NEW
425
                // (typically a type mismatch the validator should have caught
×
NEW
426
                // earlier). Don't extract; let the executor surface the
×
NEW
427
                // downstream error against the original literal.
×
NEW
428
                return value, false
×
NEW
429
        }
×
430
        name := c.nextName()
19✔
431
        c.synthArgs[name] = coerced
19✔
432
        c.newVarDefs = append(c.newVarDefs, ast.NewVariableDefinition(&ast.VariableDefinition{
19✔
433
                Variable: ast.NewVariable(&ast.Variable{Name: ast.NewName(&ast.Name{Value: name})}),
19✔
434
                Type:     typeASTFromGoType(expected),
19✔
435
        }))
19✔
436
        return ast.NewVariable(&ast.Variable{Name: ast.NewName(&ast.Name{Value: name})}), true
19✔
437
}
438

439
// typeASTFromGoType maps a runtime Type to its AST form so we can
440
// build VariableDefinition.Type. NonNull and List wrap; named types
441
// terminate with an *ast.Named referencing the type's name.
442
func typeASTFromGoType(t Input) ast.Type {
23✔
443
        switch tt := t.(type) {
23✔
444
        case *NonNull:
2✔
445
                inner := typeASTFromGoType(tt.OfType.(Input))
2✔
446
                return ast.NewNonNull(&ast.NonNull{Type: inner})
2✔
447
        case *List:
2✔
448
                inner := typeASTFromGoType(tt.OfType.(Input))
2✔
449
                return ast.NewList(&ast.List{Type: inner})
2✔
450
        default:
19✔
451
                name := ""
19✔
452
                if named, ok := t.(interface{ Name() string }); ok {
38✔
453
                        name = named.Name()
19✔
454
                }
19✔
455
                return ast.NewNamed(&ast.Named{Name: ast.NewName(&ast.Name{Value: name})})
19✔
456
        }
457
}
458

459
// unwrapToNamed strips NonNull and List wrappers to expose the inner
460
// named type. Mirrors plan.go's unwrapNamedType for Output positions
461
// but takes any Type for use against fieldDef.Type which is Output.
462
func unwrapToNamed(t Type) Type {
11✔
463
        for {
27✔
464
                switch tt := t.(type) {
16✔
NEW
465
                case *NonNull:
×
NEW
466
                        t = tt.OfType
×
467
                case *List:
5✔
468
                        t = tt.OfType
5✔
469
                default:
11✔
470
                        return tt
11✔
471
                }
472
        }
473
}
474

475
// cloneOperation deep-copies the parts of an OperationDefinition we
476
// mutate: SelectionSet (recursively, only Fields' Arguments) and the
477
// VariableDefinitions slice (we append; original stays read-only).
478
// Other fields share with the original — we never write through them.
479
func cloneOperation(op *ast.OperationDefinition) *ast.OperationDefinition {
31✔
480
        out := &ast.OperationDefinition{
31✔
481
                Kind:                op.Kind,
31✔
482
                Loc:                 op.Loc,
31✔
483
                Operation:           op.Operation,
31✔
484
                Name:                op.Name,
31✔
485
                Directives:          op.Directives,
31✔
486
                VariableDefinitions: append([]*ast.VariableDefinition(nil), op.VariableDefinitions...),
31✔
487
                SelectionSet:        cloneSelectionSet(op.SelectionSet),
31✔
488
        }
31✔
489
        return out
31✔
490
}
31✔
491

492
func cloneSelectionSet(sel *ast.SelectionSet) *ast.SelectionSet {
71✔
493
        if sel == nil {
98✔
494
                return nil
27✔
495
        }
27✔
496
        out := &ast.SelectionSet{Kind: sel.Kind, Loc: sel.Loc, Selections: make([]ast.Selection, len(sel.Selections))}
44✔
497
        for i, s := range sel.Selections {
90✔
498
                switch n := s.(type) {
46✔
499
                case *ast.Field:
38✔
500
                        out.Selections[i] = cloneField(n)
38✔
501
                case *ast.InlineFragment:
2✔
502
                        out.Selections[i] = &ast.InlineFragment{
2✔
503
                                Kind:          n.Kind,
2✔
504
                                Loc:           n.Loc,
2✔
505
                                TypeCondition: n.TypeCondition,
2✔
506
                                Directives:    n.Directives,
2✔
507
                                SelectionSet:  cloneSelectionSet(n.SelectionSet),
2✔
508
                        }
2✔
509
                default:
6✔
510
                        // FragmentSpread or anything else: share by reference.
6✔
511
                        out.Selections[i] = s
6✔
512
                }
513
        }
514
        return out
44✔
515
}
516

517
func cloneField(f *ast.Field) *ast.Field {
38✔
518
        args := make([]*ast.Argument, len(f.Arguments))
38✔
519
        for i, a := range f.Arguments {
63✔
520
                // Shallow clone of *ast.Argument so we can swap a.Value
25✔
521
                // without mutating the original.
25✔
522
                ac := *a
25✔
523
                args[i] = &ac
25✔
524
        }
25✔
525
        return &ast.Field{
38✔
526
                Kind:         f.Kind,
38✔
527
                Loc:          f.Loc,
38✔
528
                Alias:        f.Alias,
38✔
529
                Name:         f.Name,
38✔
530
                Arguments:    args,
38✔
531
                Directives:   f.Directives,
38✔
532
                SelectionSet: cloneSelectionSet(f.SelectionSet),
38✔
533
        }
38✔
534
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc