1// Copyright 2013 The Go Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style
3// license that can be found in the LICENSE file.
4
5// Package astutil contains common utilities for working with the Go AST.
6package astutil // import "golang.org/x/tools/go/ast/astutil"
7
8import (
9 "fmt"
10 "go/ast"
11 "go/token"
12 "strconv"
13 "strings"
14)
15
16// AddImport adds the import path to the file f, if absent.
17func AddImport(fset *token.FileSet, f *ast.File, ipath string) (added bool) {
18 return AddNamedImport(fset, f, "", ipath)
19}
20
21// AddNamedImport adds the import path to the file f, if absent.
22// If name is not empty, it is used to rename the import.
23//
24// For example, calling
25// AddNamedImport(fset, f, "pathpkg", "path")
26// adds
27// import pathpkg "path"
28func AddNamedImport(fset *token.FileSet, f *ast.File, name, ipath string) (added bool) {
29 if imports(f, ipath) {
30 return false
31 }
32
33 newImport := &ast.ImportSpec{
34 Path: &ast.BasicLit{
35 Kind: token.STRING,
36 Value: strconv.Quote(ipath),
37 },
38 }
39 if name != "" {
40 newImport.Name = &ast.Ident{Name: name}
41 }
42
43 // Find an import decl to add to.
44 // The goal is to find an existing import
45 // whose import path has the longest shared
46 // prefix with ipath.
47 var (
48 bestMatch = -1 // length of longest shared prefix
49 lastImport = -1 // index in f.Decls of the file's final import decl
50 impDecl *ast.GenDecl // import decl containing the best match
51 impIndex = -1 // spec index in impDecl containing the best match
52
53 isThirdPartyPath = isThirdParty(ipath)
54 )
55 for i, decl := range f.Decls {
56 gen, ok := decl.(*ast.GenDecl)
57 if ok && gen.Tok == token.IMPORT {
58 lastImport = i
59 // Do not add to import "C", to avoid disrupting the
60 // association with its doc comment, breaking cgo.
61 if declImports(gen, "C") {
62 continue
63 }
64
65 // Match an empty import decl if that's all that is available.
66 if len(gen.Specs) == 0 && bestMatch == -1 {
67 impDecl = gen
68 }
69
70 // Compute longest shared prefix with imports in this group and find best
71 // matched import spec.
72 // 1. Always prefer import spec with longest shared prefix.
73 // 2. While match length is 0,
74 // - for stdlib package: prefer first import spec.
75 // - for third party package: prefer first third party import spec.
76 // We cannot use last import spec as best match for third party package
77 // because grouped imports are usually placed last by goimports -local
78 // flag.
79 // See issue #19190.
80 seenAnyThirdParty := false
81 for j, spec := range gen.Specs {
82 impspec := spec.(*ast.ImportSpec)
83 p := importPath(impspec)
84 n := matchLen(p, ipath)
85 if n > bestMatch || (bestMatch == 0 && !seenAnyThirdParty && isThirdPartyPath) {
86 bestMatch = n
87 impDecl = gen
88 impIndex = j
89 }
90 seenAnyThirdParty = seenAnyThirdParty || isThirdParty(p)
91 }
92 }
93 }
94
95 // If no import decl found, add one after the last import.
96 if impDecl == nil {
97 impDecl = &ast.GenDecl{
98 Tok: token.IMPORT,
99 }
100 if lastImport >= 0 {
101 impDecl.TokPos = f.Decls[lastImport].End()
102 } else {
103 // There are no existing imports.
104 // Our new import, preceded by a blank line, goes after the package declaration
105 // and after the comment, if any, that starts on the same line as the
106 // package declaration.
107 impDecl.TokPos = f.Package
108
109 file := fset.File(f.Package)
110 pkgLine := file.Line(f.Package)
111 for _, c := range f.Comments {
112 if file.Line(c.Pos()) > pkgLine {
113 break
114 }
115 // +2 for a blank line
116 impDecl.TokPos = c.End() + 2
117 }
118 }
119 f.Decls = append(f.Decls, nil)
120 copy(f.Decls[lastImport+2:], f.Decls[lastImport+1:])
121 f.Decls[lastImport+1] = impDecl
122 }
123
124 // Insert new import at insertAt.
125 insertAt := 0
126 if impIndex >= 0 {
127 // insert after the found import
128 insertAt = impIndex + 1
129 }
130 impDecl.Specs = append(impDecl.Specs, nil)
131 copy(impDecl.Specs[insertAt+1:], impDecl.Specs[insertAt:])
132 impDecl.Specs[insertAt] = newImport
133 pos := impDecl.Pos()
134 if insertAt > 0 {
135 // If there is a comment after an existing import, preserve the comment
136 // position by adding the new import after the comment.
137 if spec, ok := impDecl.Specs[insertAt-1].(*ast.ImportSpec); ok && spec.Comment != nil {
138 pos = spec.Comment.End()
139 } else {
140 // Assign same position as the previous import,
141 // so that the sorter sees it as being in the same block.
142 pos = impDecl.Specs[insertAt-1].Pos()
143 }
144 }
145 if newImport.Name != nil {
146 newImport.Name.NamePos = pos
147 }
148 newImport.Path.ValuePos = pos
149 newImport.EndPos = pos
150
151 // Clean up parens. impDecl contains at least one spec.
152 if len(impDecl.Specs) == 1 {
153 // Remove unneeded parens.
154 impDecl.Lparen = token.NoPos
155 } else if !impDecl.Lparen.IsValid() {
156 // impDecl needs parens added.
157 impDecl.Lparen = impDecl.Specs[0].Pos()
158 }
159
160 f.Imports = append(f.Imports, newImport)
161
162 if len(f.Decls) <= 1 {
163 return true
164 }
165
166 // Merge all the import declarations into the first one.
167 var first *ast.GenDecl
168 for i := 0; i < len(f.Decls); i++ {
169 decl := f.Decls[i]
170 gen, ok := decl.(*ast.GenDecl)
171 if !ok || gen.Tok != token.IMPORT || declImports(gen, "C") {
172 continue
173 }
174 if first == nil {
175 first = gen
176 continue // Don't touch the first one.
177 }
178 // We now know there is more than one package in this import
179 // declaration. Ensure that it ends up parenthesized.
180 first.Lparen = first.Pos()
181 // Move the imports of the other import declaration to the first one.
182 for _, spec := range gen.Specs {
183 spec.(*ast.ImportSpec).Path.ValuePos = first.Pos()
184 first.Specs = append(first.Specs, spec)
185 }
186 f.Decls = append(f.Decls[:i], f.Decls[i+1:]...)
187 i--
188 }
189
190 return true
191}
192
193func isThirdParty(importPath string) bool {
194 // Third party package import path usually contains "." (".com", ".org", ...)
195 // This logic is taken from golang.org/x/tools/imports package.
196 return strings.Contains(importPath, ".")
197}
198
199// DeleteImport deletes the import path from the file f, if present.
200func DeleteImport(fset *token.FileSet, f *ast.File, path string) (deleted bool) {
201 return DeleteNamedImport(fset, f, "", path)
202}
203
204// DeleteNamedImport deletes the import with the given name and path from the file f, if present.
205func DeleteNamedImport(fset *token.FileSet, f *ast.File, name, path string) (deleted bool) {
206 var delspecs []*ast.ImportSpec
207 var delcomments []*ast.CommentGroup
208
209 // Find the import nodes that import path, if any.
210 for i := 0; i < len(f.Decls); i++ {
211 decl := f.Decls[i]
212 gen, ok := decl.(*ast.GenDecl)
213 if !ok || gen.Tok != token.IMPORT {
214 continue
215 }
216 for j := 0; j < len(gen.Specs); j++ {
217 spec := gen.Specs[j]
218 impspec := spec.(*ast.ImportSpec)
219 if impspec.Name == nil && name != "" {
220 continue
221 }
222 if impspec.Name != nil && impspec.Name.Name != name {
223 continue
224 }
225 if importPath(impspec) != path {
226 continue
227 }
228
229 // We found an import spec that imports path.
230 // Delete it.
231 delspecs = append(delspecs, impspec)
232 deleted = true
233 copy(gen.Specs[j:], gen.Specs[j+1:])
234 gen.Specs = gen.Specs[:len(gen.Specs)-1]
235
236 // If this was the last import spec in this decl,
237 // delete the decl, too.
238 if len(gen.Specs) == 0 {
239 copy(f.Decls[i:], f.Decls[i+1:])
240 f.Decls = f.Decls[:len(f.Decls)-1]
241 i--
242 break
243 } else if len(gen.Specs) == 1 {
244 if impspec.Doc != nil {
245 delcomments = append(delcomments, impspec.Doc)
246 }
247 if impspec.Comment != nil {
248 delcomments = append(delcomments, impspec.Comment)
249 }
250 for _, cg := range f.Comments {
251 // Found comment on the same line as the import spec.
252 if cg.End() < impspec.Pos() && fset.Position(cg.End()).Line == fset.Position(impspec.Pos()).Line {
253 delcomments = append(delcomments, cg)
254 break
255 }
256 }
257
258 spec := gen.Specs[0].(*ast.ImportSpec)
259
260 // Move the documentation right after the import decl.
261 if spec.Doc != nil {
262 for fset.Position(gen.TokPos).Line+1 < fset.Position(spec.Doc.Pos()).Line {
263 fset.File(gen.TokPos).MergeLine(fset.Position(gen.TokPos).Line)
264 }
265 }
266 for _, cg := range f.Comments {
267 if cg.End() < spec.Pos() && fset.Position(cg.End()).Line == fset.Position(spec.Pos()).Line {
268 for fset.Position(gen.TokPos).Line+1 < fset.Position(spec.Pos()).Line {
269 fset.File(gen.TokPos).MergeLine(fset.Position(gen.TokPos).Line)
270 }
271 break
272 }
273 }
274 }
275 if j > 0 {
276 lastImpspec := gen.Specs[j-1].(*ast.ImportSpec)
277 lastLine := fset.Position(lastImpspec.Path.ValuePos).Line
278 line := fset.Position(impspec.Path.ValuePos).Line
279
280 // We deleted an entry but now there may be
281 // a blank line-sized hole where the import was.
282 if line-lastLine > 1 {
283 // There was a blank line immediately preceding the deleted import,
284 // so there's no need to close the hole.
285 // Do nothing.
286 } else if line != fset.File(gen.Rparen).LineCount() {
287 // There was no blank line. Close the hole.
288 fset.File(gen.Rparen).MergeLine(line)
289 }
290 }
291 j--
292 }
293 }
294
295 // Delete imports from f.Imports.
296 for i := 0; i < len(f.Imports); i++ {
297 imp := f.Imports[i]
298 for j, del := range delspecs {
299 if imp == del {
300 copy(f.Imports[i:], f.Imports[i+1:])
301 f.Imports = f.Imports[:len(f.Imports)-1]
302 copy(delspecs[j:], delspecs[j+1:])
303 delspecs = delspecs[:len(delspecs)-1]
304 i--
305 break
306 }
307 }
308 }
309
310 // Delete comments from f.Comments.
311 for i := 0; i < len(f.Comments); i++ {
312 cg := f.Comments[i]
313 for j, del := range delcomments {
314 if cg == del {
315 copy(f.Comments[i:], f.Comments[i+1:])
316 f.Comments = f.Comments[:len(f.Comments)-1]
317 copy(delcomments[j:], delcomments[j+1:])
318 delcomments = delcomments[:len(delcomments)-1]
319 i--
320 break
321 }
322 }
323 }
324
325 if len(delspecs) > 0 {
326 panic(fmt.Sprintf("deleted specs from Decls but not Imports: %v", delspecs))
327 }
328
329 return
330}
331
332// RewriteImport rewrites any import of path oldPath to path newPath.
333func RewriteImport(fset *token.FileSet, f *ast.File, oldPath, newPath string) (rewrote bool) {
334 for _, imp := range f.Imports {
335 if importPath(imp) == oldPath {
336 rewrote = true
337 // record old End, because the default is to compute
338 // it using the length of imp.Path.Value.
339 imp.EndPos = imp.End()
340 imp.Path.Value = strconv.Quote(newPath)
341 }
342 }
343 return
344}
345
346// UsesImport reports whether a given import is used.
347func UsesImport(f *ast.File, path string) (used bool) {
348 spec := importSpec(f, path)
349 if spec == nil {
350 return
351 }
352
353 name := spec.Name.String()
354 switch name {
355 case "<nil>":
356 // If the package name is not explicitly specified,
357 // make an educated guess. This is not guaranteed to be correct.
358 lastSlash := strings.LastIndex(path, "/")
359 if lastSlash == -1 {
360 name = path
361 } else {
362 name = path[lastSlash+1:]
363 }
364 case "_", ".":
365 // Not sure if this import is used - err on the side of caution.
366 return true
367 }
368
369 ast.Walk(visitFn(func(n ast.Node) {
370 sel, ok := n.(*ast.SelectorExpr)
371 if ok && isTopName(sel.X, name) {
372 used = true
373 }
374 }), f)
375
376 return
377}
378
379type visitFn func(node ast.Node)
380
381func (fn visitFn) Visit(node ast.Node) ast.Visitor {
382 fn(node)
383 return fn
384}
385
386// imports returns true if f imports path.
387func imports(f *ast.File, path string) bool {
388 return importSpec(f, path) != nil
389}
390
391// importSpec returns the import spec if f imports path,
392// or nil otherwise.
393func importSpec(f *ast.File, path string) *ast.ImportSpec {
394 for _, s := range f.Imports {
395 if importPath(s) == path {
396 return s
397 }
398 }
399 return nil
400}
401
402// importPath returns the unquoted import path of s,
403// or "" if the path is not properly quoted.
404func importPath(s *ast.ImportSpec) string {
405 t, err := strconv.Unquote(s.Path.Value)
406 if err == nil {
407 return t
408 }
409 return ""
410}
411
412// declImports reports whether gen contains an import of path.
413func declImports(gen *ast.GenDecl, path string) bool {
414 if gen.Tok != token.IMPORT {
415 return false
416 }
417 for _, spec := range gen.Specs {
418 impspec := spec.(*ast.ImportSpec)
419 if importPath(impspec) == path {
420 return true
421 }
422 }
423 return false
424}
425
426// matchLen returns the length of the longest path segment prefix shared by x and y.
427func matchLen(x, y string) int {
428 n := 0
429 for i := 0; i < len(x) && i < len(y) && x[i] == y[i]; i++ {
430 if x[i] == '/' {
431 n++
432 }
433 }
434 return n
435}
436
437// isTopName returns true if n is a top-level unresolved identifier with the given name.
438func isTopName(n ast.Expr, name string) bool {
439 id, ok := n.(*ast.Ident)
440 return ok && id.Name == name && id.Obj == nil
441}
442
443// Imports returns the file imports grouped by paragraph.
444func Imports(fset *token.FileSet, f *ast.File) [][]*ast.ImportSpec {
445 var groups [][]*ast.ImportSpec
446
447 for _, decl := range f.Decls {
448 genDecl, ok := decl.(*ast.GenDecl)
449 if !ok || genDecl.Tok != token.IMPORT {
450 break
451 }
452
453 group := []*ast.ImportSpec{}
454
455 var lastLine int
456 for _, spec := range genDecl.Specs {
457 importSpec := spec.(*ast.ImportSpec)
458 pos := importSpec.Path.ValuePos
459 line := fset.Position(pos).Line
460 if lastLine > 0 && pos > 0 && line-lastLine > 1 {
461 groups = append(groups, group)
462 group = []*ast.ImportSpec{}
463 }
464 group = append(group, importSpec)
465 lastLine = line
466 }
467 groups = append(groups, group)
468 }
469
470 return groups
471}