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 }