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

livetribe / yama / 30318502794

28 Jul 2026 12:50AM UTC coverage: 65.17% (+0.2%) from 65.017%
30318502794

push

github

maguro
Silence slog for the whole contract suite run

TestRT drives real lifecycles through panicking and failing paths to
prove recovery works, but no spec asserts on what gets logged — so
every recovered panic printed to stderr on every green run. A
BeforeSuite now swaps in a discard handler for the run, the same
swap-and-restore shape rt/internal/exec's captureSlog already uses for
specs that do need to inspect the log.

958 of 1470 relevant lines covered (65.17%)

37.14 hits per line

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

88.0
/internal/generator/parse.go
1
// Copyright (c) 2026 the original author or authors.
2
//
3
// Licensed under the Apache License, Version 2.0 (the "License");
4
// you may not use this file except in compliance with the License.
5
// You may obtain a copy of the License at
6
//
7
//     http://www.apache.org/licenses/LICENSE-2.0
8
//
9
// Unless required by applicable law or agreed to in writing, software
10
// distributed under the License is distributed on an "AS IS" BASIS,
11
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
// See the License for the specific language governing permissions and
13
// limitations under the License.
14

15
package generator
16

17
import (
18
        "go/ast"
19
        "go/token"
20
        "go/types"
21
)
22

23
// cleanupResultIndex is the position of the aggregated cleanup closure in an
24
// injector's return list, when the injector returns one.
25
const cleanupResultIndex = 1
26

27
// Parse walks the AST of a generated wire_gen.go and extracts one independent
28
// dependency graph per injector function. It performs no type analysis and
29
// derives no lifecycle levels; it records the ordered creation events, their
30
// dependency edges, each injector's returned root, and any Google Wire cleanup
31
// functions.
32
//
33
// A shape it cannot derive ordering from is reported as a *ParseError naming the
34
// injector and source position, never a panic and never a silent omission.
35
func Parse(fset *token.FileSet, file *ast.File) (*ParsedFile, error) {
21✔
36
        valueVars := packageValueVars(file)
21✔
37

21✔
38
        var injectors []*Injector
21✔
39
        for _, decl := range file.Decls {
56✔
40
                fn, ok := decl.(*ast.FuncDecl)
35✔
41
                if !ok || fn.Body == nil {
44✔
42
                        continue
9✔
43
                }
44

45
                inj, err := parseInjector(fset, fn, valueVars)
26✔
46
                if err != nil {
29✔
47
                        return nil, err
3✔
48
                }
3✔
49
                injectors = append(injectors, inj)
23✔
50
        }
51

52
        if err := checkResultNameCollisions(fset, injectors); err != nil {
19✔
53
                return nil, err
1✔
54
        }
1✔
55

56
        return &ParsedFile{Fset: fset, Syntax: file, Injectors: injectors}, nil
17✔
57
}
58

59
// parseInjector extracts one injector's graph. The body's final statement must be
60
// the return; every earlier statement is a creation event, an error guard that is
61
// skipped, or an unsupported shape that fails.
62
func parseInjector(fset *token.FileSet, fn *ast.FuncDecl, valueVars map[string]ast.Expr) (*Injector, error) {
26✔
63
        inj := &Injector{
26✔
64
                Name:     fn.Name.Name,
26✔
65
                Params:   extractFields(fn.Type.Params),
26✔
66
                Results:  extractFields(fn.Type.Results),
26✔
67
                FuncDecl: fn,
26✔
68
        }
26✔
69

26✔
70
        body := fn.Body.List
26✔
71
        if len(body) == 0 {
26✔
72
                return nil, newParseError(fset, inj.Name, fn.Pos(), "injector body is empty")
×
73
        }
×
74

75
        ret, ok := body[len(body)-1].(*ast.ReturnStmt)
26✔
76
        if !ok {
26✔
77
                return nil, newParseError(fset, inj.Name, body[len(body)-1].Pos(),
×
78
                        "injector body does not end in a return statement")
×
79
        }
×
80
        inj.Return = ret
26✔
81

26✔
82
        byName := map[string]*Component{}
26✔
83
        for _, stmt := range body[:len(body)-1] {
188✔
84
                c, err := parseStatement(fset, inj.Name, stmt)
162✔
85
                if err != nil {
164✔
86
                        return nil, err
2✔
87
                }
2✔
88
                if c != nil {
312✔
89
                        inj.Components = append(inj.Components, c)
152✔
90
                        byName[c.Name] = c
152✔
91
                }
152✔
92
        }
93

94
        deriveEdges(inj.Components, byName)
24✔
95
        if err := detectCleanups(fset, inj.Name, inj.Components, ret); err != nil {
25✔
96
                return nil, err
1✔
97
        }
1✔
98
        resolveValueExprs(inj.Components, valueVars)
23✔
99

23✔
100
        return inj, nil
23✔
101
}
102

