mirror of https://github.com/golang/go.git
215 lines
5.4 KiB
Go
215 lines
5.4 KiB
Go
// 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"
|
|
)
|
|
|
|
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, pkg Package) (map[VersionedFileIdentity][]*Diagnostic, error) {
|
|
if len(pkg.CompiledGoFiles()) == 0 {
|
|
return nil, nil
|
|
}
|
|
pkgDir := filepath.Dir(pkg.CompiledGoFiles()[0].URI.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 := 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,
|
|
}
|
|
_, 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 != 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 !showDiagnostic(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{
|
|
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
|
|
}
|