1 // 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.
17 "golang.org/x/tools/internal/gocommand"
18 "golang.org/x/tools/internal/lsp/protocol"
19 "golang.org/x/tools/internal/span"
22 type Annotation string
25 // Nil controls nil checks.
26 Nil Annotation = "nil"
28 // Escape controls diagnostics about escape choices.
29 Escape Annotation = "escape"
31 // Inline controls diagnostics about inlining choices.
32 Inline Annotation = "inline"
34 // Bounds controls bounds checking diagnostics.
35 Bounds Annotation = "bounds"
38 func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, pkg Package) (map[VersionedFileIdentity][]*Diagnostic, error) {
39 if len(pkg.CompiledGoFiles()) == 0 {
42 pkgDir := filepath.Dir(pkg.CompiledGoFiles()[0].URI.Filename())
43 outDir := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.details", os.Getpid()))
45 if err := os.MkdirAll(outDir, 0700); err != nil {
48 tmpFile, err := ioutil.TempFile(os.TempDir(), "gopls-x")
52 defer os.Remove(tmpFile.Name())
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))
60 inv := &gocommand.Invocation{
63 fmt.Sprintf("-gcflags=-json=0,%s", outDirURI),
64 fmt.Sprintf("-o=%s", tmpFile.Name()),
69 _, err = snapshot.RunGoCommandDirect(ctx, Normal, inv)
73 files, err := findJSONFiles(outDir)
77 reports := make(map[VersionedFileIdentity][]*Diagnostic)
78 opts := snapshot.View().Options()
80 for _, fn := range files {
81 uri, diagnostics, err := parseDetailsFile(fn, opts)
83 // expect errors for all the files, save 1
86 fh := snapshot.FindFile(uri)
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.
96 reports[fh.VersionedFileIdentity()] = diagnostics
98 return reports, parseError
101 func parseDetailsFile(filename string, options *Options) (span.URI, []*Diagnostic, error) {
102 buf, err := ioutil.ReadFile(filename)
109 diagnostics []*Diagnostic
111 type metadata struct {
112 File string `json:"file,omitempty"`
114 for dec := json.NewDecoder(bytes.NewReader(buf)); dec.More(); {
115 // The first element always contains metadata.
119 if err := dec.Decode(m); err != nil {
122 if !strings.HasSuffix(m.File, ".go") {
123 continue // <autogenerated>
125 uri = span.URIFromPath(m.File)
128 d := new(protocol.Diagnostic)
129 if err := dec.Decode(d); err != nil {
132 msg := d.Code.(string)
134 msg = fmt.Sprintf("%s(%s)", msg, d.Message)
136 if !showDiagnostic(msg, d.Source, options) {
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),
147 diagnostic := &Diagnostic{
149 Range: zeroIndexedRange(d.Range),
151 Severity: d.Severity,
152 Source: OptimizationDetailsError, // d.Source is always "go compiler" as of 1.16, use our own
156 diagnostics = append(diagnostics, diagnostic)
159 return uri, diagnostics, nil
162 // showDiagnostic reports whether a given diagnostic should be shown to the end
163 // user, given the current options.
164 func showDiagnostic(msg, source string, o *Options) bool {
165 if source != "go compiler" {
168 if o.Annotations == nil {
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]
187 // The range produced by the compiler is 1-indexed, so subtract range by 1.
188 func 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,
194 End: protocol.Position{
195 Line: rng.End.Line - 1,
196 Character: rng.End.Character - 1,
201 func findJSONFiles(dir string) ([]string, error) {
203 f := func(path string, fi os.FileInfo, _ error) error {
207 if strings.HasSuffix(path, ".json") {
208 ans = append(ans, path)
212 err := filepath.Walk(dir, f)