1
2
3
4
5
6 package astutil
7
8 import (
9 "fmt"
10 "go/ast"
11 "go/token"
12 "reflect"
13 "slices"
14 "strconv"
15 "strings"
16 )
17
18
19 func AddImport(fset *token.FileSet, f *ast.File, path string) (added bool) {
20 return AddNamedImport(fset, f, "", path)
21 }
22
23
24
25
26
27
28
29
30
31
32
33 func AddNamedImport(fset *token.FileSet, f *ast.File, name, path string) (added bool) {
34 if imports(f, name, path) {
35 return false
36 }
37
38 newImport := &ast.ImportSpec{
39 Path: &ast.BasicLit{
40 Kind: token.STRING,
41 Value: strconv.Quote(path),
42 },
43 }
44 if name != "" {
45 newImport.Name = &ast.Ident{Name: name}
46 }
47
48
49
50
51
52 var (
53 bestMatch = -1
54 lastImport = -1
55 impDecl *ast.GenDecl
56 impIndex = -1
57
58 isThirdPartyPath = isThirdParty(path)
59 )
60 for i, decl := range f.Decls {
61 gen, ok := decl.(*ast.GenDecl)
62 if ok && gen.Tok == token.IMPORT {
63 lastImport = i
64
65
66 if declImports(gen, "C") {
67 continue
68 }
69
70
71 if len(gen.Specs) == 0 && bestMatch == -1 {
72 impDecl = gen
73 }
74
75
76
77
78
79
80
81
82
83
84
85 seenAnyThirdParty := false
86 for j, spec := range gen.Specs {
87 impspec := spec.(*ast.ImportSpec)
88 p := importPath(impspec)
89 n := matchLen(p, path)
90 if n > bestMatch || (bestMatch == 0 && !seenAnyThirdParty && isThirdPartyPath) {
91 bestMatch = n
92 impDecl = gen
93 impIndex = j
94 }
95 seenAnyThirdParty = seenAnyThirdParty || isThirdParty(p)
96 }
97 }
98 }
99
100
101 if impDecl == nil {
102 impDecl = &ast.GenDecl{
103 Tok: token.IMPORT,
104 }
105 if lastImport >= 0 {
106 impDecl.TokPos = f.Decls[lastImport].End()
107 } else {
108
109
110
111
112 impDecl.TokPos = f.Package
113
114 file := fset.File(f.Package)
115 pkgLine := file.Line(f.Package)
116 for _, c := range f.Comments {
117 if file.Line(c.Pos()) > pkgLine {
118 break
119 }
120
121 impDecl.TokPos = c.End() + 2
122 }
123 }
124 f.Decls = append(f.Decls, nil)
125 copy(f.Decls[lastImport+2:], f.Decls[lastImport+1:])
126 f.Decls[lastImport+1] = impDecl
127 }
128
129
130 insertAt := 0
131 if impIndex >= 0 {
132
133 insertAt = impIndex + 1
134 }
135 impDecl.Specs = append(impDecl.Specs, nil)
136 copy(impDecl.Specs[insertAt+1:], impDecl.Specs[insertAt:])
137 impDecl.Specs[insertAt] = newImport
138 pos := impDecl.Pos()
139 if insertAt > 0 {
140
141
142 if spec, ok := impDecl.Specs[insertAt-1].(*ast.ImportSpec); ok && spec.Comment != nil {
143 pos = spec.Comment.End()
144 } else {
145
146
147 pos = impDecl.Specs[insertAt-1].Pos()
148 }
149 }
150 if newImport.Name != nil {
151 newImport.Name.NamePos = pos
152 }
153 updateBasicLitPos(newImport.Path, pos)
154 newImport.EndPos = pos
155
156
157 if len(impDecl.Specs) == 1 {
158
159 impDecl.Lparen = token.NoPos
160 } else if !impDecl.Lparen.IsValid() {
161
162 impDecl.Lparen = impDecl.Specs[0].Pos()
163 }
164
165 f.Imports = append(f.Imports, newImport)
166
167 if len(f.Decls) <= 1 {
168 return true
169 }
170
171
172 var first *ast.GenDecl
173 for i := 0; i < len(f.Decls); i++ {
174 decl := f.Decls[i]
175 gen, ok := decl.(*ast.GenDecl)
176 if !ok || gen.Tok != token.IMPORT || declImports(gen, "C") {
177 continue
178 }
179 if first == nil {
180 first = gen
181 continue
182 }
183
184
185 first.Lparen = first.Pos()
186
187 for _, spec := range gen.Specs {
188 updateBasicLitPos(spec.(*ast.ImportSpec).Path, first.Pos())
189 first.Specs = append(first.Specs, spec)
190 }
191 f.Decls = slices.Delete(f.Decls, i, i+1)
192 i--
193 }
194
195 return true
196 }
197
198 func isThirdParty(importPath string) bool {
199
200
201 return strings.Contains(importPath, ".")
202 }
203
204
205
206 func DeleteImport(fset *token.FileSet, f *ast.File, path string) (deleted bool) {
207 return DeleteNamedImport(fset, f, "", path)
208 }
209
210
211
212 func DeleteNamedImport(fset *token.FileSet, f *ast.File, name, path string) (deleted bool) {
213 var (
214 delspecs = make(map[*ast.ImportSpec]bool)
215 delcomments = make(map[*ast.CommentGroup]bool)
216 )
217
218
219 for i := 0; i < len(f.Decls); i++ {
220 gen, ok := f.Decls[i].(*ast.GenDecl)
221 if !ok || gen.Tok != token.IMPORT {
222 continue
223 }
224 for j := 0; j < len(gen.Specs); j++ {
225 impspec := gen.Specs[j].(*ast.ImportSpec)
226 if importName(impspec) != name || importPath(impspec) != path {
227 continue
228 }
229
230
231
232 delspecs[impspec] = true
233 deleted = true
234 gen.Specs = slices.Delete(gen.Specs, j, j+1)
235
236
237
238 if len(gen.Specs) == 0 {
239 f.Decls = slices.Delete(f.Decls, i, i+1)
240 i--
241 break
242 } else if len(gen.Specs) == 1 {
243 if impspec.Doc != nil {
244 delcomments[impspec.Doc] = true
245 }
246 if impspec.Comment != nil {
247 delcomments[impspec.Comment] = true
248 }
249 for _, cg := range f.Comments {
250
251 if cg.End() < impspec.Pos() && fset.Position(cg.End()).Line == fset.Position(impspec.Pos()).Line {
252 delcomments[cg] = true
253 break
254 }
255 }
256
257 spec := gen.Specs[0].(*ast.ImportSpec)
258
259
260 if spec.Doc != nil {
261 for fset.Position(gen.TokPos).Line+1 < fset.Position(spec.Doc.Pos()).Line {
262 fset.File(gen.TokPos).MergeLine(fset.Position(gen.TokPos).Line)
263 }
264 }
265 for _, cg := range f.Comments {
266 if cg.End() < spec.Pos() && fset.Position(cg.End()).Line == fset.Position(spec.Pos()).Line {
267 for fset.Position(gen.TokPos).Line+1 < fset.Position(spec.Pos()).Line {
268 fset.File(gen.TokPos).MergeLine(fset.Position(gen.TokPos).Line)
269 }
270 break
271 }
272 }
273 }
274 if j > 0 {
275 lastImpspec := gen.Specs[j-1].(*ast.ImportSpec)
276 lastLine := fset.PositionFor(lastImpspec.Path.ValuePos, false).Line
277 line := fset.PositionFor(impspec.Path.ValuePos, false).Line
278
279
280
281 if line-lastLine > 1 || !gen.Rparen.IsValid() {
282
283
284
285
286 } else if line != fset.File(gen.Rparen).LineCount() {
287
288 fset.File(gen.Rparen).MergeLine(line)
289 }
290 }
291 j--
292 }
293 }
294
295
296 before := len(f.Imports)
297 f.Imports = slices.DeleteFunc(f.Imports, func(imp *ast.ImportSpec) bool {
298 _, ok := delspecs[imp]
299 return ok
300 })
301 if len(f.Imports)+len(delspecs) != before {
302
303 panic(fmt.Sprintf("deleted specs from Decls but not Imports: %v", delspecs))
304 }
305
306
307 f.Comments = slices.DeleteFunc(f.Comments, func(cg *ast.CommentGroup) bool {
308 _, ok := delcomments[cg]
309 return ok
310 })
311
312 return
313 }
314
315
316 func RewriteImport(fset *token.FileSet, f *ast.File, oldPath, newPath string) (rewrote bool) {
317 for _, imp := range f.Imports {
318 if importPath(imp) == oldPath {
319 rewrote = true
320
321
322 imp.EndPos = imp.End()
323 imp.Path.Value = strconv.Quote(newPath)
324 }
325 }
326 return
327 }
328
329
330
331
332 func UsesImport(f *ast.File, path string) (used bool) {
333 if f.Scope == nil {
334 panic("file f was not parsed with syntactic object resolution")
335 }
336 spec := importSpec(f, path)
337 if spec == nil {
338 return
339 }
340
341 name := spec.Name.String()
342 switch name {
343 case "<nil>":
344
345
346 lastSlash := strings.LastIndex(path, "/")
347 if lastSlash == -1 {
348 name = path
349 } else {
350 name = path[lastSlash+1:]
351 }
352 case "_", ".":
353
354 return true
355 }
356
357 ast.Walk(visitFn(func(n ast.Node) {
358 sel, ok := n.(*ast.SelectorExpr)
359 if ok && isTopName(sel.X, name) {
360 used = true
361 }
362 }), f)
363
364 return
365 }
366
367 type visitFn func(node ast.Node)
368
369 func (fn visitFn) Visit(node ast.Node) ast.Visitor {
370 fn(node)
371 return fn
372 }
373
374
375 func imports(f *ast.File, name, path string) bool {
376 for _, s := range f.Imports {
377 if importName(s) == name && importPath(s) == path {
378 return true
379 }
380 }
381 return false
382 }
383
384
385
386 func importSpec(f *ast.File, path string) *ast.ImportSpec {
387 for _, s := range f.Imports {
388 if importPath(s) == path {
389 return s
390 }
391 }
392 return nil
393 }
394
395
396
397 func importName(s *ast.ImportSpec) string {
398 if s.Name == nil {
399 return ""
400 }
401 return s.Name.Name
402 }
403
404
405
406 func importPath(s *ast.ImportSpec) string {
407 t, err := strconv.Unquote(s.Path.Value)
408 if err != nil {
409 return ""
410 }
411 return t
412 }
413
414
415 func declImports(gen *ast.GenDecl, path string) bool {
416 if gen.Tok != token.IMPORT {
417 return false
418 }
419 for _, spec := range gen.Specs {
420 impspec := spec.(*ast.ImportSpec)
421 if importPath(impspec) == path {
422 return true
423 }
424 }
425 return false
426 }
427
428
429 func matchLen(x, y string) int {
430 n := 0
431 for i := 0; i < len(x) && i < len(y) && x[i] == y[i]; i++ {
432 if x[i] == '/' {
433 n++
434 }
435 }
436 return n
437 }
438
439
440 func isTopName(n ast.Expr, name string) bool {
441 id, ok := n.(*ast.Ident)
442 return ok && id.Name == name && id.Obj == nil
443 }
444
445
446 func Imports(fset *token.FileSet, f *ast.File) [][]*ast.ImportSpec {
447 var groups [][]*ast.ImportSpec
448
449 for _, decl := range f.Decls {
450 genDecl, ok := decl.(*ast.GenDecl)
451 if !ok || genDecl.Tok != token.IMPORT {
452 break
453 }
454
455 group := []*ast.ImportSpec{}
456
457 var lastLine int
458 for _, spec := range genDecl.Specs {
459 importSpec := spec.(*ast.ImportSpec)
460 pos := importSpec.Path.ValuePos
461 line := fset.Position(pos).Line
462 if lastLine > 0 && pos > 0 && line-lastLine > 1 {
463 groups = append(groups, group)
464 group = []*ast.ImportSpec{}
465 }
466 group = append(group, importSpec)
467 lastLine = line
468 }
469 groups = append(groups, group)
470 }
471
472 return groups
473 }
474
475
476
477
478 func updateBasicLitPos(lit *ast.BasicLit, pos token.Pos) {
479 len := lit.End() - lit.Pos()
480 lit.ValuePos = pos
481
482
483 v := reflect.ValueOf(lit).Elem().FieldByName("ValueEnd")
484 if v.IsValid() && v.Int() != 0 {
485 v.SetInt(int64(pos + len))
486 }
487 }
488
View as plain text