mirror of
https://github.com/golang/go
synced 2024-11-18 17:54:57 -07:00
go/ast/astutil: allow AddNamedImport to add imports with different names
This change makes AddNamedImport more symmetrical to DeleteNamedImport in behavior, signature, and documentation. In Go, it is valid to import a package (identified by an import path) with different names in a single .go file¹. For example, this is ok, and the program will build as long as there are no unused imports²: import ( "path" . "path" _ "path" pathpkg "path" ) The four imports (represented by ast.ImportSpec type) have the same import path, but different names. It's currently possible to use DeleteNamedImport to delete the exact {name, import path} ast.ImportSpec from an AST. Previously, it wasn't possible to use AddNamedImport to add an exact {name, import path} ast.ImportSpec if the AST already has another ast.ImportSpec with the same import path (but different name). This change fixes that, making it possible to use AddNamedImport as expected. Rename the ipath parameter to path in AddNamedImport for consistency with DeleteNamedImport. Also change the language used in its documentation to be more precise and consistent with DeleteNamedImport. Add test cases for this behavior. The DeleteNamedImport test cases were passing before any changes to the astutil code. Add test coverage for the return bool value of {Add,Delete}NamedImport. Document the behavior of DeleteNamedImport when there are duplicate matching imports. Simplify string(buf.Bytes()) to buf.String() in test helper code. ¹ https://golang.org/ref/spec#Import_declarations ² https://play.golang.org/p/bi6L34rQbcD Fixes golang/go#28605 Fixes golang/go#16411 Change-Id: I70e887f5174764ab1cf32a761c8c734e216eea67 Reviewed-on: https://go-review.googlesource.com/c/147448 Run-TryBot: Dmitri Shuralyov <dmitshur@golang.org> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: Alan Donovan <adonovan@google.com>
This commit is contained in:
parent
34b416bd17
commit
4ca49954c3
@ -14,26 +14,26 @@ import (
|
||||
)
|
||||
|
||||
// AddImport adds the import path to the file f, if absent.
|
||||
func AddImport(fset *token.FileSet, f *ast.File, ipath string) (added bool) {
|
||||
return AddNamedImport(fset, f, "", ipath)
|
||||
func AddImport(fset *token.FileSet, f *ast.File, path string) (added bool) {
|
||||
return AddNamedImport(fset, f, "", path)
|
||||
}
|
||||
|
||||
// AddNamedImport adds the import path to the file f, if absent.
|
||||
// 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 AddNamedImport(fset *token.FileSet, f *ast.File, name, ipath string) (added bool) {
|
||||
if imports(f, ipath) {
|
||||
func AddNamedImport(fset *token.FileSet, f *ast.File, name, path string) (added bool) {
|
||||
if imports(f, name, path) {
|
||||
return false
|
||||
}
|
||||
|
||||
newImport := &ast.ImportSpec{
|
||||
Path: &ast.BasicLit{
|
||||
Kind: token.STRING,
|
||||
Value: strconv.Quote(ipath),
|
||||
Value: strconv.Quote(path),
|
||||
},
|
||||
}
|
||||
if name != "" {
|
||||
@ -43,14 +43,14 @@ func AddNamedImport(fset *token.FileSet, f *ast.File, name, ipath string) (added
|
||||
// Find an import decl to add to.
|
||||
// The goal is to find an existing import
|
||||
// whose import path has the longest shared
|
||||
// prefix with ipath.
|
||||
// prefix with path.
|
||||
var (
|
||||
bestMatch = -1 // length of longest shared prefix
|
||||
lastImport = -1 // index in f.Decls of the file's final import decl
|
||||
impDecl *ast.GenDecl // import decl containing the best match
|
||||
impIndex = -1 // spec index in impDecl containing the best match
|
||||
|
||||
isThirdPartyPath = isThirdParty(ipath)
|
||||
isThirdPartyPath = isThirdParty(path)
|
||||
)
|
||||
for i, decl := range f.Decls {
|
||||
gen, ok := decl.(*ast.GenDecl)
|
||||
@ -81,7 +81,7 @@ func AddNamedImport(fset *token.FileSet, f *ast.File, name, ipath string) (added
|
||||
for j, spec := range gen.Specs {
|
||||
impspec := spec.(*ast.ImportSpec)
|
||||
p := importPath(impspec)
|
||||
n := matchLen(p, ipath)
|
||||
n := matchLen(p, path)
|
||||
if n > bestMatch || (bestMatch == 0 && !seenAnyThirdParty && isThirdPartyPath) {
|
||||
bestMatch = n
|
||||
impDecl = gen
|
||||
@ -197,11 +197,13 @@ func isThirdParty(importPath string) bool {
|
||||
}
|
||||
|
||||
// DeleteImport deletes the import path from the file f, if present.
|
||||
// If there are duplicate import declarations, all matching ones are deleted.
|
||||
func DeleteImport(fset *token.FileSet, f *ast.File, path string) (deleted bool) {
|
||||
return DeleteNamedImport(fset, f, "", path)
|
||||
}
|
||||
|
||||
// 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 DeleteNamedImport(fset *token.FileSet, f *ast.File, name, path string) (deleted bool) {
|
||||
var delspecs []*ast.ImportSpec
|
||||
var delcomments []*ast.CommentGroup
|
||||
@ -216,13 +218,7 @@ func DeleteNamedImport(fset *token.FileSet, f *ast.File, name, path string) (del
|
||||
for j := 0; j < len(gen.Specs); j++ {
|
||||
spec := gen.Specs[j]
|
||||
impspec := spec.(*ast.ImportSpec)
|
||||
if impspec.Name == nil && name != "" {
|
||||
continue
|
||||
}
|
||||
if impspec.Name != nil && impspec.Name.Name != name {
|
||||
continue
|
||||
}
|
||||
if importPath(impspec) != path {
|
||||
if importName(impspec) != name || importPath(impspec) != path {
|
||||
continue
|
||||
}
|
||||
|
||||
@ -383,9 +379,14 @@ func (fn visitFn) Visit(node ast.Node) ast.Visitor {
|
||||
return fn
|
||||
}
|
||||
|
||||
// imports returns true if f imports path.
|
||||
func imports(f *ast.File, path string) bool {
|
||||
return importSpec(f, path) != nil
|
||||
// imports reports whether f has an import with the specified name and path.
|
||||
func imports(f *ast.File, name, path string) bool {
|
||||
for _, s := range f.Imports {
|
||||
if importName(s) == name && importPath(s) == path {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// importSpec returns the import spec if f imports path,
|
||||
@ -399,14 +400,23 @@ func importSpec(f *ast.File, path string) *ast.ImportSpec {
|
||||
return nil
|
||||
}
|
||||
|
||||
// importName returns the name of s,
|
||||
// or "" if the import is not named.
|
||||
func importName(s *ast.ImportSpec) string {
|
||||
if s.Name == nil {
|
||||
return ""
|
||||
}
|
||||
return s.Name.Name
|
||||
}
|
||||
|
||||
// importPath returns the unquoted import path of s,
|
||||
// or "" if the path is not properly quoted.
|
||||
func importPath(s *ast.ImportSpec) string {
|
||||
t, err := strconv.Unquote(s.Path.Value)
|
||||
if err == nil {
|
||||
return t
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return ""
|
||||
return t
|
||||
}
|
||||
|
||||
// declImports reports whether gen contains an import of path.
|
||||
|
@ -30,7 +30,7 @@ func print(t *testing.T, name string, f *ast.File) string {
|
||||
if err := format.Node(&buf, fset, f); err != nil {
|
||||
t.Fatalf("%s gofmt: %v", name, err)
|
||||
}
|
||||
return string(buf.Bytes())
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
type test struct {
|
||||
@ -39,7 +39,7 @@ type test struct {
|
||||
pkg string
|
||||
in string
|
||||
out string
|
||||
broken bool // known broken
|
||||
unchanged bool // Expect added/deleted return value to be false.
|
||||
}
|
||||
|
||||
var addTests = []test{
|
||||
@ -58,6 +58,7 @@ import (
|
||||
"os"
|
||||
)
|
||||
`,
|
||||
unchanged: true,
|
||||
},
|
||||
{
|
||||
name: "import.1",
|
||||
@ -657,6 +658,117 @@ import (
|
||||
)
|
||||
`,
|
||||
},
|
||||
|
||||
// Issue 28605: Add specified import, even if that import path is imported under another name
|
||||
{
|
||||
name: "issue 28605 add unnamed path",
|
||||
renamedPkg: "",
|
||||
pkg: "path",
|
||||
in: `package main
|
||||
|
||||
import (
|
||||
. "path"
|
||||
_ "path"
|
||||
pathpkg "path"
|
||||
)
|
||||
`,
|
||||
out: `package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
. "path"
|
||||
_ "path"
|
||||
pathpkg "path"
|
||||
)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "issue 28605 add pathpkg-renamed path",
|
||||
renamedPkg: "pathpkg",
|
||||
pkg: "path",
|
||||
in: `package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
. "path"
|
||||
_ "path"
|
||||
)
|
||||
`,
|
||||
out: `package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
. "path"
|
||||
_ "path"
|
||||
pathpkg "path"
|
||||
)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "issue 28605 add blank identifier path",
|
||||
renamedPkg: "_",
|
||||
pkg: "path",
|
||||
in: `package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
. "path"
|
||||
pathpkg "path"
|
||||
)
|
||||
`,
|
||||
out: `package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
. "path"
|
||||
_ "path"
|
||||
pathpkg "path"
|
||||
)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "issue 28605 add dot import path",
|
||||
renamedPkg: ".",
|
||||
pkg: "path",
|
||||
in: `package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
_ "path"
|
||||
pathpkg "path"
|
||||
)
|
||||
`,
|
||||
out: `package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
. "path"
|
||||
_ "path"
|
||||
pathpkg "path"
|
||||
)
|
||||
`,
|
||||
},
|
||||
|
||||
{
|
||||
name: "duplicate import declarations, add existing one",
|
||||
renamedPkg: "f",
|
||||
pkg: "fmt",
|
||||
in: `package main
|
||||
|
||||
import "fmt"
|
||||
import "fmt"
|
||||
import f "fmt"
|
||||
import f "fmt"
|
||||
`,
|
||||
out: `package main
|
||||
|
||||
import "fmt"
|
||||
import "fmt"
|
||||
import f "fmt"
|
||||
import f "fmt"
|
||||
`,
|
||||
unchanged: true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestAddImport(t *testing.T) {
|
||||
@ -664,18 +776,26 @@ func TestAddImport(t *testing.T) {
|
||||
file := parse(t, test.name, test.in)
|
||||
var before bytes.Buffer
|
||||
ast.Fprint(&before, fset, file, nil)
|
||||
AddNamedImport(fset, file, test.renamedPkg, test.pkg)
|
||||
added := AddNamedImport(fset, file, test.renamedPkg, test.pkg)
|
||||
if got := print(t, test.name, file); got != test.out {
|
||||
if test.broken {
|
||||
t.Logf("%s is known broken:\ngot: %s\nwant: %s", test.name, got, test.out)
|
||||
} else {
|
||||
t.Errorf("%s:\ngot: %s\nwant: %s", test.name, got, test.out)
|
||||
}
|
||||
t.Errorf("first run: %s:\ngot: %s\nwant: %s", test.name, got, test.out)
|
||||
var after bytes.Buffer
|
||||
ast.Fprint(&after, fset, file, nil)
|
||||
|
||||
t.Logf("AST before:\n%s\nAST after:\n%s\n", before.String(), after.String())
|
||||
}
|
||||
if got, want := added, !test.unchanged; got != want {
|
||||
t.Errorf("first run: %s: added = %v, want %v", test.name, got, want)
|
||||
}
|
||||
|
||||
// AddNamedImport should be idempotent. Verify that by calling it again,
|
||||
// expecting no change to the AST, and the returned added value to always be false.
|
||||
added = AddNamedImport(fset, file, test.renamedPkg, test.pkg)
|
||||
if got := print(t, test.name, file); got != test.out {
|
||||
t.Errorf("second run: %s:\ngot: %s\nwant: %s", test.name, got, test.out)
|
||||
}
|
||||
if got, want := added, false; got != want {
|
||||
t.Errorf("second run: %s: added = %v, want %v", test.name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1405,14 +1525,161 @@ import (
|
||||
)
|
||||
`,
|
||||
},
|
||||
|
||||
// Issue 28605: Delete specified import, even if that import path is imported under another name
|
||||
{
|
||||
name: "import.38",
|
||||
renamedPkg: "",
|
||||
pkg: "path",
|
||||
in: `package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
. "path"
|
||||
_ "path"
|
||||
pathpkg "path"
|
||||
)
|
||||
`,
|
||||
out: `package main
|
||||
|
||||
import (
|
||||
. "path"
|
||||
_ "path"
|
||||
pathpkg "path"
|
||||
)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "import.39",
|
||||
renamedPkg: "pathpkg",
|
||||
pkg: "path",
|
||||
in: `package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
. "path"
|
||||
_ "path"
|
||||
pathpkg "path"
|
||||
)
|
||||
`,
|
||||
out: `package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
. "path"
|
||||
_ "path"
|
||||
)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "import.40",
|
||||
renamedPkg: "_",
|
||||
pkg: "path",
|
||||
in: `package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
. "path"
|
||||
_ "path"
|
||||
pathpkg "path"
|
||||
)
|
||||
`,
|
||||
out: `package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
. "path"
|
||||
pathpkg "path"
|
||||
)
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "import.41",
|
||||
renamedPkg: ".",
|
||||
pkg: "path",
|
||||
in: `package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
. "path"
|
||||
_ "path"
|
||||
pathpkg "path"
|
||||
)
|
||||
`,
|
||||
out: `package main
|
||||
|
||||
import (
|
||||
"path"
|
||||
_ "path"
|
||||
pathpkg "path"
|
||||
)
|
||||
`,
|
||||
},
|
||||
|
||||
// Duplicate import declarations, all matching ones are deleted.
|
||||
{
|
||||
name: "import.42",
|
||||
renamedPkg: "f",
|
||||
pkg: "fmt",
|
||||
in: `package main
|
||||
|
||||
import "fmt"
|
||||
import "fmt"
|
||||
import f "fmt"
|
||||
import f "fmt"
|
||||
`,
|
||||
out: `package main
|
||||
|
||||
import "fmt"
|
||||
import "fmt"
|
||||
`,
|
||||
},
|
||||
{
|
||||
name: "import.43",
|
||||
renamedPkg: "x",
|
||||
pkg: "fmt",
|
||||
in: `package main
|
||||
|
||||
import "fmt"
|
||||
import "fmt"
|
||||
import f "fmt"
|
||||
import f "fmt"
|
||||
`,
|
||||
out: `package main
|
||||
|
||||
import "fmt"
|
||||
import "fmt"
|
||||
import f "fmt"
|
||||
import f "fmt"
|
||||
`,
|
||||
unchanged: true,
|
||||
},
|
||||
}
|
||||
|
||||
func TestDeleteImport(t *testing.T) {
|
||||
for _, test := range deleteTests {
|
||||
file := parse(t, test.name, test.in)
|
||||
DeleteNamedImport(fset, file, test.renamedPkg, test.pkg)
|
||||
var before bytes.Buffer
|
||||
ast.Fprint(&before, fset, file, nil)
|
||||
deleted := DeleteNamedImport(fset, file, test.renamedPkg, test.pkg)
|
||||
if got := print(t, test.name, file); got != test.out {
|
||||
t.Errorf("%s:\ngot: %s\nwant: %s", test.name, got, test.out)
|
||||
t.Errorf("first run: %s:\ngot: %s\nwant: %s", test.name, got, test.out)
|
||||
var after bytes.Buffer
|
||||
ast.Fprint(&after, fset, file, nil)
|
||||
t.Logf("AST before:\n%s\nAST after:\n%s\n", before.String(), after.String())
|
||||
}
|
||||
if got, want := deleted, !test.unchanged; got != want {
|
||||
t.Errorf("first run: %s: deleted = %v, want %v", test.name, got, want)
|
||||
}
|
||||
|
||||
// DeleteNamedImport should be idempotent. Verify that by calling it again,
|
||||
// expecting no change to the AST, and the returned deleted value to always be false.
|
||||
deleted = DeleteNamedImport(fset, file, test.renamedPkg, test.pkg)
|
||||
if got := print(t, test.name, file); got != test.out {
|
||||
t.Errorf("second run: %s:\ngot: %s\nwant: %s", test.name, got, test.out)
|
||||
}
|
||||
if got, want := deleted, false; got != want {
|
||||
t.Errorf("second run: %s: deleted = %v, want %v", test.name, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user