Skip to content

Commit a259f2b

Browse files
committed
Use effective reachable versions of modules when building graph
1 parent a569bdf commit a259f2b

File tree

2 files changed

+143
-18
lines changed

2 files changed

+143
-18
lines changed

cmd/utils.go

Lines changed: 110 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -115,38 +115,131 @@ func sliceContains(val []Chain, key Chain) bool {
115115
return false
116116
}
117117

118+
type module struct {
119+
name string
120+
version string
121+
}
122+
123+
func parseModule(s string) module {
124+
if strings.Contains(s, "@") {
125+
parts := strings.SplitN(s, "@", 2)
126+
return module{name: parts[0], version: parts[1]}
127+
}
128+
return module{name: s}
129+
}
130+
118131
func generateGraph(goModGraphOutputString string, mainModules []string) DependencyOverview {
119132
depGraph := DependencyOverview{MainModules: mainModules}
133+
versionedGraph := make(map[module][]module)
134+
var lhss []module
120135
graph := make(map[string][]string)
121136
scanner := bufio.NewScanner(strings.NewReader(goModGraphOutputString))
122137

138+
var versionedMainModules []module
139+
var seenVersionedMainModules = map[module]bool{}
123140
for scanner.Scan() {
124141
line := scanner.Text()
125142
words := strings.Fields(line)
126-
// remove versions
127-
words[0] = (strings.Split(words[0], "@"))[0]
128-
words[1] = (strings.Split(words[1], "@"))[0]
129143

130-
// we don't want to add the same dep again
131-
if !contains(graph[words[0]], words[1]) {
132-
graph[words[0]] = append(graph[words[0]], words[1])
144+
lhs := parseModule(words[0])
145+
if len(versionedMainModules) == 0 || contains(mainModules, lhs.name) {
146+
if !seenVersionedMainModules[lhs] {
147+
// remember our root module and listed main modules
148+
versionedMainModules = append(versionedMainModules, lhs)
149+
seenVersionedMainModules[lhs] = true
150+
}
133151
}
134-
135152
if len(depGraph.MainModules) == 0 {
136-
depGraph.MainModules = append(depGraph.MainModules, words[0])
153+
// record the first module we see as the main module by default
154+
depGraph.MainModules = append(depGraph.MainModules, lhs.name)
155+
}
156+
rhs := parseModule(words[1])
157+
158+
// remember the order we observed lhs modules in
159+
if len(versionedGraph[lhs]) == 0 {
160+
lhss = append(lhss, lhs)
161+
}
162+
// record this lhs -> rhs relationship
163+
versionedGraph[lhs] = append(versionedGraph[lhs], rhs)
164+
}
165+
166+
// record effective versions of modules required by our main modules
167+
// in go1.17+, the main module records effective versions of all dependencies, even indirect ones
168+
effectiveVersions := map[string]string{}
169+
for _, mm := range versionedMainModules {
170+
for _, m := range versionedGraph[mm] {
171+
if effectiveVersions[m.name] < m.version {
172+
effectiveVersions[m.name] = m.version
173+
}
174+
}
175+
}
176+
177+
type edge struct {
178+
from module
179+
to module
180+
}
181+
182+
// figure out which modules in the graph are reachable from the effective versions required by our main modules
183+
reachableModules := map[string]module{}
184+
// start with our main modules
185+
var toVisit []edge
186+
for _, m := range versionedMainModules {
187+
toVisit = append(toVisit, edge{to: m})
188+
}
189+
for len(toVisit) > 0 {
190+
from := toVisit[0].from
191+
v := toVisit[0].to
192+
toVisit = toVisit[1:]
193+
if _, reachable := reachableModules[v.name]; reachable {
194+
// already flagged as reachable
195+
continue
196+
}
197+
// mark as reachable
198+
reachableModules[v.name] = from
199+
if effectiveVersion, ok := effectiveVersions[v.name]; ok && effectiveVersion > v.version {
200+
// replace with the effective version if applicable
201+
v.version = effectiveVersion
202+
} else {
203+
// set the effective version
204+
effectiveVersions[v.name] = v.version
137205
}
206+
// queue dependants of this to check for reachability
207+
for _, m := range versionedGraph[v] {
208+
toVisit = append(toVisit, edge{from: v, to: m})
209+
}
210+
}
138211

139-
// if the LHS is a mainModule
140-
// then RHS is a direct dep else transitive dep
141-
if contains(depGraph.MainModules, words[0]) && contains(depGraph.MainModules, words[1]) {
212+
for _, lhs := range lhss {
213+
if _, reachable := reachableModules[lhs.name]; !reachable {
214+
// this is not reachable via required versions, skip it
215+
continue
216+
}
217+
if effectiveVersion, ok := effectiveVersions[lhs.name]; ok && effectiveVersion != lhs.version {
218+
// this is not the effective version in our graph, skip it
142219
continue
143-
} else if contains(depGraph.MainModules, words[0]) {
144-
if !contains(depGraph.DirectDepList, words[1]) {
145-
depGraph.DirectDepList = append(depGraph.DirectDepList, words[1])
220+
}
221+
// fmt.Println(lhs.name, "via", reachableModules[lhs.name])
222+
223+
for _, rhs := range versionedGraph[lhs] {
224+
// we don't want to add the same dep again
225+
if !contains(graph[lhs.name], rhs.name) {
226+
graph[lhs.name] = append(graph[lhs.name], rhs.name)
146227
}
147-
} else if !contains(depGraph.MainModules, words[0]) {
148-
if !contains(depGraph.TransDepList, words[1]) {
149-
depGraph.TransDepList = append(depGraph.TransDepList, words[1])
228+
229+
// if the LHS is a mainModule
230+
// then RHS is a direct dep else transitive dep
231+
if contains(depGraph.MainModules, lhs.name) && contains(depGraph.MainModules, rhs.name) {
232+
continue
233+
} else if contains(depGraph.MainModules, lhs.name) {
234+
if !contains(depGraph.DirectDepList, rhs.name) {
235+
// fmt.Println(rhs.name, "via", lhs)
236+
depGraph.DirectDepList = append(depGraph.DirectDepList, rhs.name)
237+
}
238+
} else if !contains(depGraph.MainModules, lhs.name) {
239+
if !contains(depGraph.TransDepList, rhs.name) {
240+
// fmt.Println(rhs.name, "via", lhs)
241+
depGraph.TransDepList = append(depGraph.TransDepList, rhs.name)
242+
}
150243
}
151244
}
152245
}

cmd/utils_test.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -359,7 +359,7 @@ func getGoModGraphTestData() string {
359359
| \ | / \
360360
F C E
361361
*/
362-
goModGraphOutputString := `[email protected] G@1.2
362+
goModGraphOutputString := `[email protected] G@1.5
363363
364364
365365
@@ -408,3 +408,35 @@ func Test_generateGraph_custom_mainModule(t *testing.T) {
408408
t.Errorf("Expected transitive dependencies are %s but got %s", transitiveDependencyList, depGraph.TransDepList)
409409
}
410410
}
411+
412+
func Test_generateGraph_overridden_versions(t *testing.T) {
413+
mainModules := []string{"A", "D"}
414+
// obsolete C@v1 has a cycle with D@v1 and a transitive ref to unwanted dependency E@v1
415+
// effective version C@v2 updates to D@v2, which still has a cycle back to C@v2, but no dependency on E
416+
depGraph := generateGraph(`A B@v2
417+
A C@v2
418+
A D@v2
419+
B@v2 C@v1
420+
C@v1 D@v1
421+
D@v1 C@v1
422+
D@v1 E@v1
423+
C@v2 D@v2
424+
C@v2 F@v2
425+
D@v2 C@v2
426+
D@v2 G@v2`, mainModules)
427+
428+
transitiveDependencyList := []string{"C", "D", "F"}
429+
directDependencyList := []string{"B", "C", "G"}
430+
431+
if !isSliceSame(depGraph.MainModules, mainModules) {
432+
t.Errorf("Expected mainModules are %s but got %s", mainModules, depGraph.MainModules)
433+
}
434+
435+
if !isSliceSame(depGraph.DirectDepList, directDependencyList) {
436+
t.Errorf("Expected direct dependecies are %s but got %s", directDependencyList, depGraph.DirectDepList)
437+
}
438+
439+
if !isSliceSame(depGraph.TransDepList, transitiveDependencyList) {
440+
t.Errorf("Expected transitive dependencies are %s but got %s", transitiveDependencyList, depGraph.TransDepList)
441+
}
442+
}

0 commit comments

Comments
 (0)