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