-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathdepth.go
More file actions
144 lines (127 loc) · 4.42 KB
/
Copy pathdepth.go
File metadata and controls
144 lines (127 loc) · 4.42 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
// Package depth flags modules that sit too deep in the import chain — i.e.
// whose longest transitive dependency path is unusually long. Deep modules are
// slow to understand and load-bearing: a change at the bottom of a long chain
// can force rebuilds/retests all the way up.
package depth
import (
"context"
"fmt"
"sort"
"strings"
"github.com/enola-labs/enola/internal/explainers/common"
"github.com/enola-labs/enola/internal/facts"
)
const (
// minDepth is the longest-chain length at or above which a module is
// reported. A chain of N means N modules are imported in sequence below it.
minDepth = 5
// maxInsights caps how many deep modules are reported, deepest first.
maxInsights = 10
)
// DepthExplainer detects modules deep in the dependency chain.
type DepthExplainer struct{}
// New creates a new DepthExplainer.
func New() *DepthExplainer {
return &DepthExplainer{}
}
func (e *DepthExplainer) Name() string {
return "dependency-depth"
}
// Explain builds the module import graph and computes each module's longest
// downstream dependency chain (cycle-safe), reporting the deepest ones.
func (e *DepthExplainer) Explain(ctx context.Context, store *facts.Store) ([]facts.Insight, error) {
graph := common.BuildModuleGraph(store)
if len(graph) == 0 {
return nil, nil
}
// Determinism: sort each neighbor list and the root iteration order so the
// memoized longest-path results don't depend on Go's randomized map
// iteration. Without this, cycles make the computed depths vary run to run.
roots := make([]string, 0, len(graph))
for mod := range graph {
roots = append(roots, mod)
sort.Strings(graph[mod])
}
sort.Strings(roots)
memo := make(map[string][]string) // module -> deepest chain starting at module
visiting := make(map[string]bool)
for _, mod := range roots {
longestChain(mod, graph, memo, visiting)
}
type result struct {
module string
chain []string
}
var results []result
for mod, chain := range memo {
if len(chain) >= minDepth {
results = append(results, result{module: mod, chain: chain})
}
}
// Deepest first; break ties by name for determinism.
sort.Slice(results, func(i, j int) bool {
if len(results[i].chain) != len(results[j].chain) {
return len(results[i].chain) > len(results[j].chain)
}
return results[i].module < results[j].module
})
var insights []facts.Insight
for i, r := range results {
if i >= maxInsights {
break
}
depth := len(r.chain)
evidence := make([]facts.Evidence, 0, len(r.chain))
for _, m := range r.chain {
evidence = append(evidence, facts.Evidence{Fact: m})
}
insights = append(insights, facts.Insight{
// Title format is parsed by pkg/explain (Code health section); keep stable.
Title: fmt.Sprintf("Deep dependency chain: %s (depth %d)", r.module, depth),
Description: fmt.Sprintf(
"Module %q has a longest dependency chain of %d modules: %s. "+
"Deep chains slow comprehension and widen rebuild/retest impact when a "+
"module near the bottom changes.",
r.module, depth, strings.Join(r.chain, " -> "),
),
Confidence: 0.7,
Evidence: evidence,
Actions: []string{
"Flatten the chain by depending on shared abstractions instead of deep transitive modules",
"Check whether intermediate modules are pass-through layers that can be removed",
"Introduce interfaces to decouple the deepest modules from their consumers",
},
})
}
return insights, nil
}
// longestChain returns the longest dependency chain starting at module, as a
// slice beginning with module. Results are memoized. The visiting set breaks
// cycles: a back-edge to a module already on the current path contributes no
// further depth, so cyclic graphs terminate.
func longestChain(module string, graph map[string][]string, memo map[string][]string, visiting map[string]bool) []string {
if chain, ok := memo[module]; ok {
return chain
}
if visiting[module] {
// Cycle back-edge: the target is already on the current path, so
// following it would revisit a module. Contribute no further depth
// (returning the module here would double-count it up the chain).
return nil
}
visiting[module] = true
var best []string
for _, dep := range graph[module] {
if dep == module {
continue // self-import; ignore
}
child := longestChain(dep, graph, memo, visiting)
if len(child) > len(best) {
best = child
}
}
visiting[module] = false
chain := append([]string{module}, best...)
memo[module] = chain
return chain
}