|
| 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