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 }