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

graphql-go / graphql / 1887

09 May 2026 07:53PM UTC coverage: 83.711% (-8.6%) from 92.284%
1887

Pull #740

circleci

Nthalk
chore: bump Go floor to 1.21

atomic.Uint64 (used in PlanCache) needs Go 1.19+. Raise go.mod to
1.21 and refresh CI: drop the 1.8/1.9/1.11 jobs and the
test_without_go_modules path, switch to cimg/go images at 1.21 and
1.26, and replace `go get goveralls` with `go install …@latest`.
Pull Request #740: Plan + PlanQuery + ExecutePlan + PlanCache (cacheable execution shape)

790 of 1055 new or added lines in 4 files covered. (74.88%)

406 existing lines in 1 file now uncovered.

5797 of 6925 relevant lines covered (83.71%)

263.7 hits per line

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

58.24
/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) {
5✔
52
        if schema == nil || doc == nil {
5✔
NEW
53
                return doc, nil, "", nil
×
NEW
54
        }
×
55

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

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

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

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

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

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

5✔
97
        // Build a new doc containing the normalized op + the rest of the
5✔
98
        // definitions (fragments) unchanged. Order preserved: op stays in
5✔
99
        // its original slot.
5✔
100
        newDefs := make([]ast.Node, 0, len(doc.Definitions))
5✔
101
        for _, def := range doc.Definitions {
10✔
102
                if def == op {
10✔
103
                        newDefs = append(newDefs, newOp)
5✔
104
                } else {
5✔
NEW
105
                        newDefs = append(newDefs, def)
×
NEW
106
                }
×
107
        }
108
        newDoc := &ast.Document{Kind: doc.Kind, Loc: doc.Loc, Definitions: newDefs}
5✔
109
        return newDoc, ctx.synthArgs, fingerprintDocument(newDoc, newOp, operationName), nil
5✔
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 {
5✔
130
        h := fnv.New64a()
5✔
131
        w := fingerprintWriter{h: h, fragments: collectFragmentDefs(doc)}
5✔
132
        w.writeString("OP:")
5✔
133
        w.writeString(string(op.Operation))
5✔
134
        w.writeByte(0)
5✔
135
        w.writeString(operationName)
5✔
136
        w.writeByte(0)
5✔
137
        w.writeVariableDefs(op.VariableDefinitions)
5✔
138
        w.writeSelectionSet(op.SelectionSet)
5✔
139
        return strconv.FormatUint(h.Sum64(), 16)
5✔
140
}
5✔
141

142
func collectFragmentDefs(doc *ast.Document) map[string]*ast.FragmentDefinition {
5✔
143
        out := map[string]*ast.FragmentDefinition{}
5✔
144
        for _, def := range doc.Definitions {
10✔
145
                if fd, ok := def.(*ast.FragmentDefinition); ok && fd.Name != nil {
5✔
NEW
146
                        out[fd.Name.Value] = fd
×
NEW
147
                }
×
148
        }
149
        return out
5✔
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)) }
50✔
164
func (w *fingerprintWriter) writeByte(b byte)     { _, _ = w.h.Write([]byte{b}) }
80✔
165

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

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

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

