golang.org/x/tools/gopls@v0.15.3/internal/golang/gc_annotations.go (about)

     1  // Copyright 2020 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package golang
     6  
     7  import (
     8  	"bytes"
     9  	"context"
    10  	"encoding/json"
    11  	"fmt"
    12  	"os"
    13  	"path/filepath"
    14  	"strings"
    15  
    16  	"golang.org/x/tools/gopls/internal/cache"
    17  	"golang.org/x/tools/gopls/internal/cache/metadata"
    18  	"golang.org/x/tools/gopls/internal/protocol"
    19  	"golang.org/x/tools/gopls/internal/settings"
    20  	"golang.org/x/tools/internal/gocommand"
    21  )
    22  
    23  func GCOptimizationDetails(ctx context.Context, snapshot *cache.Snapshot, mp *metadata.Package) (map[protocol.DocumentURI][]*cache.Diagnostic, error) {
    24  	if len(mp.CompiledGoFiles) == 0 {
    25  		return nil, nil
    26  	}
    27  	pkgDir := filepath.Dir(mp.CompiledGoFiles[0].Path())
    28  	outDir := filepath.Join(os.TempDir(), fmt.Sprintf("gopls-%d.details", os.Getpid()))
    29  
    30  	if err := os.MkdirAll(outDir, 0700); err != nil {
    31  		return nil, err
    32  	}
    33  	tmpFile, err := os.CreateTemp(os.TempDir(), "gopls-x")
    34  	if err != nil {
    35  		return nil, err
    36  	}
    37  	tmpFile.Close() // ignore error
    38  	defer os.Remove(tmpFile.Name())
    39  
    40  	outDirURI := protocol.URIFromPath(outDir)
    41  	// GC details doesn't handle Windows URIs in the form of "file:///C:/...",
    42  	// so rewrite them to "file://C:/...". See golang/go#41614.
    43  	if !strings.HasPrefix(outDir, "/") {
    44  		outDirURI = protocol.DocumentURI(strings.Replace(string(outDirURI), "file:///", "file://", 1))
    45  	}
    46  	inv := &gocommand.Invocation{
    47  		Verb: "build",
    48  		Args: []string{
    49  			fmt.Sprintf("-gcflags=-json=0,%s", outDirURI),
    50  			fmt.Sprintf("-o=%s", tmpFile.Name()),
    51  			".",
    52  		},
    53  		WorkingDir: pkgDir,
    54  	}
    55  	_, err = snapshot.RunGoCommandDirect(ctx, cache.Normal, inv)
    56  	if err != nil {
    57  		return nil, err
    58  	}
    59  	files, err := findJSONFiles(outDir)
    60  	if err != nil {
    61  		return nil, err
    62  	}
    63  	reports := make(map[protocol.DocumentURI][]*cache.Diagnostic)
    64  	opts := snapshot.Options()
    65  	var parseError error
    66  	for _, fn := range files {
    67  		uri, diagnostics, err := parseDetailsFile(fn, opts)
    68  		if err != nil {
    69  			// expect errors for all the files, save 1
    70  			parseError = err
    71  		}
    72  		fh := snapshot.FindFile(uri)
    73  		if fh == nil {
    74  			continue
    75  		}
    76  		if pkgDir != filepath.Dir(fh.URI().Path()) {
    77  			// https://github.com/golang/go/issues/42198
    78  			// sometimes the detail diagnostics generated for files
    79  			// outside the package can never be taken back.
    80  			continue
    81  		}
    82  		reports[fh.URI()] = diagnostics
    83  	}
    84  	return reports, parseError
    85  }
    86  
    87  func parseDetailsFile(filename string, options *settings.Options) (protocol.DocumentURI, []*cache.Diagnostic, error) {
    88  	buf, err := os.ReadFile(filename)
    89  	if err != nil {
    90  		return "", nil, err
    91  	}
    92  	var (
    93  		uri         protocol.DocumentURI
    94  		i           int
    95  		diagnostics []*cache.Diagnostic
    96  	)
    97  	type metadata struct {
    98  		File string `json:"file,omitempty"`
    99  	}
   100  	for dec := json.NewDecoder(bytes.NewReader(buf)); dec.More(); {
   101  		// The first element always contains metadata.
   102  		if i == 0 {
   103  			i++
   104  			m := new(metadata)
   105  			if err := dec.Decode(m); err != nil {
   106  				return "", nil, err
   107  			}
   108  			if !strings.HasSuffix(m.File, ".go") {
   109  				continue // <autogenerated>
   110  			}
   111  			uri = protocol.URIFromPath(m.File)
   112  			continue
   113  		}
   114  		d := new(protocol.Diagnostic)
   115  		if err := dec.Decode(d); err != nil {
   116  			return "", nil, err
   117  		}
   118  		d.Tags = []protocol.DiagnosticTag{} // must be an actual slice
   119  		msg := d.Code.(string)
   120  		if msg != "" {
   121  			msg = fmt.Sprintf("%s(%s)", msg, d.Message)
   122  		}
   123  		if !showDiagnostic(msg, d.Source, options) {
   124  			continue
   125  		}
   126  		var related []protocol.DiagnosticRelatedInformation
   127  		for _, ri := range d.RelatedInformation {
   128  			// TODO(rfindley): The compiler uses LSP-like JSON to encode gc details,
   129  			// however the positions it uses are 1-based UTF-8:
   130  			// https://github.com/golang/go/blob/master/src/cmd/compile/internal/logopt/log_opts.go
   131  			//
   132  			// Here, we adjust for 0-based positions, but do not translate UTF-8 to UTF-16.
   133  			related = append(related, protocol.DiagnosticRelatedInformation{
   134  				Location: protocol.Location{
   135  					URI:   ri.Location.URI,
   136  					Range: zeroIndexedRange(ri.Location.Range),
   137  				},
   138  				Message: ri.Message,
   139  			})
   140  		}
   141  		diagnostic := &cache.Diagnostic{
   142  			URI:      uri,
   143  			Range:    zeroIndexedRange(d.Range),
   144  			Message:  msg,
   145  			Severity: d.Severity,
   146  			Source:   cache.OptimizationDetailsError, // d.Source is always "go compiler" as of 1.16, use our own
   147  			Tags:     d.Tags,
   148  			Related:  related,
   149  		}
   150  		diagnostics = append(diagnostics, diagnostic)
   151  		i++
   152  	}
   153  	return uri, diagnostics, nil
   154  }
   155  
   156  // showDiagnostic reports whether a given diagnostic should be shown to the end
   157  // user, given the current options.
   158  func showDiagnostic(msg, source string, o *settings.Options) bool {
   159  	if source != "go compiler" {
   160  		return false
   161  	}
   162  	if o.Annotations == nil {
   163  		return true
   164  	}
   165  	switch {
   166  	case strings.HasPrefix(msg, "canInline") ||
   167  		strings.HasPrefix(msg, "cannotInline") ||
   168  		strings.HasPrefix(msg, "inlineCall"):
   169  		return o.Annotations[settings.Inline]
   170  	case strings.HasPrefix(msg, "escape") || msg == "leak":
   171  		return o.Annotations[settings.Escape]
   172  	case strings.HasPrefix(msg, "nilcheck"):
   173  		return o.Annotations[settings.Nil]
   174  	case strings.HasPrefix(msg, "isInBounds") ||
   175  		strings.HasPrefix(msg, "isSliceInBounds"):
   176  		return o.Annotations[settings.Bounds]
   177  	}
   178  	return false
   179  }
   180  
   181  // The range produced by the compiler is 1-indexed, so subtract range by 1.
   182  func zeroIndexedRange(rng protocol.Range) protocol.Range {
   183  	return protocol.Range{
   184  		Start: protocol.Position{
   185  			Line:      rng.Start.Line - 1,
   186  			Character: rng.Start.Character - 1,
   187  		},
   188  		End: protocol.Position{
   189  			Line:      rng.End.Line - 1,
   190  			Character: rng.End.Character - 1,
   191  		},
   192  	}
   193  }
   194  
   195  func findJSONFiles(dir string) ([]string, error) {
   196  	ans := []string{}
   197  	f := func(path string, fi os.FileInfo, _ error) error {
   198  		if fi.IsDir() {
   199  			return nil
   200  		}
   201  		if strings.HasSuffix(path, ".json") {
   202  			ans = append(ans, path)
   203  		}
   204  		return nil
   205  	}
   206  	err := filepath.Walk(dir, f)
   207  	return ans, err
   208  }