src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

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 }