NEW
245
func (w *fingerprintWriter) writeFragmentBody(name string) {
×
NEW
246
        if w.visited == nil {
×
NEW
247
                w.visited = map[string]bool{}
×
NEW
248
        }
×
NEW
249
        if w.visited[name] {
×
NEW
250
                return
×
NEW
251
        }
×
NEW
252
        w.visited[name] = true
×
NEW
253
        frag, ok := w.fragments[name]
×
NEW
254
        if !ok {
×
NEW
255
                return
×
NEW
256
        }
×
NEW
257
        w.writeByte('F')
×
NEW
258
        if frag.TypeCondition != nil && frag.TypeCondition.Name != nil {
×
NEW
259
                w.writeString(frag.TypeCondition.Name.Value)
×
NEW
260
        }
×
NEW
261
        w.writeSelectionSet(frag.SelectionSet)
×
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) {
5✔
270
        switch n := v.(type) {
5✔
NEW
271
        case nil:
×
NEW
272
                w.writeByte('n')
×
273
        case *ast.Variable:
5✔
274
                w.writeByte('V')
5✔
275
                if n.Name != nil {
10✔
276
                        w.writeString(n.Name.Value)
5✔
277
                }
5✔
NEW
278
        case *ast.IntValue:
×
NEW
279
                w.writeByte('i')
×
NEW
280
                w.writeString(n.Value)
×
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)
×
NEW
297
        case *ast.ListValue:
×
NEW
298
                w.writeByte('[')
×
NEW
299
                for _, item := range n.Values {
×
NEW
300
                        w.writeValue(item)
×
NEW
301
                        w.writeByte(',')
×
NEW
302
                }
×
NEW
303
                w.writeByte(']')
×
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 {
5✔
329
        n := fmt.Sprintf("__pcv%d", c.counter)
5✔
330
        c.counter++
5✔
331
        return n
5✔
332
}
5✔
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) {
10✔
338
        if sel == nil {
10✔
NEW
339
                return
×
NEW
340
        }
×
341
        for _, isel := range sel.Selections {
20✔
342
                switch s := isel.(type) {
10✔
343
                case *ast.Field:
10✔
344
                        c.normalizeField(s, parentType)
10✔
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)
×
NEW
356
                case *ast.FragmentSpread:
×
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) {
10✔
366
        fieldName := ""
10✔
367
        if f.Name != nil {
20✔
368
                fieldName = f.Name.Value
10✔
369
        }
10✔
370
        fieldDef := getFieldDef(*c.schema, parentType, fieldName)
10✔
371
        if fieldDef == nil {
10✔
NEW
372
                return
×
NEW
373
        }
×
374
        if len(f.Arguments) > 0 {
15✔
375
                // Build an arg-name → argDef map for O(1) lookup.
5✔
376
                argDefByName := make(map[string]*Argument, len(fieldDef.Args))
5✔
377
                for _, ad := range fieldDef.Args {
10✔
378
                        argDefByName[ad.PrivateName] = ad
5✔
379
                }
5✔
380
                for _, arg := range f.Arguments {
10✔
381
                        if arg == nil || arg.Name == nil {
5✔
NEW
382
                                continue
×
383
                        }
384
                        ad := argDefByName[arg.Name.Value]
5✔
385
                        if ad == nil {
5✔
NEW
386
                                continue
×
387
                        }
388
                        if newVal, ok := c.tryExtract(arg.Value, ad.Type); ok {
10✔
389
                                arg.Value = newVal
5✔
390
                        }
5✔
391
                }
392
        }
393
        // Recurse into sub-selection on concrete object returns. List and
394
        // NonNull wrappers are unwrapped here.
395
        if f.SelectionSet != nil {
15✔
396
                t := unwrapToNamed(fieldDef.Type)
5✔
397
                if obj, ok := t.(*Object); ok {
10✔
398
                        c.normalizeSelectionSet(f.SelectionSet, obj)
5✔
399
                }
5✔
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) {
5✔
408
        if value == nil {
5✔
NEW
409
                return value, false
×
NEW
410
        }
×
411
        if _, isVar := value.(*ast.Variable); isVar {
5✔
NEW
412
                return value, false
×
NEW
413
        }
×
414
        if valueHasVariables(value) {
5✔
NEW
415
                return value, false
×
NEW
416
        }
×
417
        if expected == nil {
5✔
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)
5✔
423
        if coerced == nil {
5✔
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()
5✔
431
        c.synthArgs[name] = coerced
5✔
432
        c.newVarDefs = append(c.newVarDefs, ast.NewVariableDefinition(&ast.VariableDefinition{
5✔
433
                Variable: ast.NewVariable(&ast.Variable{Name: ast.NewName(&ast.Name{Value: name})}),
5✔
434
                Type:     typeASTFromGoType(expected),
5✔
435
        }))
5✔
436
        return ast.NewVariable(&ast.Variable{Name: ast.NewName(&ast.Name{Value: name})}), true
5✔
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 {
5✔
443
        switch tt := t.(type) {
5✔
NEW
444
        case *NonNull:
×
NEW
445
                inner := typeASTFromGoType(tt.OfType.(Input))
×
NEW
446
                return ast.NewNonNull(&ast.NonNull{Type: inner})
×
NEW
447
        case *List:
×
NEW
448
                inner := typeASTFromGoType(tt.OfType.(Input))
×
NEW
449
                return ast.NewList(&ast.List{Type: inner})
×
450
        default:
5✔
451
                name := ""
5✔
452
                if named, ok := t.(interface{ Name() string }); ok {
10✔
453
                        name = named.Name()
5✔
454
                }
5✔
455
                return ast.NewNamed(&ast.Named{Name: ast.NewName(&ast.Name{Value: name})})
5✔
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 {
5✔
463
        for {
15✔
464
                switch tt := t.(type) {
10✔
NEW
465
                case *NonNull:
×
NEW
466
                        t = tt.OfType
×
467
                case *List:
5✔
468
                        t = tt.OfType
5✔
469
                default:
5✔
470
                        return tt
5✔
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 {
5✔
480
        out := &ast.OperationDefinition{
5✔
481
                Kind:                op.Kind,
5✔
482
                Loc:                 op.Loc,
5✔
483
                Operation:           op.Operation,
5✔
484
                Name:                op.Name,
5✔
485
                Directives:          op.Directives,
5✔
486
                VariableDefinitions: append([]*ast.VariableDefinition(nil), op.VariableDefinitions...),
5✔
487
                SelectionSet:        cloneSelectionSet(op.SelectionSet),
5✔
488
        }
5✔
489
        return out
5✔
490
}
5✔
491

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

517
func cloneField(f *ast.Field) *ast.Field {
10✔
518
        args := make([]*ast.Argument, len(f.Arguments))
10✔
519
        for i, a := range f.Arguments {
15✔
520
                // Shallow clone of *ast.Argument so we can swap a.Value
5✔
521
                // without mutating the original.
5✔
522
                ac := *a
5✔
523
                args[i] = &ac
5✔
524
        }
5✔
525
        return &ast.Field{
10✔
526
                Kind:         f.Kind,
10✔
527
                Loc:          f.Loc,
10✔
528
                Alias:        f.Alias,
10✔
529
                Name:         f.Name,
10✔
530
                Arguments:    args,
10✔
531
                Directives:   f.Directives,
10✔
532
                SelectionSet: cloneSelectionSet(f.SelectionSet),
10✔
533
        }
10✔
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