diff --git a/cmd/gbc/artifact/internal_plugin_test.go b/cmd/gbc/artifact/internal_plugin_test.go index 8b40e3c..9d211c9 100644 --- a/cmd/gbc/artifact/internal_plugin_test.go +++ b/cmd/gbc/artifact/internal_plugin_test.go @@ -108,7 +108,6 @@ func (suite *InternalPluginTestSuit) TestNewPlugin() { assert.True(t, test.wantErr == (err != nil)) if !test.wantErr { assert.Equal(t, test.module, plugin.module) - assert.True(t, lo.Contains([]string{"v1.58.1", "v1.57.2", "v1.1.1", "v1.11.0"}, plugin.Version())) } }) } diff --git a/cmd/gbc/artifact/project.go b/cmd/gbc/artifact/project.go index 6031993..a3806dd 100644 --- a/cmd/gbc/artifact/project.go +++ b/cmd/gbc/artifact/project.go @@ -8,12 +8,13 @@ import ( "github.com/kcmvp/gob/utils" "github.com/samber/lo" //nolint "github.com/spf13/viper" //nolint - "io/fs" + "go/types" + "golang.org/x/mod/modfile" + "golang.org/x/tools/go/packages" "log" "os" "os/exec" "path/filepath" - "regexp" "runtime" "strings" "sync" @@ -30,10 +31,10 @@ var ( ) type Project struct { - root string - module string - deps []string - cfgs sync.Map // store all the configuration + root string + mod *modfile.File + cfgs sync.Map // store all the configuration + pkgs []*packages.Package } func (project *Project) load() *viper.Viper { @@ -77,32 +78,28 @@ func (project *Project) HookDir() string { } func init() { - cmd := exec.Command("go", "list", "-m", "-f", "{{.Dir}}_:_{{.Path}}") - output, err := cmd.Output() + output, err := exec.Command("go", "list", "-m", "-f", "{{.Dir}}_:_{{.Path}}").CombinedOutput() if err != nil || len(string(output)) == 0 { - log.Fatal(color.RedString("Error: please execute command in project root directory %s", string(output))) + log.Fatal(color.RedString("please execute command in project root directory %s", string(output))) } - item := strings.Split(strings.TrimSpace(string(output)), "_:_") - project = Project{ - root: item[0], - module: item[1], - cfgs: sync.Map{}, + project = Project{cfgs: sync.Map{}, root: item[0]} + data, err := os.ReadFile(filepath.Join(project.root, "go.mod")) + if err != nil { + log.Fatal(color.RedString(err.Error())) } - cmd = exec.Command("go", "list", "-f", "{{if not .Standard}}{{.ImportPath}}{{end}}", "-deps", "./...") - output, err = cmd.Output() + project.mod, err = modfile.Parse("go.mod", data, nil) if err != nil { - log.Fatal(color.RedString("Error: please execute command in project root directory")) + log.Fatal(color.RedString("please execute command in project root directory %s", string(output))) } - scanner := bufio.NewScanner(strings.NewReader(string(output))) - var deps []string - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if len(line) > 0 { - deps = append(deps, line) - } + cfg := &packages.Config{ + Mode: packages.NeedName | packages.NeedTypes | packages.NeedTypesInfo | packages.NeedFiles | packages.NeedTypesInfo | packages.NeedDeps | packages.NeedImports | packages.NeedSyntax, + Dir: project.root, + } + project.pkgs, err = packages.Load(cfg, "./...") + if err != nil { + log.Fatal(color.RedString("failed to load project %s", err.Error())) } - project.deps = deps } // CurProject return Project struct @@ -117,7 +114,7 @@ func (project *Project) Root() string { // Module return current project module name func (project *Project) Module() string { - return project.module + return project.mod.Module.Mod.Path } func (project *Project) Target() string { @@ -148,37 +145,22 @@ func (project *Project) sourceFileInPkg(pkg string) ([]string, error) { } func (project *Project) MainFiles() []string { - var mainFiles []string - dirs, _ := project.sourceFileInPkg("main") - re := regexp.MustCompile(`func\s+main\s*\(\s*\)`) - lo.ForEach(dirs, func(dir string, _ int) { - _ = filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { - if err != nil { - return err - } - if d.IsDir() && dir != path { - return filepath.SkipDir - } - if d.IsDir() || !strings.HasSuffix(d.Name(), ".go") || strings.HasSuffix(d.Name(), "_test.go") { - return nil - } - file, err := os.Open(path) - if err != nil { - return err - } - defer file.Close() - scanner := bufio.NewScanner(file) - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if re.MatchString(line) { - mainFiles = append(mainFiles, path) - return filepath.SkipDir + return lo.FilterMap(project.pkgs, func(pkg *packages.Package, _ int) (string, bool) { + if pkg.Name != "main" { + return "", false + } + scope := pkg.Types.Scope() + for _, name := range scope.Names() { + obj := scope.Lookup(name) + if f, ok := obj.(*types.Func); ok { + signature := f.Type().(*types.Signature) + if f.Name() == "main" && signature.Params().Len() == 0 && signature.Results().Len() == 0 { + return pkg.Fset.Position(obj.Pos()).Filename, true } } - return scanner.Err() - }) + } + return "", false }) - return mainFiles } func (project *Project) Plugins() []Plugin { @@ -201,15 +183,18 @@ func (project *Project) Plugins() []Plugin { } } -func (project *Project) Dependencies() []string { - return project.deps +func (project *Project) Dependencies() []*modfile.Require { + return project.mod.Require } func (project *Project) InstallDependency(dep string) error { - if !lo.Contains(project.deps, dep) { - exec.Command("go", "get", "-u", dep).CombinedOutput() //nolint + var err error + if lo.NoneBy(project.mod.Require, func(r *modfile.Require) bool { + return lo.Contains(r.Syntax.Token, dep) + }) { + _, err = exec.Command("go", "get", "-u", dep).CombinedOutput() //nolint } - return nil + return err } func (project *Project) InstallPlugin(plugin Plugin) error { diff --git a/cmd/gbc/artifact/project_test.go b/cmd/gbc/artifact/project_test.go index 9beff8c..5c48ded 100644 --- a/cmd/gbc/artifact/project_test.go +++ b/cmd/gbc/artifact/project_test.go @@ -6,6 +6,7 @@ import ( "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/suite" + "golang.org/x/mod/modfile" "io" "os" "path/filepath" @@ -49,8 +50,10 @@ func TestBasic(t *testing.T) { func (suite *ProjectTestSuite) TestDeps() { deps := CurProject().Dependencies() - assert.Equal(suite.T(), 58, len(deps)) - assert.True(suite.T(), lo.Contains(deps, "github.com/spf13/viper")) + assert.Equal(suite.T(), 50, len(deps)) + assert.True(suite.T(), lo.ContainsBy(deps, func(require *modfile.Require) bool { + return require.Mod.Path == "github.com/spf13/viper" + })) } func (suite *ProjectTestSuite) TestPlugins() { diff --git a/cmd/gbc/command/deps.go b/cmd/gbc/command/deps.go index 79a07a3..66e2164 100644 --- a/cmd/gbc/command/deps.go +++ b/cmd/gbc/command/deps.go @@ -1,8 +1,8 @@ package command import ( - "bufio" "fmt" + "golang.org/x/mod/modfile" //nolint "os" "os/exec" "path/filepath" @@ -21,93 +21,50 @@ var ( yellow = color.New(color.FgYellow) ) -// parseMod return a tuple which the fourth element is the indicator of direct or indirect reference -func parseMod(mod *os.File) (string, string, []*lo.Tuple4[string, string, string, int], error) { - scanner := bufio.NewScanner(mod) - var deps []*lo.Tuple4[string, string, string, int] - var module, version string - for scanner.Scan() { - line := strings.TrimSpace(scanner.Text()) - if len(line) == 0 || line == ")" || line == "//" || strings.HasPrefix(line, "require") { - continue - } - if strings.HasPrefix(line, "module ") { - module = strings.Split(line, " ")[1] - } else if strings.HasPrefix(line, "go ") { - version = strings.Split(line, " ")[1] - } else { - entry := strings.Split(line, " ") - m := strings.TrimSpace(entry[0]) - v := strings.TrimSpace(entry[1]) - dep := lo.T4(m, v, v, lo.If(len(entry) > 2, 0).Else(1)) - deps = append(deps, &dep) - } - } - return module, version, deps, scanner.Err() -} - // dependencyTree build dependency tree of the project, an empty tree returns when runs into error func dependencyTree() (treeprint.Tree, error) { - mod, err := os.Open(filepath.Join(artifact.CurProject().Root(), "go.mod")) - if err != nil { - return nil, fmt.Errorf(color.RedString(err.Error())) - } exec.Command("go", "mod", "tidy").CombinedOutput() //nolint - if output, err := exec.Command("go", "build", "./...").CombinedOutput(); err != nil { - return nil, fmt.Errorf(color.RedString(string(output))) - } - module, _, dependencies, err := parseMod(mod) - if len(dependencies) < 1 { - return nil, nil - } - if err != nil { - return nil, fmt.Errorf(err.Error()) - } tree := treeprint.New() - tree.SetValue(module) - direct := lo.FilterMap(dependencies, func(item *lo.Tuple4[string, string, string, int], _ int) (string, bool) { - return item.A, item.D == 1 + tree.SetValue(artifact.CurProject().Module()) + directs := lo.FilterMap(artifact.CurProject().Dependencies(), func(item *modfile.Require, _ int) (lo.Tuple2[string, string], bool) { + return lo.Tuple2[string, string]{A: item.Mod.Path, B: item.Mod.Version}, !item.Indirect }) // get the latest version - versions := artifact.LatestVersion(direct...) - for _, dep := range dependencies { - if version, ok := lo.Find(versions, func(t lo.Tuple2[string, string]) bool { - return dep.A == t.A && dep.B != t.B - }); ok { - dep.C = version.B - } - } + versions := artifact.LatestVersion(lo.Map(directs, func(item lo.Tuple2[string, string], _ int) string { + return item.A + })...) // parse the dependency tree cache := []string{os.Getenv("GOPATH"), "pkg", "mod", "cache", "download"} - for _, dependency := range dependencies { - if dependency.D == 1 { - label := lo.IfF(dependency.B == dependency.C, func() string { - return fmt.Sprintf("%s@%s", dependency.A, dependency.B) + for _, dependency := range artifact.CurProject().Dependencies() { + if !dependency.Indirect { + m, ok := lo.Find(versions, func(item lo.Tuple2[string, string]) bool { + return dependency.Mod.Path == item.A && dependency.Mod.Version != item.B + }) + label := lo.IfF(!ok, func() string { + return dependency.Mod.String() }).ElseF(func() string { - return yellow.Sprintf("* %s@%s (%s)", dependency.A, dependency.B, dependency.C) + return yellow.Sprintf("* %s (%s)", dependency.Mod.String(), m.B) }) - child := tree.AddBranch(label) - dir := append(cache, strings.Split(dependency.A, "/")...) - dir = append(dir, []string{"@v", fmt.Sprintf("%s.mod", dependency.B)}...) - mod, err = os.Open(filepath.Join(dir...)) - if err != nil { - return tree, fmt.Errorf(color.RedString(err.Error())) - } - _, _, cDeps, err := parseMod(mod) + direct := tree.AddBranch(label) + dir := append(cache, strings.Split(dependency.Mod.Path, "/")...) + dir = append(dir, []string{"@v", fmt.Sprintf("%s.mod", dependency.Mod.Version)}...) + data, err := os.ReadFile(filepath.Join(dir...)) if err != nil { - return tree, fmt.Errorf(color.RedString(err.Error())) + color.Yellow("failed to get latest version of %s", dependency.Mod.Path) + continue } - inter := lo.Filter(cDeps, func(c *lo.Tuple4[string, string, string, int], _ int) bool { - return lo.ContainsBy(dependencies, func(p *lo.Tuple4[string, string, string, int]) bool { - return p.A == c.A + mod, _ := modfile.Parse("go.mod", data, nil) + children := lo.Filter(artifact.CurProject().Dependencies(), func(p *modfile.Require, _ int) bool { + return p.Indirect && lo.ContainsBy(mod.Require, func(c *modfile.Require) bool { + return !c.Indirect && p.Mod.Path == c.Mod.Path }) }) - for _, l := range inter { - child.AddNode(fmt.Sprintf("%s@%s", l.A, l.B)) + for _, c := range children { + direct.AddNode(c.Mod.String()) } } } - return tree, err + return tree, nil } // depCmd represents the dep command diff --git a/cmd/gbc/command/deps_test.go b/cmd/gbc/command/deps_test.go index 63f3b5a..d5cc070 100644 --- a/cmd/gbc/command/deps_test.go +++ b/cmd/gbc/command/deps_test.go @@ -6,33 +6,19 @@ import ( "github.com/samber/lo" "github.com/stretchr/testify/assert" "github.com/xlab/treeprint" + "golang.org/x/mod/modfile" "os" - "path/filepath" "strings" "testing" ) -func TestParseMod(t *testing.T) { - os.Chdir(artifact.CurProject().Root()) - mod, _ := os.Open(filepath.Join(artifact.CurProject().Root(), "go.mod")) - m, _, deps, err := parseMod(mod) - assert.NoError(t, err) - assert.Equal(t, m, "github.com/kcmvp/gob") - assert.Equal(t, 15, len(lo.Filter(deps, func(item *lo.Tuple4[string, string, string, int], _ int) bool { - return item.D == 1 - }))) - assert.Equal(t, 48, len(deps)) -} - func TestDependency(t *testing.T) { os.Chdir(artifact.CurProject().Root()) - mod, _ := os.Open(filepath.Join(artifact.CurProject().Root(), "go.mod")) - _, _, deps, _ := parseMod(mod) tree, err := dependencyTree() assert.NoError(t, err) tree.VisitAll(func(item *treeprint.Node) { - contains := lo.ContainsBy(deps, func(dep *lo.Tuple4[string, string, string, int]) bool { - return strings.Contains(fmt.Sprintf("%s", item.Value), fmt.Sprintf("%s", dep.A)) + contains := lo.ContainsBy(artifact.CurProject().Dependencies(), func(dep *modfile.Require) bool { + return strings.Contains(fmt.Sprintf("%s", item.Value), dep.Mod.Path) }) assert.True(t, contains) }) diff --git a/cmd/gbc/command/root.go b/cmd/gbc/command/root.go index c840f01..d446ea0 100644 --- a/cmd/gbc/command/root.go +++ b/cmd/gbc/command/root.go @@ -94,21 +94,19 @@ func installPlugins(cmd *cobra.Command, args []string) error { func installDeps(cmd *cobra.Command, args []string) error { result, err := parseArtifacts(cmd, args, "deps") + if err != nil { + return err + } if result.Exists() { var cfgDeps []string err = json.Unmarshal([]byte(result.Raw), &cfgDeps) - for _, dep := range lo.Filter(cfgDeps, func(url string, _ int) bool { - return !lo.Contains(artifact.CurProject().Dependencies(), url) - }) { - if err = artifact.CurProject().InstallDependency(dep); err != nil { - break + for _, dep := range cfgDeps { + if err := artifact.CurProject().InstallDependency(dep); err != nil { + return err } } } - if err != nil { - return errors.New(color.RedString(err.Error())) - } - return err + return nil } // rootCmd represents the base command when called without any subcommands diff --git a/go.mod b/go.mod index f616dad..63cacc9 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,8 @@ require ( github.com/stretchr/testify v1.9.0 github.com/tidwall/gjson v1.17.1 github.com/xlab/treeprint v1.2.0 + golang.org/x/mod v0.12.0 + golang.org/x/tools v0.13.0 ) require ( diff --git a/go.sum b/go.sum index 2d7a2e1..f6298a6 100644 --- a/go.sum +++ b/go.sum @@ -110,6 +110,10 @@ go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI= go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ= golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g= golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k= +golang.org/x/mod v0.12.0 h1:rmsUpXtvNzj340zd98LZ4KntptpfRHwpFOHG188oHXc= +golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= +golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= @@ -120,6 +124,8 @@ golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= +golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=