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  

View as plain text