diff --git a/go.mod b/go.mod index f6632b2..50f53da 100644 --- a/go.mod +++ b/go.mod @@ -1,9 +1,17 @@ module github.com/vburenin/ifacemaker +go 1.18 + require ( - github.com/davecgh/go-spew v1.1.1 // indirect github.com/jessevdk/go-flags v1.4.1-0.20181029123624-5de817a9aa20 - github.com/pmezard/go-difflib v1.0.0 // indirect github.com/stretchr/testify v1.2.2 golang.org/x/tools v0.1.10 ) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 // indirect + golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 // indirect + golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect +) diff --git a/go.sum b/go.sum index aae14e1..2853184 100644 --- a/go.sum +++ b/go.sum @@ -1,39 +1,16 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= -github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/jessevdk/go-flags v1.4.1-0.20181029123624-5de817a9aa20 h1:dAOsPLhnBzIyxu0VvmnKjlNcIlgMK+erD6VRHDtweMI= github.com/jessevdk/go-flags v1.4.1-0.20181029123624-5de817a9aa20/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3 h1:kQgndtyPBW/JIYERgdxfwMYh3AVStj88WQTlNDi2a+o= golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654 h1:id054HUawV2/6IGm2IV8KZQjqtwAOo2CYlOToYqa0d0= golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.10 h1:QjFRCZxdOhBJ/UNgnBZLbNV13DlbnK0quyivTnXJM20= golang.org/x/tools v0.1.10/go.mod h1:Uh6Zz+xoGYZom868N8YTex3t7RhtHDBrE8Gzo9bV56E= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/ifacemaker.go b/ifacemaker.go index d40af71..fe0e23f 100644 --- a/ifacemaker.go +++ b/ifacemaker.go @@ -8,6 +8,7 @@ import ( "path/filepath" "github.com/jessevdk/go-flags" + "github.com/vburenin/ifacemaker/maker" ) @@ -17,6 +18,7 @@ type cmdlineArgs struct { IfaceName string `short:"i" long:"iface" description:"Name of the generated interface" required:"true"` PkgName string `short:"p" long:"pkg" description:"Package name for the generated interface" required:"true"` IfaceComment string `short:"y" long:"iface-comment" description:"Comment for the interface, default is '// ...'"` + ImportModule string `short:"m" long:"import-module" description:"Fully qualified module import for packages with a different target package '// ...'"` // jessevdk/go-flags doesn't support default values for boolean flags, // so we use a string for backwards-compatibility and then convert it to a bool later. @@ -58,7 +60,17 @@ func main() { } files = append(files, matches...) } - result, err := maker.Make(files, args.StructType, args.Comment, args.PkgName, args.IfaceName, args.IfaceComment, args.copyDocs, args.CopyTypeDoc) + result, err := maker.Make(maker.MakeOptions{ + Files: files, + StructType: args.StructType, + Comment: args.Comment, + PkgName: args.PkgName, + IfaceName: args.IfaceName, + IfaceComment: args.IfaceComment, + CopyDocs: args.copyDocs, + CopyTypeDoc: args.CopyTypeDoc, + ImportModule: args.ImportModule, + }) if err != nil { log.Fatal(err.Error()) } @@ -66,6 +78,8 @@ func main() { if args.Output == "" { fmt.Println(string(result)) } else { - ioutil.WriteFile(args.Output, result, 0644) + if err := ioutil.WriteFile(args.Output, result, 0644); err != nil { + log.Fatal(err) + } } } diff --git a/ifacemaker_test.go b/ifacemaker_test.go index ec1550f..14b000a 100644 --- a/ifacemaker_test.go +++ b/ifacemaker_test.go @@ -106,10 +106,22 @@ type User struct { Name string }` +var src4 = `package footest + +// Hammer is in the same package but in a different file. +type Smiter struct { + options Options +} +func (s *Smiter) Smite(weapon Hammer) error { + return nil +} +` + var srcFile = os.TempDir() + "/ifacemaker_src.go" var srcFile2 = os.TempDir() + "/test_impl.go" var srcFile2_ext = os.TempDir() + "/test_impl_extended.go" var srcFile3 = os.TempDir() + "/footest/footest.go" +var srcFile4 = os.TempDir() + "/footest/smiter.go" func TestMain(m *testing.M) { dirPath := os.TempDir() + "/footest" @@ -123,6 +135,7 @@ func TestMain(m *testing.M) { writeTestSourceFile(src2, srcFile2) writeTestSourceFile(src2_extend, srcFile2_ext) writeTestSourceFile(src3, srcFile3) + writeTestSourceFile(src4, srcFile4) os.Exit(m.Run()) } @@ -335,6 +348,30 @@ type TestInterface interface { assert.Equal(t, expected, out) } +func TestMainUsingDeclarationInSamePackage(t *testing.T) { + os.Args = []string{"cmd", "-f", srcFile4, "-m", "github.com/test/footest", "-s", "Smiter", "-i", "Smiter", "-p", "another", "-c", "DO NOT EDIT: Auto generated", "-d=false"} + out := captureStdout(func() { + main() + }) + + expected := `// DO NOT EDIT: Auto generated + +package another + +import ( + . "github.com/test/footest" +) + +// Smiter ... +type Smiter interface { + Smite(weapon Hammer) error +} + +` + + assert.Equal(t, expected, out) +} + // not thread safe func captureStdout(f func()) string { old := os.Stdout diff --git a/maker/maker.go b/maker/maker.go index 0ed5e23..82cea86 100644 --- a/maker/maker.go +++ b/maker/maker.go @@ -9,6 +9,7 @@ import ( "io/ioutil" "log" "regexp" + "strconv" "strings" "golang.org/x/tools/imports" @@ -22,8 +23,8 @@ type Method struct { } // declaredType identifies the name and package of a type declaration. -type declaredType struct { - Name string +type declaredType struct { + Name string Package string } @@ -32,7 +33,6 @@ func (dt declaredType) Fullname() string { return fmt.Sprintf("%s.%s", dt.Package, dt.Name) } - // Lines return a []string consisting of // the documentation and code appended // in chronological order @@ -62,7 +62,7 @@ func GetTypeDeclarationName(decl ast.Decl) string { return "" } typeName = typeSpec.Name.Name - break // assuming first value is the good one. + break // assuming first value is the good one. } return typeName @@ -204,8 +204,6 @@ func MakeInterface(comment, pkgName, ifaceName, ifaceComment string, methods []s return FormatCode(code) } - - // ParseDeclaredTypes inspect given src code to find type declaractions. func ParseDeclaredTypes(src []byte) (declaredTypes []declaredType) { fset := token.NewFileSet() @@ -221,7 +219,7 @@ func ParseDeclaredTypes(src []byte) (declaredTypes []declaredType) { name = GetTypeDeclarationName(d) if name != "" { declaredTypes = append(declaredTypes, declaredType{ - Name: name, + Name: name, Package: sourcePackageName, }) } @@ -230,10 +228,6 @@ func ParseDeclaredTypes(src []byte) (declaredTypes []declaredType) { return } - - - - // ParseStruct takes in a piece of source code as a // []byte, the name of the struct it should base the // interface on and a bool saying whether it should @@ -246,7 +240,7 @@ func ParseDeclaredTypes(src []byte) (declaredTypes []declaredType) { // not, the imports not used will be removed later using the // 'imports' pkg If anything goes wrong, this method will // fatally stop the execution -func ParseStruct(src []byte, structName string, copyDocs bool, copyTypeDocs bool, pkgName string, declaredTypes []declaredType) (methods []Method, imports []string, typeDoc string) { +func ParseStruct(src []byte, structName string, copyDocs bool, copyTypeDocs bool, pkgName string, declaredTypes []declaredType, importModule string) (methods []Method, imports []string, typeDoc string) { fset := token.NewFileSet() a, err := parser.ParseFile(fset, "", src, parser.ParseComments) if err != nil { @@ -261,6 +255,10 @@ func ParseStruct(src []byte, structName string, copyDocs bool, copyTypeDocs bool } } + if importModule != "" { + imports = append(imports, fmt.Sprintf(". %s", strconv.Quote(importModule))) + } + for _, d := range a.Decls { if a, fd := GetReceiverTypeName(src, d); a == structName { if !fd.Name.IsExported() { @@ -295,19 +293,34 @@ func ParseStruct(src []byte, structName string, copyDocs bool, copyTypeDocs bool return } -func Make(files []string, structType, comment, pkgName, ifaceName, ifaceComment string, copyDocs, copyTypeDoc bool) ([]byte, error) { - allMethods := []string{} - allImports := []string{} - allDeclaredTypes := []declaredType{} +// MakeOptions contains options for the Make function. +type MakeOptions struct { + Files []string + StructType string + Comment string + PkgName string + IfaceName string + IfaceComment string + ImportModule string + CopyDocs bool + CopyTypeDoc bool +} + +func Make(options MakeOptions) ([]byte, error) { + var ( + allMethods []string + allImports []string + allDeclaredTypes []declaredType - mset := make(map[string]struct{}) - iset := make(map[string]struct{}) - tset := make(map[string]struct{}) + mset = make(map[string]struct{}) + iset = make(map[string]struct{}) + tset = make(map[string]struct{}) + ) var typeDoc string // First pass on all files to find declared types - for _, f := range files { + for _, f := range options.Files { src, err := ioutil.ReadFile(f) if err != nil { return nil, err @@ -322,12 +335,12 @@ func Make(files []string, structType, comment, pkgName, ifaceName, ifaceComment } // Second pass to build up the interface - for _, f := range files { + for _, f := range options.Files { src, err := ioutil.ReadFile(f) if err != nil { return nil, err } - methods, imports, parsedTypeDoc := ParseStruct(src, structType, copyDocs, copyTypeDoc, pkgName, allDeclaredTypes) + methods, imports, parsedTypeDoc := ParseStruct(src, options.StructType, options.CopyDocs, options.CopyTypeDoc, options.PkgName, allDeclaredTypes, options.ImportModule) for _, m := range methods { if _, ok := mset[m.Code]; !ok { allMethods = append(allMethods, m.Lines()...) @@ -346,14 +359,13 @@ func Make(files []string, structType, comment, pkgName, ifaceName, ifaceComment } if typeDoc != "" { - ifaceComment = fmt.Sprintf("%s\n%s", ifaceComment, typeDoc) + options.IfaceComment = fmt.Sprintf("%s\n%s", options.IfaceComment, typeDoc) } - result, err := MakeInterface(comment, pkgName, ifaceName, ifaceComment, allMethods, allImports) + result, err := MakeInterface(options.Comment, options.PkgName, options.IfaceName, options.IfaceComment, allMethods, allImports) if err != nil { return nil, err } return result, nil } - diff --git a/maker/maker_test.go b/maker/maker_test.go index 859304e..360d0e7 100644 --- a/maker/maker_test.go +++ b/maker/maker_test.go @@ -93,7 +93,6 @@ var ( } type SomeType struct {}`) - ) func TestLines(t *testing.T) { @@ -113,7 +112,7 @@ func TestParseDeclaredTypes(t *testing.T) { Name: "Person", Package: "main", }, - declaredTypes[0]) + declaredTypes[0]) assert.Equal(t, declaredType{ Name: "SomeType", Package: "main", @@ -122,7 +121,7 @@ func TestParseDeclaredTypes(t *testing.T) { } func TestParseStruct(t *testing.T) { - methods, imports, typeDoc := ParseStruct(src, "Person", true, true, "", nil) + methods, imports, typeDoc := ParseStruct(src, "Person", true, true, "", nil, "") assert.Equal(t, "Name() (string)", methods[0].Code) @@ -133,6 +132,19 @@ func TestParseStruct(t *testing.T) { assert.Equal(t, "Person ...", typeDoc) } +func TestParseStructWithImportModule(t *testing.T) { + methods, imports, typeDoc := ParseStruct(src, "Person", true, true, "", nil, "github.com/test/test") + + assert.Equal(t, "Name() (string)", methods[0].Code) + + imp, module := imports[0], imports[1] + trimmedImp := strings.TrimSpace(imp) + + assert.Equal(t, `notmain "fmt"`, trimmedImp) + assert.Equal(t, `. "github.com/test/test"`, module) + assert.Equal(t, "Person ...", typeDoc) +} + func TestGetReceiverTypeName(t *testing.T) { fset := token.NewFileSet() a, err := parser.ParseFile(fset, "", src, parser.ParseComments) @@ -203,7 +215,7 @@ func TestFormatFieldList(t *testing.T) { } func TestNoCopyTypeDocs(t *testing.T) { - _, _, typeDoc := ParseStruct(src, "Person", true, false, "", nil) + _, _, typeDoc := ParseStruct(src, "Person", true, false, "", nil, "") assert.Equal(t, "", typeDoc) }