Skip to content

Commit 5f75105

Browse files
authored
Merge pull request #49 from neongreen/copilot/fix-comments-preservation
Fix comment preservation when extracting functions with `dissect move`
2 parents 9b6f18a + 3b59539 commit 5f75105

File tree

5 files changed

+962
-21
lines changed

5 files changed

+962
-21
lines changed

dissect/AGENTS.md

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
# Agent Guidelines for Dissect
2+
3+
## Code Manipulation Requirements
4+
5+
### AST-Based Manipulation Only
6+
7+
**All Go code manipulation in the dissect tool MUST use AST (Abstract Syntax Tree) operations.**
8+
9+
### Prohibited Approaches
10+
11+
**String manipulation of Go code is strictly prohibited**
12+
13+
This includes but is not limited to:
14+
- String slicing to extract code snippets
15+
- String concatenation to build Go code
16+
- Text-based search and replace on Go source
17+
- Regex operations on Go source code
18+
- Manual text parsing of Go syntax
19+
20+
### Required Approaches
21+
22+
**AST-based operations are required**
23+
24+
All code manipulation must use the Go AST packages:
25+
- `go/ast` - Abstract syntax tree representation
26+
- `go/parser` - Parsing Go source to AST
27+
- `go/token` - Position information and file sets
28+
- `go/format` - Formatting AST back to source
29+
- `go/printer` - Printing AST nodes
30+
31+
### Rationale
32+
33+
String-based manipulation of Go code is fragile and error-prone because it:
34+
35+
1. **Cannot handle syntax variations**: Go allows multiple valid formatting styles. String matching fails when code is formatted differently.
36+
37+
2. **Breaks with whitespace changes**: String slicing by position breaks when whitespace, comments, or formatting changes.
38+
39+
3. **Cannot preserve semantic information**: Type information, scope, and other semantic data are lost.
40+
41+
4. **Fails on edge cases**: Build tags, compiler directives, and other Go features require proper parsing.
42+
43+
5. **Cannot properly handle comments**: Comments in Go have complex relationships with declarations that only AST preserves correctly.
44+
45+
### Examples
46+
47+
**Prohibited** - String slicing:
48+
```go
49+
content, _ := os.ReadFile(file)
50+
funcText := string(content[start:end]) // String manipulation!
51+
```
52+
53+
**Required** - AST manipulation:
54+
```go
55+
fset := token.NewFileSet()
56+
node, _ := parser.ParseFile(fset, file, nil, parser.ParseComments)
57+
// Work with node.Decls, node.Comments, etc.
58+
format.Node(output, fset, node)
59+
```
60+
61+
### When to Use gopls
62+
63+
The `gopls` language server can be used for complex refactoring operations, but:
64+
- The results must still be processed using AST operations
65+
- Do not extract string snippets from gopls output
66+
- Parse gopls results with `go/parser` and manipulate the resulting AST
67+
68+
### Exception
69+
70+
The only exception is when dealing with non-Go files or when the output is for display purposes only (logging, error messages, etc.).

dissect/cmd/move.go

Lines changed: 44 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
package main
22

33
import (
4+
"bytes"
45
"dissect/pkg/commands"
56
"dissect/pkg/gopls"
67
"dissect/pkg/goutils"
78
"fmt"
89
"go/ast"
10+
"go/printer"
911
"log/slog"
1012
"os"
1113
"path/filepath"
@@ -156,18 +158,12 @@ func moveIdentifier(sourceFile string, identifier string, targetFile string, mod
156158
// Remove the temp file when done
157159
defer os.Remove(tempFile)
158160

159-
// Parse the temp file to extract the function declaration and imports using AST
160-
_, tempNode, err := goutils.ReadGoFile(tempFile)
161+
// Parse the temp file to extract the function (with comments preserved by gopls)
162+
tempFset, tempNode, err := goutils.ReadGoFile(tempFile)
161163
if err != nil {
162164
return fmt.Errorf("error parsing temp file: %w", err)
163165
}
164166

165-
// Parse the target file
166-
targetFset, targetNode, err := goutils.ReadGoFile(targetFile)
167-
if err != nil {
168-
return fmt.Errorf("error parsing target file: %w", err)
169-
}
170-
171167
// Find the function in the temp file
172168
var funcToMove *ast.FuncDecl
173169
for _, decl := range tempNode.Decls {
@@ -180,28 +176,55 @@ func moveIdentifier(sourceFile string, identifier string, targetFile string, mod
180176
if funcToMove == nil {
181177
return fmt.Errorf("no function found in temp file")
182178
}
179+
180+
// Extract imports from temp file for later merging
181+
var tempImports []*ast.ImportSpec
182+
for _, imp := range tempNode.Imports {
183+
tempImports = append(tempImports, imp)
184+
}
183185

184-
// Merge imports from temp file to target file
185-
// Build a map of existing imports in target
186+
// Serialize just the function declaration (not a whole file) using printer
187+
// This gives us the function with its Doc comments as source text
188+
var funcBuf bytes.Buffer
189+
cfg := printer.Config{Mode: printer.UseSpaces | printer.TabIndent, Tabwidth: 8}
190+
if err := cfg.Fprint(&funcBuf, tempFset, funcToMove); err != nil {
191+
return fmt.Errorf("error serializing function: %w", err)
192+
}
193+
194+
// Read the current target file content
195+
targetContent, err := os.ReadFile(targetFile)
196+
if err != nil {
197+
return fmt.Errorf("error reading target file: %w", err)
198+
}
199+
200+
// Append the serialized function to the target file
201+
// printer.Fprint on a FuncDecl includes the Doc comments automatically
202+
newContent := string(targetContent) + "\n" + funcBuf.String() + "\n"
203+
if err := os.WriteFile(targetFile, []byte(newContent), 0644); err != nil {
204+
return fmt.Errorf("error writing target file: %w", err)
205+
}
206+
207+
// Now reparse the entire target file to get a proper AST
208+
targetFset, targetNode, err := goutils.ReadGoFile(targetFile)
209+
if err != nil {
210+
return fmt.Errorf("error reparsing target file: %w", err)
211+
}
212+
213+
// Merge imports from temp file using AST operations
186214
existingImports := make(map[string]bool)
187215
for _, imp := range targetNode.Imports {
188216
existingImports[imp.Path.Value] = true
189217
}
190-
191-
// Add new imports from temp file
192-
for _, imp := range tempNode.Imports {
218+
219+
for _, imp := range tempImports {
193220
if !existingImports[imp.Path.Value] {
194221
targetNode.Imports = append(targetNode.Imports, imp)
195222
}
196223
}
197-
198-
// Add the function declaration to the target file
199-
targetNode.Decls = append(targetNode.Decls, funcToMove)
200-
201-
// Write the modified target file back using AST
202-
err = goutils.WriteGoFile(targetFile, targetFset, targetNode)
203-
if err != nil {
204-
return fmt.Errorf("error writing target file: %w", err)
224+
225+
// Write back the target file with merged imports
226+
if err := goutils.WriteGoFile(targetFile, targetFset, targetNode); err != nil {
227+
return fmt.Errorf("error writing target file with imports: %w", err)
205228
}
206229

207230
// Run goimports to organize imports and format properly

0 commit comments

Comments
 (0)