imports.go (14003B)
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. 6 package astutil // import "golang.org/x/tools/go/ast/astutil" 7 8 import ( 9 "fmt" 10 "go/ast" 11 "go/token" 12 "reflect" 13 "slices" 14 "strconv" 15 "strings" 16 ) 17 18 // AddImport adds the import path to the file f, if absent. 19 func AddImport(fset *token.FileSet, f *ast.File, path string) (added bool) { 20 return AddNamedImport(fset, f, "", path) 21 } 22 23 // AddNamedImport adds the import with the given name and path to the file f, if absent. 24 // If name is not empty, it is used to rename the import. 25 // 26 // For example, calling 27 // 28 // AddNamedImport(fset, f, "pathpkg", "path") 29 // 30 // adds 31 // 32 // import pathpkg "path" 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 // Find an import decl to add to. 49 // The goal is to find an existing import 50 // whose import path has the longest shared 51 // prefix with path. 52 var ( 53 bestMatch = -1 // length of longest shared prefix 54 lastImport = -1 // index in f.Decls of the file's final import decl 55 impDecl *ast.GenDecl // import decl containing the best match 56 impIndex = -1 // spec index in impDecl containing the best match 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 // Do not add to import "C", to avoid disrupting the 65 // association with its doc comment, breaking cgo. 66 if declImports(gen, "C") { 67 continue 68 } 69 70 // Match an empty import decl if that's all that is available. 71 if len(gen.Specs) == 0 && bestMatch == -1 { 72 impDecl = gen 73 } 74 75 // Compute longest shared prefix with imports in this group and find best 76 // matched import spec. 77 // 1. Always prefer import spec with longest shared prefix. 78 // 2. While match length is 0, 79 // - for stdlib package: prefer first import spec. 80 // - for third party package: prefer first third party import spec. 81 // We cannot use last import spec as best match for third party package 82 // because grouped imports are usually placed last by goimports -local 83 // flag. 84 // See issue #19190. 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 // If no import decl found, add one after the last import. 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 // There are no existing imports. 109 // Our new import, preceded by a blank line, goes after the package declaration 110 // and after the comment, if any, that starts on the same line as the 111 // package declaration. 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 // +2 for a blank line 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 // Insert new import at insertAt. 130 insertAt := 0 131 if impIndex >= 0 { 132 // insert after the found import 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 // If there is a comment after an existing import, preserve the comment 141 // position by adding the new import after the comment. 142 if spec, ok := impDecl.Specs[insertAt-1].(*ast.ImportSpec); ok && spec.Comment != nil { 143 pos = spec.Comment.End() 144 } else { 145 // Assign same position as the previous import, 146 // so that the sorter sees it as being in the same block. 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 // Clean up parens. impDecl contains at least one spec. 157 if len(impDecl.Specs) == 1 { 158 // Remove unneeded parens. 159 impDecl.Lparen = token.NoPos 160 } else if !impDecl.Lparen.IsValid() { 161 // impDecl needs parens added. 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 // Merge all the import declarations into the first one. 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 // Don't touch the first one. 182 } 183 // We now know there is more than one package in this import 184 // declaration. Ensure that it ends up parenthesized. 185 first.Lparen = first.Pos() 186 // Move the imports of the other import declaration to the first one. 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 // Third party package import path usually contains "." (".com", ".org", ...) 200 // This logic is taken from golang.org/x/tools/imports package. 201 return strings.Contains(importPath, ".") 202 } 203 204 // DeleteImport deletes the import path from the file f, if present. 205 // If there are duplicate import declarations, all matching ones are deleted. 206 func DeleteImport(fset *token.FileSet, f *ast.File, path string) (deleted bool) { 207 return DeleteNamedImport(fset, f, "", path) 208 } 209 210 // DeleteNamedImport deletes the import with the given name and path from the file f, if present. 211 // If there are duplicate import declarations, all matching ones are deleted. 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 // Find the import nodes that import path, if any. 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 // We found an import spec that imports path. 231 // Delete it. 232 delspecs[impspec] = true 233 deleted = true 234 gen.Specs = slices.Delete(gen.Specs, j, j+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 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 // Found comment on the same line as the import spec. 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 // Move the documentation right after the import decl. 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 // We deleted an entry but now there may be 280 // a blank line-sized hole where the import was. 281 if line-lastLine > 1 || !gen.Rparen.IsValid() { 282 // There was a blank line immediately preceding the deleted import, 283 // so there's no need to close the hole. The right parenthesis is 284 // invalid after AddImport to an import statement without parenthesis. 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 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 // This can happen when the AST is invalid (i.e. imports differ between f.Decls and f.Imports). 303 panic(fmt.Sprintf("deleted specs from Decls but not Imports: %v", delspecs)) 304 } 305 306 // Delete comments from f.Comments. 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 // RewriteImport rewrites any import of path oldPath to path newPath. 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 // record old End, because the default is to compute 321 // it using the length of imp.Path.Value. 322 imp.EndPos = imp.End() 323 imp.Path.Value = strconv.Quote(newPath) 324 } 325 } 326 return 327 } 328 329 // UsesImport reports whether a given import is used. 330 // The provided File must have been parsed with syntactic object resolution 331 // (not using go/parser.SkipObjectResolution). 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 // If the package name is not explicitly specified, 345 // make an educated guess. This is not guaranteed to be correct. 346 lastSlash := strings.LastIndex(path, "/") 347 if lastSlash == -1 { 348 name = path 349 } else { 350 name = path[lastSlash+1:] 351 } 352 case "_", ".": 353 // Not sure if this import is used - err on the side of caution. 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 // imports reports whether f has an import with the specified name and path. 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 // importSpec returns the import spec if f imports path, 385 // or nil otherwise. 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 // importName returns the name of s, 396 // or "" if the import is not named. 397 func importName(s *ast.ImportSpec) string { 398 if s.Name == nil { 399 return "" 400 } 401 return s.Name.Name 402 } 403 404 // importPath returns the unquoted import path of s, 405 // or "" if the path is not properly quoted. 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 // declImports reports whether gen contains an import of path. 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 // matchLen returns the length of the longest path segment prefix shared by x and y. 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 // isTopName returns true if n is a top-level unresolved identifier with the given name. 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 // Imports returns the file imports grouped by paragraph. 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 // updateBasicLitPos updates lit.Pos, 476 // ensuring that lit.End (if set) is displaced by the same amount. 477 // (See https://go.dev/issue/76395.) 478 func updateBasicLitPos(lit *ast.BasicLit, pos token.Pos) { 479 len := lit.End() - lit.Pos() 480 lit.ValuePos = pos 481 // TODO(adonovan): after go1.26, simplify to: 482 // lit.ValueEnd = pos + len 483 v := reflect.ValueOf(lit).Elem().FieldByName("ValueEnd") 484 if v.IsValid() && v.Int() != 0 { 485 v.SetInt(int64(pos + len)) 486 } 487 }