Skip to content

Commit 466b340

Browse files
authored
Merge pull request #48 from rubensousa/transitive_dependencies
Improve transitive dependency verifications
2 parents 4e8fe90 + a3355c1 commit 466b340

17 files changed

Lines changed: 323 additions & 232 deletions

projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/ProjectGuardPlugin.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,7 @@ class ProjectGuardPlugin : Plugin<Project> {
290290
group = "other"
291291
description = "Generates a JSON containing the dependencies of this module."
292292
projectPath.set(project.path)
293-
dependencies.set(graphBuilder.buildFromProject(project))
293+
dependencyGraph.set(graphBuilder.buildFromProject(project))
294294
outputFile.set(
295295
project.layout.buildDirectory.file(dependenciesFilePath)
296296
)

projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/DependencyConfiguration.kt

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,15 @@ package com.rubensousa.projectguard.plugin.internal
1818

1919
internal object DependencyConfiguration {
2020

21+
const val COMPILE = "compileClasspath"
22+
const val TEST = "testCompileClasspath"
23+
const val TEST_FIXTURE = "testFixturesCompileClasspath"
24+
2125
private val supportedConfigurations = mutableSetOf(
2226
"androidTestUtil",
23-
"compileClasspath",
24-
"testCompileClasspath",
25-
"testFixturesCompileClasspath",
27+
COMPILE,
28+
TEST,
29+
TEST_FIXTURE,
2630
)
2731

2832
fun isConfigurationSupported(configurationId: String): Boolean {
@@ -38,4 +42,8 @@ internal object DependencyConfiguration {
3842
&& !lowerCaseConfiguration.contains("metadata")
3943
}
4044

45+
fun isTestConfiguration(configurationId: String): Boolean {
46+
return configurationId.lowercase().contains("test")
47+
}
48+
4149
}

projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/DependencyGraph.kt

Lines changed: 106 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -18,85 +18,145 @@ package com.rubensousa.projectguard.plugin.internal
1818

1919
import java.io.Serializable
2020

21-
internal class DependencyGraph(
22-
val configurationId: String,
23-
val nodes: MutableMap<String, MutableSet<String>> = mutableMapOf(),
24-
) : Serializable {
21+
internal class DependencyGraph : Serializable {
2522

23+
private val configurations = mutableMapOf<String, Configuration>()
2624
private val libraries = mutableSetOf<String>()
2725

28-
fun addInternalDependency(module: String, dependency: String) {
29-
addDependency(module, dependency)
30-
}
26+
fun getConfigurations() = configurations.values.toList()
3127

32-
fun addExternalDependency(module: String, dependency: String) {
33-
addDependency(module, dependency)
34-
libraries.add(dependency)
28+
fun addInternalDependency(
29+
module: String,
30+
dependency: String,
31+
configurationId: String = DependencyConfiguration.COMPILE,
32+
) {
33+
addDependency(
34+
module = module,
35+
dependency = dependency,
36+
configurationId = configurationId
37+
)
3538
}
3639

37-
private fun addDependency(module: String, dependency: String) {
38-
val existingDependencies = nodes.getOrPut(module) { mutableSetOf() }
39-
existingDependencies.add(dependency)
40+
fun addExternalDependency(
41+
module: String,
42+
dependency: String,
43+
configurationId: String = DependencyConfiguration.COMPILE,
44+
) {
45+
addDependency(
46+
module = module,
47+
dependency = dependency,
48+
configurationId = configurationId
49+
)
50+
libraries.add(dependency)
4051
}
4152

4253
fun isExternalLibrary(dependency: String): Boolean {
4354
return libraries.contains(dependency)
4455
}
4556

46-
fun getDependencies(module: String): Set<String> {
47-
return nodes[module] ?: emptySet()
48-
}
49-
50-
fun getAllDependencies(module: String): List<Dependency> {
51-
/**
52-
* Until https://github.com/rubensousa/ProjectGuard/issues/3 is resolved,
53-
* exclude transitive dependency traversals for test configurations
54-
*/
55-
if (configurationId.contains("test")) {
56-
return getDependencies(module).map {
57-
DirectDependency(it)
58-
}
59-
}
57+
fun getDependencies(module: String): List<Dependency> {
6058
val visitedDependencies = mutableSetOf<String>()
6159
val paths = mutableMapOf<String, Dependency>()
62-
val stack = ArrayDeque<TraversalState>()
63-
stack.addAll(getDependencies(module).map { dependency ->
64-
TraversalState(dependency, emptyList())
65-
})
66-
while (stack.isNotEmpty()) {
67-
val currentModule = stack.removeFirst()
68-
val currentDependency = currentModule.dependency
60+
val queue = ArrayDeque<TraversalState>()
61+
configurations.values.forEach { configuration ->
62+
configuration.getDependencies(module).forEach { dependency ->
63+
queue.addFirst(
64+
TraversalState(
65+
configurationId = configuration.id,
66+
dependency = dependency,
67+
path = emptyList()
68+
)
69+
)
70+
}
71+
}
72+
while (queue.isNotEmpty()) {
73+
val currentTraversal = queue.removeFirst()
74+
val currentDependency = currentTraversal.dependency
6975
if (visitedDependencies.contains(currentDependency)) {
7076
continue
7177
}
72-
paths[currentDependency] = if (currentModule.path.isEmpty()) {
78+
paths[currentDependency] = if (currentTraversal.path.isEmpty()) {
7379
DirectDependency(currentDependency)
7480
} else {
7581
TransitiveDependency(
7682
currentDependency,
77-
currentModule.path + currentDependency
83+
currentTraversal.path + currentDependency
7884
)
7985
}
8086
visitedDependencies.add(currentDependency)
81-
getDependencies(currentDependency).forEach { nextDependency ->
82-
stack.addFirst(
83-
TraversalState(
84-
nextDependency,
85-
currentModule.path + currentDependency
86-
)
87-
)
87+
configurations.values.forEach { configuration ->
88+
// Search only for non-test configurations as they're not considered transitive at this point
89+
if (!DependencyConfiguration.isTestConfiguration(configuration.id)) {
90+
configuration.getDependencies(currentDependency).forEach { nextDependency ->
91+
queue.addFirst(
92+
TraversalState(
93+
configurationId = configuration.id,
94+
dependency = nextDependency,
95+
path = currentTraversal.path + currentDependency
96+
)
97+
)
98+
}
99+
}
88100
}
89101
}
90-
return paths.values.toList()
102+
return paths.values.sortedBy { it.id }
91103
}
92104

93-
override fun toString(): String {
94-
return "DependencyGraph(configurationId='$configurationId', nodes=$nodes)"
105+
private fun addDependency(
106+
module: String,
107+
dependency: String,
108+
configurationId: String,
109+
) {
110+
val configuration = configurations.getOrPut(configurationId) {
111+
Configuration(configurationId)
112+
}
113+
configuration.add(module = module, dependency = dependency)
95114
}
96115

97116
private data class TraversalState(
117+
val configurationId: String,
98118
val dependency: String,
99119
val path: List<String>,
100120
)
101121

122+
class Configuration(val id: String) : Serializable {
123+
124+
private val nodes = mutableMapOf<String, MutableSet<String>>()
125+
126+
fun add(module: String, dependency: String) {
127+
val existingDependencies = nodes.getOrPut(module) { mutableSetOf() }
128+
existingDependencies.add(dependency)
129+
}
130+
131+
fun getDependencies(module: String): Set<String> {
132+
return nodes[module] ?: emptySet()
133+
}
134+
135+
override fun equals(other: Any?): Boolean {
136+
return other is Configuration
137+
&& other.id == this.id
138+
&& other.nodes == this.nodes
139+
}
140+
141+
override fun hashCode(): Int {
142+
var result = id.hashCode()
143+
result = 31 * result + nodes.hashCode()
144+
return result
145+
}
146+
147+
}
148+
149+
override fun equals(other: Any?): Boolean {
150+
return other is DependencyGraph
151+
&& other.libraries == this.libraries
152+
&& other.configurations == this.configurations
153+
}
154+
155+
override fun hashCode(): Int {
156+
var result = configurations.hashCode()
157+
result = 31 * result + libraries.hashCode()
158+
return result
159+
}
160+
161+
102162
}

projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/DependencyGraphBuilder.kt

Lines changed: 13 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -23,38 +23,35 @@ import org.gradle.api.artifacts.ProjectDependency
2323

2424
internal class DependencyGraphBuilder {
2525

26-
fun buildFromDump(projectDump: DependencyGraphDump): List<DependencyGraph> {
27-
val graphs = mutableMapOf<String, DependencyGraph>()
26+
fun buildFromDump(projectDump: DependencyGraphDump): DependencyGraph {
27+
val graph = DependencyGraph()
2828
projectDump.modules.forEach { report ->
2929
report.configurations.forEach { configuration ->
30-
val graph = graphs.getOrPut(configuration.id) {
31-
DependencyGraph(configurationId = configuration.id)
32-
}
3330
configuration.dependencies.forEach { dependency ->
3431
if (dependency.isLibrary) {
3532
graph.addExternalDependency(
33+
configurationId = configuration.id,
3634
module = report.module,
3735
dependency = dependency.id,
3836
)
3937
} else {
4038
graph.addInternalDependency(
39+
configurationId = configuration.id,
4140
module = report.module,
4241
dependency = dependency.id,
4342
)
4443
}
4544
}
4645
}
4746
}
48-
return graphs.values.toList()
47+
return graph
4948
}
5049

51-
fun buildFromProject(project: Project): List<DependencyGraph> {
52-
return project.configurations
50+
fun buildFromProject(project: Project): DependencyGraph {
51+
val graph = DependencyGraph()
52+
project.configurations
5353
.filter { config -> config.isCanBeResolved && DependencyConfiguration.isConfigurationSupported(config.name) }
54-
.map { config ->
55-
val graph = DependencyGraph(
56-
configurationId = config.name,
57-
)
54+
.forEach { config ->
5855
val moduleId = project.path
5956
config.incoming.dependencies
6057
.forEach { dependency ->
@@ -63,7 +60,8 @@ internal class DependencyGraphBuilder {
6360
if (dependency.path != moduleId) {
6461
graph.addInternalDependency(
6562
module = moduleId,
66-
dependency = dependency.path
63+
dependency = dependency.path,
64+
configurationId = config.name
6765
)
6866
}
6967
}
@@ -72,11 +70,12 @@ internal class DependencyGraphBuilder {
7270
graph.addExternalDependency(
7371
module = moduleId,
7472
dependency = "${dependency.group}:${dependency.name}",
73+
configurationId = config.name
7574
)
7675
}
7776
}
7877
}
79-
graph
8078
}
79+
return graph
8180
}
8281
}

projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/DependencyRestrictionFinder.kt

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -16,35 +16,22 @@
1616

1717
package com.rubensousa.projectguard.plugin.internal
1818

19-
internal class DependencyRestrictionFinder {
19+
internal class DependencyRestrictionFinder(
20+
private val graph: DependencyGraph,
21+
) {
2022

2123
fun find(
2224
moduleId: String,
23-
graph: DependencyGraph,
24-
spec: ProjectGuardSpec,
25-
): List<DependencyRestriction> {
26-
return find(
27-
moduleId = moduleId,
28-
graphs = listOf(graph),
29-
spec = spec
30-
)
31-
}
32-
33-
fun find(
34-
moduleId: String,
35-
graphs: List<DependencyGraph>,
3625
spec: ProjectGuardSpec,
3726
): List<DependencyRestriction> {
3827
val restrictions = mutableListOf<DependencyRestriction>()
39-
graphs.forEach { graph ->
40-
graph.getAllDependencies(moduleId).forEach { dependency ->
41-
fillRestrictions(
42-
restrictions = restrictions,
43-
moduleId = moduleId,
44-
dependency = dependency,
45-
spec = spec,
46-
)
47-
}
28+
graph.getDependencies(moduleId).forEach { dependency ->
29+
fillRestrictions(
30+
restrictions = restrictions,
31+
moduleId = moduleId,
32+
dependency = dependency,
33+
spec = spec,
34+
)
4835
}
4936
// We might find multiple restrictions to the same dependency, just filter them out
5037
return filterRestrictions(moduleId, restrictions)

projectguard/src/main/kotlin/com/rubensousa/projectguard/plugin/internal/task/TaskCheck.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,10 @@ internal class CheckExecutor(
9898
val htmlReportGenerator = HtmlReportGenerator()
9999
htmlReportGenerator.generate(report, reportDir)
100100
if (fatalMatches.isNotEmpty()) {
101-
throw VerificationException("Found ${fatalMatches.size} fatal match(es). See report at file:///$reportFilePath")
101+
throw VerificationException(
102+
"${fatalMatches.take(10).joinToString("\n\n") { it.getDescription() }} \n " +
103+
"Found ${fatalMatches.size} fatal match(es). See full report at file:///$reportFilePath"
104+
)
102105
} else {
103106
println("No fatal matches found. See report at file:///$reportFilePath")
104107
}

0 commit comments

Comments
 (0)