返回 DeepSeek-Reasonix
delete_symbol.go
根目录 / internal / tool / builtin / delete_symbol.go
1 package builtin
2
3 import (
4 "context"
5 "encoding/json"
6 "fmt"
7 "go/ast"
8 "go/parser"
9 "go/token"
10 "os"
11 "path/filepath"
12 "strings"
13
14 "reasonix/internal/diff"
15 "reasonix/internal/tool"
16 )
17
18 func init() { tool.RegisterBuiltin(deleteSymbol{}) }
19
20 type deleteSymbol struct {
21 roots []string
22 guard SessionDataGuard
23 managed ManagedConfigPaths
24 workDir string
25 }
26
27 type symbolMatch struct {
28 name string
29 kind string
30 parent string
31 line int
32 start token.Pos
33 docStart token.Pos // start of the symbol's doc comment, if any (excluded from start)
34 end token.Pos
35 siblings []string
36 }
37
38 func (deleteSymbol) Name() string { return "delete_symbol" }
39
40 func (deleteSymbol) Description() string {
41 return "Delete a named symbol (function, method, type, interface, const, var) from a Go source file using AST parsing. For non-Go files, use delete_range with manual anchors."
42 }
43
44 func (deleteSymbol) Schema() json.RawMessage {
45 return json.RawMessage(`{
46 "type":"object",
47 "properties":{
48 "path":{"type":"string","description":"Path to the source file"},
49 "name":{"type":"string","description":"Name of the symbol to delete"},
50 "kind":{"type":"string","description":"Optional kind filter: func, method, type, interface, const, var"},
51 "parent":{"type":"string","description":"Optional parent struct name for method disambiguation"}
52 },
53 "required":["path","name"]
54 }`)
55 }
56
57 func (deleteSymbol) ReadOnly() bool { return false }
58
59 func (d deleteSymbol) Execute(ctx context.Context, args json.RawMessage) (string, error) {
60 var p struct {
61 Path string `json:"path"`
62 Name string `json:"name"`
63 Kind string `json:"kind"`
64 Parent string `json:"parent"`
65 }
66 if err := json.Unmarshal(args, &p); err != nil {
67 return "", fmt.Errorf("invalid args: %w", err)
68 }
69 if p.Path == "" {
70 return "", fmt.Errorf("path is required")
71 }
72 if p.Name == "" {
73 return "", fmt.Errorf("name is required")
74 }
75 p.Path = resolveIn(d.workDir, p.Path)
76 if err := confineWrite(ctx, d.roots, d.guard, d.managed, p.Path); err != nil {
77 return "", err
78 }
79
80 ext := strings.ToLower(filepath.Ext(p.Path))
81 if ext != ".go" {
82 return "", fmt.Errorf("delete_symbol only supports Go files — use delete_range for %s files", ext)
83 }
84
85 m, fset, err := d.findSymbol(p.Path, p.Name, p.Kind, p.Parent)
86 if err != nil {
87 return "", err
88 }
89
90 src, err := os.ReadFile(p.Path)
91 if err != nil {
92 return "", fmt.Errorf("read %s: %w", p.Path, err)
93 }
94 original := string(src)
95
96 newContent := deleteLines(original, fset, m)
97 if err := os.WriteFile(p.Path, []byte(newContent), 0o644); err != nil {
98 return "", fmt.Errorf("write %s: %w", p.Path, err)
99 }
100
101 change := diff.Build(p.Path, original, newContent, diff.Modify)
102 return change.Diff, nil
103 }
104
105 func (d deleteSymbol) Preview(args json.RawMessage) (diff.Change, error) {
106 var p struct {
107 Path string `json:"path"`
108 Name string `json:"name"`
109 Kind string `json:"kind"`
110 Parent string `json:"parent"`
111 }
112 if err := json.Unmarshal(args, &p); err != nil {
113 return diff.Change{}, fmt.Errorf("invalid args: %w", err)
114 }
115 if p.Path == "" {
116 return diff.Change{}, fmt.Errorf("path is required")
117 }
118 if p.Name == "" {
119 return diff.Change{}, fmt.Errorf("name is required")
120 }
121 p.Path = resolveIn(d.workDir, p.Path)
122 if err := confinePreview(d.roots, d.guard, d.managed, p.Path); err != nil {
123 return diff.Change{}, err
124 }
125
126 ext := strings.ToLower(filepath.Ext(p.Path))
127 if ext != ".go" {
128 return diff.Change{}, fmt.Errorf("delete_symbol only supports Go files")
129 }
130
131 m, fset, err := d.findSymbol(p.Path, p.Name, p.Kind, p.Parent)
132 if err != nil {
133 return diff.Change{}, err
134 }
135
136 src, err := os.ReadFile(p.Path)
137 if err != nil {
138 return diff.Change{}, fmt.Errorf("read %s: %w", p.Path, err)
139 }
140 original := string(src)
141
142 newContent := deleteLines(original, fset, m)
143 return diff.Build(p.Path, original, newContent, diff.Modify), nil
144 }
145
146 func (d deleteSymbol) findSymbol(path, name, kind, parent string) (symbolMatch, *token.FileSet, error) {
147 fset := token.NewFileSet()
148 f, err := parser.ParseFile(fset, path, nil, parser.ParseComments)
149 if err != nil {
150 return symbolMatch{}, nil, fmt.Errorf("parse %s: %w", path, err)
151 }
152
153 matches := collectSymbols(fset, f)
154
155 var byName []symbolMatch
156 for _, m := range matches {
157 if m.name == name {
158 byName = append(byName, m)
159 }
160 }
161 if len(byName) == 0 {
162 return symbolMatch{}, nil, fmt.Errorf("symbol %q not found in %s", name, path)
163 }
164
165 filtered := byName
166 if kind != "" {
167 var byKind []symbolMatch
168 for _, m := range filtered {
169 if m.kind == kind {
170 byKind = append(byKind, m)
171 }
172 }
173 if len(byKind) == 0 {
174 return symbolMatch{}, nil, fmt.Errorf("symbol %q with kind %q not found", name, kind)
175 }
176 filtered = byKind
177 }
178 if parent != "" {
179 var byParent []symbolMatch
180 for _, m := range filtered {
181 if m.parent == parent {
182 byParent = append(byParent, m)
183 }
184 }
185 if len(byParent) == 0 {
186 return symbolMatch{}, nil, fmt.Errorf("symbol %q (kind=%q parent=%q) not found", name, kind, parent)
187 }
188 filtered = byParent
189 }
190
191 if len(filtered) > 1 {
192 var b strings.Builder
193 fmt.Fprintf(&b, "Multiple matches for %q — disambiguate with kind/parent:\n", name)
194 for _, m := range filtered {
195 fmt.Fprintf(&b, " line %d: %s %s", m.line, m.kind, m.name)
196 if m.parent != "" {
197 fmt.Fprintf(&b, " (on %s)", m.parent)
198 }
199 b.WriteString("\n")
200 }
201 return symbolMatch{}, nil, fmt.Errorf("%s", b.String())
202 }
203
204 if len(filtered[0].siblings) > 1 {
205 return symbolMatch{}, nil, fmt.Errorf("%s %q is declared in a multi-name %s spec with %s; delete_symbol refuses to remove it because that would also delete sibling symbols", filtered[0].kind, name, filtered[0].kind, strings.Join(filtered[0].siblings, ", "))
206 }
207
208 return filtered[0], fset, nil
209 }
210
211 func collectSymbols(fset *token.FileSet, f *ast.File) []symbolMatch {
212 var matches []symbolMatch
213 for _, decl := range f.Decls {
214 switch d := decl.(type) {
215 case *ast.FuncDecl:
216 m := symbolMatch{
217 name: d.Name.Name,
218 kind: "func",
219 start: d.Pos(),
220 end: d.End(),
221 line: fset.Position(d.Pos()).Line,
222 }
223 if d.Doc != nil {
224 m.docStart = d.Doc.Pos()
225 }
226 if d.Recv != nil && len(d.Recv.List) > 0 {
227 m.kind = "method"
228 recvType := d.Recv.List[0].Type
229 if se, ok := recvType.(*ast.StarExpr); ok {
230 if ident, ok := se.X.(*ast.Ident); ok {
231 m.parent = ident.Name
232 }
233 } else if ident, ok := recvType.(*ast.Ident); ok {
234 m.parent = ident.Name
235 }
236 }
237 matches = append(matches, m)
238 case *ast.GenDecl:
239 for _, spec := range d.Specs {
240 switch s := spec.(type) {
241 case *ast.TypeSpec:
242 m := symbolMatch{
243 name: s.Name.Name,
244 start: s.Pos(),
245 end: s.End(),
246 line: fset.Position(s.Pos()).Line,
247 }
248 if _, ok := s.Type.(*ast.InterfaceType); ok {
249 m.kind = "interface"
250 } else {
251 m.kind = "type"
252 }
253 if doc := specDoc(d, s.Doc); doc != nil {
254 m.docStart = doc.Pos()
255 }
256 matches = append(matches, m)
257 case *ast.ValueSpec:
258 kind := "var"
259 if d.Tok == token.CONST {
260 kind = "const"
261 }
262 names := make([]string, 0, len(s.Names))
263 for _, ident := range s.Names {
264 names = append(names, ident.Name)
265 }
266 var docStart token.Pos
267 if doc := specDoc(d, s.Doc); doc != nil {
268 docStart = doc.Pos()
269 }
270 for _, ident := range s.Names {
271 matches = append(matches, symbolMatch{
272 name: ident.Name,
273 kind: kind,
274 start: ident.Pos(),
275 docStart: docStart,
276 end: s.End(), // whole spec, incl. a multi-line value — ident.End() stops at the name
277 line: fset.Position(ident.Pos()).Line,
278 siblings: names,
279 })
280 }
281 }
282 }
283 }
284 }
285 return matches
286 }
287
288 // specDoc returns the doc comment governing one spec of a GenDecl: the spec's own
289 // doc when grouped (type/const/var (...)), else the GenDecl's doc for an
290 // unparenthesized single declaration — where the parser attaches the comment to
291 // the GenDecl, not the spec. nil for an undocumented spec, and never the group's
292 // own doc when deleting just one spec of a parenthesized block.
293 func specDoc(gen *ast.GenDecl, own *ast.CommentGroup) *ast.CommentGroup {
294 if own != nil {
295 return own
296 }
297 if gen.Lparen == token.NoPos {
298 return gen.Doc
299 }
300 return nil
301 }
302
303 func deleteLines(content string, fset *token.FileSet, m symbolMatch) string {
304 start := m.start
305 if m.docStart.IsValid() {
306 start = m.docStart // delete the doc comment along with the symbol, not orphan it
307 }
308 startOff := fset.Position(start).Offset
309 endOff := fset.Position(m.end).Offset
310
311 lineStart := startOff
312 for lineStart > 0 && content[lineStart-1] != '\n' {
313 lineStart--
314 }
315
316 lineEnd := endOff
317 for lineEnd < len(content) && content[lineEnd] != '\n' {
318 lineEnd++
319 }
320 if lineEnd < len(content) {
321 lineEnd++
322 }
323
324 return content[:lineStart] + content[lineEnd:]
325 }
326
326 lines GO