Skip to content

Commit 1886e39

Browse files
committed
refactor(analyzer): Simplify applying the replace directive
Go's module graph as output by [1] does not adhere to any replace directive. In fact, a replace directive only replaces the source code for a specified module while there is no effect on the dependency tree in the subtree of the respective replaced module [2]. Also [3] operates on the module graph and therefore runs regardless of any replace directives. The current implementation in `GoMod` applies the replace directive in a very first step to all `ModuleInfo`s. So, all further logic needs to adhere to the already applied replace directive. This adds unneeded complexity, because the whole dependency construction can be done regardless of any replace directive. It is simpler and suffices to apply the replace directive when mapping a `ModuleInfo` to an ORT `Project` or `Package` in a very last step. So, refactor the code to do exactly that in order to reduce complexity and as a preparation for handling local module dependencies in a following change. This aligns the representation of the module graph and the module infos again with what the tooling actually returns, instead of using patched up / modified data, which by itself is already a major reduction of complexity. Note: It is required to make the Graph operate on `(moduleName, ModuleVersion)` tuples, because the previously used `Identifier`s do depend on the replace directive, of which the graph should be and now is made unware of. [1] `go mod graph` [2] golang/go#40513 [3] `go mod why -m` Signed-off-by: Frank Viernau <[email protected]>
1 parent fd9c2cd commit 1886e39

File tree

1 file changed

+59
-52
lines changed
  • analyzer/src/main/kotlin/managers

1 file changed

+59
-52
lines changed

analyzer/src/main/kotlin/managers/GoMod.kt

