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