cuelang.org/go@v0.10.1/internal/golangorgx/gopls/cache/errors.go (about) 1 // Copyright 2019 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 cache 6 7 // This file defines routines to convert diagnostics from go list, go 8 // get, go/packages, parsing, type checking, and analysis into 9 // golang.Diagnostic form, and suggesting quick fixes. 10 11 import ( 12 "context" 13 "fmt" 14 "go/parser" 15 "go/scanner" 16 "go/token" 17 "path/filepath" 18 "regexp" 19 "strconv" 20 "strings" 21 22 "cuelang.org/go/internal/golangorgx/gopls/cache/metadata" 23 "cuelang.org/go/internal/golangorgx/gopls/file" 24 "cuelang.org/go/internal/golangorgx/gopls/protocol" 25 "cuelang.org/go/internal/golangorgx/gopls/protocol/command" 26 "cuelang.org/go/internal/golangorgx/gopls/settings" 27 "cuelang.org/go/internal/golangorgx/gopls/util/bug" 28 "cuelang.org/go/internal/golangorgx/tools/typesinternal" 29 "golang.org/x/tools/go/packages" 30 ) 31 32 // goPackagesErrorDiagnostics translates the given go/packages Error into a 33 // diagnostic, using the provided metadata and filesource. 34 // 35 // The slice of diagnostics may be empty. 36 func goPackagesErrorDiagnostics(ctx context.Context, e packages.Error, mp *metadata.Package, fs file.Source) ([]*Diagnostic, error) { 37 if diag, err := parseGoListImportCycleError(ctx, e, mp, fs); err != nil { 38 return nil, err 39 } else if diag != nil { 40 return []*Diagnostic{diag}, nil 41 } 42 43 // Parse error location and attempt to convert to protocol form. 44 loc, err := func() (protocol.Location, error) { 45 filename, line, col8 := parseGoListError(e, mp.LoadDir) 46 uri := protocol.URIFromPath(filename) 47 48 fh, err := fs.ReadFile(ctx, uri) 49 if err != nil { 50 return protocol.Location{}, err 51 } 52 content, err := fh.Content() 53 if err != nil { 54 return protocol.Location{}, err 55 } 56 mapper := protocol.NewMapper(uri, content) 57 posn, err := mapper.LineCol8Position(line, col8) 58 if err != nil { 59 return protocol.Location{}, err 60 } 61 return protocol.Location{ 62 URI: uri, 63 Range: protocol.Range{ 64 Start: posn, 65 End: posn, 66 }, 67 }, nil 68 }() 69 70 // TODO(rfindley): in some cases the go command outputs invalid spans, for 71 // example (from TestGoListErrors): 72 // 73 // package a 74 // import 75 // 76 // In this case, the go command will complain about a.go:2:8, which is after 77 // the trailing newline but still considered to be on the second line, most 78 // likely because *token.File lacks information about newline termination. 79 // 80 // We could do better here by handling that case. 81 if err != nil { 82 // Unable to parse a valid position. 83 // Apply the error to all files to be safe. 84 var diags []*Diagnostic 85 for _, uri := range mp.CompiledGoFiles { 86 diags = append(diags, &Diagnostic{ 87 URI: uri, 88 Severity: protocol.SeverityError, 89 Source: ListError, 90 Message: e.Msg, 91 }) 92 } 93 return diags, nil 94 } 95 return []*Diagnostic{{ 96 URI: loc.URI, 97 Range: loc.Range, 98 Severity: protocol.SeverityError, 99 Source: ListError, 100 Message: e.Msg, 101 }}, nil 102 } 103 104 func parseErrorDiagnostics(pkg *syntaxPackage, errList scanner.ErrorList) ([]*Diagnostic, error) { 105 // The first parser error is likely the root cause of the problem. 106 if errList.Len() <= 0 { 107 return nil, fmt.Errorf("no errors in %v", errList) 108 } 109 e := errList[0] 110 pgf, err := pkg.File(protocol.URIFromPath(e.Pos.Filename)) 111 if err != nil { 112 return nil, err 113 } 114 rng, err := pgf.Mapper.OffsetRange(e.Pos.Offset, e.Pos.Offset) 115 if err != nil { 116 return nil, err 117 } 118 return []*Diagnostic{{ 119 URI: pgf.URI, 120 Range: rng, 121 Severity: protocol.SeverityError, 122 Source: ParseError, 123 Message: e.Msg, 124 }}, nil 125 } 126 127 var importErrorRe = regexp.MustCompile(`could not import ([^\s]+)`) 128 var unsupportedFeatureRe = regexp.MustCompile(`.*require.* go(\d+\.\d+) or later`) 129 130 func goGetQuickFixes(moduleMode bool, uri protocol.DocumentURI, pkg string) []SuggestedFix { 131 // Go get only supports module mode for now. 132 if !moduleMode { 133 return nil 134 } 135 title := fmt.Sprintf("go get package %v", pkg) 136 cmd, err := command.NewGoGetPackageCommand(title, command.GoGetPackageArgs{ 137 URI: uri, 138 AddRequire: true, 139 Pkg: pkg, 140 }) 141 if err != nil { 142 bug.Reportf("internal error building 'go get package' fix: %v", err) 143 return nil 144 } 145 return []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)} 146 } 147 148 func editGoDirectiveQuickFix(moduleMode bool, uri protocol.DocumentURI, version string) []SuggestedFix { 149 // Go mod edit only supports module mode. 150 if !moduleMode { 151 return nil 152 } 153 title := fmt.Sprintf("go mod edit -go=%s", version) 154 cmd, err := command.NewEditGoDirectiveCommand(title, command.EditGoDirectiveArgs{ 155 URI: uri, 156 Version: version, 157 }) 158 if err != nil { 159 bug.Reportf("internal error constructing 'edit go directive' fix: %v", err) 160 return nil 161 } 162 return []SuggestedFix{SuggestedFixFromCommand(cmd, protocol.QuickFix)} 163 } 164 165 // encodeDiagnostics gob-encodes the given diagnostics. 166 func encodeDiagnostics(srcDiags []*Diagnostic) []byte { 167 var gobDiags []gobDiagnostic 168 for _, srcDiag := range srcDiags { 169 var gobFixes []gobSuggestedFix 170 for _, srcFix := range srcDiag.SuggestedFixes { 171 gobFix := gobSuggestedFix{ 172 Message: srcFix.Title, 173 ActionKind: srcFix.ActionKind, 174 } 175 for uri, srcEdits := range srcFix.Edits { 176 for _, srcEdit := range srcEdits { 177 gobFix.TextEdits = append(gobFix.TextEdits, gobTextEdit{ 178 Location: protocol.Location{ 179 URI: uri, 180 Range: srcEdit.Range, 181 }, 182 NewText: []byte(srcEdit.NewText), 183 }) 184 } 185 } 186 if srcCmd := srcFix.Command; srcCmd != nil { 187 gobFix.Command = &gobCommand{ 188 Title: srcCmd.Title, 189 Command: srcCmd.Command, 190 Arguments: srcCmd.Arguments, 191 } 192 } 193 gobFixes = append(gobFixes, gobFix) 194 } 195 var gobRelated []gobRelatedInformation 196 for _, srcRel := range srcDiag.Related { 197 gobRel := gobRelatedInformation(srcRel) 198 gobRelated = append(gobRelated, gobRel) 199 } 200 gobDiag := gobDiagnostic{ 201 Location: protocol.Location{ 202 URI: srcDiag.URI, 203 Range: srcDiag.Range, 204 }, 205 Severity: srcDiag.Severity, 206 Code: srcDiag.Code, 207 CodeHref: srcDiag.CodeHref, 208 Source: string(srcDiag.Source), 209 Message: srcDiag.Message, 210 SuggestedFixes: gobFixes, 211 Related: gobRelated, 212 Tags: srcDiag.Tags, 213 } 214 gobDiags = append(gobDiags, gobDiag) 215 } 216 return diagnosticsCodec.Encode(gobDiags) 217 } 218 219 // decodeDiagnostics decodes the given gob-encoded diagnostics. 220 func decodeDiagnostics(data []byte) []*Diagnostic { 221 var gobDiags []gobDiagnostic 222 diagnosticsCodec.Decode(data, &gobDiags) 223 var srcDiags []*Diagnostic 224 for _, gobDiag := range gobDiags { 225 var srcFixes []SuggestedFix 226 for _, gobFix := range gobDiag.SuggestedFixes { 227 srcFix := SuggestedFix{ 228 Title: gobFix.Message, 229 ActionKind: gobFix.ActionKind, 230 } 231 for _, gobEdit := range gobFix.TextEdits { 232 if srcFix.Edits == nil { 233 srcFix.Edits = make(map[protocol.DocumentURI][]protocol.TextEdit) 234 } 235 srcEdit := protocol.TextEdit{ 236 Range: gobEdit.Location.Range, 237 NewText: string(gobEdit.NewText), 238 } 239 uri := gobEdit.Location.URI 240 srcFix.Edits[uri] = append(srcFix.Edits[uri], srcEdit) 241 } 242 if gobCmd := gobFix.Command; gobCmd != nil { 243 srcFix.Command = &protocol.Command{ 244 Title: gobCmd.Title, 245 Command: gobCmd.Command, 246 Arguments: gobCmd.Arguments, 247 } 248 } 249 srcFixes = append(srcFixes, srcFix) 250 } 251 var srcRelated []protocol.DiagnosticRelatedInformation 252 for _, gobRel := range gobDiag.Related { 253 srcRel := protocol.DiagnosticRelatedInformation(gobRel) 254 srcRelated = append(srcRelated, srcRel) 255 } 256 srcDiag := &Diagnostic{ 257 URI: gobDiag.Location.URI, 258 Range: gobDiag.Location.Range, 259 Severity: gobDiag.Severity, 260 Code: gobDiag.Code, 261 CodeHref: gobDiag.CodeHref, 262 Source: DiagnosticSource(gobDiag.Source), 263 Message: gobDiag.Message, 264 Tags: gobDiag.Tags, 265 Related: srcRelated, 266 SuggestedFixes: srcFixes, 267 } 268 srcDiags = append(srcDiags, srcDiag) 269 } 270 return srcDiags 271 } 272 273 // toSourceDiagnostic converts a gobDiagnostic to "source" form. 274 func toSourceDiagnostic(srcAnalyzer *settings.Analyzer, gobDiag *gobDiagnostic) *Diagnostic { 275 var related []protocol.DiagnosticRelatedInformation 276 for _, gobRelated := range gobDiag.Related { 277 related = append(related, protocol.DiagnosticRelatedInformation(gobRelated)) 278 } 279 280 severity := srcAnalyzer.Severity 281 if severity == 0 { 282 severity = protocol.SeverityWarning 283 } 284 285 diag := &Diagnostic{ 286 URI: gobDiag.Location.URI, 287 Range: gobDiag.Location.Range, 288 Severity: severity, 289 Code: gobDiag.Code, 290 CodeHref: gobDiag.CodeHref, 291 Source: DiagnosticSource(gobDiag.Source), 292 Message: gobDiag.Message, 293 Related: related, 294 Tags: srcAnalyzer.Tag, 295 } 296 297 // If the fixes only delete code, assume that the diagnostic is reporting dead code. 298 if onlyDeletions(diag.SuggestedFixes) { 299 diag.Tags = append(diag.Tags, protocol.Unnecessary) 300 } 301 return diag 302 } 303 304 // onlyDeletions returns true if fixes is non-empty and all of the suggested 305 // fixes are deletions. 306 func onlyDeletions(fixes []SuggestedFix) bool { 307 for _, fix := range fixes { 308 if fix.Command != nil { 309 return false 310 } 311 for _, edits := range fix.Edits { 312 for _, edit := range edits { 313 if edit.NewText != "" { 314 return false 315 } 316 if protocol.ComparePosition(edit.Range.Start, edit.Range.End) == 0 { 317 return false 318 } 319 } 320 } 321 } 322 return len(fixes) > 0 323 } 324 325 func typesCodeHref(linkTarget string, code typesinternal.ErrorCode) string { 326 return BuildLink(linkTarget, "cuelang.org/go/internal/golangorgx/tools/typesinternal", code.String()) 327 } 328 329 // BuildLink constructs a URL with the given target, path, and anchor. 330 func BuildLink(target, path, anchor string) string { 331 link := fmt.Sprintf("https://%s/%s", target, path) 332 if anchor == "" { 333 return link 334 } 335 return link + "#" + anchor 336 } 337 338 func parseGoListError(e packages.Error, dir string) (filename string, line, col8 int) { 339 input := e.Pos 340 if input == "" { 341 // No position. Attempt to parse one out of a 342 // go list error of the form "file:line:col: 343 // message" by stripping off the message. 344 input = strings.TrimSpace(e.Msg) 345 if i := strings.Index(input, ": "); i >= 0 { 346 input = input[:i] 347 } 348 } 349 350 filename, line, col8 = splitFileLineCol(input) 351 if !filepath.IsAbs(filename) { 352 filename = filepath.Join(dir, filename) 353 } 354 return filename, line, col8 355 } 356 357 // splitFileLineCol splits s into "filename:line:col", 358 // where line and col consist of decimal digits. 359 func splitFileLineCol(s string) (file string, line, col8 int) { 360 // Beware that the filename may contain colon on Windows. 361 362 // stripColonDigits removes a ":%d" suffix, if any. 363 stripColonDigits := func(s string) (rest string, num int) { 364 if i := strings.LastIndex(s, ":"); i >= 0 { 365 if v, err := strconv.ParseInt(s[i+1:], 10, 32); err == nil { 366 return s[:i], int(v) 367 } 368 } 369 return s, -1 370 } 371 372 // strip col ":%d" 373 s, n1 := stripColonDigits(s) 374 if n1 < 0 { 375 return s, 0, 0 // "filename" 376 } 377 378 // strip line ":%d" 379 s, n2 := stripColonDigits(s) 380 if n2 < 0 { 381 return s, n1, 0 // "filename:line" 382 } 383 384 return s, n2, n1 // "filename:line:col" 385 } 386 387 // parseGoListImportCycleError attempts to parse the given go/packages error as 388 // an import cycle, returning a diagnostic if successful. 389 // 390 // If the error is not detected as an import cycle error, it returns nil, nil. 391 func parseGoListImportCycleError(ctx context.Context, e packages.Error, mp *metadata.Package, fs file.Source) (*Diagnostic, error) { 392 re := regexp.MustCompile(`(.*): import stack: \[(.+)\]`) 393 matches := re.FindStringSubmatch(strings.TrimSpace(e.Msg)) 394 if len(matches) < 3 { 395 return nil, nil 396 } 397 msg := matches[1] 398 importList := strings.Split(matches[2], " ") 399 // Since the error is relative to the current package. The import that is causing 400 // the import cycle error is the second one in the list. 401 if len(importList) < 2 { 402 return nil, nil 403 } 404 // Imports have quotation marks around them. 405 circImp := strconv.Quote(importList[1]) 406 for _, uri := range mp.CompiledGoFiles { 407 pgf, err := parseGoURI(ctx, fs, uri, ParseHeader) 408 if err != nil { 409 return nil, err 410 } 411 // Search file imports for the import that is causing the import cycle. 412 for _, imp := range pgf.File.Imports { 413 if imp.Path.Value == circImp { 414 rng, err := pgf.NodeMappedRange(imp) 415 if err != nil { 416 return nil, nil 417 } 418 419 return &Diagnostic{ 420 URI: pgf.URI, 421 Range: rng.Range(), 422 Severity: protocol.SeverityError, 423 Source: ListError, 424 Message: msg, 425 }, nil 426 } 427 } 428 } 429 return nil, nil 430 } 431 432 // parseGoURI is a helper to parse the Go file at the given URI from the file 433 // source fs. The resulting syntax and token.File belong to an ephemeral, 434 // encapsulated FileSet, so this file stands only on its own: it's not suitable 435 // to use in a list of file of a package, for example. 436 // 437 // It returns an error if the file could not be read. 438 // 439 // TODO(rfindley): eliminate this helper. 440 func parseGoURI(ctx context.Context, fs file.Source, uri protocol.DocumentURI, mode parser.Mode) (*ParsedGoFile, error) { 441 fh, err := fs.ReadFile(ctx, uri) 442 if err != nil { 443 return nil, err 444 } 445 return parseGoImpl(ctx, token.NewFileSet(), fh, mode, false) 446 } 447 448 // parseModURI is a helper to parse the Mod file at the given URI from the file 449 // source fs. 450 // 451 // It returns an error if the file could not be read. 452 func parseModURI(ctx context.Context, fs file.Source, uri protocol.DocumentURI) (*ParsedModule, error) { 453 fh, err := fs.ReadFile(ctx, uri) 454 if err != nil { 455 return nil, err 456 } 457 return parseModImpl(ctx, fh) 458 }