1
2
3
4
5 package unitchecker_test
6
7
8
9 import (
10 "bytes"
11 "encoding/json"
12 "fmt"
13 "go/token"
14 "go/types"
15 "io"
16 "os"
17 "path/filepath"
18 "strings"
19 "sync/atomic"
20 "testing"
21
22 "golang.org/x/tools/go/analysis/passes/printf"
23 "golang.org/x/tools/go/analysis/unitchecker"
24 "golang.org/x/tools/go/gcexportdata"
25 "golang.org/x/tools/go/packages"
26 "golang.org/x/tools/internal/testenv"
27 "golang.org/x/tools/internal/testfiles"
28 "golang.org/x/tools/txtar"
29 )
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56 func TestExampleSeparateAnalysis(t *testing.T) {
57 testenv.NeedsGoPackages(t)
58
59
60 const src = `
61 -- go.mod --
62 module separate
63 go 1.18
64
65 -- main/main.go --
66 package main
67
68 import "separate/lib"
69
70 func main() {
71 lib.MyPrintf("%s", 123)
72 }
73
74 -- lib/lib.go --
75 package lib
76
77 import "fmt"
78
79 func MyPrintf(format string, args ...any) {
80 fmt.Printf(format, args...)
81 }
82 `
83
84
85 tmpdir := t.TempDir()
86 if err := testfiles.ExtractTxtar(tmpdir, txtar.Parse([]byte(src))); err != nil {
87 t.Fatal(err)
88 }
89
90
91 cfg := &packages.Config{
92 Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedImports | packages.NeedModule,
93 Dir: tmpdir,
94 Env: append(os.Environ(),
95 "GOPROXY=off",
96 "GOWORK=off",
97 ),
98 Logf: t.Logf,
99 }
100 pkgs, err := packages.Load(cfg, "separate/main")
101 if err != nil {
102 t.Fatal(err)
103 }
104
105 if packages.PrintErrors(pkgs) > 0 {
106 t.Fatal("there were errors among loaded packages")
107 }
108
109
110
111
112
113
114
115 roots := make(map[*packages.Package]bool)
116 for _, pkg := range pkgs {
117 roots[pkg] = true
118 }
119
120
121
122 var nextID atomic.Int32
123
124 var allDiagnostics []string
125
126
127
128 packages.Visit(pkgs, nil, func(pkg *packages.Package) {
129 if pkg.PkgPath == "unsafe" {
130 return
131 }
132
133
134
135
136
137 prefix := fmt.Sprintf("%s/%d", tmpdir, nextID.Add(1))
138 pkg.ExportFile = prefix
139
140
141 var (
142 importMap = make(map[string]string)
143 packageFile = make(map[string]string)
144 packageVetx = make(map[string]string)
145 )
146 for importPath, dep := range pkg.Imports {
147 importMap[importPath] = dep.PkgPath
148 if depPrefix := dep.ExportFile; depPrefix != "" {
149 packageFile[dep.PkgPath] = depPrefix + ".types"
150 packageVetx[dep.PkgPath] = depPrefix + ".facts"
151 }
152 }
153 cfg := unitchecker.Config{
154 ID: pkg.ID,
155 ImportPath: pkg.PkgPath,
156 GoFiles: pkg.CompiledGoFiles,
157 NonGoFiles: pkg.OtherFiles,
158 IgnoredFiles: pkg.IgnoredFiles,
159 ImportMap: importMap,
160 PackageFile: packageFile,
161 PackageVetx: packageVetx,
162 VetxOnly: !roots[pkg],
163 VetxOutput: prefix + ".facts",
164 }
165 if pkg.Module != nil {
166 if v := pkg.Module.GoVersion; v != "" {
167 cfg.GoVersion = "go" + v
168 }
169 }
170
171
172 cfgData, err := json.Marshal(cfg)
173 if err != nil {
174 t.Fatalf("internal error in json.Marshal: %v", err)
175 }
176 cfgFile := prefix + ".cfg"
177 if err := os.WriteFile(cfgFile, cfgData, 0666); err != nil {
178 t.Fatal(err)
179 }
180
181
182 cmd := testenv.Command(t, os.Args[0], "-json", cfgFile)
183 cmd.Stderr = os.Stderr
184 cmd.Stdout = new(bytes.Buffer)
185 cmd.Env = append(os.Environ(), "ENTRYPOINT=worker")
186 if err := cmd.Run(); err != nil {
187 t.Fatal(err)
188 }
189
190
191 dec := json.NewDecoder(cmd.Stdout.(io.Reader))
192 for {
193 type jsonDiagnostic struct {
194 Posn string `json:"posn"`
195 Message string `json:"message"`
196 }
197
198 var results map[string]map[string][]jsonDiagnostic
199 if err := dec.Decode(&results); err != nil {
200 if err == io.EOF {
201 break
202 }
203 t.Fatalf("internal error decoding JSON: %v", err)
204 }
205 for _, result := range results {
206 for analyzer, diags := range result {
207 for _, diag := range diags {
208 rel := strings.ReplaceAll(diag.Posn, tmpdir, "")
209 rel = filepath.ToSlash(rel)
210 msg := fmt.Sprintf("%s: [%s] %s", rel, analyzer, diag.Message)
211 allDiagnostics = append(allDiagnostics, msg)
212 }
213 }
214 }
215 }
216 })
217
218
219
220
221 const want = `/main/main.go:6:2: [printf] separate/lib.MyPrintf format %s has arg 123 of wrong type int`
222 if got := strings.Join(allDiagnostics, "\n"); got != want {
223 t.Errorf("Got: %s\nWant: %s", got, want)
224 }
225 }
226
227
228
229
230
231 func worker() {
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250 unitchecker.SetTypeImportExport(makeTypesImporter, exportTypes)
251
252 unitchecker.Main(printf.Analyzer)
253 }
254
255 func makeTypesImporter(cfg *unitchecker.Config, fset *token.FileSet) types.Importer {
256 imports := make(map[string]*types.Package)
257 return importerFunc(func(importPath string) (*types.Package, error) {
258
259 path, ok := cfg.ImportMap[importPath]
260 if !ok {
261 return nil, fmt.Errorf("can't resolve import %q", path)
262 }
263 if path == "unsafe" {
264 return types.Unsafe, nil
265 }
266
267
268 file, ok := cfg.PackageFile[path]
269 if !ok {
270 return nil, fmt.Errorf("no package file for %q", path)
271 }
272 f, err := os.Open(file)
273 if err != nil {
274 return nil, err
275 }
276 defer f.Close()
277 return gcexportdata.Read(f, fset, imports, path)
278 })
279 }
280
281 func exportTypes(cfg *unitchecker.Config, fset *token.FileSet, pkg *types.Package) error {
282 var out bytes.Buffer
283 if err := gcexportdata.Write(&out, fset, pkg); err != nil {
284 return err
285 }
286 typesFile := strings.TrimSuffix(cfg.VetxOutput, ".facts") + ".types"
287 return os.WriteFile(typesFile, out.Bytes(), 0666)
288 }
289
290
291
292 type importerFunc func(path string) (*types.Package, error)
293
294 func (f importerFunc) Import(path string) (*types.Package, error) { return f(path) }
295
View as plain text