gopls/internal/golang: MoveType - extract type to new file
Implements computing the edits to a new file after
adding the type declaration, imports, and build constraint comments.
Also deletes the type declaration from its current location.
Adds several checks for currently illegal type moves, which may be
relaxed later in the implementation.
diff --git a/gopls/internal/golang/codeaction.go b/gopls/internal/golang/codeaction.go
index d09fc47..a38a4e0 100644
--- a/gopls/internal/golang/codeaction.go
+++ b/gopls/internal/golang/codeaction.go
@@ -255,7 +255,7 @@
{kind: settings.RefactorExtractVariableAll, fn: refactorExtractVariableAll, needPkg: true},
{kind: settings.RefactorInlineCall, fn: refactorInlineCall, needPkg: true},
{kind: settings.RefactorInlineVariable, fn: refactorInlineVariable, needPkg: true},
- // {kind: settings.RefactorMoveType, fn: refactorMoveType, needPkg: true},
+ {kind: settings.RefactorMoveType, fn: refactorMoveType, needPkg: true},
{kind: settings.RefactorRewriteChangeQuote, fn: refactorRewriteChangeQuote},
{kind: settings.RefactorRewriteFillStruct, fn: refactorRewriteFillStruct, needPkg: true},
{kind: settings.RefactorRewriteFillSwitch, fn: refactorRewriteFillSwitch, needPkg: true},
@@ -1136,7 +1136,7 @@
func refactorMoveType(ctx context.Context, req *codeActionsRequest) error {
curSel, _ := req.pgf.Cursor.FindByPos(req.start, req.end)
- if _, _, _, typeName, ok := selectionContainsType(curSel); ok {
+ if _, _, _, typeName, ok := selectionContainsTypeDecl(curSel); ok {
cmd := command.NewMoveTypeCommand(fmt.Sprintf("Move type %s", typeName), command.MoveTypeArgs{Location: req.loc})
req.addCommandAction(cmd, false)
}
diff --git a/gopls/internal/golang/extracttofile.go b/gopls/internal/golang/extracttofile.go
index 6aef663..a07c8f5 100644
--- a/gopls/internal/golang/extracttofile.go
+++ b/gopls/internal/golang/extracttofile.go
@@ -135,54 +135,11 @@
return nil, err
}
- var importDeletes []protocol.TextEdit
- // For unparenthesised declarations like `import "fmt"` we remove
- // the whole declaration because simply removing importSpec leaves
- // `import \n`, which does not compile.
- // For parenthesised declarations like `import ("fmt"\n "log")`
- // we only remove the ImportSpec, because removing the whole declaration
- // might remove other ImportsSpecs we don't want to touch.
- unparenthesizedImports := unparenthesizedImports(pgf)
- for _, importSpec := range deletes {
- if decl := unparenthesizedImports[importSpec]; decl != nil {
- importDeletes = append(importDeletes, removeNode(pgf, decl))
- } else {
- importDeletes = append(importDeletes, removeNode(pgf, importSpec))
- }
- }
+ importDeletes := importDeletes(pgf, deletes)
- var buf bytes.Buffer
- if c := CopyrightComment(pgf.File); c != nil {
- text, err := pgf.NodeText(c)
- if err != nil {
- return nil, err
- }
- buf.Write(text)
- // One empty line between copyright header and following.
- buf.WriteString("\n\n")
- }
-
- if c := buildConstraintComment(pgf.File); c != nil {
- text, err := pgf.NodeText(c)
- if err != nil {
- return nil, err
- }
- buf.Write(text)
- // One empty line between build constraint and following.
- buf.WriteString("\n\n")
- }
-
- fmt.Fprintf(&buf, "package %s\n", pgf.File.Name.Name)
- if len(adds) > 0 {
- buf.WriteString("import (")
- for _, importSpec := range adds {
- if importSpec.Name != nil {
- fmt.Fprintf(&buf, "%s %s\n", importSpec.Name.Name, importSpec.Path.Value)
- } else {
- fmt.Fprintf(&buf, "%s\n", importSpec.Path.Value)
- }
- }
- buf.WriteString(")\n")
+ buf, err := newFileContentFromImports(pgf, adds, pgf.File.Name.Name)
+ if err != nil {
+ return nil, err
}
newFile, err := chooseNewFile(ctx, snapshot, pgf.URI.DirPath(), firstSymbol)
@@ -208,6 +165,67 @@
})}, nil
}
+// importDeletes returns the text edits necessary to delete the given imports
+// from the given file.
+func importDeletes(pgf *parsego.File, deletes []*ast.ImportSpec) []protocol.TextEdit {
+ var importDeletes []protocol.TextEdit
+ // For unparenthesised declarations like `import "fmt"` we remove
+ // the whole declaration because simply removing importSpec leaves
+ // `import \n`, which does not compile.
+ // For parenthesised declarations like `import ("fmt"\n "log")`
+ // we only remove the ImportSpec, because removing the whole declaration
+ // might remove other ImportsSpecs we don't want to touch.
+ unparenthesizedImports := unparenthesizedImports(pgf)
+ for _, importSpec := range deletes {
+ if decl := unparenthesizedImports[importSpec]; decl != nil {
+ importDeletes = append(importDeletes, removeNode(pgf, decl))
+ } else {
+ importDeletes = append(importDeletes, removeNode(pgf, importSpec))
+ }
+ }
+ return importDeletes
+}
+
+// newFileContentFromImports returns the unformatted file content resulting from adding the
+// imports in the given pgf to a new file, plus copyright and build constraint
+// comments.
+func newFileContentFromImports(pgf *parsego.File, adds []*ast.ImportSpec, pkgName string) (bytes.Buffer, error) {
+ var buf bytes.Buffer
+ if c := CopyrightComment(pgf.File); c != nil {
+ text, err := pgf.NodeText(c)
+ if err != nil {
+ return bytes.Buffer{}, err
+ }
+ buf.Write(text)
+ // One empty line between copyright header and following.
+ buf.WriteString("\n\n")
+ }
+
+ if c := buildConstraintComment(pgf.File); c != nil {
+ text, err := pgf.NodeText(c)
+ if err != nil {
+ return bytes.Buffer{}, err
+ }
+ buf.Write(text)
+ // One empty line between build constraint and following.
+ buf.WriteString("\n\n")
+ }
+
+ fmt.Fprintf(&buf, "package %s\n", pkgName)
+ if len(adds) > 0 {
+ buf.WriteString("import (")
+ for _, importSpec := range adds {
+ if importSpec.Name != nil {
+ fmt.Fprintf(&buf, "%s %s\n", importSpec.Name.Name, importSpec.Path.Value)
+ } else {
+ fmt.Fprintf(&buf, "%s\n", importSpec.Path.Value)
+ }
+ }
+ buf.WriteString(")\n")
+ }
+ return buf, nil
+}
+
// chooseNewFile chooses a new filename in dir, based on the name of the
// first extracted symbol, and if necessary to disambiguate, a numeric suffix.
func chooseNewFile(ctx context.Context, snapshot *cache.Snapshot, dir string, firstSymbol string) (file.Handle, error) {
diff --git a/gopls/internal/golang/movetype.go b/gopls/internal/golang/movetype.go
index 3ea8ece..379a313 100644
--- a/gopls/internal/golang/movetype.go
+++ b/gopls/internal/golang/movetype.go
@@ -8,10 +8,14 @@
"context"
"fmt"
"go/ast"
+ "go/format"
"go/token"
+ "go/types"
+ "path/filepath"
"golang.org/x/tools/go/ast/inspector"
"golang.org/x/tools/gopls/internal/cache"
+ "golang.org/x/tools/gopls/internal/cache/parsego"
"golang.org/x/tools/gopls/internal/file"
"golang.org/x/tools/gopls/internal/protocol"
"golang.org/x/tools/internal/moreiters"
@@ -19,12 +23,93 @@
// MoveType moves the selected type declaration into a new package and updates all references.
func MoveType(ctx context.Context, fh file.Handle, snapshot *cache.Snapshot, loc protocol.Location, newPkgDir string) ([]protocol.DocumentChange, error) {
- return nil, fmt.Errorf("MoveType: not yet supported")
+ pkg, pgf, err := NarrowestPackageForFile(ctx, snapshot, fh.URI())
+ if err != nil {
+ return nil, err
+ }
+
+ start, end, err := pgf.RangePos(loc.Range)
+ if err != nil {
+ return nil, err
+ }
+
+ curSel, ok := pgf.Cursor.FindByPos(start, end)
+ if !ok {
+ return nil, err
+ }
+ _, decl, typS, _, _ := selectionContainsTypeDecl(curSel) // we also check this condition before providing the move type codeaction
+
+ if err := canMoveType(pkg.TypesInfo(), typS); err != nil {
+ return nil, err
+ }
+
+ changes, err := addTypeToPackage(ctx, snapshot, pgf, pkg, pkg.TypesInfo(), decl, newPkgDir, filepath.Base(newPkgDir))
+ if err != nil {
+ return nil, err
+ }
+
+ // Get the range to delete the type decl from the current file including any
+ // comments associated with it.
+ declStart, declEnd := decl.Pos(), decl.End()
+ if decl.Doc != nil {
+ declStart = decl.Doc.Pos()
+ }
+ typeDeclRange, err := pgf.PosRange(declStart, declEnd)
+ if err != nil {
+ return nil, fmt.Errorf("invalid range: %v", err)
+ }
+
+ // Delete the old type declaration.
+ changes = append(changes, protocol.DocumentChangeEdit(fh, []protocol.TextEdit{
+ {Range: typeDeclRange, NewText: ""},
+ }))
+
+ return changes, nil
}
-// selectionContainsType returns the Cursor, GenDecl and TypeSpec of the type
+// addTypeToPackage returns the document changes necessary to add the type to a new file in the given package.
+// It only produces edits for the new file.
+func addTypeToPackage(ctx context.Context, snapshot *cache.Snapshot, pgf *parsego.File, pkg *cache.Package, info *types.Info, decl *ast.GenDecl, pkgDir, pkgName string) ([]protocol.DocumentChange, error) {
+ adds, _, err := findImportEdits(pgf.File, info, decl.Pos(), decl.End())
+ if err != nil {
+ return nil, fmt.Errorf("error calculating import edits: %v", err)
+ }
+
+ buf, err := newFileContentFromImports(pgf, adds, pkgName)
+ if err != nil {
+ return nil, fmt.Errorf("error getting new file content: %v", err)
+ }
+
+ // Add the type decl to the new file content.
+ err = format.Node(&buf, pkg.FileSet(), decl)
+ if err != nil {
+ return nil, fmt.Errorf("error adding type decl to new file: %v", err)
+ }
+
+ // Format the file content.
+ newFileContent, err := format.Source(buf.Bytes())
+ if err != nil {
+ return nil, fmt.Errorf("error formatting new file content: %v", err)
+ }
+
+ newFile, err := chooseNewFile(ctx, snapshot, pkgDir, pkgName)
+ if err != nil {
+ return nil, err
+ }
+
+ return []protocol.DocumentChange{
+ // Create the new file.
+ protocol.DocumentChangeCreate(newFile.URI()),
+ // Edit the created file.
+ protocol.DocumentChangeEdit(newFile, []protocol.TextEdit{
+ {Range: protocol.Range{}, NewText: string(newFileContent)},
+ }),
+ }, nil
+}
+
+// selectionContainsTypeDecl returns the Cursor, GenDecl and TypeSpec of the type
// declaration that encloses cursor if one exists. Otherwise it returns false.
-func selectionContainsType(cursor inspector.Cursor) (inspector.Cursor, *ast.GenDecl, *ast.TypeSpec, string, bool) {
+func selectionContainsTypeDecl(cursor inspector.Cursor) (inspector.Cursor, *ast.GenDecl, *ast.TypeSpec, string, bool) {
declCur, ok := moreiters.First(cursor.Enclosing((*ast.GenDecl)(nil)))
if !ok {
return inspector.Cursor{}, &ast.GenDecl{}, &ast.TypeSpec{}, "", false
@@ -44,3 +129,36 @@
return declCur, declNode, declNode.Specs[0].(*ast.TypeSpec), typSpec.Name.Name, true
}
+
+// canMoveType reports if it is valid to move the input type spec to a new package, returning
+// an error if invalid.
+func canMoveType(info *types.Info, typ *ast.TypeSpec) error {
+ if !typ.Name.IsExported() {
+ // TODO(mkalil): have an option to turn unexported type into exported type
+ return fmt.Errorf("cannot move type: type is not exported")
+ }
+ typObj := info.Defs[typ.Name]
+ if typExp, ok := typ.Type.(*ast.StructType); ok {
+ for _, field := range typExp.Fields.List {
+
+ for _, name := range field.Names {
+ // TODO(mkalil): check if this field is accessed. What if they are moving the
+ // type before using it? We can have an option to turn unexported type fields into
+ // exported fields.
+ if !name.IsExported() {
+ return fmt.Errorf("cannot move type: would result in reference to unexported fields")
+ }
+ }
+
+ // TODO(mkalil): How should we handle moving types that have fields which are
+ // types defined at the package level? Should we also move those types to the
+ // new package, recursively? (If we don't, it could cause an import cycle).
+ // Or throw an error?
+ fieldType := info.TypeOf(field.Type)
+ if fieldObj, ok := fieldType.(*types.Named); ok && fieldObj.Obj().Pkg() == typObj.Pkg() {
+ return fmt.Errorf("cannot move type: would result in import cycle")
+ }
+ }
+ }
+ return nil
+}
diff --git a/gopls/internal/server/command.go b/gopls/internal/server/command.go
index 7e62ab5..576a035 100644
--- a/gopls/internal/server/command.go
+++ b/gopls/internal/server/command.go
@@ -1897,7 +1897,7 @@
err := c.run(ctx, commandConfig{
forURI: args.Location.URI,
}, func(ctx context.Context, deps commandDeps) error {
- changes, err := golang.MoveType(ctx, deps.fh, deps.snapshot, args.Location, "newpkg/new.go")
+ changes, err := golang.MoveType(ctx, deps.fh, deps.snapshot, args.Location, "newpkg/new")
if err != nil {
return err
}
diff --git a/gopls/internal/settings/default.go b/gopls/internal/settings/default.go
index f197ad2..612d95d 100644
--- a/gopls/internal/settings/default.go
+++ b/gopls/internal/settings/default.go
@@ -71,7 +71,7 @@
RefactorExtractVariable: true,
RefactorExtractVariableAll: true,
RefactorExtractToNewFile: true,
- RefactorMoveType: true, // off while implementation unfinished
+ RefactorMoveType: true,
// Not GoTest: it must be explicit in CodeActionParams.Context.Only
},
file.Mod: {
diff --git a/gopls/internal/test/marker/testdata/codeaction/movetype.txt b/gopls/internal/test/marker/testdata/codeaction/movetype.txt
new file mode 100644
index 0000000..54be1fa
--- /dev/null
+++ b/gopls/internal/test/marker/testdata/codeaction/movetype.txt
@@ -0,0 +1,35 @@
+This test checks the behavior of the 'move type' code action.
+
+-- flags --
+-ignore_extra_diags
+
+-- go.mod --
+module example.com
+
+-- a/a.go --
+package a
+
+import (
+ "bytes"
+ "context"
+)
+
+type a struct { //@codeaction("a", "refactor.move.moveType", err=re"type is not exported")
+ Buf bytes.Buffer
+ Ctx context.Context
+}
+
+type A struct { //@codeaction("A", "refactor.move.moveType", err=re"unexported fields")
+ buf bytes.Buffer
+ ctx context.Context
+}
+
+type B struct { //@codeaction("B", "refactor.move.moveType", err=re"import cycle")
+ C
+}
+
+type BB struct { //@codeaction("B", "refactor.move.moveType", err=re"import cycle")
+ Field C
+}
+
+type C string
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
I was hoping there would be a way to enable the code action in the marker tests but disable producing the code action in real requests until it's fully implemented but could not figure this out.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
I was hoping there would be a way to enable the code action in the marker tests but disable producing the code action in real requests until it's fully implemented but could not figure this out.
Nevermind - added a hacky "testing.Testing()" to disable the code action when not inside a test.
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
Solid start. Many comments but mostly superficial, and the big stuff can be in a follow-up CL.
func importDeletes(pgf *parsego.File, deletes []*ast.ImportSpec) []protocol.TextEdit {How about 'importDeleteEdits'
// imports in the given pgf to a new file, plus copyright and build constraintIs this all the imports in the given file? Looks like it's only a fixed slice. Perhaps this comment is stale?
func newFileContentFromImports(pgf *parsego.File, adds []*ast.ImportSpec, pkgName string) (bytes.Buffer, error) {Document this.
Also, how about rearranging the arguments a bit:
```
// newFileWithImports creates a new file with the given package name and imports
// as well as copyright comment and build constraints matching fromPGF.
//
// It returns a buffer containing the file content, or an error.
func newFileWithImports(pkgName string, imports []*ast.ImportSpec, fromPGF *parsego.File) (bytes.Buffer, error)
```
func newFileContentFromImports(pgf *parsego.File, adds []*ast.ImportSpec, pkgName string) (bytes.Buffer, error) {How about 'newFileWithImports'?
// One empty line between copyright header and following."after the copyright header."
buf.WriteString(")\n")Nit: should we add a trailing newline? What should the state of the buffer be, exactly (let's also document this in the doc comment).
_, decl, typS, _, _ := selectionContainsTypeDecl(curSel) // we also check this condition before providing the move type codeactionShouldn't we still check 'ok' here? If !ok, none of the other results are meaningful.
// Get the range to delete the type from the current file. If the type specThe asymmetry of this bothers me: if we're able to handle moving an individual spec, not just an entire decl, then why don't we check selectionContainsTypeSpec, instead of looking at whole decls.
Or maybe I'm missing something.
// TODO(mkalil): comments inside struct types are lost, i.e:
// type A struct { // this comment gets lost.
// B int
// }Looks like a case for CommentMap and CommentedNode!
func addTypeToPackage(ctx context.Context, snapshot *cache.Snapshot, pgf *parsego.File, pkg *cache.Package, info *types.Info, spec *ast.TypeSpec, pkgDir, pkgName string) ([]protocol.DocumentChange, error) {If you pass in a CommentMap, you can create a CommentedNode for formatting, and preserve inline comments.
typSpec, ok := declNode.Specs[0].(*ast.TypeSpec)
if !ok {
return inspector.Cursor{}, &ast.GenDecl{}, &ast.TypeSpec{}, "", false
}This looks wrong--shouldn't we find the best matching spec?
(I know we discussed what to do here, but this has the following unintuitive behavior, IIUC)
```
type (
A int
B string // <- cursor here
)
```
With the cursor on B, the move will actually move A!
func canMoveType(info *types.Info, typ *ast.TypeSpec) error {What about methods on the type? We need to move them as well.
...and then we should allow moving the type if the unexported fields are only accessed from its own methods.
Perhaps add a big TODO explaining the plan.
// TODO(mkalil): check if this field is accessed. What if they are moving the
// type before using it? We can have an option to turn unexported type fields into+1, nice call out
Add a test, and then check the provided info: should be straightforward.
(For another CL).
return fmt.Errorf("cannot move type: would result in import cycle")s/would/could 😊
We don't know without actually doing the analysis!
// RefactorMoveType is enabled for testing but disabled otherwise.
// This is temporary while the implementation of move type refactoring is unfinished.
if !testing.Testing() && kind == settings.RefactorMoveType {
return false
}Prefer doing this with an internal gopls setting. WDYT?
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
| Commit-Queue | +1 |
func importDeletes(pgf *parsego.File, deletes []*ast.ImportSpec) []protocol.TextEdit {Madeline KalilHow about 'importDeleteEdits'
Done
// imports in the given pgf to a new file, plus copyright and build constraintIs this all the imports in the given file? Looks like it's only a fixed slice. Perhaps this comment is stale?
Updated comment
func newFileContentFromImports(pgf *parsego.File, adds []*ast.ImportSpec, pkgName string) (bytes.Buffer, error) {Document this.
Also, how about rearranging the arguments a bit:
```
// newFileWithImports creates a new file with the given package name and imports
// as well as copyright comment and build constraints matching fromPGF.
//
// It returns a buffer containing the file content, or an error.
func newFileWithImports(pkgName string, imports []*ast.ImportSpec, fromPGF *parsego.File) (bytes.Buffer, error)
```
Done
func newFileContentFromImports(pgf *parsego.File, adds []*ast.ImportSpec, pkgName string) (bytes.Buffer, error) {Madeline KalilHow about 'newFileWithImports'?
Done
// One empty line between copyright header and following.Madeline Kalil"after the copyright header."
Done
Nit: should we add a trailing newline? What should the state of the buffer be, exactly (let's also document this in the doc comment).
This was preexisting, so I'm not sure of the original reasoning, but when I comment this line out and run all the tests, a few fail. I'll update the doc comment to specify that we add a new line after the import block.
_, decl, typS, _, _ := selectionContainsTypeDecl(curSel) // we also check this condition before providing the move type codeactionShouldn't we still check 'ok' here? If !ok, none of the other results are meaningful.
We should be able to assume ok is true here, because we check selectionContainsTypeDecl before providing the codeaction that calls this command.
// Get the range to delete the type from the current file. If the type specThe asymmetry of this bothers me: if we're able to handle moving an individual spec, not just an entire decl, then why don't we check selectionContainsTypeSpec, instead of looking at whole decls.
Or maybe I'm missing something.
Yeah, realizing this logic is messed up. Fixing and renaming. (But we still need the GenDecl node since if the type spec in question is the only one in the type decl, we should remove the entire decl)
// TODO(mkalil): comments inside struct types are lost, i.e:
// type A struct { // this comment gets lost.
// B int
// }Looks like a case for CommentMap and CommentedNode!
Done
func addTypeToPackage(ctx context.Context, snapshot *cache.Snapshot, pgf *parsego.File, pkg *cache.Package, info *types.Info, spec *ast.TypeSpec, pkgDir, pkgName string) ([]protocol.DocumentChange, error) {If you pass in a CommentMap, you can create a CommentedNode for formatting, and preserve inline comments.
Fixed this particular comment preservation issue, but now comments above type specs are formatted strangely. I left a TODO
typSpec, ok := declNode.Specs[0].(*ast.TypeSpec)
if !ok {
return inspector.Cursor{}, &ast.GenDecl{}, &ast.TypeSpec{}, "", false
}This looks wrong--shouldn't we find the best matching spec?
(I know we discussed what to do here, but this has the following unintuitive behavior, IIUC)
```
type (
A int
B string // <- cursor here
)
```With the cursor on B, the move will actually move A!
Fixed and added a test case
func canMoveType(info *types.Info, typ *ast.TypeSpec) error {What about methods on the type? We need to move them as well.
...and then we should allow moving the type if the unexported fields are only accessed from its own methods.
Perhaps add a big TODO explaining the plan.
Done
return fmt.Errorf("cannot move type: would result in import cycle")s/would/could 😊
We don't know without actually doing the analysis!
True, thanks!
// RefactorMoveType is enabled for testing but disabled otherwise.
// This is temporary while the implementation of move type refactoring is unfinished.
if !testing.Testing() && kind == settings.RefactorMoveType {
return false
}Prefer doing this with an internal gopls setting. WDYT?
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |
gopls/internal/golang: MoveType - extract type to new fileDoes this always move the type to a new package? I was confused because the extract logic appears to move it to a new file in the same package (based on the package declaration), but the command handler seems to create the file name of a new directory.
importDeletes := importDeletesEdits(pgf, deletes)
The combination of importDeletesEdits and format.Source is now provided by driverutil.FormatSourceRemoveImports. This should make things easier in findImportEdits and eliminate this step.
(I should probably move it out of driverutil, but it's a legitimate dependency of the golang package.)
newFile, err := chooseNewFile(ctx, snapshot, pgf.URI.DirPath(), firstSymbol)
if err != nil {
return nil, fmt.Errorf("%s: %w", errorPrefix, err)
}
Move this down so the buf-related stuff stays together.
func importDeletesEdits(pgf *parsego.File, deletes []*ast.ImportSpec) []protocol.TextEdit {See above; I think this is no longer needed.
func newFileWithImports(pkgName string, imports []*ast.ImportSpec, fromPGF *parsego.File) (bytes.Buffer, error) {*bytes.Buffer
(Values of this type, and indeed most opaque types T with *T methods, should not be copied.)
But why not have the caller provide the buffer? That's usually more efficient since there is often stuff to be written to it before and after.
fmt.Fprintf(&buf, "package %s\n", pkgName) fmt.Fprintf(&buf, "package %s\n", pkgName)\n\n
for _, importSpec := range imports {spec (for brevity)
if importSpec.Name != nil {
fmt.Fprintf(&buf, "%s %s\n", importSpec.Name.Name, importSpec.Path.Value)
} else {
fmt.Fprintf(&buf, "%s\n", importSpec.Path.Value)
}Factor the Path parts.
curSel, ok := pgf.Cursor.FindByPos(start, end)astutil.Select may be more appropriate here; it handles whitespace more permissively.
_, decl, typS, _, _ := selectionContainsTypeSpec(curSel) // we also check this condition before providing the move type codeactionIs decl just the parent of spec? If so, return a cursor and let the caller call Parent.
_, decl, typS, _, _ := selectionContainsTypeSpec(curSel) // we also check this condition before providing the move type codeactionspec
changes, err := addTypeToPackage(ctx, snapshot, pgf, pkg, pkg.TypesInfo(), typS, decl, newDir, filepath.Base(newDir))This is redundant w.r.t pkg.
changes, err := addTypeToPackage(ctx, snapshot, pgf, pkg, pkg.TypesInfo(), typS, decl, newDir, filepath.Base(newDir))Ditto w.r.t. newDir.
}This is a recurring pattern. The common parts could be factored as:
```
var n Node = spec
if len(decl.Specs) == 1 {
n = decl // singleton: delete entire decl
}
start, end = n.Pos(), n.End()
if doc := astutil.DocComment(n); doc != nil {
start = doc.Pos() // include doc comment
}
if c := astutil.PostComment(n); c != nil {
end = c.End() // include inline comment
}
```
However, only DocComment exists; you'll need to define PostComment. We could use it in dozens of places.
typeDeclRange, err := pgf.PosRange(typStart, typEnd)rng
return nil, fmt.Errorf("invalid range: %v", err)err is fine.
// Use CommentedNode to avoid losing comments inside struct types
// that are neither doc comments nor line comments.
// type A struct { // this comment gets lost.
// B int
// }
// TODO(mkalil): Now these comments are preserved but
// comments above a type spec are formatted in an undesirable way.
// (See @multitypespecB/d/newpkg/b.go in movetype.txt)
var declComments []*ast.CommentGroup
if len(decl.Specs) == 1 {
for _, comment := range pgf.File.Comments {
if decl.Pos() <= comment.Pos() && comment.End() <= decl.End() {
declComments = append(declComments, comment)
}
}
}
// Add the type decl to the new file content.
newDecl := &ast.GenDecl{
Tok: token.TYPE,
Specs: []ast.Spec{spec},
}
commentedNode := &printer.CommentedNode{
Node: newDecl,
Comments: declComments,
}
err = format.Node(&buf, pkg.FileSet(), commentedNode)
if err != nil {
return nil, fmt.Errorf("error adding type decl to new file: %v", err)
}
It might be simpler here to copy `pgf.NodeText(spec or decl)` directly into buf. Then you don't to think about comments or format.Source.
specCur, ok := moreiters.First(cursor.Enclosing((*ast.TypeSpec)(nil)))```
spec, curSpec := cursorutil.FirstEnclosing[*ast.TypeSpec](cur)
if spec == nil { ... }
```
then delete L170,
return inspector.Cursor{}, &ast.GenDecl{}, &ast.TypeSpec{}, "", falsenil, nil
(throughout)
declNode := declCur.Node().(*ast.GenDecl)decl
typSpec, _ := specCur.Node().(*ast.TypeSpec) // can't faildelete (since it can't fail)
return declCur, declNode, typSpec, typSpec.Name.Name, trueThis seems redundant w.r.t. type typSpec result.
// TODO(mkalil): For now, we only allow moving a type if the type itself and all itsWe also need to check that:
if typExp, ok := typ.Type.(*ast.StructType); ok {
for _, field := range typExp.Fields.List {
for _, name := range field.Names {This kind of check, which relates to symbol references, is best done using the type checker's data structures.
// TODO(mkalil): How should we handle moving types that have fields which are
// types defined at the package level? Should we also move those types to the
// new package, recursively? (If we don't, it could cause an import cycle).
// Or throw an error?We should probably attempt to move them recursively (applying the same validity checks). If the user didn't want that, they can always undo, but there's no good way to ask the user "move type T2 too?" (yet).
// RefactorMoveType is enabled for testing but disabled otherwise.
// This is temporary while the implementation of move type refactoring is unfinished.
if kind == settings.RefactorMoveType && !snapshot.Options().MoveType {
return false
}I wonder why this option is needed. How many steps are there to finishing the feature? Is it too much for this CL, or a small stack that can be merged in the same day or two?
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |