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