Lines changed: 59 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -119,30 +119,30 @@ class GoMod(
119119
stashDirectories(projectDir.resolve("vendor")).use { _ ->
120120
val moduleInfoForModuleName = getModuleInfos(projectDir)
121121
val graph = getModuleGraph(projectDir, moduleInfoForModuleName)
122-
val projectId = moduleInfoForModuleName.getMainModuleId()
123-
val packageIds = graph.nodes - projectId
124-
val packages = packageIds.mapTo(mutableSetOf()) { moduleInfoForModuleName.getValue(it.name).toPackage() }
125-
val projectVcs = processProjectVcs(projectDir)
122+
val packages = graph.nodes.mapNotNullTo(mutableSetOf()) {
123+
moduleInfoForModuleName.getValue(it.name).toPackage()
124+
}
126125

127-
val dependenciesScopePackageIds = getTransitiveMainModuleDependencies(projectDir).let { moduleNames ->
126+
val projectVcs = processProjectVcs(projectDir)
127+
val mainScopeModules = getTransitiveMainModuleDependencies(projectDir).let { moduleNames ->
128128
graph.nodes.filterTo(mutableSetOf()) { it.name in moduleNames }
129129
}
130130

131131
val scopes = setOf(
132132
Scope(
133133
name = "main",
134-
dependencies = graph.subgraph(dependenciesScopePackageIds).toPackageReferenceForest(projectId)
134+
dependencies = graph.subgraph(mainScopeModules).toPackageReferenceForest(moduleInfoForModuleName)
135135
),
136136
Scope(
137137
name = "vendor",
138-
dependencies = graph.toPackageReferenceForest(projectId)
138+
dependencies = graph.toPackageReferenceForest(moduleInfoForModuleName)
139139
)
140140
)
141141

142142
return listOf(
143143
ProjectAnalyzerResult(
144144
project = Project(
145-
id = projectId,
145+
id = moduleInfoForModuleName.values.single { it.main }.toId(),
146146
definitionFilePath = VersionControlSystem.getPathInfo(definitionFile).path,
147147
authors = emptySet(), // Go mod doesn't support author information.
148148
declaredLicenses = emptySet(), // Go mod doesn't support declared licenses.
@@ -160,16 +160,20 @@ class GoMod(
160160
/**
161161
* Return the module graph output from `go mod graph` with non-vendor dependencies removed.
162162
*/
163-
private fun getModuleGraph(projectDir: File, moduleInfoForModuleName: Map<String, ModuleInfo>): Graph<Identifier> {
163+
private fun getModuleGraph(projectDir: File, moduleInfoForModuleName: Map<String, ModuleInfo>): Graph<GoModule> {
164164
fun moduleInfo(moduleName: String): ModuleInfo = moduleInfoForModuleName.getValue(moduleName)
165165

166-
fun parseModuleEntry(entry: String): Identifier =
167-
entry.substringBefore('@').let { moduleName ->
168-
moduleInfo(moduleName).toId()
169-
}
166+
fun parseModuleEntry(entry: String): GoModule =
167+
GoModule(
168+
name = entry.substringBefore('@'),
169+
version = entry.substringAfter('@', "")
170+
)
171+
172+
val mainModule = moduleInfoForModuleName.values.single { it.main }.run {
173+
GoModule(path, normalizeModuleVersion(version))
174+
}
170175

171-
val mainModuleId = moduleInfoForModuleName.getMainModuleId()
172-
var graph = Graph<Identifier>().apply { addNode(mainModuleId) }
176+
var graph = Graph<GoModule>().apply { addNode(mainModule) }
173177

174178
val edges = runGo("mod", "graph", workingDir = projectDir)
175179

@@ -199,12 +203,7 @@ class GoMod(
199203
graph.addEdge(parent, child)
200204
}
201205

202-
val mainModuleName = moduleInfoForModuleName.getMainModuleId().name
203-
val replacedModules = moduleInfoForModuleName.mapNotNull { (name, info) ->
204-
(info.path to name).takeIf { name != info.path }
205-
}.toMap()
206-
207-
val vendorModules = getVendorModules(graph, projectDir, mainModuleName, replacedModules)
206+
val vendorModules = getVendorModules(graph, projectDir, mainModule.name)
208207
if (vendorModules.size < graph.size) {
209208
logger.debug {
210209
"Removing ${graph.size - vendorModules.size} non-vendor modules from the dependency graph."
@@ -216,8 +215,10 @@ class GoMod(
216215
return graph.breakCycles()
217216
}
218217

219-
private fun ModuleInfo.toId(): Identifier =
220-
if (version.isBlank()) {
218+
private fun ModuleInfo.toId(): Identifier {
219+
if (replace != null) return replace.toId() // Apply replace directive.
220+
221+
return if (version.isBlank()) {
221222
// If the version is blank, it is a project in ORT speak.
222223
check(main) { "Found a local module dependency which is not supported." }
223224

@@ -238,6 +239,7 @@ class GoMod(
238239
version = normalizeModuleVersion(version)
239240
)
240241
}
242+
}
241243

242244
/**
243245
* Return the list of all modules contained in the dependency tree with resolved versions and the 'replace'
@@ -248,44 +250,27 @@ class GoMod(
248250

249251
val moduleInfos = list.stdout.byteInputStream().use { JSON.decodeToSequence<ModuleInfo>(it) }
250252

251-
return buildMap {
252-
moduleInfos.forEach { moduleInfo ->
253-
if (moduleInfo.replace != null) {
254-
// The `replace` object in the output of `go list` does not have the `indirect` flag, so copy it
255-
// from the replaced module.
256-
val replace = moduleInfo.replace.copy(indirect = moduleInfo.indirect)
257-
put(moduleInfo.path, replace)
258-
put(moduleInfo.replace.path, replace)
259-
} else {
260-
put(moduleInfo.path, moduleInfo)
261-
}
262-
}
263-
}
253+
return moduleInfos.associateBy { it.path }
264254
}
265255

266256
/**
267257
* Return the subset of the modules in [graph] required for building and testing the main module. So, test
268258
* dependencies of dependencies are filtered out.
269259
*/
270-
private fun getVendorModules(
271-
graph: Graph<Identifier>,
272-
projectDir: File,
273-
mainModuleName: String,
274-
replacedModules: Map<String, String>
275-
): Set<Identifier> {
260+
private fun getVendorModules(graph: Graph<GoModule>, projectDir: File, mainModuleName: String): Set<GoModule> {
276261
val vendorModuleNames = mutableSetOf(mainModuleName)
277262

278263
graph.nodes.chunked(WHY_CHUNK_SIZE).forEach { ids ->
279264
// Use the names of replaced modules, because `go mod why` returns only results for those.
280-
val moduleNames = ids.map { replacedModules[it.name] ?: it.name }.toTypedArray()
265+
val moduleNames = ids.map { it.name }.toTypedArray()
281266
// Use the ´-m´ switch to use module names because the graph also uses module names, not package names.
282267
// This fixes the accidental dropping of some modules.
283268
val why = runGo("mod", "why", "-m", "-vendor", *moduleNames, workingDir = projectDir)
284269

285270
vendorModuleNames += parseWhyOutput(why.stdout)
286271
}
287272

288-
return graph.nodes.filterTo(mutableSetOf()) { (replacedModules[it.name] ?: it.name) in vendorModuleNames }
273+
return graph.nodes.filterTo(mutableSetOf()) { it.name in vendorModuleNames }
289274
}
290275

291276
/**
@@ -302,7 +287,13 @@ class GoMod(
302287
}
303288
}
304289

305-
private fun ModuleInfo.toPackage(): Package {
290+
private fun ModuleInfo.toPackage(): Package? {
291+
// A ModuleInfo with blank version should be represented by a Project:
292+
if (version.isBlank()) return null
293+
294+
// Apply the replace directive:
295+
if (replace != null) return replace.toPackage()
296+
306297
val vcsInfo = toVcsInfo().orEmpty()
307298

308299
return Package(
@@ -348,27 +339,31 @@ class GoMod(
348339
private fun runGo(vararg args: CharSequence, workingDir: File? = null) =
349340
run(args = args, workingDir = workingDir, environment = environment)
350341

351-
private fun Map<String, ModuleInfo>.getMainModuleId(): Identifier = values.single { it.main }.toId()
352-
353342
/**
354343
* Convert this [Graph] to a set of [PackageReference]s that spawn the dependency trees of the direct dependencies
355344
* of the given [root] package. The graph must not contain any cycles, so [Graph.breakCycles] should be called
356345
* before.
357346
*/
358-
private fun Graph<Identifier>.toPackageReferenceForest(root: Identifier): Set<PackageReference> {
359-
fun getPackageReference(id: Identifier): PackageReference {
360-
val dependencies = getDependencies(id).mapTo(mutableSetOf()) {
347+
private fun Graph<GoModule>.toPackageReferenceForest(
348+
moduleInfoForModuleName: Map<String, ModuleInfo>
349+
): Set<PackageReference> {
350+
fun getPackageReference(module: GoModule): PackageReference {
351+
val dependencies = getDependencies(module).mapTo(mutableSetOf()) {
361352
getPackageReference(it)
362353
}
363354

364355
return PackageReference(
365-
id = id,
356+
id = moduleInfoForModuleName.getValue(module.name).toId(),
366357
linkage = PackageLinkage.PROJECT_STATIC,
367358
dependencies = dependencies
368359
)
369360
}
370361

371-
return getDependencies(root).mapTo(mutableSetOf()) { getPackageReference(it) }
362+
val mainModule = moduleInfoForModuleName.values.single { it.main }.run {
363+
GoModule(path, version)
364+
}
365+
366+
return getDependencies(mainModule).mapTo(mutableSetOf()) { getPackageReference(it) }
372367
}
373368
}
374369

@@ -411,6 +406,18 @@ private data class DepInfo(
411406
val module: ModuleInfo? = null
412407
)
413408

409+
private data class GoModule(
410+
val name: String,
411+
val version: String
412+
) {
413+
override fun toString(): String =
414+
if (version.isBlank()) {
415+
name
416+
} else {
417+
"$name@$version"
418+
}
419+
}
420+
414421
/**
415422
* The format of `.info` the Go command line tools cache under '$GOPATH/pkg/mod'.
416423
*/

0 commit comments

Comments
 (0)