diff --git a/analyzer/src/main/kotlin/managers/Gradle.kt b/analyzer/src/main/kotlin/managers/Gradle.kt index 718693611517c..731c842ff7536 100644 --- a/analyzer/src/main/kotlin/managers/Gradle.kt +++ b/analyzer/src/main/kotlin/managers/Gradle.kt @@ -36,7 +36,7 @@ import org.gradle.tooling.internal.consumer.DefaultGradleConnector import org.ossreviewtoolkit.analyzer.AbstractPackageManagerFactory import org.ossreviewtoolkit.analyzer.PackageManager -import org.ossreviewtoolkit.analyzer.managers.utils.GradleDependencyGraphBuilder +import org.ossreviewtoolkit.analyzer.managers.utils.DependencyGraphBuilder import org.ossreviewtoolkit.analyzer.managers.utils.MavenSupport import org.ossreviewtoolkit.analyzer.managers.utils.identifier import org.ossreviewtoolkit.downloader.VersionControlSystem @@ -222,10 +222,11 @@ class Gradle( "The Gradle project '$projectName' uses the following Maven repositories: $repositories" } - val graphBuilder = GradleDependencyGraphBuilder(managerName, maven) + val dependencyHandler = GradleDependencyHandler(managerName, maven, repositories) + val graphBuilder = DependencyGraphBuilder(dependencyHandler) dependencyTreeModel.configurations.forEach { configuration -> configuration.dependencies.forEach { dependency -> - graphBuilder.addDependency(configuration.name, dependency, repositories) + graphBuilder.addDependency(configuration.name, dependency) } // Make sure that scopes without dependencies are recorded. diff --git a/analyzer/src/main/kotlin/managers/GradleDependencyHandler.kt b/analyzer/src/main/kotlin/managers/GradleDependencyHandler.kt new file mode 100644 index 0000000000000..3b4d39a177943 --- /dev/null +++ b/analyzer/src/main/kotlin/managers/GradleDependencyHandler.kt @@ -0,0 +1,136 @@ +/* + * Copyright (C) 2021 Bosch.IO GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.analyzer.managers + +import Dependency + +import org.apache.maven.project.ProjectBuildingException + +import org.eclipse.aether.artifact.DefaultArtifact +import org.eclipse.aether.repository.RemoteRepository + +import org.ossreviewtoolkit.analyzer.managers.utils.DependencyHandler +import org.ossreviewtoolkit.analyzer.managers.utils.MavenSupport +import org.ossreviewtoolkit.model.Identifier +import org.ossreviewtoolkit.model.OrtIssue +import org.ossreviewtoolkit.model.Package +import org.ossreviewtoolkit.model.PackageLinkage +import org.ossreviewtoolkit.model.Severity +import org.ossreviewtoolkit.model.createAndLogIssue +import org.ossreviewtoolkit.utils.collectMessagesAsString +import org.ossreviewtoolkit.utils.showStackTrace + +/** + * A specialized [DependencyHandler] implementation for Gradle's dependency model. + */ +class GradleDependencyHandler( + /** The name of the associated package manager. */ + val managerName: String, + + /** The helper object to resolve packages via Maven. */ + private val maven: MavenSupport, + + /** A list with repositories to use when resolving packages. */ + private val repositories: List +) : DependencyHandler { + override fun identifierFor(dependency: Dependency): String = + "${dependency.dependencyType()}:${dependency.groupId}:${dependency.artifactId}:${dependency.version}" + + override fun dependenciesFor(dependency: Dependency): Collection = dependency.dependencies + + override fun issuesForDependency(dependency: Dependency): Collection = + listOfNotNull( + dependency.error?.let { + createAndLogIssue( + source = managerName, + message = it, + severity = Severity.ERROR + ) + }, + + dependency.warning?.let { + createAndLogIssue( + source = managerName, + message = it, + severity = Severity.WARNING + ) + } + ) + + override fun linkageFor(dependency: Dependency): PackageLinkage = dependency.linkage() + + override fun createPackage(identifier: String, dependency: Dependency, issues: MutableList): Package? { + // Only look for a package if there was no error resolving the dependency and it is no project dependency. + if (dependency.error != null || dependency.isProjectDependency()) return null + + return try { + val artifact = DefaultArtifact( + dependency.groupId, dependency.artifactId, dependency.classifier, + dependency.extension, dependency.version + ) + + maven.parsePackage(artifact, repositories) + } catch (e: ProjectBuildingException) { + e.showStackTrace() + + issues += createAndLogIssue( + source = managerName, + message = "Could not get package information for dependency '$identifier': " + + e.collectMessagesAsString() + ) + + Package.EMPTY.copy( + id = Identifier( + type = "Maven", + namespace = dependency.groupId, + name = dependency.artifactId, + version = dependency.version + ) + ) + } + } + + /** + * Determine the type of this dependency. This manager implementation uses Maven to resolve packages, so + * the type of dependencies to packages is typically _Maven_ unless no pom is available. Only for module + * dependencies, the type of this manager is used. + */ + private fun Dependency.dependencyType(): String = + if (isProjectDependency()) { + managerName + } else { + pomFile?.let { "Maven" } ?: "Unknown" + } +} + +/** + * Determine the [PackageLinkage] for this [Dependency]. + */ +private fun Dependency.linkage() = + if (isProjectDependency()) { + PackageLinkage.PROJECT_DYNAMIC + } else { + PackageLinkage.DYNAMIC + } + +/** + * Return a flag whether this dependency references another project in the current build. + */ +private fun Dependency.isProjectDependency() = localPath != null diff --git a/analyzer/src/main/kotlin/managers/utils/GradleDependencyGraphBuilder.kt b/analyzer/src/main/kotlin/managers/utils/DependencyGraphBuilder.kt similarity index 51% rename from analyzer/src/main/kotlin/managers/utils/GradleDependencyGraphBuilder.kt rename to analyzer/src/main/kotlin/managers/utils/DependencyGraphBuilder.kt index 97c39305c4a3d..2d309491d4eaa 100644 --- a/analyzer/src/main/kotlin/managers/utils/GradleDependencyGraphBuilder.kt +++ b/analyzer/src/main/kotlin/managers/utils/DependencyGraphBuilder.kt @@ -19,66 +19,55 @@ package org.ossreviewtoolkit.analyzer.managers.utils -import Dependency - -import org.apache.maven.project.ProjectBuildingException - -import org.eclipse.aether.artifact.DefaultArtifact -import org.eclipse.aether.repository.RemoteRepository - import org.ossreviewtoolkit.model.DependencyGraph import org.ossreviewtoolkit.model.DependencyReference -import org.ossreviewtoolkit.model.Identifier import org.ossreviewtoolkit.model.OrtIssue import org.ossreviewtoolkit.model.Package -import org.ossreviewtoolkit.model.PackageLinkage import org.ossreviewtoolkit.model.RootDependencyIndex -import org.ossreviewtoolkit.model.Severity -import org.ossreviewtoolkit.model.createAndLogIssue -import org.ossreviewtoolkit.utils.collectMessagesAsString -import org.ossreviewtoolkit.utils.showStackTrace /** - * Internal class to represent the result of a search in the dependency graph. The outcome of the search determines - * how to integrate a specific dependency into the dependency graph. + * Internal class to represent the result of a search in the dependency graph. The outcome of the search + * determines how to integrate a specific dependency into the dependency graph. */ -private sealed class GraphSearchResult - -/** - * A specialized [GraphSearchResult] that indicates that the dependency that was searched for is already present in - * the dependency graph. This is the easiest case, as the [DependencyReference] that was found can directly be - * reused. - */ -private data class GraphSearchResultFound( - /** The reference to the dependency that was searched in the graph. */ - val ref: DependencyReference -) : GraphSearchResult() +private sealed class DependencyGraphSearchResult { + /** + * A specialized [DependencyGraphSearchResult] that indicates that the dependency that was searched for is already + * present in the dependency graph. This is the easiest case, as the [DependencyReference] that was found + * can directly be reused. + */ + data class Found( + /** The reference to the dependency that was searched in the graph. */ + val ref: DependencyReference + ) : DependencyGraphSearchResult() -/** - * A specialized [GraphSearchResult] that indicates that the dependency that was searched for was not found in the - * dependency graph, but a fragment has been detected, to which it can be added. - */ -private data class GraphSearchResultNotFound( - /** The index of the fragment to which to add the dependency. */ - val fragmentIndex: Int -) : GraphSearchResult() + /** + * A specialized [DependencyGraphSearchResult] that indicates that the dependency that was searched for was not + * found in the dependency graph, but a fragment has been detected, to which it can be added. + */ + data class NotFound( + /** The index of the fragment to which to add the dependency. */ + val fragmentIndex: Int + ) : DependencyGraphSearchResult() -/** - * A specialized [GraphSearchResult] that indicates that the dependency that was searched for in its current form - * cannot be added to any of the existing fragments. This means that either there are no fragments yet or each - * fragment contains a variant of the dependency with an incompatible dependency tree. In this case, a new fragment - * has to be added to the graph to host this special variant of this dependency. - */ -private object GraphSearchResultIncompatible : GraphSearchResult() + /** + * A specialized [DependencyGraphSearchResult] that indicates that the dependency that was searched for in its + * current form cannot be added to any of the existing fragments. This means that either there are no + * fragments yet or each fragment contains a variant of the dependency with an incompatible dependency + * tree. In this case, a new fragment has to be added to the graph to host this special variant of this + * dependency. + */ + object Incompatible : DependencyGraphSearchResult() +} /** - * A class that can construct a [DependencyGraph] from a set of Gradle dependencies that provides an efficient storage - * format for large dependency sets in many scopes. + * A class that can construct a [DependencyGraph] from a set of dependencies that provides an efficient storage format + * for large dependency sets in many scopes. * - * Especially in Android projects, it is common habit to have many scopes that all define their own sets of (often - * identical) dependencies. Duplicating the dependency trees for the different scopes leads to a high consumption of - * memory. This class addresses this problem by sharing the components of the dependency graph between the scopes as - * far as possible. + * For larger projects the network of transitive dependencies tends to become complex. Single packages can occur many + * times in this structure if they are referenced by multiple scopes or are fundamental libraries, on which many other + * packages depend. A naive implementation, which simply duplicates the dependency trees for each scope and package + * therefore leads to a high consumption of memory. This class addresses this problem by generating an optimized + * structure that shares the components of the dependency graph between the scopes as far as possible. * * This builder class provides the _addDependency()_ function, which has to be called for all the direct dependencies * of the different scopes. From these dependencies it constructs a single, optimized graph that is referenced by all @@ -86,16 +75,26 @@ private object GraphSearchResultIncompatible : GraphSearchResult() * so that references in the graph are just numbers. * * Ideally, the resulting dependency graph contains each dependency exactly once. There are, however, cases, in which - * packages occur multiple times in the project's dependency graph with different dependencies. Such cases are - * detected, and the corresponding packages form different nodes in the graph, so that they can be distinguished - * correctly. + * packages occur multiple times in the project's dependency graph with different dependencies, for instance if + * exclusions for transitive dependencies are used or a version resolution mechanism comes into play. In such cases, + * the corresponding packages need to form different nodes in the graph, so that they can be distinguished, and for + * packages depending on them, it must be ensured that the correct node is referenced. In the terminology of this class + * this is referred to as "fragmentation": A fragment is a consistent sub graph, in which each package occurs only + * once. Packages appearing multiple times with different dependencies need to be placed in separate fragments. It is + * then possible to uniquely identify a specific package by a combination of its numeric identifier and the index of + * the fragment it belongs to. + * + * This class implements the full logic to construct a [DependencyGraph], independent on the concrete representation of + * dependencies [D] used by specific package managers. To make this class compatible with such a dependency + * representation, the package manager implementation has to provide a [DependencyHandler]. Via this handler, all the + * relevant information about dependencies can be extracted. */ -class GradleDependencyGraphBuilder( - /** The name of the dependency manager to use as type of identifiers. */ - private val managerName: String, - - /** The helper object to resolve packages via Maven. */ - private val maven: MavenSupport +class DependencyGraphBuilder( + /** + * The [DependencyHandler] used by this builder instance to extract information from the dependency objects when + * constructing the [DependencyGraph]. + */ + val dependencyHandler: DependencyHandler ) { /** * A list storing the identifiers of all dependencies added to this builder. This list is then used to resolve @@ -104,8 +103,7 @@ class GradleDependencyGraphBuilder( private val dependencyIds = mutableListOf() /** - * A map listing the dependencies known to this builder and their numeric indices. This is used for a fast - * calculation of the index for a specific dependency. + * A mapping of the identifiers of the dependencies known to this builder to their numeric indices. */ private val dependencyIndexMapping = mutableMapOf() @@ -138,11 +136,12 @@ class GradleDependencyGraphBuilder( } /** - * Add the given [dependency] for the scope with the given [scopeName] to this builder. Use the provided - * [repositories] to resolve the package if necessary. + * Add the given [dependency] for the scope with the given [scopeName] to this builder. This function needs to be + * called all the direct dependencies of all scopes. That way the builder gets sufficient information to construct + * the [DependencyGraph]. */ - fun addDependency(scopeName: String, dependency: Dependency, repositories: List) { - addDependencyToGraph(scopeName, dependency, repositories, transitive = false) + fun addDependency(scopeName: String, dependency: D) { + addDependencyToGraph(scopeName, dependency, transitive = false) } /** @@ -157,32 +156,28 @@ class GradleDependencyGraphBuilder( /** * Update the dependency graph by adding the given [dependency], which may be [transitive], for the scope with name - * [scopeName]. Use the provided [repositories] to resolve the package if necessary. All the dependencies of this - * dependency are processed recursively. + * [scopeName]. All the dependencies of this dependency are processed recursively. */ - private fun addDependencyToGraph( - scopeName: String, - dependency: Dependency, - repositories: List, - transitive: Boolean - ): DependencyReference { - val identifier = identifierFor(dependency) - val issues = issuesForDependency(dependency) - val index = updateDependencyMappingAndPackages(identifier, dependency, repositories, issues) + private fun addDependencyToGraph(scopeName: String, dependency: D, transitive: Boolean): DependencyReference { + val identifier = dependencyHandler.identifierFor(dependency) + val issues = dependencyHandler.issuesForDependency(dependency).toMutableList() + val index = updateDependencyMappingAndPackages(identifier, dependency, issues) val ref = when (val result = findDependencyInGraph(index, dependency)) { - is GraphSearchResultFound -> result.ref - is GraphSearchResultNotFound -> + is DependencyGraphSearchResult.Found -> result.ref + + is DependencyGraphSearchResult.NotFound -> { insertIntoGraph( RootDependencyIndex(index, result.fragmentIndex), scopeName, dependency, - repositories, issues, transitive ) - is GraphSearchResultIncompatible -> - insertIntoNewFragment(index, scopeName, dependency, repositories, issues, transitive) + } + + is DependencyGraphSearchResult.Incompatible -> + insertIntoNewFragment(index, scopeName, dependency, issues, transitive) } return updateScopeMapping(scopeName, ref, transitive) @@ -193,16 +188,11 @@ class GradleDependencyGraphBuilder( * to the data managed by this instance, resolve the package, and update the [issues] if necessary. Return the * numeric index for this dependency. */ - private fun updateDependencyMappingAndPackages( - id: String, - dependency: Dependency, - repositories: List, - issues: MutableList - ): Int { + private fun updateDependencyMappingAndPackages(id: String, dependency: D, issues: MutableList): Int { val dependencyIndex = dependencyIndexMapping[id] if (dependencyIndex != null) return dependencyIndex - updateResolvedPackages(id, dependency, repositories, issues) + updateResolvedPackages(id, dependency, issues) return dependencyIds.size.also { dependencyIds += id dependencyIndexMapping[id] = it @@ -211,15 +201,16 @@ class GradleDependencyGraphBuilder( /** * Search for the [dependency] with the given [index] in the fragments of the dependency graph. Return a - * [GraphSearchResult] that indicates how to proceed with this dependency. + * [DependencyGraphSearchResult] that indicates how to proceed with this dependency. */ - private fun findDependencyInGraph(index: Int, dependency: Dependency): GraphSearchResult { + private fun findDependencyInGraph(index: Int, dependency: D): DependencyGraphSearchResult { val mappingForCompatibleFragment = referenceMappings.find { mapping -> mapping[index]?.takeIf { dependencyTreeEquals(it, dependency) } != null } val compatibleReference = mappingForCompatibleFragment?.let { it[index] } - return compatibleReference?.let { GraphSearchResultFound(it) } ?: handleNoCompatibleDependencyInGraph(index) + return compatibleReference?.let { DependencyGraphSearchResult.Found(it) } + ?: handleNoCompatibleDependencyInGraph(index) } /** @@ -227,9 +218,10 @@ class GradleDependencyGraphBuilder( * in the dependency graph. Try to find a fragment, in which the dependency can be inserted. If this fails, a new * fragment has to be added. */ - private fun handleNoCompatibleDependencyInGraph(index: Int): GraphSearchResult { + private fun handleNoCompatibleDependencyInGraph(index: Int): DependencyGraphSearchResult { val mappingToInsert = referenceMappings.withIndex().find { index !in it.value } - return mappingToInsert?.let { GraphSearchResultNotFound(it.index) } ?: GraphSearchResultIncompatible + return mappingToInsert?.let { DependencyGraphSearchResult.NotFound(it.index) } + ?: DependencyGraphSearchResult.Incompatible } /** @@ -237,11 +229,12 @@ class GradleDependencyGraphBuilder( * packages are identified that occur multiple times in the dependency graph with different sets of dependencies; * these have to be placed in separate fragments of the dependency graph. */ - private fun dependencyTreeEquals(ref: DependencyReference, dependency: Dependency): Boolean { - if (ref.dependencies.size != dependency.dependencies.size) return false + private fun dependencyTreeEquals(ref: DependencyReference, dependency: D): Boolean { + val dependencies = dependencyHandler.dependenciesFor(dependency) + if (ref.dependencies.size != dependencies.size) return false val dependencies1 = ref.dependencies.map { dependencyIds[it.pkg] } - val dependencies2 = dependency.dependencies.associateBy(::identifierFor) + val dependencies2 = dependencies.associateBy(dependencyHandler::identifierFor) if (!dependencies2.keys.containsAll(dependencies1)) return false return ref.dependencies.all { refDep -> @@ -253,38 +246,35 @@ class GradleDependencyGraphBuilder( * Add a new fragment to the dependency graph for the [dependency] with the given [index], which may be * [transitive] and belongs to the scope with the given [scopeName]. This function is called for dependencies that * cannot be added to already existing fragments. Therefore, create a new fragment and add the [dependency] to it, - * together with its own dependencies. Store the given [issues] for the dependency and use the given - * [repositories] to resolve packages. + * together with its own dependencies. Store the given [issues] for the dependency. */ private fun insertIntoNewFragment( - index: Int, scopeName: String, - dependency: Dependency, - repositories: List, + index: Int, + scopeName: String, + dependency: D, issues: List, transitive: Boolean ): DependencyReference { val fragmentMapping = mutableMapOf() val dependencyIndex = RootDependencyIndex(index, referenceMappings.size) referenceMappings += fragmentMapping - return insertIntoGraph(dependencyIndex, scopeName, dependency, repositories, issues, transitive) + return insertIntoGraph(dependencyIndex, scopeName, dependency, issues, transitive) } /** * Insert the [dependency] with the given [RootDependencyIndex][index], which belongs to the scope with the given * [scopeName] and may be [transitive] into the dependency graph. Insert the dependencies of this [dependency] - * recursively and use the [repositories] to resolve packages. Create a new [DependencyReference] for the - * dependency and initialize it with the list of [issues]. + * recursively. Create a new [DependencyReference] for the dependency and initialize it with the list of [issues]. */ private fun insertIntoGraph( index: RootDependencyIndex, scopeName: String, - dependency: Dependency, - repositories: List, + dependency: D, issues: List, transitive: Boolean ): DependencyReference { - val transitiveDependencies = dependency.dependencies.map { - addDependencyToGraph(scopeName, it, repositories, transitive = true) + val transitiveDependencies = dependencyHandler.dependenciesFor(dependency).map { + addDependencyToGraph(scopeName, it, transitive = true) } val fragmentMapping = referenceMappings[index.fragment] @@ -292,7 +282,7 @@ class GradleDependencyGraphBuilder( pkg = index.root, fragment = index.fragment, dependencies = transitiveDependencies.toSortedSet(), - linkage = dependency.linkage(), + linkage = dependencyHandler.linkageFor(dependency), issues = issues ) @@ -301,70 +291,11 @@ class GradleDependencyGraphBuilder( } /** - * Return a list of issues that is initially populated with errors or warnings from the given [dependency]. + * Construct a [Package] for the given [dependency]. Add the new package to the set managed by this object. If this + * fails, record a corresponding message in [issues]. */ - private fun issuesForDependency(dependency: Dependency): MutableList { - val issues = mutableListOf() - - dependency.error?.let { - issues += createAndLogIssue( - source = managerName, - message = it, - severity = Severity.ERROR - ) - } - - dependency.warning?.let { - issues += createAndLogIssue( - source = managerName, - message = it, - severity = Severity.WARNING - ) - } - - return issues - } - - /** - * Construct a [Package] for the given [dependency] using the [repositories] provided. Add the new package to the - * set managed by this object. If this fails, record a corresponding message in [issues]. - */ - private fun updateResolvedPackages( - identifier: String, - dependency: Dependency, - repositories: List, - issues: MutableList - ) { - // Only look for a package if there was no error resolving the dependency and it is no project dependency. - if (dependency.error != null || dependency.isProjectDependency()) return - - val pkg = try { - val artifact = DefaultArtifact( - dependency.groupId, dependency.artifactId, dependency.classifier, - dependency.extension, dependency.version - ) - - maven.parsePackage(artifact, repositories) - } catch (e: ProjectBuildingException) { - e.showStackTrace() - - issues += createAndLogIssue( - source = managerName, - message = "Could not get package information for dependency '$identifier': " + - e.collectMessagesAsString() - ) - - Package.EMPTY.copy( - id = Identifier( - type = "Maven", - namespace = dependency.groupId, - name = dependency.artifactId, - version = dependency.version - ) - ) - } - - resolvedPackages += pkg + private fun updateResolvedPackages(identifier: String, dependency: D, issues: MutableList) { + dependencyHandler.createPackage(identifier, dependency, issues)?.let { resolvedPackages += it } } /** @@ -394,38 +325,4 @@ class GradleDependencyGraphBuilder( return ref } - - /** - * Generate a string that uniquely identifies this [dependency]. This string is also used to construct an - * [Identifier] for this package. - */ - private fun identifierFor(dependency: Dependency): String = - "${dependencyType(dependency)}:${dependency.groupId}:${dependency.artifactId}:${dependency.version}" - - /** - * Determine the type of the given [dependency]. This manager implementation uses Maven to resolve packages, so - * the type of dependencies to packages is typically _Maven_ unless no pom is available. Only for module - * dependencies, the type of this manager is used. - */ - private fun dependencyType(dependency: Dependency): String = - if (dependency.isProjectDependency()) { - managerName - } else { - dependency.pomFile?.let { "Maven" } ?: "Unknown" - } } - -/** - * Determine the [PackageLinkage] for this [Dependency]. - */ -private fun Dependency.linkage() = - if (isProjectDependency()) { - PackageLinkage.PROJECT_DYNAMIC - } else { - PackageLinkage.DYNAMIC - } - -/** - * Return a flag whether this dependency references another project in the current build. - */ -private fun Dependency.isProjectDependency() = localPath != null diff --git a/analyzer/src/main/kotlin/managers/utils/DependencyHandler.kt b/analyzer/src/main/kotlin/managers/utils/DependencyHandler.kt new file mode 100644 index 0000000000000..49054c4551633 --- /dev/null +++ b/analyzer/src/main/kotlin/managers/utils/DependencyHandler.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2021 Bosch.IO GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * License-Filename: LICENSE + */ + +package org.ossreviewtoolkit.analyzer.managers.utils + +import org.ossreviewtoolkit.analyzer.PackageManager +import org.ossreviewtoolkit.model.Identifier +import org.ossreviewtoolkit.model.OrtIssue +import org.ossreviewtoolkit.model.Package +import org.ossreviewtoolkit.model.PackageLinkage + +/** + * An interface used by [DependencyGraphBuilder] to handle the specific representations of concrete [PackageManager] + * implementations in a generic way. + * + * A package manager may use its own, internal representation of a type [D] of a dependency. When constructing the + * [org.ossreviewtoolkit.model.DependencyGraph] by passing the dependencies of the single scopes, the builder must be + * able to extract certain information from the dependency objects. This is done via an implementation of this + * interface. + */ +interface DependencyHandler { + /** + * Construct a unique identifier for the given [dependency]. This identifier should be derived from the + * coordinates of the dependency. It is later converted to an [Identifier]; so it has to adhere to the + * string-representation of this class. + */ + fun identifierFor(dependency: D): String + + /** + * Return a collection with the dependencies of the given [dependency]. [DependencyGraphBuilder] invokes this + * function to construct the whole dependency tree spawned by this [dependency]. + */ + fun dependenciesFor(dependency: D): Collection + + /** + * Return the [PackageLinkage] for the given [dependency]. + */ + fun linkageFor(dependency: D): PackageLinkage + + /** + * Create a [Package] to represent the [dependency] with the given [identifier]. This is used to populate the + * packages in the analyzer result. The creation of a package may fail, e.g. if the dependency cannot be resolved. + * In this case, a concrete implementation is expected to return a dummy [Package] with correct coordinates and + * add a corresponding issue to the provided [issues] list. If the [dependency] does not map to a package, an + * implementation should return *null*. + */ + fun createPackage(identifier: String, dependency: D, issues: MutableList): Package? + + /** + * Return a collection with known issues for the given [dependency]. Some package manager implementations may + * already encounter problems when obtaining dependency representations. These can be reported here. This base + * implementation returns an empty collection. + */ + fun issuesForDependency(dependency: D): Collection = emptyList() +} diff --git a/analyzer/src/test/kotlin/managers/utils/GradleDependencyGraphBuilderTest.kt b/analyzer/src/test/kotlin/managers/GradleDependencyHandlerTest.kt similarity index 84% rename from analyzer/src/test/kotlin/managers/utils/GradleDependencyGraphBuilderTest.kt rename to analyzer/src/test/kotlin/managers/GradleDependencyHandlerTest.kt index 4d24faddb9f0e..2cdfb63983f6e 100644 --- a/analyzer/src/test/kotlin/managers/utils/GradleDependencyGraphBuilderTest.kt +++ b/analyzer/src/test/kotlin/managers/GradleDependencyHandlerTest.kt @@ -17,7 +17,7 @@ * License-Filename: LICENSE */ -package org.ossreviewtoolkit.analyzer.managers.utils +package org.ossreviewtoolkit.analyzer.managers import Dependency @@ -44,6 +44,8 @@ import java.util.SortedSet import org.eclipse.aether.artifact.DefaultArtifact import org.eclipse.aether.repository.RemoteRepository +import org.ossreviewtoolkit.analyzer.managers.utils.DependencyGraphBuilder +import org.ossreviewtoolkit.analyzer.managers.utils.MavenSupport import org.ossreviewtoolkit.model.Identifier import org.ossreviewtoolkit.model.Package import org.ossreviewtoolkit.model.PackageReference @@ -52,21 +54,24 @@ import org.ossreviewtoolkit.model.RootDependencyIndex import org.ossreviewtoolkit.model.Scope import org.ossreviewtoolkit.model.VcsInfo -class GradleDependencyGraphBuilderTest : WordSpec({ - "GradleDependencyGraphBuilder" should { +/** + * A test class to test the integration of the [Gradle] package manager with [DependencyGraphBuilder]. This class + * not only tests the dependency handler implementation itself but also the logic of the + */ +class GradleDependencyHandlerTest : WordSpec({ + "DependencyGraphBuilder" should { "collect the direct dependencies of scopes" { val scope1 = "compile" val scope2 = "test" val dep1 = createDependency("org.apache.commons", "commons-lang3", "3.11") val dep2 = createDependency("org.apache.commons", "commons-collections4", "4.4") val dep3 = createDependency("my-project", "my-module", "1.0", path = "subPath") - val maven = createMavenSupport() - val builder = GradleDependencyGraphBuilder(NAME, maven) + val builder = createGraphBuilder() - builder.addDependency(scope1, dep1, remoteRepositories) - builder.addDependency(scope1, dep3, remoteRepositories) - builder.addDependency(scope2, dep2, remoteRepositories) - builder.addDependency(scope2, dep1, remoteRepositories) + builder.addDependency(scope1, dep1) + builder.addDependency(scope1, dep3) + builder.addDependency(scope2, dep2) + builder.addDependency(scope2, dep1) val graph = builder.build() graph.scopeRoots shouldHaveSize 3 @@ -84,10 +89,9 @@ class GradleDependencyGraphBuilderTest : WordSpec({ "collect a dependency of type Maven" { val scope = "TheScope" val dep = createDependency("org.apache.commons", "commons-lang3", "3.10") - val maven = createMavenSupport() - val builder = GradleDependencyGraphBuilder(NAME, maven) + val builder = createGraphBuilder() - builder.addDependency(scope, dep, remoteRepositories) + builder.addDependency(scope, dep) val graph = builder.build() val scopes = graph.createScopes() @@ -98,10 +102,9 @@ class GradleDependencyGraphBuilderTest : WordSpec({ val scope = "TheScope" val dep = createDependency("org.apache.commons", "commons-lang3", "3.10") every { dep.pomFile } returns null - val maven = createMavenSupport() - val builder = GradleDependencyGraphBuilder(NAME, maven) + val builder = createGraphBuilder() - builder.addDependency(scope, dep, remoteRepositories) + builder.addDependency(scope, dep) val graph = builder.build() val scopes = graph.createScopes() @@ -111,10 +114,9 @@ class GradleDependencyGraphBuilderTest : WordSpec({ "collect a project dependency" { val scope = "TheScope" val dep = createDependency("a-project", "a-module", "1.0", path = "p") - val maven = createMavenSupport() - val builder = GradleDependencyGraphBuilder(NAME, maven) + val builder = createGraphBuilder() - builder.addDependency(scope, dep, remoteRepositories) + builder.addDependency(scope, dep) val graph = builder.build() val scopes = graph.createScopes() @@ -125,12 +127,11 @@ class GradleDependencyGraphBuilderTest : WordSpec({ val dep1 = createDependency("org.apache.commons", "commons-lang3", "3.11") val dep2 = createDependency("org.apache.commons", "commons-collections4", "4.4") val dep3 = createDependency("my-project", "my-module", "1.0", path = "foo") - val maven = createMavenSupport() - val builder = GradleDependencyGraphBuilder(NAME, maven) + val builder = createGraphBuilder() - builder.addDependency("s1", dep1, remoteRepositories) - builder.addDependency("s2", dep2, remoteRepositories) - builder.addDependency("s3", dep3, remoteRepositories) + builder.addDependency("s1", dep1) + builder.addDependency("s2", dep2) + builder.addDependency("s3", dep3) val packageIds = builder.packages().map { it.id } packageIds shouldContainExactlyInAnyOrder setOf(dep1.toId(), dep2.toId()) @@ -149,15 +150,14 @@ class GradleDependencyGraphBuilderTest : WordSpec({ ) val dep4 = createDependency("org.apache.commons", "commons-csv", "1.5", dependencies = listOf(dep1)) val dep5 = createDependency("com.acme", "dep", "0.7", dependencies = listOf(dep3)) - val maven = createMavenSupport() - val builder = GradleDependencyGraphBuilder(NAME, maven) - - builder.addDependency(scope1, dep1, remoteRepositories) - builder.addDependency(scope2, dep1, remoteRepositories) - builder.addDependency(scope2, dep2, remoteRepositories) - builder.addDependency(scope1, dep5, remoteRepositories) - builder.addDependency(scope1, dep3, remoteRepositories) - builder.addDependency(scope2, dep4, remoteRepositories) + val builder = createGraphBuilder() + + builder.addDependency(scope1, dep1) + builder.addDependency(scope2, dep1) + builder.addDependency(scope2, dep2) + builder.addDependency(scope1, dep5) + builder.addDependency(scope1, dep3) + builder.addDependency(scope2, dep4) val graph = builder.build() graph.scopeRoots shouldHaveSize 2 @@ -198,10 +198,10 @@ class GradleDependencyGraphBuilderTest : WordSpec({ dependencies = listOf(depConfig2) ) val depLib = createDependency("com.business", "lib", "1", dependencies = listOf(depConfig1, depAcmeExclude)) - val builder = GradleDependencyGraphBuilder(NAME, createMavenSupport()) + val builder = createGraphBuilder() - builder.addDependency(scope, depAcme, remoteRepositories) - builder.addDependency(scope, depLib, remoteRepositories) + builder.addDependency(scope, depAcme) + builder.addDependency(scope, depLib) val graph = builder.build() val scopeDependencies = scopeDependencies(graph.createScopes(), scope) @@ -236,11 +236,11 @@ class GradleDependencyGraphBuilderTest : WordSpec({ "com.acme", "lib-exclude", "1.1", dependencies = listOf(depConfig2) ) - val builder = GradleDependencyGraphBuilder(NAME, createMavenSupport()) + val builder = createGraphBuilder() - builder.addDependency(scope1, depLog, remoteRepositories) - builder.addDependency(scope1, depConfig1, remoteRepositories) - builder.addDependency(scope2, depAcmeExclude, remoteRepositories) + builder.addDependency(scope1, depLog) + builder.addDependency(scope1, depConfig1) + builder.addDependency(scope2, depAcmeExclude) val graph = builder.build() val scopes = graph.createScopes() @@ -262,7 +262,7 @@ class GradleDependencyGraphBuilderTest : WordSpec({ "support scopes without dependencies" { val scope = "EmptyScope" - val builder = GradleDependencyGraphBuilder(NAME, createMavenSupport()) + val builder = createGraphBuilder() builder.addScope(scope) val graph = builder.build() @@ -277,8 +277,8 @@ class GradleDependencyGraphBuilderTest : WordSpec({ "not override a scope's dependencies when adding it again" { val scope = "compile" val dep = createDependency("org.apache.commons", "commons-lang3", "3.11") - val builder = GradleDependencyGraphBuilder(NAME, createMavenSupport()) - builder.addDependency(scope, dep, remoteRepositories) + val builder = createGraphBuilder() + builder.addDependency(scope, dep) builder.addScope(scope) val graph = builder.build() @@ -319,6 +319,15 @@ private fun createDependency( return dependency } +/** + * Create a [DependencyGraphBuilder] equipped with a [GradleDependencyHandler] that is used by the test cases in + * this class. + */ +private fun createGraphBuilder(): DependencyGraphBuilder { + val dependencyHandler = GradleDependencyHandler(NAME, createMavenSupport(), remoteRepositories) + return DependencyGraphBuilder(dependencyHandler) +} + /** * Create a [MavenSupport] mock object which is prepared to convert arbitrary artifacts to [Package] objects. */