blob: 4aac541d74df4d85b4f7d53b8fa1dfd6802e96e9 [file] [log] [blame]
// 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 // <autogenerated>
}
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
}