@@ -45,6 +45,9 @@ type DependencyOverview struct {
4545func getDepInfo (mainModules []string ) * DependencyOverview {
4646 // get output of "go mod graph" in a string
4747 goModGraph := exec .Command ("go" , "mod" , "graph" )
48+ if dir != "" {
49+ goModGraph .Dir = dir
50+ }
4851 goModGraphOutput , err := goModGraph .Output ()
4952 if err != nil {
5053 log .Fatal (err )
@@ -112,38 +115,131 @@ func sliceContains(val []Chain, key Chain) bool {
112115 return false
113116}
114117
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+
115131func generateGraph (goModGraphOutputString string , mainModules []string ) DependencyOverview {
116132 depGraph := DependencyOverview {MainModules : mainModules }
133+ versionedGraph := make (map [module ][]module )
134+ var lhss []module
117135 graph := make (map [string ][]string )
118136 scanner := bufio .NewScanner (strings .NewReader (goModGraphOutputString ))
119137
138+ var versionedMainModules []module
139+ var seenVersionedMainModules = map [module ]bool {}
120140 for scanner .Scan () {
121141 line := scanner .Text ()
122142 words := strings .Fields (line )
123- // remove versions
124- words [0 ] = (strings .Split (words [0 ], "@" ))[0 ]
125- words [1 ] = (strings .Split (words [1 ], "@" ))[0 ]
126143
127- // we don't want to add the same dep again
128- if ! contains (graph [words [0 ]], words [1 ]) {
129- 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+ }
130151 }
131-
132152 if len (depGraph .MainModules ) == 0 {
133- 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 )
134155 }
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+ }
135176
136- // if the LHS is a mainModule
137- // then RHS is a direct dep else transitive dep
138- if contains (depGraph .MainModules , words [0 ]) && contains (depGraph .MainModules , words [1 ]) {
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
205+ }
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+ }
211+
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
139219 continue
140- } else if contains (depGraph .MainModules , words [0 ]) {
141- if ! contains (depGraph .DirectDepList , words [1 ]) {
142- 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 )
143227 }
144- } else if ! contains (depGraph .MainModules , words [0 ]) {
145- if ! contains (depGraph .TransDepList , words [1 ]) {
146- 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+ }
147243 }
148244 }
149245 }
0 commit comments