// Copyright 2013 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package astutil contains common utilities for working with the Go AST.
package astutil // import "golang.org/x/tools/go/ast/astutil" import ( ) // AddImport adds the import path to the file f, if absent. func ( *token.FileSet, *ast.File, string) ( bool) { return AddNamedImport(, , "", ) } // AddNamedImport adds the import with the given name and path to the file f, if absent. // If name is not empty, it is used to rename the import. // // For example, calling // // AddNamedImport(fset, f, "pathpkg", "path") // // adds // // import pathpkg "path" func ( *token.FileSet, *ast.File, , string) ( bool) { if imports(, , ) { return false } := &ast.ImportSpec{ Path: &ast.BasicLit{ Kind: token.STRING, Value: strconv.Quote(), }, } if != "" { .Name = &ast.Ident{Name: } } // Find an import decl to add to. // The goal is to find an existing import // whose import path has the longest shared // prefix with path. var ( = -1 // length of longest shared prefix = -1 // index in f.Decls of the file's final import decl *ast.GenDecl // import decl containing the best match = -1 // spec index in impDecl containing the best match = isThirdParty() ) for , := range .Decls { , := .(*ast.GenDecl) if && .Tok == token.IMPORT { = // Do not add to import "C", to avoid disrupting the // association with its doc comment, breaking cgo. if declImports(, "C") { continue } // Match an empty import decl if that's all that is available. if len(.Specs) == 0 && == -1 { = } // Compute longest shared prefix with imports in this group and find best // matched import spec. // 1. Always prefer import spec with longest shared prefix. // 2. While match length is 0, // - for stdlib package: prefer first import spec. // - for third party package: prefer first third party import spec. // We cannot use last import spec as best match for third party package // because grouped imports are usually placed last by goimports -local // flag. // See issue #19190. := false for , := range .Specs { := .(*ast.ImportSpec) := importPath() := matchLen(, ) if > || ( == 0 && ! && ) { = = = } = || isThirdParty() } } } // If no import decl found, add one after the last import. if == nil { = &ast.GenDecl{ Tok: token.IMPORT, } if >= 0 { .TokPos = .Decls[].End() } else { // There are no existing imports. // Our new import, preceded by a blank line, goes after the package declaration // and after the comment, if any, that starts on the same line as the // package declaration. .TokPos = .Package := .File(.Package) := .Line(.Package) for , := range .Comments { if .Line(.Pos()) > { break } // +2 for a blank line .TokPos = .End() + 2 } } .Decls = append(.Decls, nil) copy(.Decls[+2:], .Decls[+1:]) .Decls[+1] = } // Insert new import at insertAt. := 0 if >= 0 { // insert after the found import = + 1 } .Specs = append(.Specs, nil) copy(.Specs[+1:], .Specs[:]) .Specs[] = := .Pos() if > 0 { // If there is a comment after an existing import, preserve the comment // position by adding the new import after the comment. if , := .Specs[-1].(*ast.ImportSpec); && .Comment != nil { = .Comment.End() } else { // Assign same position as the previous import, // so that the sorter sees it as being in the same block. = .Specs[-1].Pos() } } if .Name != nil { .Name.NamePos = } updateBasicLitPos(.Path, ) .EndPos = // Clean up parens. impDecl contains at least one spec. if len(.Specs) == 1 { // Remove unneeded parens. .Lparen = token.NoPos } else if !.Lparen.IsValid() { // impDecl needs parens added. .Lparen = .Specs[0].Pos() } .Imports = append(.Imports, ) if len(.Decls) <= 1 { return true } // Merge all the import declarations into the first one. var *ast.GenDecl for := 0; < len(.Decls); ++ { := .Decls[] , := .(*ast.GenDecl) if ! || .Tok != token.IMPORT || declImports(, "C") { continue } if == nil { = continue // Don't touch the first one. } // We now know there is more than one package in this import // declaration. Ensure that it ends up parenthesized. .Lparen = .Pos() // Move the imports of the other import declaration to the first one. for , := range .Specs { updateBasicLitPos(.(*ast.ImportSpec).Path, .Pos()) .Specs = append(.Specs, ) } .Decls = slices.Delete(.Decls, , +1) -- } return true } func isThirdParty( string) bool { // Third party package import path usually contains "." (".com", ".org", ...) // This logic is taken from golang.org/x/tools/imports package. return strings.Contains(, ".") } // DeleteImport deletes the import path from the file f, if present. // If there are duplicate import declarations, all matching ones are deleted. func ( *token.FileSet, *ast.File, string) ( bool) { return DeleteNamedImport(, , "", ) } // DeleteNamedImport deletes the import with the given name and path from the file f, if present. // If there are duplicate import declarations, all matching ones are deleted. func ( *token.FileSet, *ast.File, , string) ( bool) { var ( = make(map[*ast.ImportSpec]bool) = make(map[*ast.CommentGroup]bool) ) // Find the import nodes that import path, if any. for := 0; < len(.Decls); ++ { , := .Decls[].(*ast.GenDecl) if ! || .Tok != token.IMPORT { continue } for := 0; < len(.Specs); ++ { := .Specs[].(*ast.ImportSpec) if importName() != || importPath() != { continue } // We found an import spec that imports path. // Delete it. [] = true = true .Specs = slices.Delete(.Specs, , +1) // If this was the last import spec in this decl, // delete the decl, too. if len(.Specs) == 0 { .Decls = slices.Delete(.Decls, , +1) -- break } else if len(.Specs) == 1 { if .Doc != nil { [.Doc] = true } if .Comment != nil { [.Comment] = true } for , := range .Comments { // Found comment on the same line as the import spec. if .End() < .Pos() && .Position(.End()).Line == .Position(.Pos()).Line { [] = true break } } := .Specs[0].(*ast.ImportSpec) // Move the documentation right after the import decl. if .Doc != nil { for .Position(.TokPos).Line+1 < .Position(.Doc.Pos()).Line { .File(.TokPos).MergeLine(.Position(.TokPos).Line) } } for , := range .Comments { if .End() < .Pos() && .Position(.End()).Line == .Position(.Pos()).Line { for .Position(.TokPos).Line+1 < .Position(.Pos()).Line { .File(.TokPos).MergeLine(.Position(.TokPos).Line) } break } } } if > 0 { := .Specs[-1].(*ast.ImportSpec) := .PositionFor(.Path.ValuePos, false).Line := .PositionFor(.Path.ValuePos, false).Line // We deleted an entry but now there may be // a blank line-sized hole where the import was. if - > 1 || !.Rparen.IsValid() { // There was a blank line immediately preceding the deleted import, // so there's no need to close the hole. The right parenthesis is // invalid after AddImport to an import statement without parenthesis. // Do nothing. } else if != .File(.Rparen).LineCount() { // There was no blank line. Close the hole. .File(.Rparen).MergeLine() } } -- } } // Delete imports from f.Imports. := len(.Imports) .Imports = slices.DeleteFunc(.Imports, func( *ast.ImportSpec) bool { , := [] return }) if len(.Imports)+len() != { // This can happen when the AST is invalid (i.e. imports differ between f.Decls and f.Imports). panic(fmt.Sprintf("deleted specs from Decls but not Imports: %v", )) } // Delete comments from f.Comments. .Comments = slices.DeleteFunc(.Comments, func( *ast.CommentGroup) bool { , := [] return }) return } // RewriteImport rewrites any import of path oldPath to path newPath. func ( *token.FileSet, *ast.File, , string) ( bool) { for , := range .Imports { if importPath() == { = true // record old End, because the default is to compute // it using the length of imp.Path.Value. .EndPos = .End() .Path.Value = strconv.Quote() } } return } // UsesImport reports whether a given import is used. // The provided File must have been parsed with syntactic object resolution // (not using go/parser.SkipObjectResolution). func ( *ast.File, string) ( bool) { if .Scope == nil { panic("file f was not parsed with syntactic object resolution") } := importSpec(, ) if == nil { return } := .Name.String() switch { case "<nil>": // If the package name is not explicitly specified, // make an educated guess. This is not guaranteed to be correct. := strings.LastIndex(, "/") if == -1 { = } else { = [+1:] } case "_", ".": // Not sure if this import is used - err on the side of caution. return true } ast.Walk(visitFn(func( ast.Node) { , := .(*ast.SelectorExpr) if && isTopName(.X, ) { = true } }), ) return } type visitFn func(node ast.Node) func ( visitFn) ( ast.Node) ast.Visitor { () return } // imports reports whether f has an import with the specified name and path. func imports( *ast.File, , string) bool { for , := range .Imports { if importName() == && importPath() == { return true } } return false } // importSpec returns the import spec if f imports path, // or nil otherwise. func importSpec( *ast.File, string) *ast.ImportSpec { for , := range .Imports { if importPath() == { return } } return nil } // importName returns the name of s, // or "" if the import is not named. func importName( *ast.ImportSpec) string { if .Name == nil { return "" } return .Name.Name } // importPath returns the unquoted import path of s, // or "" if the path is not properly quoted. func importPath( *ast.ImportSpec) string { , := strconv.Unquote(.Path.Value) if != nil { return "" } return } // declImports reports whether gen contains an import of path. func declImports( *ast.GenDecl, string) bool { if .Tok != token.IMPORT { return false } for , := range .Specs { := .(*ast.ImportSpec) if importPath() == { return true } } return false } // matchLen returns the length of the longest path segment prefix shared by x and y. func matchLen(, string) int { := 0 for := 0; < len() && < len() && [] == []; ++ { if [] == '/' { ++ } } return } // isTopName returns true if n is a top-level unresolved identifier with the given name. func isTopName( ast.Expr, string) bool { , := .(*ast.Ident) return && .Name == && .Obj == nil } // Imports returns the file imports grouped by paragraph. func ( *token.FileSet, *ast.File) [][]*ast.ImportSpec { var [][]*ast.ImportSpec for , := range .Decls { , := .(*ast.GenDecl) if ! || .Tok != token.IMPORT { break } := []*ast.ImportSpec{} var int for , := range .Specs { := .(*ast.ImportSpec) := .Path.ValuePos := .Position().Line if > 0 && > 0 && - > 1 { = append(, ) = []*ast.ImportSpec{} } = append(, ) = } = append(, ) } return } // updateBasicLitPos updates lit.Pos, // ensuring that lit.End (if set) is displaced by the same amount. // (See https://go.dev/issue/76395.) func updateBasicLitPos( *ast.BasicLit, token.Pos) { := .End() - .Pos() .ValuePos = // TODO(adonovan): after go1.26, simplify to: // lit.ValueEnd = pos + len := reflect.ValueOf().Elem().FieldByName("ValueEnd") if .IsValid() && .Int() != 0 { .SetInt(int64( + )) } }