blob: 111cd4fb8c0894ad1ec6d43f21e36984be314484 [file] [log] [blame]
Robert Findleyb15dac22022-08-30 14:40:12 -04001// Copyright 2020 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5package source
6
7import (
8 "bytes"
9 "context"
10 "encoding/json"
11 "fmt"
12 "io/ioutil"
13 "os"
14 "path/filepath"
15 "strings"
16
17 "golang.org/x/tools/internal/gocommand"
18 "golang.org/x/tools/gopls/internal/lsp/protocol"
19 "golang.org/x/tools/internal/span"
20)
21
22type Annotation string
23
24const (
25 // Nil controls nil checks.
26 Nil Annotation = "nil"
27
28 // Escape controls diagnostics about escape choices.
29 Escape Annotation = "escape"
30
31 // Inline controls diagnostics about inlining choices.
32 Inline Annotation = "inline"
33
34 // Bounds controls bounds checking diagnostics.
35 Bounds Annotation = "bounds"
36)
37
38func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, pkg Package) (map[VersionedFileIdentity][]*Diagnostic, error) {
39 if len(pkg.CompiledGoFiles()) == 0 {
40 return nil, nil
41 }
42 pkgDir := filepath.Dir(pkg.CompiledGoFiles()[0].URI.Filename())
43 outDir := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.details", os.Getpid()))
44
45 if err := os.MkdirAll(outDir, 0700); err != nil {
46 return nil, err
47 }
48 tmpFile, err := ioutil.TempFile(os.TempDir(), "gopls-x")
49 if err != nil {
50 return nil, err
51 }
52 defer os.Remove(tmpFile.Name())
53
54 outDirURI := span.URIFromPath(outDir)
55 // GC details doesn't handle Windows URIs in the form of "file:///C:/...",
56 // so rewrite them to "file://C:/...". See golang/go#41614.
57 if !strings.HasPrefix(outDir, "/") {
58 outDirURI = span.URI(strings.Replace(string(outDirURI), "file:///", "file://", 1))
59 }
60 inv := &gocommand.Invocation{
61 Verb: "build",
62 Args: []string{
63 fmt.Sprintf("-gcflags=-json=0,%s", outDirURI),
64 fmt.Sprintf("-o=%s", tmpFile.Name()),
65 ".",
66 },
67 WorkingDir: pkgDir,
68 }
69 _, err = snapshot.RunGoCommandDirect(ctx, Normal, inv)
70 if err != nil {
71 return nil, err
72 }
73 files, err := findJSONFiles(outDir)
74 if err != nil {
75 return nil, err
76 }
77 reports := make(map[VersionedFileIdentity][]*Diagnostic)
78 opts := snapshot.View().Options()
79 var parseError error
80 for _, fn := range files {
81 uri, diagnostics, err := parseDetailsFile(fn, opts)
82 if err != nil {
83 // expect errors for all the files, save 1
84 parseError = err
85 }
86 fh := snapshot.FindFile(uri)
87 if fh == nil {
88 continue
89 }
90 if pkgDir != filepath.Dir(fh.URI().Filename()) {
91 // https://github.com/golang/go/issues/42198
92 // sometimes the detail diagnostics generated for files
93 // outside the package can never be taken back.
94 continue
95 }
96 reports[fh.VersionedFileIdentity()] = diagnostics
97 }
98 return reports, parseError
99}
100
101func parseDetailsFile(filename string, options *Options) (span.URI, []*Diagnostic, error) {
102 buf, err := ioutil.ReadFile(filename)
103 if err != nil {
104 return "", nil, err
105 }
106 var (
107 uri span.URI
108 i int
109 diagnostics []*Diagnostic
110 )
111 type metadata struct {
112 File string `json:"file,omitempty"`
113 }
114 for dec := json.NewDecoder(bytes.NewReader(buf)); dec.More(); {
115 // The first element always contains metadata.
116 if i == 0 {
117 i++
118 m := new(metadata)
119 if err := dec.Decode(m); err != nil {
120 return "", nil, err
121 }
122 if !strings.HasSuffix(m.File, ".go") {
123 continue // <autogenerated>
124 }
125 uri = span.URIFromPath(m.File)
126 continue
127 }
128 d := new(protocol.Diagnostic)
129 if err := dec.Decode(d); err != nil {
130 return "", nil, err
131 }
132 msg := d.Code.(string)
133 if msg != "" {
134 msg = fmt.Sprintf("%s(%s)", msg, d.Message)
135 }
136 if !showDiagnostic(msg, d.Source, options) {
137 continue
138 }
139 var related []RelatedInformation
140 for _, ri := range d.RelatedInformation {
141 related = append(related, RelatedInformation{
142 URI: ri.Location.URI.SpanURI(),
143 Range: zeroIndexedRange(ri.Location.Range),
144 Message: ri.Message,
145 })
146 }
147 diagnostic := &Diagnostic{
148 URI: uri,
149 Range: zeroIndexedRange(d.Range),
150 Message: msg,
151 Severity: d.Severity,
152 Source: OptimizationDetailsError, // d.Source is always "go compiler" as of 1.16, use our own
153 Tags: d.Tags,
154 Related: related,
155 }
156 diagnostics = append(diagnostics, diagnostic)
157 i++
158 }
159 return uri, diagnostics, nil
160}
161
162// showDiagnostic reports whether a given diagnostic should be shown to the end
163// user, given the current options.
164func showDiagnostic(msg, source string, o *Options) bool {
165 if source != "go compiler" {
166 return false
167 }
168 if o.Annotations == nil {
169 return true
170 }
171 switch {
172 case strings.HasPrefix(msg, "canInline") ||
173 strings.HasPrefix(msg, "cannotInline") ||
174 strings.HasPrefix(msg, "inlineCall"):
175 return o.Annotations[Inline]
176 case strings.HasPrefix(msg, "escape") || msg == "leak":
177 return o.Annotations[Escape]
178 case strings.HasPrefix(msg, "nilcheck"):
179 return o.Annotations[Nil]
180 case strings.HasPrefix(msg, "isInBounds") ||
181 strings.HasPrefix(msg, "isSliceInBounds"):
182 return o.Annotations[Bounds]
183 }
184 return false
185}
186
187// The range produced by the compiler is 1-indexed, so subtract range by 1.
188func zeroIndexedRange(rng protocol.Range) protocol.Range {
189 return protocol.Range{
190 Start: protocol.Position{
191 Line: rng.Start.Line - 1,
192 Character: rng.Start.Character - 1,
193 },
194 End: protocol.Position{
195 Line: rng.End.Line - 1,
196 Character: rng.End.Character - 1,
197 },
198 }
199}
200
201func findJSONFiles(dir string) ([]string, error) {
202 ans := []string{}
203 f := func(path string, fi os.FileInfo, _ error) error {
204 if fi.IsDir() {
205 return nil
206 }
207 if strings.HasSuffix(path, ".json") {
208 ans = append(ans, path)
209 }
210 return nil
211 }
212 err := filepath.Walk(dir, f)
213 return ans, err
214}