@@ -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+
118131func 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 }
0 commit comments