| // Copyright 2020 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package source |
| |
| import ( |
| "bytes" |
| "context" |
| "encoding/json" |
| "fmt" |
| "os" |
| "path/filepath" |
| "strings" |
| |
| "golang.org/x/tools/gopls/internal/lsp/protocol" |
| "golang.org/x/tools/gopls/internal/span" |
| "golang.org/x/tools/internal/gocommand" |
| ) |
| |
| type Annotation string |
| |
| const ( |
| // Nil controls nil checks. |
| Nil Annotation = "nil" |
| |
| // Escape controls diagnostics about escape choices. |
| Escape Annotation = "escape" |
| |
| // Inline controls diagnostics about inlining choices. |
| Inline Annotation = "inline" |
| |
| // Bounds controls bounds checking diagnostics. |
| Bounds Annotation = "bounds" |
| ) |
| |
| func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, m *Metadata) (map[span.URI][]*Diagnostic, error) { |
| if len(m.CompiledGoFiles) == 0 { |
| return nil, nil |
| } |
| pkgDir := filepath.Dir(m.CompiledGoFiles[0].Filename()) |
| outDir := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.details", os.Getpid())) |
| |
| if err := os.MkdirAll(outDir, 0700); err != nil { |
| return nil, err |
| } |
| tmpFile, err := os.CreateTemp(os.TempDir(), "gopls-x") |
| if err != nil { |
| return nil, err |
| } |
| defer os.Remove(tmpFile.Name()) |
| |
| outDirURI := span.URIFromPath(outDir) |
| // GC details doesn't handle Windows URIs in the form of "file:///C:/...", |
| // so rewrite them to "file://C:/...". See golang/go#41614. |
| if !strings.HasPrefix(outDir, "/") { |
| outDirURI = span.URI(strings.Replace(string(outDirURI), "file:///", "file://", 1)) |
| } |
| inv := &gocommand.Invocation{ |
| Verb: "build", |
| Args: []string{ |
| fmt.Sprintf("-gcflags=-json=0,%s", outDirURI), |
| fmt.Sprintf("-o=%s", tmpFile.Name()), |
| ".", |
| }, |
| WorkingDir: pkgDir, |
| } |
| _, err = snapshot.RunGoCommandDirect(ctx, Normal, inv) |
| if err != nil { |
| return nil, err |
| } |
| files, err := findJSONFiles(outDir) |
| if err != nil { |
| return nil, err |
| } |
| reports := make(map[span.URI][]*Diagnostic) |
| opts := snapshot.Options() |
| var parseError error |
| for _, fn := range files { |
| uri, diagnostics, err := parseDetailsFile(fn, opts) |
| if err != nil { |
| // expect errors for all the files, save 1 |
| parseError = err |
| } |
| fh := snapshot.FindFile(uri) |
| if fh == nil { |
| continue |
| } |
| if pkgDir != filepath.Dir(fh.URI().Filename()) { |
| // https://github.com/golang/go/issues/42198 |
| // sometimes the detail diagnostics generated for files |
| // outside the package can never be taken back. |
| continue |
| } |
| reports[fh.URI()] = diagnostics |
| } |
| return reports, parseError |
| } |
| |
| func parseDetailsFile(filename string, options *Options) (span.URI, []*Diagnostic, error) { |
| buf, err := os.ReadFile(filename) |
| if err != nil { |
| return "", nil, err |
| } |
| var ( |
| uri span.URI |
| i int |
| diagnostics []*Diagnostic |
| ) |
| type metadata struct { |
| File string `json:"file,omitempty"` |
| } |
| for dec := json.NewDecoder(bytes.NewReader(buf)); dec.More(); { |
| // The first element always contains metadata. |
| if i == 0 { |
| i++ |
| m := new(metadata) |
| if err := dec.Decode(m); err != nil { |
| return "", nil, err |
| } |
| if !strings.HasSuffix(m.File, ".go") { |
| continue // <autogenerated> |
| } |
| uri = span.URIFromPath(m.File) |
| continue |
| } |
| d := new(protocol.Diagnostic) |
| if err := dec.Decode(d); err != nil { |
| return "", nil, err |
| } |
| d.Tags = []protocol.DiagnosticTag{} // must be an actual slice |
| msg := d.Code.(string) |
| if msg != "" { |
| msg = fmt.Sprintf("%s(%s)", msg, d.Message) |
| } |
| if !showDiagnostic(msg, d.Source, options) { |
| continue |
| } |
| var related []protocol.DiagnosticRelatedInformation |
| for _, ri := range d.RelatedInformation { |
| // TODO(rfindley): The compiler uses LSP-like JSON to encode gc details, |
| // however the positions it uses are 1-based UTF-8: |
| // https://github.com/golang/go/blob/master/src/cmd/compile/internal/logopt/log_opts.go |
| // |
| // Here, we adjust for 0-based positions, but do not translate UTF-8 to UTF-16. |
| related = append(related, protocol.DiagnosticRelatedInformation{ |
| Location: protocol.Location{ |
| URI: ri.Location.URI, |
| Range: zeroIndexedRange(ri.Location.Range), |
| }, |
| Message: ri.Message, |
| }) |
| } |
| diagnostic := &Diagnostic{ |
| URI: uri, |
| Range: zeroIndexedRange(d.Range), |
| Message: msg, |
| Severity: d.Severity, |
| Source: OptimizationDetailsError, // d.Source is always "go compiler" as of 1.16, use our own |
| Tags: d.Tags, |
| Related: related, |
| } |
| diagnostics = append(diagnostics, diagnostic) |
| i++ |
| } |
| return uri, diagnostics, nil |
| } |
| |
| // showDiagnostic reports whether a given diagnostic should be shown to the end |
| // user, given the current options. |
| func showDiagnostic(msg, source string, o *Options) bool { |
| if source != "go compiler" { |
| return false |
| } |
| if o.Annotations == nil { |
| return true |
| } |
| switch { |
| case strings.HasPrefix(msg, "canInline") || |
| strings.HasPrefix(msg, "cannotInline") || |
| strings.HasPrefix(msg, "inlineCall"): |
| return o.Annotations[Inline] |
| case strings.HasPrefix(msg, "escape") || msg == "leak": |
| return o.Annotations[Escape] |
| case strings.HasPrefix(msg, "nilcheck"): |
| return o.Annotations[Nil] |
| case strings.HasPrefix(msg, "isInBounds") || |
| strings.HasPrefix(msg, "isSliceInBounds"): |
| return o.Annotations[Bounds] |
| } |
| return false |
| } |
| |
| // The range produced by the compiler is 1-indexed, so subtract range by 1. |
| func zeroIndexedRange(rng protocol.Range) protocol.Range { |
| return protocol.Range{ |
| Start: protocol.Position{ |
| Line: rng.Start.Line - 1, |
| Character: rng.Start.Character - 1, |
| }, |
| End: protocol.Position{ |
| Line: rng.End.Line - 1, |
| Character: rng.End.Character - 1, |
| }, |
| } |
| } |
| |
| func findJSONFiles(dir string) ([]string, error) { |
| ans := []string{} |
| f := func(path string, fi os.FileInfo, _ error) error { |
| if fi.IsDir() { |
| return nil |
| } |
| if strings.HasSuffix(path, ".json") { |
| ans = append(ans, path) |
| } |
| return nil |
| } |
| err := filepath.Walk(dir, f) |
| return ans, err |
| } |