103
// parseStatement classifies one non-final body statement. It returns a component
104
// for a creation event, nil for a skipped error guard, or an error for any shape
105
// the parser cannot derive ordering from.
106
func parseStatement(fset *token.FileSet, injector string, stmt ast.Stmt) (*Component, error) {
162✔
107
        switch s := stmt.(type) {
162✔
108
        case *ast.AssignStmt:
154✔
109
                if s.Tok == token.DEFINE {
307✔
110
                        return parseProviderForm(fset, injector, s)
153✔
111
                }
153✔
112
                return nil, newParseError(fset, injector, s.Pos(),
1✔
113
                        "no traceable dependency edge: %s reaches the graph through a %s assignment",
1✔
114
                        assignValueName(s), assignKind(s))
1✔
115
        case *ast.IfStmt:
8✔
116
                return nil, nil
8✔
117
        default:
×
118
                return nil, newParseError(fset, injector, stmt.Pos(),
×
119
                        "unsupported statement: %s", stmtKind(stmt))
×
120
        }
121
}
122

123
// parseProviderForm turns a `:=` statement into a component. The first left-hand
124
// identifier is the value; the right-hand side must be a shape one of the four
125
// provider kinds Google Wire emits.
126
//
127
// Its errors report an unrecognized provider *form* rather than an unrecognized
128
// provider: no provider has been identified when they fire, and the shape itself
129
// is what went unrecognized.
130
func parseProviderForm(fset *token.FileSet, injector string, s *ast.AssignStmt) (*Component, error) {
153✔
131
        ident, ok := s.Lhs[0].(*ast.Ident)
153✔
132
        if !ok {
153✔
133
                return nil, newParseError(fset, injector, s.Pos(),
×
134
                        "unrecognized provider form: assignment target is not an identifier")
×
135
        }
×
136

137
        if len(s.Rhs) != 1 {
153✔
138
                return nil, newParseError(fset, injector, s.Pos(),
×
139
                        "unrecognized provider form: %s is bound to multiple right-hand values", ident.Name)
×
140
        }
×
141

142
        rhs := s.Rhs[0]
153✔
143
        kind, ok := classifyProvider(rhs)
153✔
144
        if !ok {
154✔
145
                return nil, newParseError(fset, injector, rhs.Pos(),
1✔
146
                        "unrecognized provider form: %s is bound to a %s", ident.Name, exprKind(rhs))
1✔
147
        }
1✔
148

149
        return &Component{Name: ident.Name, Ident: ident, Provider: kind, Rhs: rhs, Assign: s}, nil
152✔
150
}
151

152
// classifyProvider maps a creation right-hand side to the provider kind that
153
// emitted it. A struct literal may be address-taken (`&T{...}`); every other
154
// recognized kind is its bare node.
155
func classifyProvider(rhs ast.Expr) (ProviderKind, bool) {
153✔
156
        switch e := rhs.(type) {
153✔
157
        case *ast.CallExpr:
116✔
158
                return ProviderCall, true
116✔
159
        case *ast.Ident:
14✔
160
                return ProviderValue, true
14✔
161
        case *ast.CompositeLit:
×
162
                return ProviderStruct, true
×
163
        case *ast.SelectorExpr:
9✔
164
                return ProviderFieldsOf, true
9✔
165
        case *ast.UnaryExpr:
13✔
166
                if e.Op == token.AND {
26✔
167
                        if _, ok := e.X.(*ast.CompositeLit); ok {
26✔
168
                                return ProviderStruct, true
13✔
169
                        }
13✔
170
                }
171
                return 0, false
×
172
        default:
1✔
173
                return 0, false
1✔
174
        }
175
}
176

177
// deriveEdges links each component to the components it consumes as arguments.
178
// Consumed identifiers that name a parameter or a package-level value are not
179
// components and contribute no edge.
180
func deriveEdges(components []*Component, byName map[string]*Component) {
24✔
181
        for _, c := range components {
169✔
182
                seen := map[*Component]bool{}
145✔
183
                for _, id := range consumedIdents(c) {
352✔
184
                        dep, ok := byName[id.Name]
207✔
185
                        if !ok || dep == c || seen[dep] {
212✔
186
                                continue
5✔
187
                        }
188
                        seen[dep] = true
202✔
189
                        c.Deps = append(c.Deps, dep)
202✔
190
                }
191
        }
192
}
193

