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 }