diff --git a/gopls/internal/lsp/source/rename_check_gox.go b/gopls/internal/lsp/source/rename_check_gox.go index 19d7e4b45cc..047afe635c4 100644 --- a/gopls/internal/lsp/source/rename_check_gox.go +++ b/gopls/internal/lsp/source/rename_check_gox.go @@ -82,6 +82,50 @@ func gopForEachLexicalRef(pkg Package, obj types.Object, fn func(id *ast.Ident, return ok } +// isLocal reports whether obj is local to some function. +// Precondition: not a struct field or interface method. +func isGopLocal(obj types.Object) bool { + var depth int + for scope := obj.Parent(); scope != nil; scope = scope.Parent() { + depth++ + } + return depth >= 3 +} + +func isGopPackageLevel(obj types.Object) bool { + if obj == nil { + return false + } + // goxls: fast version + return obj.Pkg().Scope().Lookup(obj.Name()) == obj +} + +// check performs safety checks of the renaming of the 'from' object to r.to. +func (r *renamer) gopCheck(from types.Object) { + if r.objsToUpdate[from] { + return + } + r.objsToUpdate[from] = true + + // NB: order of conditions is important. + if from_, ok := from.(*types.PkgName); ok { + r.checkInFileBlock(from_) + } else if from_, ok := from.(*types.Label); ok { + r.checkLabel(from_) + } else if isGopPackageLevel(from) { + r.checkInPackageBlock(from) + } else if v, ok := from.(*types.Var); ok && v.IsField() { + r.checkStructField(v) + } else if f, ok := from.(*types.Func); ok && recv(f) != nil { + r.checkMethod(f) + } else if isGopLocal(from) { + r.checkInLexicalScope(from) + } else { + r.errorf(from.Pos(), "unexpected %s object %q (please report a bug)\n", + objectKind(from), from) + } +} + func (r *renamer) gopCheckExport(id *ast.Ident, pkg *types.Package, from types.Object) bool { // Reject cross-package references if r.to is unexported. // (Such references may be qualified identifiers or field/method diff --git a/gopls/internal/lsp/source/rename_gox.go b/gopls/internal/lsp/source/rename_gox.go index eb9abb9abc9..295d18bf597 100644 --- a/gopls/internal/lsp/source/rename_gox.go +++ b/gopls/internal/lsp/source/rename_gox.go @@ -16,9 +16,11 @@ import ( "github.com/goplus/gop/ast" "github.com/goplus/gop/token" + xlog "github.com/qiniu/x/log" "golang.org/x/tools/go/types/objectpath" "golang.org/x/tools/gop/ast/astutil" "golang.org/x/tools/gopls/internal/bug" + "golang.org/x/tools/gopls/internal/goxls" "golang.org/x/tools/gopls/internal/goxls/parserutil" "golang.org/x/tools/gopls/internal/lsp/protocol" "golang.org/x/tools/gopls/internal/lsp/safetoken" @@ -306,7 +308,7 @@ func gopRenameOrdinary(ctx context.Context, snapshot Snapshot, f FileHandle, pp for obj := range targets { objects = append(objects, obj) } - editMap, _, err := renameObjects(ctx, snapshot, newName, pkg, objects...) + editMap, _, err := gopRenameObjects(ctx, snapshot, newName, pkg, objects...) return editMap, err } @@ -367,6 +369,64 @@ func gopRenameOrdinary(ctx context.Context, snapshot Snapshot, f FileHandle, pp return renameExported(ctx, snapshot, pkgs, declPkgPath, declObjPath, newName) } +// renameObjects computes the edits to the type-checked syntax package pkg +// required to rename a set of target objects to newName. +// +// It also returns the set of objects that were found (due to +// corresponding methods and embedded fields) to require renaming as a +// consequence of the requested renamings. +// +// It returns an error if the renaming would cause a conflict. +func gopRenameObjects(ctx context.Context, snapshot Snapshot, newName string, pkg Package, targets ...types.Object) (map[span.URI][]diff.Edit, map[types.Object]bool, error) { + r := renamer{ + pkg: pkg, + objsToUpdate: make(map[types.Object]bool), + from: targets[0].Name(), + to: newName, + } + + // A renaming initiated at an interface method indicates the + // intention to rename abstract and concrete methods as needed + // to preserve assignability. + // TODO(adonovan): pull this into the caller. + for _, obj := range targets { + if obj, ok := obj.(*types.Func); ok { + recv := obj.Type().(*types.Signature).Recv() + if recv != nil && types.IsInterface(recv.Type().Underlying()) { + r.changeMethods = true + break + } + } + } + + // Check that the renaming of the identifier is ok. + for _, obj := range targets { + if goxls.DbgRename { + xlog.Printf("renameObjects: %T, %s => %s\n", obj, r.from, r.to) + } + r.gopCheck(obj) + if len(r.conflicts) > 0 { + if goxls.DbgRename { + xlog.Println("renameObjects:", r.conflicts) + xlog.SingleStack() + } + // Stop at first error. + return nil, nil, fmt.Errorf("%s", strings.Join(r.conflicts, "\n")) + } + } + + editMap, err := r.update() + if err != nil { + return nil, nil, err + } + + // Remove initial targets so that only 'consequences' remain. + for _, obj := range targets { + delete(r.objsToUpdate, obj) + } + return editMap, r.objsToUpdate, nil +} + // gopRenamePackageName renames package declarations, imports, and go.mod files. func gopRenamePackageName(ctx context.Context, s Snapshot, f FileHandle, newName PackageName) (map[span.URI][]diff.Edit, error) { log.Panicln("todo: Go+ files")