blob: f9f046463f614f0e3bc037d3b32214cd7eed0e9b [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 golang
import (
"bytes"
"context"
"encoding/json"
"fmt"
"os"
"path/filepath"
"runtime"
"strings"
"golang.org/x/tools/gopls/internal/cache"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/event"
)
// CompilerOptDetails invokes the Go compiler with the "-json=0,dir"
// flag on the packages and tests in the specified directory, parses
// its log of optimization decisions, and returns them as a set of
// diagnostics.
func CompilerOptDetails(ctx context.Context, snapshot *cache.Snapshot, pkgDir protocol.DocumentURI) (map[protocol.DocumentURI][]*cache.Diagnostic, error) {
outDir, err := os.MkdirTemp("", fmt.Sprintf("gopls-%d.details", os.Getpid()))
if err != nil {
return nil, err
}
defer func() {
if err := os.RemoveAll(outDir); err != nil {
event.Error(ctx, "cleaning details dir", err)
}
}()
outDirURI := protocol.URIFromPath(outDir)
// 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 = protocol.DocumentURI(strings.Replace(string(outDirURI), "file:///", "file://", 1))
}
// We use "go test -c" not "go build" as it covers all three packages
// (p, "p [p.test]", "p_test [p.test]") in the directory, if they exist.
inv, cleanupInvocation, err := snapshot.GoCommandInvocation(cache.NoNetwork, pkgDir.Path(), "test", []string{
"-c",
"-vet=off", // weirdly -c doesn't disable vet
fmt.Sprintf("-gcflags=-json=0,%s", outDirURI), // JSON schema version 0
fmt.Sprintf("-o=%s", cond(runtime.GOOS == "windows", "NUL", "/dev/null")),
".",
})
if err != nil {
return nil, err
}
defer cleanupInvocation()
_, err = snapshot.View().GoCommandRunner().Run(ctx, *inv)
if err != nil {
return nil, err
}
files, err := findJSONFiles(outDir)
if err != nil {
return nil, err
}
reports := make(map[protocol.DocumentURI][]*cache.Diagnostic)
var parseError error
for _, fn := range files {
uri, diagnostics, err := parseDetailsFile(fn)
if err != nil {
// expect errors for all the files, save 1
parseError = err
}
fh := snapshot.FindFile(uri)
if fh == nil {
continue
}
if pkgDir != fh.URI().Dir() {
// Filter compiler diagnostics to the requested directory.
// 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
}
// parseDetailsFile parses the file written by the Go compiler which contains a JSON-encoded protocol.Diagnostic.
func parseDetailsFile(filename string) (protocol.DocumentURI, []*cache.Diagnostic, error) {
buf, err := os.ReadFile(filename)
if err != nil {
return "", nil, err
}
var (
uri protocol.DocumentURI
i int
diagnostics []*cache.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 = protocol.URIFromPath(m.File)
continue
}
d := new(protocol.Diagnostic)
if err := dec.Decode(d); err != nil {
return "", nil, err
}
if d.Source != "go compiler" {
continue
}
d.Tags = []protocol.DiagnosticTag{} // must be an actual slice
msg := d.Code.(string)
if msg != "" {
// Typical message prefixes gathered by grepping the source of
// cmd/compile for literal arguments in calls to logopt.LogOpt.
// (It is not a well defined set.)
//
// - canInlineFunction
// - cannotInlineCall
// - cannotInlineFunction
// - copy
// - escape
// - escapes
// - isInBounds
// - isSliceInBounds
// - iteration-variable-to-{heap,stack}
// - leak
// - loop-modified-{range,for}
// - nilcheck
msg = fmt.Sprintf("%s(%s)", msg, d.Message)
}
// zeroIndexedRange subtracts 1 from the line and
// range, because the compiler output neglects to
// convert from 1-based UTF-8 coordinates to 0-based UTF-16.
// (See GOROOT/src/cmd/compile/internal/logopt/log_opts.go.)
// TODO(rfindley): also translate UTF-8 to UTF-16.
zeroIndexedRange := func(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,
},
}
}
var related []protocol.DiagnosticRelatedInformation
for _, ri := range d.RelatedInformation {
related = append(related, protocol.DiagnosticRelatedInformation{
Location: protocol.Location{
URI: ri.Location.URI,
Range: zeroIndexedRange(ri.Location.Range),
},
Message: ri.Message,
})
}
diagnostic := &cache.Diagnostic{
URI: uri,
Range: zeroIndexedRange(d.Range),
Message: msg,
Severity: d.Severity,
Source: cache.CompilerOptDetailsInfo, // 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
}
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
}