Skip to content

Commit

Permalink
feat: allow passing multiple files
Browse files Browse the repository at this point in the history
  • Loading branch information
gabotechs committed Jan 31, 2024
1 parent fb5da4b commit 6e4436d
Show file tree
Hide file tree
Showing 41 changed files with 666 additions and 532 deletions.
2 changes: 1 addition & 1 deletion cmd/.root_test/__config .root_test_.dep_tree.yml.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
accepts 1 arg(s), received 0
requires at least 1 arg(s), only received 0
2 changes: 1 addition & 1 deletion cmd/.root_test/tree.txt
Original file line number Diff line number Diff line change
@@ -1 +1 @@
accepts 1 arg(s), received 0
requires at least 1 arg(s), only received 0
2 changes: 1 addition & 1 deletion cmd/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ func CheckCmd() *cobra.Command {
if len(cfg.Check.Entrypoints) == 0 {
return fmt.Errorf(`config file "%s" has no entrypoints`, configPath)
}
parserBuilder, err := makeParserBuilder(cfg.Check.Entrypoints[0], cfg)
parserBuilder, err := makeParserBuilder(cfg.Check.Entrypoints, cfg)
if err != nil {
return err
}
Expand Down
14 changes: 8 additions & 6 deletions cmd/entropy.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,23 +14,25 @@ func EntropyCmd() *cobra.Command {
Use: "entropy",
Short: "(default) Renders a 3d force-directed graph in the browser",
GroupID: renderGroupId,
Args: cobra.ExactArgs(1),
Args: cobra.MinimumNArgs(1),
RunE: func(cmd *cobra.Command, args []string) error {
entrypoint := args[0]

files, err := filesFromArgs(args)
if err != nil {
return err
}
cfg, err := loadConfig()
if err != nil {
return err
}
parserBuilder, err := makeParserBuilder(entrypoint, cfg)
parserBuilder, err := makeParserBuilder(files, cfg)
if err != nil {
return err
}
parser, err := parserBuilder(entrypoint)
parser, err := parserBuilder(files)
if err != nil {
return err
}
err = entropy.Render(parser, entropy.RenderConfig{
err = entropy.Render(parser, files, entropy.RenderConfig{
NoOpen: noBrowserOpen,
EnableGui: enableGui,
})
Expand Down
31 changes: 26 additions & 5 deletions cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"errors"
"fmt"
"os"
"path/filepath"

"github.com/spf13/cobra"
"github.com/spf13/pflag"
Expand Down Expand Up @@ -92,19 +93,39 @@ $ dep-tree check`,
return root
}

func makeParserBuilder(entrypoint string, cfg *config.Config) (language.NodeParserBuilder, error) {
func makeParserBuilder(files []string, cfg *config.Config) (language.NodeParserBuilder, error) {
if len(files) == 0 {
return nil, errors.New("at least one file must be provided")
}
// TODO: There's smarter ways to check which language are we in than just reading the
// first encountered file extension.
switch {
case utils.EndsWith(entrypoint, js.Extensions):
case utils.EndsWith(files[0], js.Extensions):
return language.ParserBuilder(js.MakeJsLanguage, &cfg.Js, cfg), nil
case utils.EndsWith(entrypoint, rust.Extensions):
case utils.EndsWith(files[0], rust.Extensions):
return language.ParserBuilder(rust.MakeRustLanguage, &cfg.Rust, cfg), nil
case utils.EndsWith(entrypoint, python.Extensions):
case utils.EndsWith(files[0], python.Extensions):
return language.ParserBuilder(python.MakePythonLanguage, &cfg.Python, cfg), nil
default:
return nil, fmt.Errorf("file \"%s\" not supported", entrypoint)
return nil, fmt.Errorf("file \"%s\" not supported", files[0])
}
}

func filesFromArgs(args []string) ([]string, error) {
var result []string
for _, arg := range args {
abs, err := filepath.Abs(arg)
if err == nil {
result = append(result, abs)
}
}
if len(result) == 0 {
return result, errors.New("no valid files where provided")
}

return result, nil
}

func loadConfig() (*config.Config, error) {
cfg, err := config.ParseConfig(configPath)
if err != nil {
Expand Down
7 changes: 4 additions & 3 deletions cmd/root_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ func TestRoot(t *testing.T) {
{
Name: "tree",
},
{
Name: "tree random.pdf",
},
// TODO: this test now refers to an absolute path, so it's not golden testable
// {
// Name: "tree random.pdf",
// },
// TODO: these will change once globstar entrypoints are allowed.
// {
// Name: "tree random.js",
Expand Down
14 changes: 8 additions & 6 deletions cmd/tree.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,29 @@ func TreeCmd() *cobra.Command {
cmd := &cobra.Command{
Use: "tree",
Short: "Render the dependency tree starting from the provided entrypoint",
Args: cobra.ExactArgs(1),
Args: cobra.MinimumNArgs(1),
GroupID: renderGroupId,
RunE: func(cmd *cobra.Command, args []string) error {
entrypoint := args[0]

files, err := filesFromArgs(args)
if err != nil {
return err
}
cfg, err := loadConfig()
if err != nil {
return err
}

parserBuilder, err := makeParserBuilder(entrypoint, cfg)
parserBuilder, err := makeParserBuilder(files, cfg)
if err != nil {
return err
}

if jsonFormat {
rendered, err := dep_tree.PrintStructured(entrypoint, parserBuilder)
rendered, err := dep_tree.PrintStructured(files, parserBuilder)
fmt.Println(rendered)
return err
} else {
return tui.Loop(entrypoint, parserBuilder, nil, true, nil)
return tui.Loop(files, parserBuilder, nil, true, nil)
}
},
}
Expand Down
119 changes: 25 additions & 94 deletions internal/check/check.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,83 +9,48 @@ import (
"github.com/gabotechs/dep-tree/internal/utils"
)

func checkEntrypoint[T any](
parserBuilder dep_tree.NodeParserBuilder[T],
cfg *Config,
entrypoint string,
) error {
parser, err := parserBuilder(entrypoint)
if err != nil {
return err
}
dt := dep_tree.NewDepTree(parser)
root, err := dt.Root()
func Check[T any](parserBuilder dep_tree.NodeParserBuilder[T], cfg *Config) error {
parser, err := parserBuilder(cfg.Entrypoints)
if err != nil {
return err
}
dt := dep_tree.NewDepTree(parser, cfg.Entrypoints).WithStdErrLoader()
err = dt.LoadGraph()
if err != nil {
return err
}
failures, err := cfg.Validate(
root.Id,
func(from string) []string {
toNodes := dt.Graph.FromId(from)
result := make([]string, len(toNodes))
for i, c := range toNodes {
result[i] = c.Id
// 1. Check for rule violations in the graph.
failures := make([]string, 0)
for _, node := range dt.Graph.AllNodes() {
for _, dep := range dt.Graph.FromId(node.Id) {
from, to := cfg.rel(node.Id), cfg.rel(dep.Id)
pass, err := cfg.Check(from, to)
if err != nil {
return err
} else if !pass {
failures = append(failures, from+" -> "+to)
}
return result
},
)
}
}
// 2. Check for cycles.
dt.LoadCycles()
if !cfg.AllowCircularDependencies && dt.Cycles.Len() > 0 {
for _, cycleId := range dt.Cycles.Keys() {
cycle, _ := dt.Cycles.Get(cycleId)
formattedCycleStack := make([]string, len(cycle.Stack))
for i, el := range cycle.Stack {
node := dt.Graph.Get(el)
if node == nil {
formattedCycleStack[i] = el
} else {
if !cfg.AllowCircularDependencies {
for el := dt.Cycles.Front(); el != nil; el = el.Next() {
formattedCycleStack := make([]string, len(el.Value.Stack))
for i, el := range el.Value.Stack {
if node := dt.Graph.Get(el); node != nil {
formattedCycleStack[i] = parser.Display(node)
} else {
formattedCycleStack[i] = el
}
}

msg := "detected circular dependency: " + strings.Join(formattedCycleStack, " -> ")
failures = append(failures, msg)
}
}
if err != nil {
return err
} else if len(failures) > 0 {
return errors.New("Check failed for entrypoint \"" + entrypoint + "\" the following dependencies are not allowed:\n" + strings.Join(failures, "\n"))
}
return nil
}

type Error []error

func (e Error) Error() string {
msg := ""
for _, err := range e {
msg += err.Error()
msg += "\n"
}
return msg
}

func Check[T any](parserBuilder dep_tree.NodeParserBuilder[T], cfg *Config) error {
errorFlag := false
errs := make([]error, len(cfg.Entrypoints))
for i, entrypoint := range cfg.Entrypoints {
errs[i] = checkEntrypoint(parserBuilder, cfg, filepath.Join(cfg.Path, entrypoint))
if errs[i] != nil {
errorFlag = true
}
}
if errorFlag {
return Error(errs)
if len(failures) > 0 {
return errors.New("Check failed, the following dependencies are not allowed:\n" + strings.Join(failures, "\n"))
}
return nil
}
Expand Down Expand Up @@ -149,37 +114,3 @@ func (c *Config) rel(p string) string {
}
return relPath
}

func (c *Config) validate(
start string,
destinations func(from string) []string,
seen map[string]bool,
) ([]string, error) {
collectedErrors := make([]string, 0)

if _, ok := seen[start]; ok {
return collectedErrors, nil
} else {
seen[start] = true
}

for _, dest := range destinations(start) {
from, to := c.rel(start), c.rel(dest)
pass, err := c.Check(from, to)
if err != nil {
return nil, err
} else if !pass {
collectedErrors = append(collectedErrors, from+" -> "+to)
}
moreErrors, err := c.validate(dest, destinations, seen)
if err != nil {
return nil, err
}
collectedErrors = append(collectedErrors, moreErrors...)
}
return collectedErrors, nil
}

func (c *Config) Validate(start string, destinations func(from string) []string) ([]string, error) {
return c.validate(start, destinations, map[string]bool{})
}
29 changes: 14 additions & 15 deletions internal/check/check_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,19 @@ func TestCheck(t *testing.T) {
tests := []struct {
Name string
Spec [][]int
Config Config
Config *Config
Failures []string
}{
{
Name: "Simple",
Spec: [][]int{
{1, 2, 3},
{2, 4},
{3, 4},
{4},
{3},
0: {1, 2, 3},
1: {2, 4},
2: {3, 4},
3: {4},
4: {3},
},
Config: Config{
Config: &Config{
Entrypoints: []string{"0"},
WhiteList: map[string][]string{
"4": {},
Expand All @@ -35,8 +35,8 @@ func TestCheck(t *testing.T) {
},
},
Failures: []string{
"4 -> 3",
"0 -> 3",
"4 -> 3",
"detected circular dependency: 3 -> 4 -> 3",
},
},
Expand All @@ -46,16 +46,15 @@ func TestCheck(t *testing.T) {
t.Run(tt.Name, func(t *testing.T) {
a := require.New(t)

err := Check(func(s string) (dep_tree.NodeParser[[]int], error) {
return &dep_tree.TestParser{
Start: s,
Spec: tt.Spec,
}, nil
}, &tt.Config) //nolint:gosec
parserBuilder := func(s []string) (dep_tree.NodeParser[[]int], error) {
return &dep_tree.TestParser{Spec: tt.Spec}, nil
}

err := Check(parserBuilder, tt.Config)
if tt.Failures != nil {
msg := err.Error()
failures := strings.Split(msg, "\n")
failures = failures[1 : len(failures)-1]
failures = failures[1:]
a.Equal(tt.Failures, failures)
}
})
Expand Down

0 comments on commit 6e4436d

Please sign in to comment.