Source file src/cmd/vendor/golang.org/x/tools/go/ast/astutil/imports.go

     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  }
   488  

View as plain text