From edea463951641055fbffdd89d394ddc639624011 Mon Sep 17 00:00:00 2001 From: Nate Finch Date: Tue, 11 Dec 2018 14:22:28 -0500 Subject: [PATCH] refactor alias/default parsing to support imported packages and namespaces (#203) fixes #192 --- internal/run.go | 114 +++++++++ mage/import_test.go | 28 ++- mage/main.go | 258 ++----------------- mage/main_test.go | 7 +- mage/testdata/mageimport/magefile.go | 7 +- parse/parse.go | 355 ++++++++++++++++++++------- parse/parse_test.go | 6 +- 7 files changed, 441 insertions(+), 334 deletions(-) create mode 100644 internal/run.go diff --git a/internal/run.go b/internal/run.go new file mode 100644 index 0000000..4e69889 --- /dev/null +++ b/internal/run.go @@ -0,0 +1,114 @@ +package internal + +import ( + "bytes" + "fmt" + "io/ioutil" + "log" + "os" + "os/exec" + "runtime" + "strings" +) + +var debug *log.Logger = log.New(ioutil.Discard, "", 0) + +func SetDebug(l *log.Logger) { + debug = l +} + +func RunDebug(cmd string, args ...string) error { + env, err := EnvWithCurrentGOOS() + if err != nil { + return err + } + buf := &bytes.Buffer{} + errbuf := &bytes.Buffer{} + debug.Println("running", cmd, strings.Join(args, " ")) + c := exec.Command(cmd, args...) + c.Env = env + c.Stderr = errbuf + c.Stdout = buf + if err := c.Run(); err != nil { + debug.Print("error running '", cmd, strings.Join(args, " "), "': ", err, ": ", errbuf) + return err + } + debug.Println(buf) + return nil +} + +func OutputDebug(cmd string, args ...string) (string, error) { + env, err := EnvWithCurrentGOOS() + if err != nil { + return "", err + } + buf := &bytes.Buffer{} + errbuf := &bytes.Buffer{} + debug.Println("running", cmd, strings.Join(args, " ")) + c := exec.Command(cmd, args...) + c.Env = env + c.Stderr = errbuf + c.Stdout = buf + if err := c.Run(); err != nil { + debug.Print("error running '", cmd, strings.Join(args, " "), "': ", err, ": ", errbuf) + return "", err + } + return strings.TrimSpace(buf.String()), nil +} + +// splitEnv takes the results from os.Environ() (a []string of foo=bar values) +// and makes a map[string]string out of it. +func splitEnv(env []string) (map[string]string, error) { + out := map[string]string{} + + for _, s := range env { + parts := strings.SplitN(s, "=", 2) + if len(parts) != 2 { + return nil, fmt.Errorf("badly formatted environment variable: %v", s) + } + out[parts[0]] = parts[1] + } + return out, nil +} + +// joinEnv converts the given map into a list of foo=bar environment variables, +// such as that outputted by os.Environ(). +func joinEnv(env map[string]string) []string { + vals := make([]string, 0, len(env)) + for k, v := range env { + vals = append(vals, k+"="+v) + } + return vals +} + +// EnvWithCurrentGOOS returns a copy of os.Environ with the GOOS and GOARCH set +// to runtime.GOOS and runtime.GOARCH. +func EnvWithCurrentGOOS() ([]string, error) { + vals, err := splitEnv(os.Environ()) + if err != nil { + return nil, err + } + vals["GOOS"] = runtime.GOOS + vals["GOARCH"] = runtime.GOARCH + return joinEnv(vals), nil +} + +// EnvWithGOOS retuns the os.Environ() values with GOOS and/or GOARCH either set +// to their runtime value, or the given value if non-empty. +func EnvWithGOOS(goos, goarch string) ([]string, error) { + env, err := splitEnv(os.Environ()) + if err != nil { + return nil, err + } + if goos == "" { + env["GOOS"] = runtime.GOOS + } else { + env["GOOS"] = goos + } + if goarch == "" { + env["GOARCH"] = runtime.GOARCH + } else { + env["GOARCH"] = goarch + } + return joinEnv(env), nil +} diff --git a/mage/import_test.go b/mage/import_test.go index 1d9704b..bf002ad 100644 --- a/mage/import_test.go +++ b/mage/import_test.go @@ -23,10 +23,12 @@ func TestMageImportsList(t *testing.T) { expected := ` Targets: root - zz:nS:deploy2 deploys stuff. + zz:nS:deploy2* deploys stuff. zz:buildSubdir2 Builds stuff. nS:deploy deploys stuff. buildSubdir Builds stuff. + +* default target `[1:] if actual != expected { @@ -97,6 +99,9 @@ func TestMageImportsNamedRoot(t *testing.T) { if actual != expected { t.Fatalf("expected: %q got: %q", expected, actual) } + if stderr := stderr.String(); stderr != "" { + t.Fatal("unexpected output to stderr: ", stderr) + } } func TestMageImportsRootImportNS(t *testing.T) { @@ -140,3 +145,24 @@ func TestMageImportsRootImport(t *testing.T) { t.Fatalf("expected: %q got: %q", expected, actual) } } + +func TestMageImportsAliasToNS(t *testing.T) { + stdout := &bytes.Buffer{} + stderr := &bytes.Buffer{} + inv := Invocation{ + Dir: "./testdata/mageimport", + Stdout: stdout, + Stderr: stderr, + Args: []string{"nsd2"}, + } + + code := Invoke(inv) + if code != 0 { + t.Fatalf("expected to exit with code 0, but got %v, stderr:\n%s", code, stderr) + } + actual := stdout.String() + expected := "deploy2\n" + if actual != expected { + t.Fatalf("expected: %q got: %q", expected, actual) + } +} diff --git a/mage/main.go b/mage/main.go index 71c90d4..aa81585 100644 --- a/mage/main.go +++ b/mage/main.go @@ -1,7 +1,6 @@ package mage import ( - "bytes" "crypto/sha1" "errors" "flag" @@ -19,6 +18,7 @@ import ( "time" "unicode" + "github.com/magefile/mage/internal" "github.com/magefile/mage/mg" "github.com/magefile/mage/parse" "github.com/magefile/mage/sh" @@ -193,18 +193,18 @@ Commands: Options: -d - run magefiles in the given directory (default ".") - -debug turn on debug messages - -h show description of a target - -f force recreation of compiled magefile - -keep keep intermediate mage files around after running + run magefiles in the given directory (default ".") + -debug turn on debug messages + -h show description of a target + -f force recreation of compiled magefile + -keep keep intermediate mage files around after running -gocmd - use the given go binary to compile the output (default: "go") - -goos sets the GOOS for the binary created by -compile (default: current OS) - -goarch sets the GOARCH for the binary created by -compile (default: current arch) + use the given go binary to compile the output (default: "go") + -goos sets the GOOS for the binary created by -compile (default: current OS) + -goarch sets the GOARCH for the binary created by -compile (default: current arch) -t - timeout in duration parsable format (e.g. 5m30s) - -v show verbose output when running mage targets + timeout in duration parsable format (e.g. 5m30s) + -v show verbose output when running mage targets `[1:]) } err = fs.Parse(args) @@ -306,7 +306,7 @@ func Invoke(inv Invocation) int { debug.Println("output exe is ", exePath) useCache := false - if s, err := outputDebug(inv.GoCmd, "env", "GOCACHE"); err == nil { + if s, err := internal.OutputDebug(inv.GoCmd, "env", "GOCACHE"); err == nil { // if GOCACHE exists, always rebuild, so we catch transitive // dependencies that have changed. if s != "" { @@ -342,53 +342,19 @@ func Invoke(inv Invocation) int { parse.EnableDebug() } debug.Println("parsing files") - info, err := parse.Package(inv.Dir, fnames) + info, err := parse.PrimaryPackage(inv.GoCmd, inv.Dir, fnames) if err != nil { errlog.Println("Error parsing magefiles:", err) return 1 } - imports, err := getNamedImports(inv.GoCmd, info.Imports) - if err != nil { - errlog.Println(err) - return 1 - } - for _, s := range info.RootImports { - imp, err := getImport(inv.GoCmd, s, "") - if err != nil { - errlog.Println(err) - return 1 - } - imports = append(imports, imp) - } - - if !checkDupes(info, imports) { - return 1 - } - - // have to set unique package names on imports - used := map[string]bool{} - for _, imp := range imports { - unique := imp.Name + "_mageimport" - x := 1 - for used[unique] { - unique = fmt.Sprintf("%s_mageimport%d", imp.Name, x) - x++ - } - used[unique] = true - imp.UniqueName = unique - for _, f := range imp.Info.Funcs { - f.Package = unique - } - } - main := filepath.Join(inv.Dir, mainfile) binaryName := "mage" if inv.CompileOut != "" { binaryName = filepath.Base(inv.CompileOut) } - err = GenerateMainfile(binaryName, main, info, imports) + err = GenerateMainfile(binaryName, main, info) if err != nil { errlog.Println("Error:", err) return 1 @@ -417,96 +383,12 @@ func Invoke(inv Invocation) int { return RunCompiled(inv, exePath, errlog) } -func checkDupes(info *parse.PkgInfo, imports []*Import) (ok bool) { - funcs := map[string][]*parse.Function{} - for _, f := range info.Funcs { - funcs[strings.ToLower(f.TargetName())] = append(funcs[strings.ToLower(f.TargetName())], f) - } - for _, imp := range imports { - for _, f := range imp.Info.Funcs { - target := strings.ToLower(f.TargetName()) - funcs[target] = append(funcs[target], f) - } - } - for alias, f := range info.Aliases { - if len(funcs[alias]) != 0 { - var ids []string - for _, f := range funcs[alias] { - ids = append(ids, f.ID()) - } - fmt.Printf("ERROR: alias %q duplicates existing target(s): %s\n", alias, strings.Join(ids, ", ")) - } - funcs[alias] = append(funcs[alias], f) - } - var dupes []string - for target, list := range funcs { - if len(list) > 1 { - dupes = append(dupes, target) - } - } - if len(dupes) == 0 { - return true - } - for _, d := range dupes { - var ids []string - for _, f := range funcs[d] { - ids = append(ids, f.ID()) - } - fmt.Printf("ERROR: %q target has multiple definitions: %s\n", d, strings.Join(ids, ", ")) - } - return false -} - -func getNamedImports(gocmd string, pkgs map[string]string) ([]*Import, error) { - var imports []*Import - for alias, pkg := range pkgs { - debug.Printf("getting import package %q, alias %q", pkg, alias) - imp, err := getImport(gocmd, pkg, alias) - if err != nil { - return nil, err - } - imports = append(imports, imp) - } - return imports, nil -} - -func getImport(gocmd, importpath, alias string) (*Import, error) { - out, err := outputDebug(gocmd, "list", "-f", "{{.Dir}}||{{.Name}}", importpath) - if err != nil { - return nil, err - } - parts := strings.Split(out, "||") - if len(parts) != 2 { - return nil, fmt.Errorf("incorrect data from go list: %s", out) - } - dir, name := parts[0], parts[1] - debug.Printf("parsing imported package %q from dir %q", importpath, dir) - info, err := parse.Package(dir, nil) - if err != nil { - return nil, err - } - for i := range info.Funcs { - debug.Printf("setting alias %q and package %q on func %v", alias, name, info.Funcs[i].Name) - info.Funcs[i].PkgAlias = alias - info.Funcs[i].ImportPath = importpath - } - return &Import{Alias: alias, Name: name, Path: importpath, Info: *info}, nil -} - -type Import struct { - Alias string - Name string - UniqueName string // a name unique across all imports - Path string - Info parse.PkgInfo -} - type mainfileTemplateData struct { Description string Funcs []*parse.Function DefaultFunc parse.Function Aliases map[string]*parse.Function - Imports []*Import + Imports []*parse.Import BinaryName string } @@ -520,7 +402,7 @@ func Magefiles(magePath, goos, goarch, goCmd string, stderr io.Writer, isDebug b return nil, err } - env, err := envWithGOOS(goos, goarch) + env, err := internal.EnvWithGOOS(goos, goarch) if err != nil { return nil, err } @@ -578,10 +460,10 @@ func Compile(goos, goarch, magePath, goCmd, compileTo string, gofiles []string, debug.Println("compiling to", compileTo) debug.Println("compiling using gocmd:", goCmd) if isDebug { - runDebug(goCmd, "version") - runDebug(goCmd, "env") + internal.RunDebug(goCmd, "version") + internal.RunDebug(goCmd, "env") } - environ, err := envWithGOOS(goos, goarch) + environ, err := internal.EnvWithGOOS(goos, goarch) if err != nil { return err } @@ -604,47 +486,8 @@ func Compile(goos, goarch, magePath, goCmd, compileTo string, gofiles []string, return nil } -func runDebug(cmd string, args ...string) error { - env, err := envWithCurrentGOOS() - if err != nil { - return err - } - buf := &bytes.Buffer{} - errbuf := &bytes.Buffer{} - debug.Println("running", cmd, strings.Join(args, " ")) - c := exec.Command(cmd, args...) - c.Env = env - c.Stderr = errbuf - c.Stdout = buf - if err := c.Run(); err != nil { - debug.Print("error running '", cmd, strings.Join(args, " "), "': ", err, ": ", errbuf) - return err - } - debug.Println(buf) - return nil -} - -func outputDebug(cmd string, args ...string) (string, error) { - env, err := envWithCurrentGOOS() - if err != nil { - return "", err - } - buf := &bytes.Buffer{} - errbuf := &bytes.Buffer{} - debug.Println("running", cmd, strings.Join(args, " ")) - c := exec.Command(cmd, args...) - c.Env = env - c.Stderr = errbuf - c.Stdout = buf - if err := c.Run(); err != nil { - debug.Print("error running '", cmd, strings.Join(args, " "), "': ", err, ": ", errbuf) - return "", err - } - return strings.TrimSpace(buf.String()), nil -} - // GenerateMainfile generates the mage mainfile at path. -func GenerateMainfile(binaryName, path string, info *parse.PkgInfo, imports []*Import) error { +func GenerateMainfile(binaryName, path string, info *parse.PkgInfo) error { debug.Println("Creating mainfile at", path) f, err := os.Create(path) @@ -656,7 +499,7 @@ func GenerateMainfile(binaryName, path string, info *parse.PkgInfo, imports []*I Description: info.Description, Funcs: info.Funcs, Aliases: info.Aliases, - Imports: imports, + Imports: info.Imports, BinaryName: binaryName, } @@ -695,7 +538,7 @@ func ExeName(goCmd, cacheDir string, files []string) (string, error) { // binary. hashes = append(hashes, fmt.Sprintf("%x", sha1.Sum([]byte(mageMainfileTplString)))) sort.Strings(hashes) - ver, err := outputDebug(goCmd, "version") + ver, err := internal.OutputDebug(goCmd, "version") if err != nil { return "", err } @@ -805,60 +648,3 @@ func removeContents(dir string) error { return nil } - -// splitEnv takes the results from os.Environ() (a []string of foo=bar values) -// and makes a map[string]string out of it. -func splitEnv(env []string) (map[string]string, error) { - out := map[string]string{} - - for _, s := range env { - parts := strings.SplitN(s, "=", 2) - if len(parts) != 2 { - return nil, fmt.Errorf("badly formatted environment variable: %v", s) - } - out[parts[0]] = parts[1] - } - return out, nil -} - -// joinEnv converts the given map into a list of foo=bar environment variables, -// such as that outputted by os.Environ(). -func joinEnv(env map[string]string) []string { - vals := make([]string, 0, len(env)) - for k, v := range env { - vals = append(vals, k+"="+v) - } - return vals -} - -// envWithCurrentGOOS returns a copy of os.Environ with the GOOS and GOARCH set -// to runtime.GOOS and runtime.GOARCH. -func envWithCurrentGOOS() ([]string, error) { - vals, err := splitEnv(os.Environ()) - if err != nil { - return nil, err - } - vals["GOOS"] = runtime.GOOS - vals["GOARCH"] = runtime.GOARCH - return joinEnv(vals), nil -} - -// envWithGOOS retuns the os.Environ() values with GOOS and/or GOARCH either set -// to their runtime value, or the given value if non-empty. -func envWithGOOS(goos, goarch string) ([]string, error) { - env, err := splitEnv(os.Environ()) - if err != nil { - return nil, err - } - if goos == "" { - env["GOOS"] = runtime.GOOS - } else { - env["GOOS"] = goos - } - if goarch == "" { - env["GOARCH"] = runtime.GOARCH - } else { - env["GOARCH"] = goarch - } - return joinEnv(env), nil -} diff --git a/mage/main_test.go b/mage/main_test.go index c992b97..286557e 100644 --- a/mage/main_test.go +++ b/mage/main_test.go @@ -23,6 +23,7 @@ import ( "testing" "time" + "github.com/magefile/mage/internal" "github.com/magefile/mage/mg" ) @@ -60,7 +61,7 @@ func testmain(m *testing.M) int { } func TestTransitiveDepCache(t *testing.T) { - cache, err := outputDebug("go", "env", "gocache") + cache, err := internal.OutputDebug("go", "env", "gocache") if err != nil { t.Fatal(err) } @@ -1256,6 +1257,10 @@ func TestNamespaceDefault(t *testing.T) { } } +func TestAliasToImport(t *testing.T) { + +} + /// This code liberally borrowed from https://github.com/rsc/goversion/blob/master/version/exe.go type exeType int diff --git a/mage/testdata/mageimport/magefile.go b/mage/testdata/mageimport/magefile.go index cf6835d..f5d286f 100644 --- a/mage/testdata/mageimport/magefile.go +++ b/mage/testdata/mageimport/magefile.go @@ -18,8 +18,11 @@ import ( "github.com/magefile/mage/mage/testdata/mageimport/subdir2" ) -// just something to keep the import from being unused. -var _ = mage.BuildSubdir2 +var Aliases = map[string]interface{}{ + "nsd2": mage.NS.Deploy2, +} + +var Default = mage.NS.Deploy2 func Root() { fmt.Println("root") diff --git a/parse/parse.go b/parse/parse.go index 2ae3fd4..ab7fbea 100644 --- a/parse/parse.go +++ b/parse/parse.go @@ -1,7 +1,6 @@ package parse import ( - "bytes" "errors" "fmt" "go/ast" @@ -11,9 +10,10 @@ import ( "io/ioutil" "log" "os" - "os/exec" "strings" "time" + + "github.com/magefile/mage/internal" ) const importTag = "mage:import" @@ -28,12 +28,13 @@ func EnableDebug() { // PkgInfo contains inforamtion about a package of files according to mage's // parsing rules. type PkgInfo struct { + AstPkg *ast.Package + DocPkg *doc.Package Description string Funcs []*Function DefaultFunc *Function Aliases map[string]*Function - Imports map[string]string - RootImports []string + Imports []*Import } // Function represented a job function from a mage file @@ -124,7 +125,64 @@ func (f Function) ExecCode() (string, error) { return "", fmt.Errorf("Error formatting ExecCode code for %#v", f) } -// Package parses a package. If files is non-empty, it will only parse the files given. +// PrimaryPackage parses a package. If files is non-empty, it will only parse the files given. +func PrimaryPackage(gocmd, path string, files []string) (*PkgInfo, error) { + info, err := Package(path, files) + if err != nil { + return nil, err + } + + if err := setImports(gocmd, info); err != nil { + return nil, err + } + + setDefault(info) + setAliases(info) + return info, nil +} + +func checkDupes(info *PkgInfo, imports []*Import) error { + funcs := map[string][]*Function{} + for _, f := range info.Funcs { + funcs[strings.ToLower(f.TargetName())] = append(funcs[strings.ToLower(f.TargetName())], f) + } + for _, imp := range imports { + for _, f := range imp.Info.Funcs { + target := strings.ToLower(f.TargetName()) + funcs[target] = append(funcs[target], f) + } + } + for alias, f := range info.Aliases { + if len(funcs[alias]) != 0 { + var ids []string + for _, f := range funcs[alias] { + ids = append(ids, f.ID()) + } + return fmt.Errorf("alias %q duplicates existing target(s): %s\n", alias, strings.Join(ids, ", ")) + } + funcs[alias] = append(funcs[alias], f) + } + var dupes []string + for target, list := range funcs { + if len(list) > 1 { + dupes = append(dupes, target) + } + } + if len(dupes) == 0 { + return nil + } + errs := make([]string, 0, len(dupes)) + for _, d := range dupes { + var ids []string + for _, f := range funcs[d] { + ids = append(ids, f.ID()) + } + errs = append(errs, fmt.Sprintf("%q target has multiple definitions: %s\n", d, strings.Join(ids, ", "))) + } + return errors.New(strings.Join(errs, "\n")) +} + +// Package compiles information about a mage package. func Package(path string, files []string) (*PkgInfo, error) { start := time.Now() defer func() { @@ -137,16 +195,15 @@ func Package(path string, files []string) (*PkgInfo, error) { } p := doc.New(pkg, "./", 0) pi := &PkgInfo{ + AstPkg: pkg, + DocPkg: p, Description: toOneLine(p.Doc), } - if err := setImports(pkg, pi); err != nil { - return nil, err - } - setNamespaces(p, pi) - setFuncs(p, pi) + setNamespaces(pi) + setFuncs(pi) - hasDupes, names := checkDupes(pi) + hasDupes, names := checkDupeTargets(pi) if hasDupes { msg := "Build targets must be case insensitive, thus the following targets conflict:\n" for _, v := range names { @@ -157,14 +214,55 @@ func Package(path string, files []string) (*PkgInfo, error) { return nil, errors.New(msg) } - setDefault(p, pi) - setAliases(p, pi) - return pi, nil } -func setFuncs(p *doc.Package, pi *PkgInfo) { - for _, f := range p.Funcs { +func getNamedImports(gocmd string, pkgs map[string]string) ([]*Import, error) { + var imports []*Import + for alias, pkg := range pkgs { + debug.Printf("getting import package %q, alias %q", pkg, alias) + imp, err := getImport(gocmd, pkg, alias) + if err != nil { + return nil, err + } + imports = append(imports, imp) + } + return imports, nil +} + +func getImport(gocmd, importpath, alias string) (*Import, error) { + out, err := internal.OutputDebug(gocmd, "list", "-f", "{{.Dir}}||{{.Name}}", importpath) + if err != nil { + return nil, err + } + parts := strings.Split(out, "||") + if len(parts) != 2 { + return nil, fmt.Errorf("incorrect data from go list: %s", out) + } + dir, name := parts[0], parts[1] + debug.Printf("parsing imported package %q from dir %q", importpath, dir) + info, err := Package(dir, nil) + if err != nil { + return nil, err + } + for i := range info.Funcs { + debug.Printf("setting alias %q and package %q on func %v", alias, name, info.Funcs[i].Name) + info.Funcs[i].PkgAlias = alias + info.Funcs[i].ImportPath = importpath + } + return &Import{Alias: alias, Name: name, Path: importpath, Info: *info}, nil +} + +type Import struct { + Alias string + Name string + UniqueName string // a name unique across all imports + Path string + Info PkgInfo +} + +func setFuncs(pi *PkgInfo) { + for _, f := range pi.DocPkg.Funcs { if f.Recv != "" { debug.Printf("skipping method %s.%s", f.Recv, f.Name) // skip methods @@ -190,12 +288,12 @@ func setFuncs(p *doc.Package, pi *PkgInfo) { } } -func setNamespaces(p *doc.Package, pi *PkgInfo) { - for _, t := range p.Types { +func setNamespaces(pi *PkgInfo) { + for _, t := range pi.DocPkg.Types { if !isNamespace(t) { continue } - debug.Printf("found namespace %s %s", p.ImportPath, t.Name) + debug.Printf("found namespace %s %s", pi.DocPkg.ImportPath, t.Name) for _, f := range t.Methods { if !ast.IsExported(f.Name) { continue @@ -204,7 +302,7 @@ func setNamespaces(p *doc.Package, pi *PkgInfo) { if typ == invalidType { continue } - debug.Printf("found namespace method %s %s.%s", p.ImportPath, t.Name, f.Name) + debug.Printf("found namespace method %s %s.%s", pi.DocPkg.ImportPath, t.Name, f.Name) pi.Funcs = append(pi.Funcs, &Function{ Name: f.Name, Receiver: t.Name, @@ -217,30 +315,63 @@ func setNamespaces(p *doc.Package, pi *PkgInfo) { } } -func setImports(pkg *ast.Package, pi *PkgInfo) error { - pi.Imports = map[string]string{} - for _, f := range pkg.Files { +func setImports(gocmd string, pi *PkgInfo) error { + importNames := map[string]string{} + rootImports := []string{} + for _, f := range pi.AstPkg.Files { for _, imp := range f.Imports { - name, alias, ok := getImport(imp) + name, alias, ok := getImportPath(imp) if !ok { continue } if alias != "" { debug.Printf("found %s: %s (%s)", importTag, name, alias) - if pi.Imports[alias] != "" { + if importNames[alias] != "" { return fmt.Errorf("duplicate import alias: %q", alias) } - pi.Imports[alias] = name + importNames[alias] = name } else { debug.Printf("found %s: %s", importTag, name) - pi.RootImports = append(pi.RootImports, name) + rootImports = append(rootImports, name) } } } + + imports, err := getNamedImports(gocmd, importNames) + if err != nil { + return err + } + for _, s := range rootImports { + imp, err := getImport(gocmd, s, "") + if err != nil { + return err + } + imports = append(imports, imp) + } + if err := checkDupes(pi, imports); err != nil { + return err + } + + // have to set unique package names on imports + used := map[string]bool{} + for _, imp := range imports { + unique := imp.Name + "_mageimport" + x := 1 + for used[unique] { + unique = fmt.Sprintf("%s_mageimport%d", imp.Name, x) + x++ + } + used[unique] = true + imp.UniqueName = unique + for _, f := range imp.Info.Funcs { + f.Package = unique + } + } + pi.Imports = imports return nil } -func getImport(imp *ast.ImportSpec) (path, alias string, ok bool) { +func getImportPath(imp *ast.ImportSpec) (path, alias string, ok bool) { if imp.Doc == nil || len(imp.Doc.List) == 9 { return "", "", false } @@ -318,8 +449,8 @@ func fieldNames(flist *ast.FieldList) string { return strings.Join(args, ", ") } -// checkDupes checks a package for duplicate target names. -func checkDupes(info *PkgInfo) (hasDupes bool, names map[string][]string) { +// checkDupeTargets checks a package for duplicate target names. +func checkDupeTargets(info *PkgInfo) (hasDupes bool, names map[string][]string) { names = map[string][]string{} lowers := map[string]bool{} for _, f := range info.Funcs { @@ -353,8 +484,8 @@ func sanitizeSynopsis(f *doc.Func) string { return synopsis } -func setDefault(p *doc.Package, pi *PkgInfo) { - for _, v := range p.Vars { +func setDefault(pi *PkgInfo) { + for _, v := range pi.DocPkg.Vars { for x, name := range v.Names { if name != "Default" { continue @@ -364,24 +495,13 @@ func setDefault(p *doc.Package, pi *PkgInfo) { log.Println("warning: default declaration has multiple values") } - var name string - var receiver string - switch v := spec.Values[0].(type) { - case *ast.Ident: - name = v.Name - case *ast.SelectorExpr: - name = v.Sel.Name - receiver = fmt.Sprintf("%s", v.X) - default: - log.Printf("warning: target for Default %s is not a function", spec.Values[0]) + f, err := getFunction(spec.Values[0], pi) + if err != nil { + log.Println("warning, default declaration malformed:", err) + return } - for _, f := range pi.Funcs { - if f.Name == name && f.Receiver == receiver { - pi.DefaultFunc = f - return - } - } - log.Println("warning: default declaration does not reference a mage target") + pi.DefaultFunc = f + return } } } @@ -393,23 +513,8 @@ func lit2string(l *ast.BasicLit) (string, bool) { return strings.Trim(l.Value, `"`), true } -func outputDebug(cmd string, args ...string) (string, error) { - buf := &bytes.Buffer{} - errbuf := &bytes.Buffer{} - debug.Println("running", cmd, strings.Join(args, " ")) - c := exec.Command(cmd, args...) - c.Env = os.Environ() - c.Stderr = errbuf - c.Stdout = buf - if err := c.Run(); err != nil { - debug.Print("error running '", cmd, strings.Join(args, " "), "': ", err, ": ", errbuf) - return "", err - } - return strings.TrimSpace(buf.String()), nil -} - -func setAliases(p *doc.Package, pi *PkgInfo) { - for _, v := range p.Vars { +func setAliases(pi *PkgInfo) { + for _, v := range pi.DocPkg.Vars { for x, name := range v.Names { if name != "Aliases" { continue @@ -431,49 +536,113 @@ func setAliases(p *doc.Package, pi *PkgInfo) { for _, elem := range comp.Elts { kv, ok := elem.(*ast.KeyValueExpr) if !ok { - log.Println("warning: alias declaration is not a map element") - return + log.Printf("warning: alias declaration %q is not a map element", elem) + continue } k, ok := kv.Key.(*ast.BasicLit) if !ok || k.Kind != token.STRING { - log.Println("warning: alias is not a string") - return + log.Printf("warning: alias key is not a string literal %q", elem) + continue } - var name string - var receiver string - switch v := kv.Value.(type) { - case *ast.Ident: - name = v.Name - case *ast.SelectorExpr: - name = v.Sel.Name - receiver = fmt.Sprintf("%s", v.X) - default: - log.Printf("warning: target for alias %s is not a function", k.Value) - continue - } alias, ok := lit2string(k) if !ok { - log.Println("warning: malformed alias declaration for alias", k.Value) + log.Println("warning: malformed name for alias", elem) continue } - valid := false - for _, f := range pi.Funcs { - if f.Name == name && f.Receiver == receiver { - pi.Aliases[alias] = f - valid = true - break - } - } - if !valid { - log.Printf("warning: alias declaration (%s) does not reference a mage target", alias) + f, err := getFunction(kv.Value, pi) + if err != nil { + log.Printf("warning, alias malformed: %v", err) + continue } + pi.Aliases[alias] = f } return } } } +func getFunction(exp ast.Expr, pi *PkgInfo) (*Function, error) { + + // selector expressions are in LIFO format. + // So, in foo.bar.baz the first selector.Name is + // actually "baz", the second is "bar", and the last is "foo" + + var pkg, receiver, funcname string + switch v := exp.(type) { + case *ast.Ident: + // "foo" : Bar + funcname = v.Name + case *ast.SelectorExpr: + // need to handle + // namespace.Func + // import.Func + // import.namespace.Func + + // "foo" : ?.bar + funcname = v.Sel.Name + switch x := v.X.(type) { + case *ast.Ident: + // "foo" : baz.bar + // this is either a namespace or package + firstname := x.Name + for _, f := range pi.Funcs { + if firstname == f.Receiver && funcname == f.Name { + return f, nil + } + } + // not a namespace, let's try imported packages + for _, imp := range pi.Imports { + if firstname == imp.Name { + for _, f := range imp.Info.Funcs { + if funcname == f.Name { + return f, nil + } + } + break + } + } + return nil, fmt.Errorf("%q is not a known target", exp) + case *ast.SelectorExpr: + // "foo" : bar.Baz.Bat + // must be package.Namespace.Func + sel, ok := v.X.(*ast.SelectorExpr) + if !ok { + return nil, fmt.Errorf("%q is must denote a target function but was %T", exp, v.X) + } + receiver = sel.Sel.Name + id, ok := sel.X.(*ast.Ident) + if !ok { + return nil, fmt.Errorf("%q is must denote a target function but was %T", exp, v.X) + } + pkg = id.Name + default: + return nil, fmt.Errorf("%q is not valid", exp) + } + default: + return nil, fmt.Errorf("target %s is not a function", exp) + } + if pkg == "" { + for _, f := range pi.Funcs { + if f.Name == funcname && f.Receiver == receiver { + return f, nil + } + } + return nil, fmt.Errorf("unknown function %s.%s", receiver, funcname) + } + for _, imp := range pi.Imports { + if imp.Name == pkg { + for _, f := range imp.Info.Funcs { + if f.Name == funcname && f.Receiver == receiver { + return f, nil + } + } + return nil, fmt.Errorf("unknown function %s.%s.%s", pkg, receiver, funcname) + } + } + return nil, fmt.Errorf("unknown package for function %q", exp) +} + // getPackage returns the non-test package at the given path. func getPackage(path string, files []string, fset *token.FileSet) (*ast.Package, error) { var filter func(f os.FileInfo) bool diff --git a/parse/parse_test.go b/parse/parse_test.go index f8b3d2a..039039a 100644 --- a/parse/parse_test.go +++ b/parse/parse_test.go @@ -6,7 +6,7 @@ import ( ) func TestParse(t *testing.T) { - info, err := Package("./testdata", []string{"func.go", "command.go", "alias.go", "repeating_synopsis.go", "subcommands.go"}) + info, err := PrimaryPackage("go", "./testdata", []string{"func.go", "command.go", "alias.go", "repeating_synopsis.go", "subcommands.go"}) if err != nil { t.Fatal(err) } @@ -49,6 +49,10 @@ func TestParse(t *testing.T) { }, } + if info.DefaultFunc == nil { + t.Fatal("expected default func to exist, but was nil") + } + // DefaultIsError if info.DefaultFunc.IsError != true { t.Fatalf("expected DefaultIsError to be true")