Source file src/cmd/vendor/golang.org/x/tools/internal/analysis/driverutil/fix.go
1 // Copyright 2018 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 driverutil defines implementation helper functions for 6 // analysis drivers such as unitchecker, {single,multi}checker, and 7 // analysistest. 8 package driverutil 9 10 // This file defines the -fix logic common to unitchecker and 11 // {single,multi}checker. 12 13 import ( 14 "bytes" 15 "fmt" 16 "go/ast" 17 "go/parser" 18 "go/printer" 19 "go/token" 20 "go/types" 21 "log" 22 "maps" 23 "os" 24 "sort" 25 "strconv" 26 27 "golang.org/x/tools/go/analysis" 28 "golang.org/x/tools/go/ast/astutil" 29 "golang.org/x/tools/internal/astutil/free" 30 "golang.org/x/tools/internal/diff" 31 ) 32 33 // FixAction abstracts a checker action (running one analyzer on one 34 // package) for the purposes of applying its diagnostics' fixes. 35 type FixAction struct { 36 Name string // e.g. "analyzer@package" 37 Pkg *types.Package // (for import removal) 38 Files []*ast.File 39 FileSet *token.FileSet 40 ReadFileFunc ReadFileFunc 41 Diagnostics []analysis.Diagnostic 42 } 43 44 // ApplyFixes attempts to apply the first suggested fix associated 45 // with each diagnostic reported by the specified actions. 46 // All fixes must have been validated by [ValidateFixes]. 47 // 48 // Each fix is treated as an independent change; fixes are merged in 49 // an arbitrary deterministic order as if by a three-way diff tool 50 // such as the UNIX diff3 command or 'git merge'. Any fix that cannot be 51 // cleanly merged is discarded, in which case the final summary tells 52 // the user to re-run the tool. 53 // TODO(adonovan): make the checker tool re-run the analysis itself. 54 // 55 // When the same file is analyzed as a member of both a primary 56 // package "p" and a test-augmented package "p [p.test]", there may be 57 // duplicate diagnostics and fixes. One set of fixes will be applied 58 // and the other will be discarded; but re-running the tool may then 59 // show zero fixes, which may cause the confused user to wonder what 60 // happened to the other ones. 61 // TODO(adonovan): consider pre-filtering completely identical fixes. 62 // 63 // A common reason for overlapping fixes is duplicate additions of the 64 // same import. The merge algorithm may often cleanly resolve such 65 // fixes, coalescing identical edits, but the merge may sometimes be 66 // confused by nearby changes. 67 // 68 // Even when merging succeeds, there is no guarantee that the 69 // composition of the two fixes is semantically correct. Coalescing 70 // identical edits is appropriate for imports, but not for, say, 71 // increments to a counter variable; the correct resolution in that 72 // case might be to increment it twice. 73 // 74 // Or consider two fixes that each delete the penultimate reference to 75 // a local variable: each fix is sound individually, and they may be 76 // textually distant from each other, but when both are applied, the 77 // program is no longer valid because it has an unreferenced local 78 // variable. (ApplyFixes solves the analogous problem for imports by 79 // eliminating imports whose name is unreferenced in the remainder of 80 // the fixed file.) 81 // 82 // Merging depends on both the order of fixes and they order of edits 83 // within them. For example, if three fixes add import "a" twice and 84 // import "b" once, the two imports of "a" may be combined if they 85 // appear in order [a, a, b], or not if they appear as [a, b, a]. 86 // TODO(adonovan): investigate an algebraic approach to imports; 87 // that is, for fixes to Go source files, convert changes within the 88 // import(...) portion of the file into semantic edits, compose those 89 // edits algebraically, then convert the result back to edits. 90 // 91 // applyFixes returns success if all fixes are valid, could be cleanly 92 // merged, and the corresponding files were successfully updated. 93 // 94 // If printDiff (from the -diff flag) is set, instead of updating the 95 // files it display the final patch composed of all the cleanly merged 96 // fixes. (It is tempting to factor printDiff as just a variant of 97 // writeFile that is provided the old and new content, but it's hard 98 // to generate a good summary that way.) 99 // 100 // TODO(adonovan): handle file-system level aliases such as symbolic 101 // links using robustio.FileID. 102 func ApplyFixes(actions []FixAction, writeFile func(filename string, content []byte) error, printDiff, verbose bool) error { 103 generated := make(map[*token.File]bool) 104 105 // Select fixes to apply. 106 // 107 // If there are several for a given Diagnostic, choose the first. 108 // Preserve the order of iteration, for determinism. 109 type fixact struct { 110 fix *analysis.SuggestedFix 111 act FixAction 112 } 113 var fixes []*fixact 114 for _, act := range actions { 115 for _, file := range act.Files { 116 tokFile := act.FileSet.File(file.FileStart) 117 // Memoize, since there may be many actions 118 // for the same package (list of files). 119 if _, seen := generated[tokFile]; !seen { 120 generated[tokFile] = ast.IsGenerated(file) 121 } 122 } 123 124 for _, diag := range act.Diagnostics { 125 for i := range diag.SuggestedFixes { 126 fix := &diag.SuggestedFixes[i] 127 if i == 0 { 128 fixes = append(fixes, &fixact{fix, act}) 129 } else { 130 // TODO(adonovan): abstract the logger. 131 log.Printf("%s: ignoring alternative fix %q", act.Name, fix.Message) 132 } 133 } 134 } 135 } 136 137 // Read file content on demand, from the virtual 138 // file system that fed the analyzer (see #62292). 139 // 140 // This cache assumes that all successful reads for the same 141 // file name return the same content. 142 // (It is tempting to group fixes by package and do the 143 // merge/apply/format steps one package at a time, but 144 // packages are not disjoint, due to test variants, so this 145 // would not really address the issue.) 146 baselineContent := make(map[string][]byte) 147 getBaseline := func(readFile ReadFileFunc, filename string) ([]byte, error) { 148 content, ok := baselineContent[filename] 149 if !ok { 150 var err error 151 content, err = readFile(filename) 152 if err != nil { 153 return nil, err 154 } 155 baselineContent[filename] = content 156 } 157 return content, nil 158 } 159 160 // Apply each fix, updating the current state 161 // only if the entire fix can be cleanly merged. 162 var ( 163 accumulatedEdits = make(map[string][]diff.Edit) 164 filePkgs = make(map[string]*types.Package) // maps each file to an arbitrary package that includes it 165 166 goodFixes = 0 // number of fixes cleanly applied 167 skippedFixes = 0 // number of fixes skipped (because e.g. edits a generated file) 168 ) 169 fixloop: 170 for _, fixact := range fixes { 171 // Skip a fix if any of its edits touch a generated file. 172 for _, edit := range fixact.fix.TextEdits { 173 file := fixact.act.FileSet.File(edit.Pos) 174 if generated[file] { 175 skippedFixes++ 176 continue fixloop 177 } 178 } 179 180 // Convert analysis.TextEdits to diff.Edits, grouped by file. 181 // Precondition: a prior call to validateFix succeeded. 182 fileEdits := make(map[string][]diff.Edit) 183 for _, edit := range fixact.fix.TextEdits { 184 file := fixact.act.FileSet.File(edit.Pos) 185 186 filePkgs[file.Name()] = fixact.act.Pkg 187 188 baseline, err := getBaseline(fixact.act.ReadFileFunc, file.Name()) 189 if err != nil { 190 log.Printf("skipping fix to file %s: %v", file.Name(), err) 191 continue fixloop 192 } 193 194 // We choose to treat size mismatch as a serious error, 195 // as it indicates a concurrent write to at least one file, 196 // and possibly others (consider a git checkout, for example). 197 if file.Size() != len(baseline) { 198 return fmt.Errorf("concurrent file modification detected in file %s (size changed from %d -> %d bytes); aborting fix", 199 file.Name(), file.Size(), len(baseline)) 200 } 201 202 fileEdits[file.Name()] = append(fileEdits[file.Name()], diff.Edit{ 203 Start: file.Offset(edit.Pos), 204 End: file.Offset(edit.End), 205 New: string(edit.NewText), 206 }) 207 } 208 209 // Apply each set of edits by merging atop 210 // the previous accumulated state. 211 after := make(map[string][]diff.Edit) 212 for file, edits := range fileEdits { 213 if prev := accumulatedEdits[file]; len(prev) > 0 { 214 merged, ok := diff.Merge(prev, edits) 215 if !ok { 216 // debugging 217 if false { 218 log.Printf("%s: fix %s conflicts", fixact.act.Name, fixact.fix.Message) 219 } 220 continue fixloop // conflict 221 } 222 edits = merged 223 } 224 after[file] = edits 225 } 226 227 // The entire fix applied cleanly; commit it. 228 goodFixes++ 229 maps.Copy(accumulatedEdits, after) 230 // debugging 231 if false { 232 log.Printf("%s: fix %s applied", fixact.act.Name, fixact.fix.Message) 233 } 234 } 235 badFixes := len(fixes) - goodFixes - skippedFixes // number of fixes that could not be applied 236 237 // Show diff or update files to final state. 238 var files []string 239 for file := range accumulatedEdits { 240 files = append(files, file) 241 } 242 sort.Strings(files) // for deterministic -diff 243 var filesUpdated, totalFiles int 244 for _, file := range files { 245 edits := accumulatedEdits[file] 246 if len(edits) == 0 { 247 continue // the diffs annihilated (a miracle?) 248 } 249 250 // Apply accumulated fixes. 251 baseline := baselineContent[file] // (cache hit) 252 final, err := diff.ApplyBytes(baseline, edits) 253 if err != nil { 254 log.Fatalf("internal error in diff.ApplyBytes: %v", err) 255 } 256 257 // Attempt to format each file. 258 if formatted, err := FormatSourceRemoveImports(filePkgs[file], final); err == nil { 259 final = formatted 260 } 261 262 if printDiff { 263 // Since we formatted the file, we need to recompute the diff. 264 unified := diff.Unified(file+" (old)", file+" (new)", string(baseline), string(final)) 265 // TODO(adonovan): abstract the I/O. 266 os.Stdout.WriteString(unified) 267 268 } else { 269 // write file 270 totalFiles++ 271 if err := writeFile(file, final); err != nil { 272 log.Println(err) 273 continue // (causes ApplyFix to return an error) 274 } 275 filesUpdated++ 276 } 277 } 278 279 // TODO(adonovan): consider returning a structured result that 280 // maps each SuggestedFix to its status: 281 // - invalid 282 // - secondary, not selected 283 // - applied 284 // - had conflicts. 285 // and a mapping from each affected file to: 286 // - its final/original content pair, and 287 // - whether formatting was successful. 288 // Then file writes and the UI can be applied by the caller 289 // in whatever form they like. 290 291 // If victory was incomplete, report an error that indicates partial progress. 292 // 293 // badFixes > 0 indicates that we decided not to attempt some 294 // fixes due to conflicts or failure to read the source; still 295 // it's a relatively benign situation since the user can 296 // re-run the tool, and we may still make progress. 297 // 298 // filesUpdated < totalFiles indicates that some file updates 299 // failed. This should be rare, but is a serious error as it 300 // may apply half a fix, or leave the files in a bad state. 301 // 302 // These numbers are potentially misleading: 303 // The denominator includes duplicate conflicting fixes due to 304 // common files in packages "p" and "p [p.test]", which may 305 // have been fixed and won't appear in the re-run. 306 // TODO(adonovan): eliminate identical fixes as an initial 307 // filtering step. 308 // 309 // TODO(adonovan): should we log that n files were updated in case of total victory? 310 if badFixes > 0 || filesUpdated < totalFiles { 311 if printDiff { 312 return fmt.Errorf("%d of %s skipped (e.g. due to conflicts)", 313 badFixes, 314 plural(len(fixes), "fix", "fixes")) 315 } else { 316 return fmt.Errorf("applied %d of %s; %s updated. (Re-run the command to apply more.)", 317 goodFixes, 318 plural(len(fixes), "fix", "fixes"), 319 plural(filesUpdated, "file", "files")) 320 } 321 } 322 323 if verbose { 324 if skippedFixes > 0 { 325 log.Printf("skipped %s that would edit generated files", 326 plural(skippedFixes, "fix", "fixes")) 327 } 328 log.Printf("applied %s, updated %s", 329 plural(len(fixes), "fix", "fixes"), 330 plural(filesUpdated, "file", "files")) 331 } 332 333 return nil 334 } 335 336 // FormatSourceRemoveImports is a variant of [format.Source] that 337 // removes imports that became redundant when fixes were applied. 338 // 339 // Import removal is necessarily heuristic since we do not have type 340 // information for the fixed file and thus cannot accurately tell 341 // whether k is among the free names of T{k: 0}, which requires 342 // knowledge of whether T is a struct type. 343 // 344 // Like [imports.Process] (the core of x/tools/cmd/goimports), it also 345 // merges import decls. 346 func FormatSourceRemoveImports(pkg *types.Package, src []byte) ([]byte, error) { 347 // This function was reduced from the "strict entire file" 348 // path through [format.Source]. 349 350 fset := token.NewFileSet() 351 file, err := parser.ParseFile(fset, "fixed.go", src, parser.ParseComments|parser.SkipObjectResolution) 352 if err != nil { 353 return nil, err 354 } 355 356 ast.SortImports(fset, file) 357 358 removeUnneededImports(fset, pkg, file) 359 360 // TODO(adonovan): to generate cleaner edits when adding an import, 361 // consider adding a call to imports.mergeImports; however, it does 362 // cause comments to migrate. 363 364 // printerNormalizeNumbers means to canonicalize number literal prefixes 365 // and exponents while printing. See https://golang.org/doc/go1.13#gofmt. 366 // 367 // This value is defined in go/printer specifically for go/format and cmd/gofmt. 368 const printerNormalizeNumbers = 1 << 30 369 cfg := &printer.Config{ 370 Mode: printer.UseSpaces | printer.TabIndent | printerNormalizeNumbers, 371 Tabwidth: 8, 372 } 373 var buf bytes.Buffer 374 if err := cfg.Fprint(&buf, fset, file); err != nil { 375 return nil, err 376 } 377 return buf.Bytes(), nil 378 } 379 380 // removeUnneededImports removes import specs that are not referenced 381 // within the fixed file. It uses [free.Names] to heuristically 382 // approximate the set of imported names needed by the body of the 383 // file based only on syntax. 384 // 385 // pkg provides type information about the unmodified package, in 386 // particular the name that would implicitly be declared by a 387 // non-renaming import of a given existing dependency. 388 func removeUnneededImports(fset *token.FileSet, pkg *types.Package, file *ast.File) { 389 // Map each existing dependency to its default import name. 390 // (We'll need this to interpret non-renaming imports.) 391 packageNames := make(map[string]string) 392 for _, imp := range pkg.Imports() { 393 packageNames[imp.Path()] = imp.Name() 394 } 395 396 // Compute the set of free names of the file, 397 // ignoring its import decls. 398 freenames := make(map[string]bool) 399 for _, decl := range file.Decls { 400 if decl, ok := decl.(*ast.GenDecl); ok && decl.Tok == token.IMPORT { 401 continue // skip import 402 } 403 404 // TODO(adonovan): we could do better than includeComplitIdents=false 405 // since we have type information about the unmodified package, 406 // which is a good source of heuristics. 407 const includeComplitIdents = false 408 maps.Copy(freenames, free.Names(decl, includeComplitIdents)) 409 } 410 411 // Check whether each import's declared name is free (referenced) by the file. 412 var deletions []func() 413 for _, spec := range file.Imports { 414 path, err := strconv.Unquote(spec.Path.Value) 415 if err != nil { 416 continue // malformed import; ignore 417 } 418 explicit := "" // explicit PkgName, if any 419 if spec.Name != nil { 420 explicit = spec.Name.Name 421 } 422 name := explicit // effective PkgName 423 if name == "" { 424 // Non-renaming import: use package's default name. 425 name = packageNames[path] 426 } 427 switch name { 428 case "": 429 continue // assume it's a new import 430 case ".": 431 continue // dot imports are tricky 432 case "_": 433 continue // keep blank imports 434 } 435 if !freenames[name] { 436 // Import's effective name is not free in (not used by) the file. 437 // Enqueue it for deletion after the loop. 438 deletions = append(deletions, func() { 439 astutil.DeleteNamedImport(fset, file, explicit, path) 440 }) 441 } 442 } 443 444 // Apply the deletions. 445 for _, del := range deletions { 446 del() 447 } 448 } 449 450 // plural returns "n nouns", selecting the plural form as approriate. 451 func plural(n int, singular, plural string) string { 452 if n == 1 { 453 return "1 " + singular 454 } else { 455 return fmt.Sprintf("%d %s", n, plural) 456 } 457 } 458