Skip to content

Commit a10c496

Browse files
authored
Add descriptions for dep-tree check (#117)
* Add descriptions for dep-tree check * Add schema.json * Update README with new check information
1 parent 32b6d2f commit a10c496

File tree

9 files changed

+445
-90
lines changed

9 files changed

+445
-90
lines changed

README.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,9 @@ check:
299299

300300
### Example configuration file
301301

302+
A `schema.json` file is provided in https://github.com/gabotechs/dep-tree/blob/main/schema.json which can be
303+
used in IDEs for providing autocompletion on `.dep-tree.yml` files.
304+
302305
Dep Tree by default will read the configuration file in `.dep-tree.yml`, which is expected to be a file
303306
that contains the following settings:
304307

@@ -360,6 +363,13 @@ check:
360363
'src/products/**':
361364
- 'src/products/**'
362365
- 'src/helpers/**'
366+
# additionally, instead of providing a simple list of allowed dependencies, you
367+
# can also provide the reason for this restriction to exist, that way, when if the
368+
# check fails, an informative error is displayed through stderr.
369+
'src/users/**':
370+
to:
371+
- 'src/helpers/**'
372+
reason: The Users domain is only allowed to import helper code, nothing else
363373

364374
# map from glob pattern to array of glob patterns that determines forbidden
365375
# dependencies. If a file that matches a key glob pattern depends on another
@@ -370,6 +380,14 @@ check:
370380
# as they are supposed to belong to different domains.
371381
'src/products/**':
372382
- 'src/users/**'
383+
# additionally, instead of providing a simple list of forbidden dependencies, you
384+
# can also provide the reason for each individual restriction to exist. If one of
385+
# these rules is broken, the reason will be displayed through stderr
386+
'src/users/**':
387+
- to: 'src/products/**'
388+
reason: The Users domain should not import anything from the Products domain
389+
- to: 'src/orders/**'
390+
reason: The Users domain should not import anything from the Orders domain
373391

374392
# typically, in a project, there is a set of files that are always good to depend
375393
# on, because they are supposed to be common helpers, or parts that are actually

internal/check/check.go

Lines changed: 43 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -26,22 +26,38 @@ func Check[T any](
2626
if err != nil {
2727
return err
2828
}
29+
2930
// 2. Check for rule violations in the graph.
30-
failures := make([]string, 0)
31+
sb := strings.Builder{}
3132
for _, node := range g.AllNodes() {
3233
for _, dep := range g.FromId(node.Id) {
3334
from, to := cfg.rel(node.Id), cfg.rel(dep.Id)
34-
pass, err := cfg.Check(from, to)
35+
pass, reason, err := cfg.Check(from, to)
3536
if err != nil {
3637
return err
3738
} else if !pass {
38-
failures = append(failures, from+" -> "+to)
39+
sb.WriteString("- ")
40+
sb.WriteString(from)
41+
sb.WriteString(" -> ")
42+
sb.WriteString(to)
43+
if reason != "" {
44+
for _, line := range strings.Split(reason, "\n") {
45+
sb.WriteString("\n ")
46+
sb.WriteString(line)
47+
}
48+
}
49+
sb.WriteString("\n")
3950
}
4051
}
4152
}
4253
// 3. Check for cycles.
4354
cycles := g.RemoveElementaryCycles()
4455
if !cfg.AllowCircularDependencies {
56+
if len(cycles) > 0 {
57+
sb.WriteString("\n")
58+
sb.WriteString("detected circular dependencies:")
59+
sb.WriteString("\n")
60+
}
4561
for _, cycle := range cycles {
4662
formattedCycleStack := make([]string, len(cycle.Stack))
4763
for i, el := range cycle.Stack {
@@ -52,64 +68,66 @@ func Check[T any](
5268
}
5369
}
5470

55-
msg := "detected circular dependency: " + strings.Join(formattedCycleStack, " -> ")
56-
failures = append(failures, msg)
71+
sb.WriteString("- ")
72+
sb.WriteString(strings.Join(formattedCycleStack, " -> "))
73+
sb.WriteString("\n")
5774
}
5875
}
59-
if len(failures) > 0 {
60-
return errors.New("Check failed, the following dependencies are not allowed:\n" + strings.Join(failures, "\n"))
76+
errorMsg := sb.String()
77+
if len(errorMsg) > 0 {
78+
return errors.New("Check failed, the following dependencies are not allowed:\n" + errorMsg)
6179
}
6280
return nil
6381
}
6482

65-
func (c *Config) whiteListCheck(from, to string) (bool, error) {
66-
for k, v := range c.WhiteList {
83+
func (c *Config) whiteListCheck(from, to string) (bool, string, error) {
84+
for k, rule := range c.WhiteList {
6785
doesMatch, err := utils.GlobstarMatch(k, from)
6886
if err != nil {
69-
return false, err
87+
return false, "", err
7088
}
7189
if doesMatch {
72-
for _, dest := range v {
90+
for _, dest := range rule.To {
7391
shouldPass, err := utils.GlobstarMatch(dest, to)
7492
if err != nil {
75-
return false, err
93+
return false, "", err
7694
}
7795
if shouldPass {
78-
return true, nil
96+
return true, "", nil
7997
}
8098
}
81-
return false, nil
99+
return false, rule.Reason, nil
82100
}
83101
}
84-
return true, nil
102+
return true, "", nil
85103
}
86104

87-
func (c *Config) blackListCheck(from, to string) (bool, error) {
105+
func (c *Config) blackListCheck(from, to string) (bool, string, error) {
88106
for k, v := range c.BlackList {
89107
doesMatch, err := utils.GlobstarMatch(k, from)
90108
if err != nil {
91-
return false, err
109+
return false, "", err
92110
}
93111
if doesMatch {
94-
for _, dest := range v {
95-
shouldReject, err := utils.GlobstarMatch(dest, to)
112+
for _, rule := range v {
113+
shouldReject, err := utils.GlobstarMatch(rule.To, to)
96114
if err != nil {
97-
return false, err
115+
return false, "", err
98116
}
99117
if shouldReject {
100-
return false, nil
118+
return false, rule.Reason, nil
101119
}
102120
}
103121
}
104122
}
105123

106-
return true, nil
124+
return true, "", nil
107125
}
108126

109-
func (c *Config) Check(from, to string) (bool, error) {
110-
pass, err := c.blackListCheck(from, to)
127+
func (c *Config) Check(from, to string) (bool, string, error) {
128+
pass, reason, err := c.blackListCheck(from, to)
111129
if err != nil || !pass {
112-
return pass, err
130+
return pass, reason, err
113131
}
114132
return c.whiteListCheck(from, to)
115133
}

internal/check/check_test.go

Lines changed: 46 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,10 @@ import (
1010

1111
func TestCheck(t *testing.T) {
1212
tests := []struct {
13-
Name string
14-
Spec [][]int
15-
Config *Config
16-
Failures []string
13+
Name string
14+
Spec [][]int
15+
Config *Config
16+
Failure string
1717
}{
1818
{
1919
Name: "Simple",
@@ -26,18 +26,48 @@ func TestCheck(t *testing.T) {
2626
},
2727
Config: &Config{
2828
Entrypoints: []string{"0"},
29-
WhiteList: map[string][]string{
29+
WhiteList: map[string]WhiteListEntries{
3030
"4": {},
3131
},
32-
BlackList: map[string][]string{
33-
"0": {"3"},
32+
BlackList: map[string][]BlackListEntry{
33+
"0": {{To: "3"}},
3434
},
3535
},
36-
Failures: []string{
37-
"0 -> 3",
38-
"4 -> 3",
39-
"detected circular dependency: 4 -> 3 -> 4",
36+
Failure: `
37+
Check failed, the following dependencies are not allowed:
38+
- 0 -> 3
39+
- 4 -> 3
40+
41+
detected circular dependencies:
42+
- 4 -> 3 -> 4`,
43+
},
44+
{
45+
Name: "With description",
46+
Spec: [][]int{
47+
0: {1, 2, 3},
48+
1: {2, 4},
49+
2: {3, 4},
50+
3: {4},
51+
4: {3},
4052
},
53+
Config: &Config{
54+
Entrypoints: []string{"0"},
55+
WhiteList: map[string]WhiteListEntries{
56+
"4": {Reason: "4 Should not be importing anything"},
57+
},
58+
BlackList: map[string][]BlackListEntry{
59+
"0": {{To: "3", Reason: "0 should not import 3"}},
60+
},
61+
},
62+
Failure: `
63+
Check failed, the following dependencies are not allowed:
64+
- 0 -> 3
65+
0 should not import 3
66+
- 4 -> 3
67+
4 Should not be importing anything
68+
69+
detected circular dependencies:
70+
- 4 -> 3 -> 4`,
4171
},
4272
}
4373

@@ -51,11 +81,11 @@ func TestCheck(t *testing.T) {
5181
tt.Config,
5282
nil,
5383
)
54-
if tt.Failures != nil {
55-
msg := err.Error()
56-
failures := strings.Split(msg, "\n")
57-
failures = failures[1:]
58-
a.Equal(tt.Failures, failures)
84+
if tt.Failure != "" {
85+
a.Equal(
86+
strings.TrimSpace(tt.Failure),
87+
strings.TrimSpace(err.Error()),
88+
)
5989
}
6090
})
6191
}

internal/check/config.go

Lines changed: 81 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,11 @@ package check
22

33
type Config struct {
44
Path string
5-
Entrypoints []string `yaml:"entrypoints"`
6-
AllowCircularDependencies bool `yaml:"allowCircularDependencies"`
7-
Aliases map[string][]string `yaml:"aliases"`
8-
WhiteList map[string][]string `yaml:"allow"`
9-
BlackList map[string][]string `yaml:"deny"`
5+
Entrypoints []string `yaml:"entrypoints"`
6+
AllowCircularDependencies bool `yaml:"allowCircularDependencies"`
7+
Aliases map[string][]string `yaml:"aliases"`
8+
WhiteList map[string]WhiteListEntries `yaml:"allow"`
9+
BlackList map[string][]BlackListEntry `yaml:"deny"`
1010
}
1111

1212
func (c *Config) Init(path string) {
@@ -15,21 +15,85 @@ func (c *Config) Init(path string) {
1515
}
1616

1717
func (c *Config) expandAliases() {
18-
lists := []map[string][]string{
19-
c.WhiteList,
20-
c.BlackList,
18+
for k, entries := range c.WhiteList {
19+
newV := make([]string, 0)
20+
for _, entry := range entries.To {
21+
if aliases, ok := c.Aliases[entry]; ok {
22+
newV = append(newV, aliases...)
23+
} else {
24+
newV = append(newV, entry)
25+
}
26+
}
27+
c.WhiteList[k] = WhiteListEntries{
28+
To: newV,
29+
Reason: entries.Reason,
30+
}
2131
}
22-
for _, list := range lists {
23-
for k, v := range list {
24-
newV := make([]string, 0)
25-
for _, entry := range v {
26-
if alias, ok := c.Aliases[entry]; ok {
27-
newV = append(newV, alias...)
28-
} else {
29-
newV = append(newV, entry)
32+
33+
for k, entries := range c.BlackList {
34+
newV := make([]BlackListEntry, 0)
35+
for _, entry := range entries {
36+
if aliases, ok := c.Aliases[entry.To]; ok {
37+
for _, alias := range aliases {
38+
newV = append(newV, BlackListEntry{
39+
To: alias,
40+
Reason: entry.Reason,
41+
})
3042
}
43+
} else {
44+
newV = append(newV, entry)
3145
}
32-
list[k] = newV
3346
}
47+
c.BlackList[k] = newV
48+
}
49+
}
50+
51+
type BlackListEntry struct {
52+
To string `yaml:"to"`
53+
Reason string `yaml:"reason"`
54+
}
55+
56+
func (v *BlackListEntry) UnmarshalYAML(unmarshal func(interface{}) error) error {
57+
var str string
58+
if err := unmarshal(&str); err == nil {
59+
v.To = str
60+
return nil
61+
}
62+
temp := struct {
63+
To string `yaml:"to"`
64+
Reason string `yaml:"reason"`
65+
}{}
66+
67+
err := unmarshal(&temp)
68+
if err != nil {
69+
return err
70+
}
71+
v.To = temp.To
72+
v.Reason = temp.Reason
73+
return nil
74+
}
75+
76+
type WhiteListEntries struct {
77+
To []string `yaml:"to"`
78+
Reason string `yaml:"reason"`
79+
}
80+
81+
func (v *WhiteListEntries) UnmarshalYAML(unmarshal func(interface{}) error) error {
82+
var strList []string
83+
if err := unmarshal(&strList); err == nil {
84+
v.To = strList
85+
return nil
86+
}
87+
88+
temp := struct {
89+
To []string `yaml:"to"`
90+
Reason string `yaml:"reason"`
91+
}{}
92+
err := unmarshal(&temp)
93+
if err != nil {
94+
return err
3495
}
96+
v.To = temp.To
97+
v.Reason = temp.Reason
98+
return nil
3599
}

0 commit comments

Comments
 (0)