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