golang.org/x/tools/gopls@v0.15.3/internal/golang/change_signature.go (about)

     1  // Copyright 2023 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  	"fmt"
    11  	"go/ast"
    12  	"go/format"
    13  	"go/parser"
    14  	"go/token"
    15  	"go/types"
    16  	"regexp"
    17  
    18  	"golang.org/x/tools/go/ast/astutil"
    19  	"golang.org/x/tools/gopls/internal/cache"
    20  	"golang.org/x/tools/gopls/internal/cache/parsego"
    21  	"golang.org/x/tools/gopls/internal/file"
    22  	"golang.org/x/tools/gopls/internal/protocol"
    23  	"golang.org/x/tools/gopls/internal/util/bug"
    24  	"golang.org/x/tools/gopls/internal/util/safetoken"
    25  	"golang.org/x/tools/imports"
    26  	internalastutil "golang.org/x/tools/internal/astutil"
    27  	"golang.org/x/tools/internal/diff"
    28  	"golang.org/x/tools/internal/refactor/inline"
    29  	"golang.org/x/tools/internal/tokeninternal"
    30  	"golang.org/x/tools/internal/typesinternal"
    31  	"golang.org/x/tools/internal/versions"
    32  )
    33  
    34  // RemoveUnusedParameter computes a refactoring to remove the parameter
    35  // indicated by the given range, which must be contained within an unused
    36  // parameter name or field.
    37  //
    38  // This operation is a work in progress. Remaining TODO:
    39  //   - Handle function assignment correctly.
    40  //   - Improve the extra newlines in output.
    41  //   - Stream type checking via ForEachPackage.
    42  //   - Avoid unnecessary additional type checking.
    43  func RemoveUnusedParameter(ctx context.Context, fh file.Handle, rng protocol.Range, snapshot *cache.Snapshot) ([]protocol.DocumentChanges, error) {
    44  	pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
    45  	if err != nil {
    46  		return nil, err
    47  	}
    48  	if perrors, terrors := pkg.GetParseErrors(), pkg.GetTypeErrors(); len(perrors) > 0 || len(terrors) > 0 {
    49  		var sample string
    50  		if len(perrors) > 0 {
    51  			sample = perrors[0].Error()
    52  		} else {
    53  			sample = terrors[0].Error()
    54  		}
    55  		return nil, fmt.Errorf("can't change signatures for packages with parse or type errors: (e.g. %s)", sample)
    56  	}
    57  
    58  	info, err := FindParam(pgf, rng)
    59  	if err != nil {
    60  		return nil, err // e.g. invalid range
    61  	}
    62  	if info.Decl.Recv != nil {
    63  		return nil, fmt.Errorf("can't change signature of methods (yet)")
    64  	}
    65  	if info.Field == nil {
    66  		return nil, fmt.Errorf("failed to find field")
    67  	}
    68  
    69  	// Create the new declaration, which is a copy of the original decl with the
    70  	// unnecessary parameter removed.
    71  	newDecl := internalastutil.CloneNode(info.Decl)
    72  	if info.Name != nil {
    73  		names := remove(newDecl.Type.Params.List[info.FieldIndex].Names, info.NameIndex)
    74  		newDecl.Type.Params.List[info.FieldIndex].Names = names
    75  	}
    76  	if len(newDecl.Type.Params.List[info.FieldIndex].Names) == 0 {
    77  		// Unnamed, or final name was removed: in either case, remove the field.
    78  		newDecl.Type.Params.List = remove(newDecl.Type.Params.List, info.FieldIndex)
    79  	}
    80  
    81  	// Compute inputs into building a wrapper function around the modified
    82  	// signature.
    83  	var (
    84  		params   = internalastutil.CloneNode(info.Decl.Type.Params) // "_" names will be modified
    85  		args     []ast.Expr                                         // arguments to delegate
    86  		variadic = false                                            // whether the signature is variadic
    87  	)
    88  	{
    89  		allNames := make(map[string]bool) // for renaming blanks
    90  		for _, fld := range params.List {
    91  			for _, n := range fld.Names {
    92  				if n.Name != "_" {
    93  					allNames[n.Name] = true
    94  				}
    95  			}
    96  		}
    97  		blanks := 0
    98  		for i, fld := range params.List {
    99  			for j, n := range fld.Names {
   100  				if i == info.FieldIndex && j == info.NameIndex {
   101  					continue
   102  				}
   103  				if n.Name == "_" {
   104  					// Create names for blank (_) parameters so the delegating wrapper
   105  					// can refer to them.
   106  					for {
   107  						newName := fmt.Sprintf("blank%d", blanks)
   108  						blanks++
   109  						if !allNames[newName] {
   110  							n.Name = newName
   111  							break
   112  						}
   113  					}
   114  				}
   115  				args = append(args, &ast.Ident{Name: n.Name})
   116  				if i == len(params.List)-1 {
   117  					_, variadic = fld.Type.(*ast.Ellipsis)
   118  				}
   119  			}
   120  		}
   121  	}
   122  
   123  	// Rewrite all referring calls.
   124  	newContent, err := rewriteCalls(ctx, signatureRewrite{
   125  		snapshot: snapshot,
   126  		pkg:      pkg,
   127  		pgf:      pgf,
   128  		origDecl: info.Decl,
   129  		newDecl:  newDecl,
   130  		params:   params,
   131  		callArgs: args,
   132  		variadic: variadic,
   133  	})
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	// Finally, rewrite the original declaration. We do this after inlining all
   139  	// calls, as there may be calls in the same file as the declaration. But none
   140  	// of the inlining should have changed the location of the original
   141  	// declaration.
   142  	{
   143  		idx := findDecl(pgf.File, info.Decl)
   144  		if idx < 0 {
   145  			return nil, bug.Errorf("didn't find original decl")
   146  		}
   147  
   148  		src, ok := newContent[pgf.URI]
   149  		if !ok {
   150  			src = pgf.Src
   151  		}
   152  		fset := tokeninternal.FileSetFor(pgf.Tok)
   153  		src, err := rewriteSignature(fset, idx, src, newDecl)
   154  		if err != nil {
   155  			return nil, err
   156  		}
   157  		newContent[pgf.URI] = src
   158  	}
   159  
   160  	// Translate the resulting state into document changes.
   161  	var changes []protocol.DocumentChanges
   162  	for uri, after := range newContent {
   163  		fh, err := snapshot.ReadFile(ctx, uri)
   164  		if err != nil {
   165  			return nil, err
   166  		}
   167  		before, err := fh.Content()
   168  		if err != nil {
   169  			return nil, err
   170  		}
   171  		edits := diff.Bytes(before, after)
   172  		mapper := protocol.NewMapper(uri, before)
   173  		pedits, err := protocol.EditsFromDiffEdits(mapper, edits)
   174  		if err != nil {
   175  			return nil, fmt.Errorf("computing edits for %s: %v", uri, err)
   176  		}
   177  		changes = append(changes, documentChanges(fh, pedits)...)
   178  	}
   179  	return changes, nil
   180  }
   181  
   182  // rewriteSignature rewrites the signature of the declIdx'th declaration in src
   183  // to use the signature of newDecl (described by fset).
   184  //
   185  // TODO(rfindley): I think this operation could be generalized, for example by
   186  // using a concept of a 'nodepath' to correlate nodes between two related
   187  // files.
   188  //
   189  // Note that with its current application, rewriteSignature is expected to
   190  // succeed. Separate bug.Errorf calls are used below (rather than one call at
   191  // the callsite) in order to have greater precision.
   192  func rewriteSignature(fset *token.FileSet, declIdx int, src0 []byte, newDecl *ast.FuncDecl) ([]byte, error) {
   193  	// Parse the new file0 content, to locate the original params.
   194  	file0, err := parser.ParseFile(fset, "", src0, parser.ParseComments|parser.SkipObjectResolution)
   195  	if err != nil {
   196  		return nil, bug.Errorf("re-parsing declaring file failed: %v", err)
   197  	}
   198  	decl0, _ := file0.Decls[declIdx].(*ast.FuncDecl)
   199  	// Inlining shouldn't have changed the location of any declarations, but do
   200  	// a sanity check.
   201  	if decl0 == nil || decl0.Name.Name != newDecl.Name.Name {
   202  		return nil, bug.Errorf("inlining affected declaration order: found %v, not func %s", decl0, newDecl.Name.Name)
   203  	}
   204  	opening0, closing0, err := safetoken.Offsets(fset.File(decl0.Pos()), decl0.Type.Params.Opening, decl0.Type.Params.Closing)
   205  	if err != nil {
   206  		return nil, bug.Errorf("can't find params: %v", err)
   207  	}
   208  
   209  	// Format the modified signature and apply a textual replacement. This
   210  	// minimizes comment disruption.
   211  	formattedType := FormatNode(fset, newDecl.Type)
   212  	expr, err := parser.ParseExprFrom(fset, "", []byte(formattedType), 0)
   213  	if err != nil {
   214  		return nil, bug.Errorf("parsing modified signature: %v", err)
   215  	}
   216  	newType := expr.(*ast.FuncType)
   217  	opening1, closing1, err := safetoken.Offsets(fset.File(newType.Pos()), newType.Params.Opening, newType.Params.Closing)
   218  	if err != nil {
   219  		return nil, bug.Errorf("param offsets: %v", err)
   220  	}
   221  	newParams := formattedType[opening1 : closing1+1]
   222  
   223  	// Splice.
   224  	var buf bytes.Buffer
   225  	buf.Write(src0[:opening0])
   226  	buf.WriteString(newParams)
   227  	buf.Write(src0[closing0+1:])
   228  	newSrc := buf.Bytes()
   229  	if len(file0.Imports) > 0 {
   230  		formatted, err := imports.Process("output", newSrc, nil)
   231  		if err != nil {
   232  			return nil, bug.Errorf("imports.Process failed: %v", err)
   233  		}
   234  		newSrc = formatted
   235  	}
   236  	return newSrc, nil
   237  }
   238  
   239  // ParamInfo records information about a param identified by a position.
   240  type ParamInfo struct {
   241  	Decl       *ast.FuncDecl // enclosing func decl (non-nil)
   242  	FieldIndex int           // index of Field in Decl.Type.Params, or -1
   243  	Field      *ast.Field    // enclosing field of Decl, or nil if range not among parameters
   244  	NameIndex  int           // index of Name in Field.Names, or nil
   245  	Name       *ast.Ident    // indicated name (either enclosing, or Field.Names[0] if len(Field.Names) == 1)
   246  }
   247  
   248  // FindParam finds the parameter information spanned by the given range.
   249  func FindParam(pgf *ParsedGoFile, rng protocol.Range) (*ParamInfo, error) {
   250  	start, end, err := pgf.RangePos(rng)
   251  	if err != nil {
   252  		return nil, err
   253  	}
   254  
   255  	path, _ := astutil.PathEnclosingInterval(pgf.File, start, end)
   256  	var (
   257  		id    *ast.Ident
   258  		field *ast.Field
   259  		decl  *ast.FuncDecl
   260  	)
   261  	// Find the outermost enclosing node of each kind, whether or not they match
   262  	// the semantics described in the docstring.
   263  	for _, n := range path {
   264  		switch n := n.(type) {
   265  		case *ast.Ident:
   266  			id = n
   267  		case *ast.Field:
   268  			field = n
   269  		case *ast.FuncDecl:
   270  			decl = n
   271  		}
   272  	}
   273  	// Check the conditions described in the docstring.
   274  	if decl == nil {
   275  		return nil, fmt.Errorf("range is not within a function declaration")
   276  	}
   277  	info := &ParamInfo{
   278  		FieldIndex: -1,
   279  		NameIndex:  -1,
   280  		Decl:       decl,
   281  	}
   282  	for fi, f := range decl.Type.Params.List {
   283  		if f == field {
   284  			info.FieldIndex = fi
   285  			info.Field = f
   286  			for ni, n := range f.Names {
   287  				if n == id {
   288  					info.NameIndex = ni
   289  					info.Name = n
   290  					break
   291  				}
   292  			}
   293  			if info.Name == nil && len(info.Field.Names) == 1 {
   294  				info.NameIndex = 0
   295  				info.Name = info.Field.Names[0]
   296  			}
   297  			break
   298  		}
   299  	}
   300  	return info, nil
   301  }
   302  
   303  // signatureRewrite defines a rewritten function signature.
   304  //
   305  // See rewriteCalls for more details.
   306  type signatureRewrite struct {
   307  	snapshot          *cache.Snapshot
   308  	pkg               *cache.Package
   309  	pgf               *parsego.File
   310  	origDecl, newDecl *ast.FuncDecl
   311  	params            *ast.FieldList
   312  	callArgs          []ast.Expr
   313  	variadic          bool
   314  }
   315  
   316  // rewriteCalls returns the document changes required to rewrite the
   317  // signature of origDecl to that of newDecl.
   318  //
   319  // This is a rather complicated factoring of the rewrite operation, but is able
   320  // to describe arbitrary rewrites. Specifically, rewriteCalls creates a
   321  // synthetic copy of pkg, where the original function declaration is changed to
   322  // be a trivial wrapper around the new declaration. params and callArgs are
   323  // used to perform this delegation: params must have the same type as origDecl,
   324  // but may have renamed parameters (such as is required for delegating blank
   325  // parameters). callArgs are the arguments of the delegated call (i.e. using
   326  // params).
   327  //
   328  // For example, consider removing the unused 'b' parameter below, rewriting
   329  //
   330  //	func Foo(a, b, c, _ int) int {
   331  //	  return a+c
   332  //	}
   333  //
   334  // To
   335  //
   336  //	func Foo(a, c, _ int) int {
   337  //	  return a+c
   338  //	}
   339  //
   340  // In this case, rewriteCalls is parameterized as follows:
   341  //   - origDecl is the original declaration
   342  //   - newDecl is the new declaration, which is a copy of origDecl less the 'b'
   343  //     parameter.
   344  //   - params is a new parameter list (a, b, c, blank0 int) to be used for the
   345  //     new wrapper.
   346  //   - callArgs is the argument list (a, c, blank0), to be used to call the new
   347  //     delegate.
   348  //
   349  // rewriting is expressed this way so that rewriteCalls can own the details
   350  // of *how* this rewriting is performed. For example, as of writing it names
   351  // the synthetic delegate G_o_p_l_s_foo, but the caller need not know this.
   352  //
   353  // By passing an entirely new declaration, rewriteCalls may be used for
   354  // signature refactorings that may affect the function body, such as removing
   355  // or adding return values.
   356  func rewriteCalls(ctx context.Context, rw signatureRewrite) (map[protocol.DocumentURI][]byte, error) {
   357  	// tag is a unique prefix that is added to the delegated declaration.
   358  	//
   359  	// It must have a ~0% probability of causing collisions with existing names.
   360  	const tag = "G_o_p_l_s_"
   361  
   362  	var (
   363  		modifiedSrc  []byte
   364  		modifiedFile *ast.File
   365  		modifiedDecl *ast.FuncDecl
   366  	)
   367  	{
   368  		delegate := internalastutil.CloneNode(rw.newDecl) // clone before modifying
   369  		delegate.Name.Name = tag + delegate.Name.Name
   370  		if obj := rw.pkg.GetTypes().Scope().Lookup(delegate.Name.Name); obj != nil {
   371  			return nil, fmt.Errorf("synthetic name %q conflicts with an existing declaration", delegate.Name.Name)
   372  		}
   373  
   374  		wrapper := internalastutil.CloneNode(rw.origDecl)
   375  		wrapper.Type.Params = rw.params
   376  		call := &ast.CallExpr{
   377  			Fun:  &ast.Ident{Name: delegate.Name.Name},
   378  			Args: rw.callArgs,
   379  		}
   380  		if rw.variadic {
   381  			call.Ellipsis = 1 // must not be token.NoPos
   382  		}
   383  
   384  		var stmt ast.Stmt
   385  		if delegate.Type.Results.NumFields() > 0 {
   386  			stmt = &ast.ReturnStmt{
   387  				Results: []ast.Expr{call},
   388  			}
   389  		} else {
   390  			stmt = &ast.ExprStmt{
   391  				X: call,
   392  			}
   393  		}
   394  		wrapper.Body = &ast.BlockStmt{
   395  			List: []ast.Stmt{stmt},
   396  		}
   397  
   398  		fset := tokeninternal.FileSetFor(rw.pgf.Tok)
   399  		var err error
   400  		modifiedSrc, err = replaceFileDecl(rw.pgf, rw.origDecl, delegate)
   401  		if err != nil {
   402  			return nil, err
   403  		}
   404  		// TODO(rfindley): we can probably get away with one fewer parse operations
   405  		// by returning the modified AST from replaceDecl. Investigate if that is
   406  		// accurate.
   407  		modifiedSrc = append(modifiedSrc, []byte("\n\n"+FormatNode(fset, wrapper))...)
   408  		modifiedFile, err = parser.ParseFile(rw.pkg.FileSet(), rw.pgf.URI.Path(), modifiedSrc, parser.ParseComments|parser.SkipObjectResolution)
   409  		if err != nil {
   410  			return nil, err
   411  		}
   412  		modifiedDecl = modifiedFile.Decls[len(modifiedFile.Decls)-1].(*ast.FuncDecl)
   413  	}
   414  
   415  	// Type check pkg again with the modified file, to compute the synthetic
   416  	// callee.
   417  	logf := logger(ctx, "change signature", rw.snapshot.Options().VerboseOutput)
   418  	pkg2, info, err := reTypeCheck(logf, rw.pkg, map[protocol.DocumentURI]*ast.File{rw.pgf.URI: modifiedFile}, false)
   419  	if err != nil {
   420  		return nil, err
   421  	}
   422  	calleeInfo, err := inline.AnalyzeCallee(logf, rw.pkg.FileSet(), pkg2, info, modifiedDecl, modifiedSrc)
   423  	if err != nil {
   424  		return nil, fmt.Errorf("analyzing callee: %v", err)
   425  	}
   426  
   427  	post := func(got []byte) []byte { return bytes.ReplaceAll(got, []byte(tag), nil) }
   428  	return inlineAllCalls(ctx, logf, rw.snapshot, rw.pkg, rw.pgf, rw.origDecl, calleeInfo, post)
   429  }
   430  
   431  // reTypeCheck re-type checks orig with new file contents defined by fileMask.
   432  //
   433  // It expects that any newly added imports are already present in the
   434  // transitive imports of orig.
   435  //
   436  // If expectErrors is true, reTypeCheck allows errors in the new package.
   437  // TODO(rfindley): perhaps this should be a filter to specify which errors are
   438  // acceptable.
   439  func reTypeCheck(logf func(string, ...any), orig *cache.Package, fileMask map[protocol.DocumentURI]*ast.File, expectErrors bool) (*types.Package, *types.Info, error) {
   440  	pkg := types.NewPackage(string(orig.Metadata().PkgPath), string(orig.Metadata().Name))
   441  	info := &types.Info{
   442  		Types:      make(map[ast.Expr]types.TypeAndValue),
   443  		Defs:       make(map[*ast.Ident]types.Object),
   444  		Uses:       make(map[*ast.Ident]types.Object),
   445  		Implicits:  make(map[ast.Node]types.Object),
   446  		Selections: make(map[*ast.SelectorExpr]*types.Selection),
   447  		Scopes:     make(map[ast.Node]*types.Scope),
   448  		Instances:  make(map[*ast.Ident]types.Instance),
   449  	}
   450  	versions.InitFileVersions(info)
   451  	{
   452  		var files []*ast.File
   453  		for _, pgf := range orig.CompiledGoFiles() {
   454  			if mask, ok := fileMask[pgf.URI]; ok {
   455  				files = append(files, mask)
   456  			} else {
   457  				files = append(files, pgf.File)
   458  			}
   459  		}
   460  
   461  		// Implement a BFS for imports in the transitive package graph.
   462  		//
   463  		// Note that this only works if any newly added imports are expected to be
   464  		// present among transitive imports. In general we cannot assume this to
   465  		// be the case, but in the special case of removing a parameter it works
   466  		// because any parameter types must be present in export data.
   467  		var importer func(importPath string) (*types.Package, error)
   468  		{
   469  			var (
   470  				importsByPath = make(map[string]*types.Package)   // cached imports
   471  				toSearch      = []*types.Package{orig.GetTypes()} // packages to search
   472  				searched      = make(map[string]bool)             // path -> (false, if present in toSearch; true, if already searched)
   473  			)
   474  			importer = func(path string) (*types.Package, error) {
   475  				if p, ok := importsByPath[path]; ok {
   476  					return p, nil
   477  				}
   478  				for len(toSearch) > 0 {
   479  					pkg := toSearch[0]
   480  					toSearch = toSearch[1:]
   481  					searched[pkg.Path()] = true
   482  					for _, p := range pkg.Imports() {
   483  						// TODO(rfindley): this is incorrect: p.Path() is a package path,
   484  						// whereas path is an import path. We can fix this by reporting any
   485  						// newly added imports from inlining, or by using the ImporterFrom
   486  						// interface and package metadata.
   487  						//
   488  						// TODO(rfindley): can't the inliner also be wrong here? It's
   489  						// possible that an import path means different things depending on
   490  						// the location.
   491  						importsByPath[p.Path()] = p
   492  						if _, ok := searched[p.Path()]; !ok {
   493  							searched[p.Path()] = false
   494  							toSearch = append(toSearch, p)
   495  						}
   496  					}
   497  					if p, ok := importsByPath[path]; ok {
   498  						return p, nil
   499  					}
   500  				}
   501  				return nil, fmt.Errorf("missing import")
   502  			}
   503  		}
   504  		cfg := &types.Config{
   505  			Sizes:    orig.Metadata().TypesSizes,
   506  			Importer: ImporterFunc(importer),
   507  		}
   508  
   509  		// Copied from cache/check.go.
   510  		// TODO(rfindley): factor this out and fix goVersionRx.
   511  		// Set Go dialect.
   512  		if module := orig.Metadata().Module; module != nil && module.GoVersion != "" {
   513  			goVersion := "go" + module.GoVersion
   514  			// types.NewChecker panics if GoVersion is invalid.
   515  			// An unparsable mod file should probably stop us
   516  			// before we get here, but double check just in case.
   517  			if goVersionRx.MatchString(goVersion) {
   518  				typesinternal.SetGoVersion(cfg, goVersion)
   519  			}
   520  		}
   521  		if expectErrors {
   522  			cfg.Error = func(err error) {
   523  				logf("re-type checking: expected error: %v", err)
   524  			}
   525  		}
   526  		typesinternal.SetUsesCgo(cfg)
   527  		checker := types.NewChecker(cfg, orig.FileSet(), pkg, info)
   528  		if err := checker.Files(files); err != nil && !expectErrors {
   529  			return nil, nil, fmt.Errorf("type checking rewritten package: %v", err)
   530  		}
   531  	}
   532  	return pkg, info, nil
   533  }
   534  
   535  // TODO(golang/go#63472): this looks wrong with the new Go version syntax.
   536  var goVersionRx = regexp.MustCompile(`^go([1-9][0-9]*)\.(0|[1-9][0-9]*)$`)
   537  
   538  func remove[T any](s []T, i int) []T {
   539  	return append(s[:i], s[i+1:]...)
   540  }
   541  
   542  // replaceFileDecl replaces old with new in the file described by pgf.
   543  //
   544  // TODO(rfindley): generalize, and combine with rewriteSignature.
   545  func replaceFileDecl(pgf *ParsedGoFile, old, new ast.Decl) ([]byte, error) {
   546  	i := findDecl(pgf.File, old)
   547  	if i == -1 {
   548  		return nil, bug.Errorf("didn't find old declaration")
   549  	}
   550  	start, end, err := safetoken.Offsets(pgf.Tok, old.Pos(), old.End())
   551  	if err != nil {
   552  		return nil, err
   553  	}
   554  	var out bytes.Buffer
   555  	out.Write(pgf.Src[:start])
   556  	fset := tokeninternal.FileSetFor(pgf.Tok)
   557  	if err := format.Node(&out, fset, new); err != nil {
   558  		return nil, bug.Errorf("formatting new node: %v", err)
   559  	}
   560  	out.Write(pgf.Src[end:])
   561  	return out.Bytes(), nil
   562  }
   563  
   564  // findDecl finds the index of decl in file.Decls.
   565  //
   566  // TODO: use slices.Index when it is available.
   567  func findDecl(file *ast.File, decl ast.Decl) int {
   568  	for i, d := range file.Decls {
   569  		if d == decl {
   570  			return i
   571  		}
   572  	}
   573  	return -1
   574  }