-
Notifications
You must be signed in to change notification settings - Fork 11
Expand file tree
/
Copy pathcommon.go
More file actions
163 lines (143 loc) · 4.87 KB
/
Copy pathcommon.go
File metadata and controls
163 lines (143 loc) · 4.87 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
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
// Package common holds helpers shared by multiple explainers: module-level
// dependency-graph construction and statistical-outlier detection. Extracting
// these here keeps the per-explainer packages small and avoids the
// copy-pasted graph/path logic that previously lived in cycles and layers.
package common
import (
"math"
"strings"
"github.com/enola-labs/enola/internal/facts"
)
// FileDir returns the directory portion of a file path, which enola uses as the
// canonical module name. A path with no separator maps to ".".
func FileDir(file string) string {
parts := strings.Split(file, "/")
if len(parts) <= 1 {
return "."
}
return strings.Join(parts[:len(parts)-1], "/")
}
// IsExternalImport reports whether an import target points outside the repo
// (Go stdlib, third-party, or an npm package) rather than at an internal module.
func IsExternalImport(path string) bool {
// Go external imports contain dots (fmt, net/http, github.com/...).
// TS external imports don't start with . or / and aren't relative.
if strings.HasPrefix(path, ".") || strings.HasPrefix(path, "/") {
return false
}
// Go standard library or third-party.
if strings.Contains(path, ".") || !strings.Contains(path, "/") {
// Likely a Go stdlib or npm package (e.g., "fmt", "react", "@types/node").
return true
}
return false
}
// ResolveRelativeImport resolves a "./x" or "../x" import target against the
// source module's path, yielding an absolute (repo-relative) module path.
func ResolveRelativeImport(sourceModule, target string) string {
if !strings.HasPrefix(target, ".") {
return target
}
parts := strings.Split(sourceModule, "/")
targetParts := strings.Split(target, "/")
for _, tp := range targetParts {
switch tp {
case ".":
continue
case "..":
if len(parts) > 0 {
parts = parts[:len(parts)-1]
}
default:
parts = append(parts, tp)
}
}
return strings.Join(parts, "/")
}
// BuildModuleGraph extracts the module-level import adjacency list from the
// store: module name -> list of internal modules it imports. External imports
// are dropped and relative imports are normalized. Every declared module is
// present as a key (with a possibly-empty edge list).
//
// Test-role modules (test bundles / spec trees, tagged module_role=test) are
// excluded as both nodes and edge endpoints: they are not part of the production
// architecture, and a test target normally imports the very module it exercises,
// which would otherwise drag test bundles into cycle, layer-violation, and
// depth findings (the classic "the cycle chain mixes Tests/ and Sources/"
// artifact). This mirrors package-metrics, which already filters non-production
// roles. Modules with an absent or non-test role are kept (consumers treat an
// absent role as included).
func BuildModuleGraph(store *facts.Store) map[string][]string {
graph := make(map[string][]string)
modules := store.ByKind(facts.KindModule)
moduleNames := make(map[string]bool)
testModules := make(map[string]bool)
for _, m := range modules {
if role, _ := m.Props[facts.PropModuleRole].(string); role == facts.ModuleRoleTest {
testModules[m.Name] = true
continue
}
moduleNames[m.Name] = true
if _, ok := graph[m.Name]; !ok {
graph[m.Name] = nil
}
}
deps := store.ByKind(facts.KindDependency)
for _, dep := range deps {
sourceModule := FileDir(dep.File)
if testModules[sourceModule] {
continue // edge out of a test bundle — not production architecture
}
for _, rel := range dep.Relations {
if rel.Kind != facts.RelImports {
continue
}
target := rel.Target
if IsExternalImport(target) {
continue
}
if strings.HasPrefix(target, ".") {
target = ResolveRelativeImport(sourceModule, target)
}
if moduleNames[target] {
graph[sourceModule] = append(graph[sourceModule], target)
}
}
}
return graph
}
// SymbolModule returns the module a symbol belongs to. Symbol names encode the
// module as the prefix before the first ".", e.g. "internal/auth.Login.Verify"
// -> "internal/auth". Names without a "." are returned unchanged.
func SymbolModule(name string) string {
if i := strings.Index(name, "."); i >= 0 {
return name[:i]
}
return name
}
// MeanStdDev returns the arithmetic mean and population standard deviation of
// the given values. Both are 0 for an empty slice.
func MeanStdDev(values []float64) (mean, std float64) {
n := len(values)
if n == 0 {
return 0, 0
}
var sum float64
for _, v := range values {
sum += v
}
mean = sum / float64(n)
var variance float64
for _, v := range values {
d := v - mean
variance += d * d
}
variance /= float64(n)
return mean, math.Sqrt(variance)
}
// OutlierThreshold returns mean + k*stddev for the given values — the cutoff
// above which a value is treated as a high outlier. Returns 0 for empty input.
func OutlierThreshold(values []float64, k float64) float64 {
mean, std := MeanStdDev(values)
return mean + k*std
}