194
// consumedIdents returns the identifiers a component's creation consumes as
195
// inputs, kind by kind. For a call, only the arguments are consumed, never the
196
// callee. collectIdents excludes field names, so a struct literal contributes its
197
// bound values and a field selection its base value.
198
func consumedIdents(c *Component) []*ast.Ident {
145✔
199
        switch c.Provider {
145✔
200
        case ProviderCall:
111✔
201
                return collectIdents(c.Rhs.(*ast.CallExpr).Args...)
111✔
202
        case ProviderFieldsOf:
9✔
203
                return collectIdents(c.Rhs)
9✔
204
        case ProviderStruct:
13✔
205
                return collectIdents(c.Rhs)
13✔
206
        case ProviderValue:
12✔
207
                return nil
12✔
208
        default:
×
209
                return nil
×
210
        }
211
}
212

213
// detectCleanups pairs each Google Wire cleanup function with the value it cleans
214
// up. The authoritative signal is membership in the aggregated cleanup closure the
215
// injector returns: every identifier that closure calls is a cleanup, and it
216
// belongs to the component bound in the same statement.
217
//
218
// A cleanup the closure calls but that pairs with no component is a shape the
219
// model cannot represent. It is reported rather than dropped, so a teardown is
220
// never silently lost.
221
func detectCleanups(fset *token.FileSet, injector string, components []*Component, ret *ast.ReturnStmt) error {
24✔
222
        if len(ret.Results) <= cleanupResultIndex {
34✔
223
                return nil
10✔
224
        }
10✔
225

226
        closure, ok := ret.Results[cleanupResultIndex].(*ast.FuncLit)
14✔
227
        if !ok {
16✔
228
                return nil
2✔
229
        }
2✔
230

231
        called := calledIdents(closure.Body)
12✔
232
        paired := map[string]bool{}
12✔
233
        for _, c := range components {
126✔
234
                for _, lhs := range c.Assign.Lhs[1:] {
141✔
235
                        id, ok := lhs.(*ast.Ident)
27✔
236
                        if ok && called[id.Name] {
46✔
237
                                c.Cleanup = &Cleanup{Name: id.Name, Ident: id, Pos: id.Pos()}
19✔
238
                                paired[id.Name] = true
19✔
239
                        }
19✔
240
                }
241
        }
242

243
        for name := range called {
32✔
244
                if !paired[name] {
21✔
245
                        return newParseError(fset, injector, closure.Pos(),
1✔
246
                                "cleanup function %s pairs with no component", name)
1✔
247
                }
1✔
248
        }
249

250
        return nil
11✔
251
}
252

253
// resolveValueExprs attaches to each ProviderValue component the initializer of the
254
// package-level _wire*Value variable it reads, so the value can be re-emitted
255
// directly rather than referencing that private variable.
256
func resolveValueExprs(components []*Component, valueVars map[string]ast.Expr) {
23✔
257
        for _, c := range components {
166✔
258
                if c.Provider != ProviderValue {
274✔
259
                        continue
131✔
260
                }
261
                if id, ok := c.Rhs.(*ast.Ident); ok {
24✔
262
                        c.ValueExpr = valueVars[id.Name]
12✔
263
                }
12✔
264
        }
265
}
266

267
// checkResultNameCollisions rejects a file whose injectors return results that
268
// share an unqualified type name but denote different types: Yama's generated
269
// namespace derives from the unqualified name and cannot deterministically tell
270
// them apart. Injectors returning the identical result type are legal and share
271
// generated types.
272
func checkResultNameCollisions(fset *token.FileSet, injectors []*Injector) error {
18✔
273
        firstByName := map[string]*Injector{}
18✔
274
        for _, inj := range injectors {
41✔
275
                resultType := inj.ResultType()
23✔
276
                if resultType == nil {
23✔
277
                        continue
×
278
                }
279

280
                short := unqualifiedTypeName(resultType)
23✔
281
                if short == "" {
23✔
282
                        continue
×
283
                }
284

285
                prev, seen := firstByName[short]
23✔
286
                if !seen {
41✔
287
                        firstByName[short] = inj
18✔
288
                        continue
18✔
289
                }
290

291
                prevType := prev.ResultType()
5✔
292
                if types.ExprString(prevType) != types.ExprString(resultType) {
6✔
293
                        return &ParseError{
1✔
294
                                Injector: prev.Name + ", " + inj.Name,
1✔
295
                                Pos:      fset.Position(resultType.Pos()),
1✔
296
                                Msg:      "result type name collision on \"" + short + "\": distinct types share one generated namespace",
1✔
297
                        }
1✔
298
                }
1✔
299
        }
300

301
        return nil
17✔
302
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc