// 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" "io/ioutil" "os" "path/filepath" "strings" "golang.org/x/tools/internal/gocommand" "golang.org/x/tools/internal/lsp/protocol" "golang.org/x/tools/internal/span" ) func GCOptimizationDetails(ctx context.Context, snapshot Snapshot, pkgDir span.URI) (map[VersionedFileIdentity][]*Diagnostic, error) { 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 := ioutil.TempFile(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.Filename(), } _, 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[VersionedFileIdentity][]*Diagnostic) opts := snapshot.View().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.Filename() != 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.VersionedFileIdentity()] = diagnostics } return reports, parseError } func parseDetailsFile(filename string, options *Options) (span.URI, []*Diagnostic, error) { buf, err := ioutil.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 // } uri = span.URIFromPath(m.File) continue } d := new(protocol.Diagnostic) if err := dec.Decode(d); err != nil { return "", nil, err } msg := d.Code.(string) if msg != "" { msg = fmt.Sprintf("%s(%s)", msg, d.Message) } if skipDiagnostic(msg, d.Source, options) { continue } var related []RelatedInformation for _, ri := range d.RelatedInformation { related = append(related, RelatedInformation{ URI: ri.Location.URI.SpanURI(), Range: zeroIndexedRange(ri.Location.Range), Message: ri.Message, }) } diagnostic := &Diagnostic{ Range: zeroIndexedRange(d.Range), Message: msg, Severity: d.Severity, Source: d.Source, Tags: d.Tags, Related: related, } diagnostics = append(diagnostics, diagnostic) i++ } return uri, diagnostics, nil } // skipDiagnostic reports whether a given diagnostic should be shown to the end // user, given the current options. func skipDiagnostic(msg, source string, o *Options) bool { if source != "go compiler" { return false } switch { case o.Annotations["noInline"]: return strings.HasPrefix(msg, "canInline") || strings.HasPrefix(msg, "cannotInline") || strings.HasPrefix(msg, "inlineCall") case o.Annotations["noEscape"]: return strings.HasPrefix(msg, "escape") || msg == "leak" case o.Annotations["noNilcheck"]: return strings.HasPrefix(msg, "nilcheck") case o.Annotations["noBounds"]: return strings.HasPrefix(msg, "isInBounds") || strings.HasPrefix(msg, "isSliceInBounds") } 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 }