Skip to content

Commit 22edd03

Browse files
authored
Merge pull request #56 from neongreen/copilot/support-moving-types-interfaces
dissect: Add support for moving types and interfaces
2 parents 0e8645c + 0341ef4 commit 22edd03

File tree

12 files changed

+1420
-207
lines changed

12 files changed

+1420
-207
lines changed

dissect/DESIGN.md

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
# dissect Design Document
2+
3+
This document explains the design decisions, implementation approach, and technical details of the `dissect` tool, particularly for the `move` command's support of different Go declaration types.
4+
5+
## Table of Contents
6+
7+
- [Overview](#overview)
8+
- [Implementation Approach](#implementation-approach)
9+
- [Why the Dual Approach](#why-the-dual-approach)
10+
- [gopls Implementation Analysis](#gopls-implementation-analysis)
11+
- [Import Management](#import-management)
12+
- [Known Limitations](#known-limitations)
13+
- [Edge Cases](#edge-cases)
14+
- [Future Improvements](#future-improvements)
15+
16+
## Overview
17+
18+
The `dissect move` command supports moving various Go declarations (functions, types, interfaces, constants, variables) from one file to another. The implementation uses two different approaches depending on the declaration type:
19+
20+
1. **Functions and Methods**: Uses gopls's `refactor.extract.toNewFile` code action
21+
2. **Types, Interfaces, Constants, Variables**: Uses manual AST manipulation
22+
23+
## Implementation Approach
24+
25+
### Functions and Methods (via gopls)
26+
27+
For functions and methods, we leverage gopls's built-in refactoring capability:
28+
29+
```go
30+
// Uses gopls codeaction -kind=refactor.extract.toNewFile
31+
tempFile, err := gopls.ExtractToNewFile(sourceFile, identifier, moduleRoot)
32+
```
33+
34+
**Advantages:**
35+
- Robust, well-tested implementation maintained by the Go team
36+
- Handles complex cases like import management automatically
37+
- Preserves comments and formatting correctly
38+
- Validates refactoring preconditions
39+
40+
**Files:** `pkg/gopls/extract_to_new_file.go`, `cmd/move.go` (moveFunctionWithGopls)
41+
42+
### Types, Interfaces, Constants, Variables (Manual AST)
43+
44+
For other declaration types, we use manual AST manipulation:
45+
46+
```go
47+
// Parse source file, find declaration, serialize it, remove from source, add to target
48+
func moveDeclarationManually(sourceFile, identifier, targetFile string, ...) error
49+
```
50+
51+
**Process:**
52+
1. Parse source file AST
53+
2. Locate the declaration by identifier name
54+
3. Serialize declaration with comments using `go/printer`
55+
4. Remove declaration from source AST
56+
5. Append serialized declaration to target file
57+
6. Run `goimports` on both files to fix imports and formatting
58+
59+
**Files:** `cmd/move.go` (moveDeclarationManually)
60+
61+
## Why the Dual Approach
62+
63+
### gopls Cannot Move Types/Interfaces in Practice
64+
65+
While gopls's implementation **technically supports** extracting types, interfaces, constants, and variables (as seen in `gopls/internal/golang/extracttofile.go`'s `selectedToplevelDecls` function), it **does not offer the code action** for these declaration types in practice.
66+
67+
**Testing confirms this behavior:**
68+
69+
```bash
70+
# Position cursor on a function name
71+
$ gopls codeaction -kind=refactor.extract.toNewFile main.go:5:6
72+
command "Extract declarations to new file" [refactor.extract.toNewFile]
73+
74+
# Position cursor on a type name
75+
$ gopls codeaction -kind=refactor.extract.toNewFile main.go:3:6
76+
# No output - no code action available
77+
```
78+
79+
**Why?** The likely reason is that gopls only triggers the code action when the cursor is positioned on a function's identifier, not on type/interface/const/var identifiers. This appears to be an intentional design choice in gopls's code action provider, possibly to avoid confusion or because the feature wasn't fully productionized for non-function declarations.
80+
81+
## gopls Implementation Analysis
82+
83+
By examining gopls's source code (`gopls/internal/golang/extracttofile.go`), we can see the sophisticated features it provides:
84+
85+
### 1. Import Management
86+
87+
gopls performs detailed import analysis:
88+
89+
```go
90+
// From gopls source
91+
func findImportEdits(file *ast.File, info *types.Info, start, end token.Pos)
92+
(adds, deletes []*ast.ImportSpec, _ error)
93+
```
94+
95+
- Tracks which imports are referenced in extracted vs remaining code
96+
- Adds necessary imports to the new file
97+
- Removes imports from source file that are only used in extracted code
98+
- Handles unparenthesized imports specially (removes entire declaration)
99+
- **Returns error** for dot imports (not supported)
100+
101+
### 2. Comment Association
102+
103+
```go
104+
// Extends selection to include doc comments
105+
if comment != nil && comment.Pos() < start {
106+
start = comment.Pos()
107+
}
108+
```
109+
110+
- Automatically includes doc comments (`decl.Doc`) with declarations
111+
- Handles trailing whitespace after declarations
112+
- Preserves comment positioning and formatting
113+
114+
### 3. Validation
115+
116+
```go
117+
func selectedToplevelDecls(pgf *parsego.File, start, end token.Pos)
118+
(token.Pos, token.Pos, string, bool)
119+
```
120+
121+
- Checks that selection doesn't intersect package declarations
122+
- Validates selection doesn't intersect import statements
123+
- Ensures selection doesn't partially intersect declarations or comments
124+
- Auto-expands selection when only keywords are selected (e.g., just "type" or "const")
125+
126+
### 4. Range Expansion
127+
128+
For better UX, gopls expands the selection intelligently:
129+
130+
```go
131+
// If only selecting keyword "func" or function name, extend to whole function
132+
if posRangeContains(decl.Pos(), decl.Name.End(), start, end) {
133+
start, end = decl.Pos(), decl.End()
134+
}
135+
```
136+
137+
## Import Management
138+
139+
### Current Implementation (via goimports)
140+
141+
Our manual approach relies on `goimports` for import management:
142+
143+
```go
144+
// After moving declaration, run goimports on both files
145+
commands.RunGoimportsOnFile(targetFile)
146+
commands.RunGoimportsOnFile(sourceFile)
147+
```
148+
149+
**How goimports works:**
150+
- Scans code for unqualified identifiers
151+
- Adds missing imports based on stdlib and local packages
152+
- Removes unused imports
153+
- Formats import blocks
154+
155+
**Advantages:**
156+
- Simple, reliable, widely used
157+
- No need to implement complex import resolution logic
158+
- Handles most cases correctly
159+
160+
**Limitations compared to gopls:**
161+
- Less precise - may add imports that could be avoided
162+
- Doesn't validate dot imports upfront
163+
- No explicit control over which imports move vs stay
164+
165+
## Known Limitations
166+
167+
### 1. Dot Imports
168+
169+
**Issue:** Files with dot imports (e.g., `import . "fmt"`) are processed without upfront validation.
170+
171+
**Behavior:** The move operation succeeds, but goimports won't add dot imports to the target file (it uses standard imports instead). This usually works but changes the code style.
172+
173+
**Example:**
174+
```go
175+
// Before move
176+
import . "fmt"
177+
Println("hello") // Uses dot-imported identifier
178+
179+
// After move (if type is moved)
180+
// Target file gets: import "fmt"
181+
// Not: import . "fmt"
182+
```
183+
184+
**gopls behavior:** Returns explicit error: `"extract to new file" does not support files containing dot imports`
185+
186+
### 2. Grouped Declarations
187+
188+
**Issue:** When moving one identifier from a grouped declaration, the entire group moves.
189+
190+
**Why:** In Go's AST, grouped declarations are a single `*ast.GenDecl` node:
191+
192+
```go
193+
const (
194+
Red = "red" // \
195+
Blue = "blue" // > Single GenDecl with multiple ValueSpecs
196+
Green = "green" // /
197+
)
198+
```
199+
200+
**Behavior:** Moving `Blue` moves all three constants. This is **by design** and documented in tests.
201+
202+
### 3. Type and Method Separation
203+
204+
**Issue:** Moving a type doesn't move its methods; they stay in the source file.
205+
206+
**Why:** Methods are separate `*ast.FuncDecl` nodes in the AST, not part of the type declaration.
207+
208+
**Behavior:** This is **valid Go** - methods can be in different files from their types. The code continues to build correctly.
209+
210+
**Example:**
211+
```go
212+
// Before
213+
type User struct { Name string }
214+
func (u User) SayHello() { ... }
215+
216+
// After moving User
217+
// target.go: type User struct { Name string }
218+
// source.go: func (u User) SayHello() { ... } // Still valid!
219+
```
220+
221+
### 4. No Partial Declaration Selection
222+
223+
**Issue:** Unlike gopls, we don't validate that operations aren't being performed on partial selections.
224+
225+
**Example:** If a user tries to move only part of a multi-declaration block, the entire block moves (as explained in #2).
226+
227+
**gopls behavior:** Validates and rejects partial selections.
228+
229+
### 5. Cross-file Type Dependencies
230+
231+
**Issue:** If Type A references Type B and both are in the same file, moving only Type A could require Type B in the target file.
232+
233+
**Mitigation:** `goimports` won't add Type B (it's not an import), so this will cause a build error, alerting the user to move both types.
234+
235+
## Edge Cases
236+
237+
The following edge cases are tested in `cmd/move_edge_cases_test.go`:
238+
239+
### 1. Grouped Constants/Variables
240+
241+
```go
242+
const (
243+
A = 1
244+
B = 2
245+
)
246+
// Moving B moves entire const block
247+
```
248+
249+
**Test:** `TestMoveEdgeCases/GroupedConstantsBehavior`
250+
251+
### 2. Type Without Methods
252+
253+
```go
254+
type User struct { Name string }
255+
func (u User) Method() {}
256+
// Moving User leaves Method in source (valid Go)
257+
```
258+
259+
**Test:** `TestMoveEdgeCases/TypeWithMethodsSeparation`
260+
261+
### 3. Import Management
262+
263+
```go
264+
import "time"
265+
type Config struct { Timeout time.Duration }
266+
// After move: target gets time import, source loses it
267+
```
268+
269+
**Test:** `TestMoveEdgeCases/ImportManagementWithTypes`
270+
271+
### 4. Dot Imports
272+
273+
```go
274+
import . "fmt"
275+
type T struct{}
276+
// After move: target doesn't get dot import (uses standard import)
277+
```
278+
279+
**Test:** `TestMoveEdgeCases/DotImportHandling`
280+
281+
## Future Improvements
282+
283+
### High Priority
284+
285+
1. **Dot Import Validation**
286+
- Add upfront check for dot imports
287+
- Return clear error message like gopls does
288+
- Prevents silent behavior changes
289+
290+
2. **Method Co-location Warning**
291+
- Detect when moving a type that has methods
292+
- Warn user that methods won't be moved
293+
- Suggest moving methods too
294+
295+
3. **Grouped Declaration Awareness**
296+
- Detect when identifier is part of grouped declaration
297+
- Warn user that entire group will move
298+
- Option to split group first?
299+
300+
### Medium Priority
301+
302+
4. **Partial Selection Validation**
303+
- Validate selection boundaries
304+
- Ensure complete declarations are selected
305+
- Return clear error for invalid selections
306+
307+
5. **Import Precision**
308+
- Track which imports are actually used in moved code
309+
- Only move necessary imports (like gopls does)
310+
- More precise than goimports
311+
312+
### Low Priority
313+
314+
6. **Comment Handling Enhancement**
315+
- Better handling of inline comments
316+
- Preserve comment positions more precisely
317+
- Handle comment groups better
318+
319+
7. **Range Expansion**
320+
- Auto-expand selection when only keyword is selected
321+
- Better UX for command-line usage
322+
- Match gopls behavior
323+
324+
## References
325+
326+
- gopls source: `golang.org/x/tools/gopls/internal/golang/extracttofile.go`
327+
- Go AST documentation: `go/ast` package
328+
- goimports: `golang.org/x/tools/cmd/goimports`
329+
330+
## See Also
331+
332+
- [README.md](README.md) - Usage documentation
333+
- [ARCHITECTURE.md](ARCHITECTURE.md) - System architecture
334+
- [TODO.md](TODO.md) - Known issues and future work

0 commit comments

Comments
 (0)