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 }