1
2
3
4
5 package refactor
6
7
8
9 import (
10 "fmt"
11 "go/ast"
12 "go/token"
13 "go/types"
14 "slices"
15
16 "golang.org/x/tools/go/analysis"
17 "golang.org/x/tools/go/ast/edge"
18 "golang.org/x/tools/go/ast/inspector"
19 "golang.org/x/tools/internal/astutil"
20 "golang.org/x/tools/internal/typesinternal"
21 "golang.org/x/tools/internal/typesinternal/typeindex"
22 )
23
24
25
26
27
28
29
30
31
32
33
34
35 func DeleteVar(tokFile *token.File, info *types.Info, curId inspector.Cursor) []analysis.TextEdit {
36 switch ek, _ := curId.ParentEdge(); ek {
37 case edge.ValueSpec_Names:
38 return deleteVarFromValueSpec(tokFile, info, curId)
39
40 case edge.AssignStmt_Lhs:
41 return deleteVarFromAssignStmt(tokFile, info, curId)
42 }
43
44
45
46 return nil
47 }
48
49
50
51
52
53
54
55 func deleteVarFromValueSpec(tokFile *token.File, info *types.Info, curIdent inspector.Cursor) []analysis.TextEdit {
56 var (
57 id = curIdent.Node().(*ast.Ident)
58 curSpec = curIdent.Parent()
59 spec = curSpec.Node().(*ast.ValueSpec)
60 )
61
62 declaresOtherNames := slices.ContainsFunc(spec.Names, func(name *ast.Ident) bool {
63 return name != id && name.Name != "_"
64 })
65 noRHSEffects := !slices.ContainsFunc(spec.Values, func(rhs ast.Expr) bool {
66 return !typesinternal.NoEffects(info, rhs)
67 })
68 if !declaresOtherNames && noRHSEffects {
69
70
71 return DeleteSpec(tokFile, curSpec)
72 }
73
74
75
76
77
78 _, index := curIdent.ParentEdge()
79
80
81 if len(spec.Values) == 0 {
82 var pos, end token.Pos
83 if index == len(spec.Names)-1 {
84
85
86
87
88 pos = spec.Names[index-1].End()
89 end = spec.Names[index].End()
90 } else {
91
92
93
94
95 pos = spec.Names[index].Pos()
96 end = spec.Names[index+1].Pos()
97 }
98 return []analysis.TextEdit{{
99 Pos: pos,
100 End: end,
101 }}
102 }
103
104
105
106 if len(spec.Names) == len(spec.Values) &&
107 typesinternal.NoEffects(info, spec.Values[index]) {
108
109 if index == len(spec.Names)-1 {
110
111
112
113
114 return []analysis.TextEdit{
115 {
116 Pos: spec.Names[index-1].End(),
117 End: spec.Names[index].End(),
118 },
119 {
120 Pos: spec.Values[index-1].End(),
121 End: spec.Values[index].End(),
122 },
123 }
124 } else {
125
126
127
128
129 return []analysis.TextEdit{
130 {
131 Pos: spec.Names[index].Pos(),
132 End: spec.Names[index+1].Pos(),
133 },
134 {
135 Pos: spec.Values[index].Pos(),
136 End: spec.Values[index+1].Pos(),
137 },
138 }
139 }
140 }
141
142
143
144 return []analysis.TextEdit{{
145 Pos: id.Pos(),
146 End: id.End(),
147 NewText: []byte("_"),
148 }}
149 }
150
151
152
153
154 func deleteVarFromAssignStmt(tokFile *token.File, info *types.Info, curIdent inspector.Cursor) []analysis.TextEdit {
155 var (
156 id = curIdent.Node().(*ast.Ident)
157 curStmt = curIdent.Parent()
158 assign = curStmt.Node().(*ast.AssignStmt)
159 )
160
161 declaresOtherNames := slices.ContainsFunc(assign.Lhs, func(lhs ast.Expr) bool {
162 lhsId, ok := lhs.(*ast.Ident)
163 return ok && lhsId != id && lhsId.Name != "_"
164 })
165 noRHSEffects := !slices.ContainsFunc(assign.Rhs, func(rhs ast.Expr) bool {
166 return !typesinternal.NoEffects(info, rhs)
167 })
168 if !declaresOtherNames && noRHSEffects {
169
170
171 if edits := DeleteStmt(tokFile, curStmt); edits != nil {
172 return edits
173 }
174
175
176 }
177
178
179
180
181
182
183
184
185 _, index := curIdent.ParentEdge()
186 if len(assign.Lhs) > 1 &&
187 len(assign.Lhs) == len(assign.Rhs) &&
188 typesinternal.NoEffects(info, assign.Rhs[index]) {
189
190 if index == len(assign.Lhs)-1 {
191
192
193
194
195 return []analysis.TextEdit{
196 {
197 Pos: assign.Lhs[index-1].End(),
198 End: assign.Lhs[index].End(),
199 },
200 {
201 Pos: assign.Rhs[index-1].End(),
202 End: assign.Rhs[index].End(),
203 },
204 }
205 } else {
206
207
208
209
210 return []analysis.TextEdit{
211 {
212 Pos: assign.Lhs[index].Pos(),
213 End: assign.Lhs[index+1].Pos(),
214 },
215 {
216 Pos: assign.Rhs[index].Pos(),
217 End: assign.Rhs[index+1].Pos(),
218 },
219 }
220 }
221 }
222
223
224
225 edits := []analysis.TextEdit{{
226 Pos: id.Pos(),
227 End: id.End(),
228 NewText: []byte("_"),
229 }}
230
231
232
233
234
235 if !declaresOtherNames {
236 edits = append(edits, analysis.TextEdit{
237 Pos: assign.TokPos,
238 End: assign.TokPos + token.Pos(len(":=")),
239 NewText: []byte("="),
240 })
241 }
242
243 return edits
244 }
245
246
247
248
249 func DeleteSpec(tokFile *token.File, curSpec inspector.Cursor) []analysis.TextEdit {
250 var (
251 spec = curSpec.Node().(ast.Spec)
252 curDecl = curSpec.Parent()
253 decl = curDecl.Node().(*ast.GenDecl)
254 )
255
256
257
258 if len(decl.Specs) == 1 {
259 return DeleteDecl(tokFile, curDecl)
260 }
261
262
263 _, index := curSpec.ParentEdge()
264 pos, end := spec.Pos(), spec.End()
265 if doc := astutil.DocComment(spec); doc != nil {
266 pos = doc.Pos()
267 }
268 if index == len(decl.Specs)-1 {
269
270 if c := eolComment(spec); c != nil {
271
272 end = c.End()
273 }
274 } else {
275
276
277
278 end = decl.Specs[index+1].Pos()
279 }
280 return []analysis.TextEdit{{
281 Pos: pos,
282 End: end,
283 }}
284 }
285
286
287
288
289 func DeleteDecl(tokFile *token.File, curDecl inspector.Cursor) []analysis.TextEdit {
290 decl := curDecl.Node().(ast.Decl)
291
292 ek, _ := curDecl.ParentEdge()
293 switch ek {
294 case edge.DeclStmt_Decl:
295 return DeleteStmt(tokFile, curDecl.Parent())
296
297 case edge.File_Decls:
298 pos, end := decl.Pos(), decl.End()
299 if doc := astutil.DocComment(decl); doc != nil {
300 pos = doc.Pos()
301 }
302
303
304
305 var (
306 file = curDecl.Parent().Node().(*ast.File)
307 lineOf = tokFile.Line
308 declEndLine = lineOf(decl.End())
309 )
310 for _, cg := range file.Comments {
311 for _, c := range cg.List {
312 if c.Pos() < end {
313 continue
314 }
315 commentEndLine := lineOf(c.End())
316 if commentEndLine > declEndLine {
317 break
318 } else if lineOf(c.Pos()) == declEndLine && commentEndLine == declEndLine {
319 end = c.End()
320 }
321 }
322 }
323
324 return []analysis.TextEdit{{
325 Pos: pos,
326 End: end,
327 }}
328
329 default:
330 panic(fmt.Sprintf("Decl parent is %v, want DeclStmt or File", ek))
331 }
332 }
333
334
335
336
337 func DeleteStmt(tokFile *token.File, curStmt inspector.Cursor) []analysis.TextEdit {
338 stmt := curStmt.Node().(ast.Stmt)
339
340
341
342
343
344
345 lineOf := tokFile.Line
346 stmtStartLine, stmtEndLine := lineOf(stmt.Pos()), lineOf(stmt.End())
347
348 var from, to token.Pos
349
350 limits := func(left, right token.Pos) {
351 if lineOf(left) == stmtStartLine {
352 from = left
353 }
354 if lineOf(right) == stmtEndLine {
355 to = right
356 }
357 }
358
359
360
361
362
363 switch parent := curStmt.Parent().Node().(type) {
364 case *ast.SwitchStmt:
365 limits(parent.Switch, parent.Body.Lbrace)
366 case *ast.TypeSwitchStmt:
367 limits(parent.Switch, parent.Body.Lbrace)
368 if parent.Assign == stmt {
369 return nil
370 }
371 case *ast.BlockStmt:
372 limits(parent.Lbrace, parent.Rbrace)
373 case *ast.CommClause:
374 limits(parent.Colon, curStmt.Parent().Parent().Node().(*ast.BlockStmt).Rbrace)
375 if parent.Comm == stmt {
376 return nil
377 }
378 case *ast.CaseClause:
379 limits(parent.Colon, curStmt.Parent().Parent().Node().(*ast.BlockStmt).Rbrace)
380 case *ast.ForStmt:
381 limits(parent.For, parent.Body.Lbrace)
382
383 default:
384 return nil
385 }
386
387 if prev, found := curStmt.PrevSibling(); found && lineOf(prev.Node().End()) == stmtStartLine {
388 from = prev.Node().End()
389 }
390 if next, found := curStmt.NextSibling(); found && lineOf(next.Node().Pos()) == stmtEndLine {
391 to = next.Node().Pos()
392 }
393
394 Outer:
395 for _, cg := range astutil.EnclosingFile(curStmt).Comments {
396 for _, co := range cg.List {
397 if lineOf(co.End()) < stmtStartLine {
398 continue
399 } else if lineOf(co.Pos()) > stmtEndLine {
400 break Outer
401 }
402 if lineOf(co.End()) == stmtStartLine && co.End() < stmt.Pos() {
403 if !from.IsValid() || co.End() > from {
404 from = co.End()
405 continue
406 }
407 }
408 if lineOf(co.Pos()) == stmtEndLine && co.Pos() > stmt.End() {
409 if !to.IsValid() || co.Pos() < to {
410 to = co.Pos()
411 continue
412 }
413 }
414 }
415 }
416
417
418 edit := analysis.TextEdit{Pos: stmt.Pos(), End: stmt.End()}
419 if from.IsValid() || to.IsValid() {
420
421
422
423
424
425
426 return []analysis.TextEdit{edit}
427 }
428
429 for lineOf(edit.Pos) == stmtStartLine {
430 edit.Pos--
431 }
432 edit.Pos++
433 for lineOf(edit.End) == stmtEndLine {
434 edit.End++
435 }
436 return []analysis.TextEdit{edit}
437 }
438
439
440
441
442 func DeleteUnusedVars(index *typeindex.Index, info *types.Info, tokFile *token.File, curDelend inspector.Cursor) []analysis.TextEdit {
443
444
445
446
447
448
449 delcount := make(map[*types.Var]int)
450 for curId := range curDelend.Preorder((*ast.Ident)(nil)) {
451 id := curId.Node().(*ast.Ident)
452 if v, ok := info.Uses[id].(*types.Var); ok &&
453 typesinternal.GetVarKind(v) == typesinternal.LocalVar {
454 delcount[v]++
455 }
456 }
457
458
459 var edits []analysis.TextEdit
460 for v, count := range delcount {
461 if len(slices.Collect(index.Uses(v))) == count {
462 if curDefId, ok := index.Def(v); ok {
463 edits = append(edits, DeleteVar(tokFile, info, curDefId)...)
464 }
465 }
466 }
467 return edits
468 }
469
470 func eolComment(n ast.Node) *ast.CommentGroup {
471
472
473 switch n := n.(type) {
474 case *ast.GenDecl:
475 if !n.TokPos.IsValid() && len(n.Specs) == 1 {
476 return eolComment(n.Specs[0])
477 }
478 case *ast.ValueSpec:
479 return n.Comment
480 case *ast.TypeSpec:
481 return n.Comment
482 }
483 return nil
484 }
485
View as plain text