Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions projectguard/api/projectguard.api
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ public abstract interface class com/rubensousa/projectguard/plugin/ModuleRestric
public abstract fun allow ([Ljava/lang/String;)V
public abstract fun allow ([Lorg/gradle/api/internal/catalog/DelegatingProjectDependency;)V
public abstract fun allow ([Lorg/gradle/api/provider/Provider;)V
public abstract fun allowExternalLibraries ()V
public abstract fun applyRule (Lcom/rubensousa/projectguard/plugin/RestrictModuleRule;)V
public abstract fun reason (Ljava/lang/String;)V
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,8 @@ interface ModuleRestrictionScope {

fun allow(vararg library: Provider<MinimalExternalModuleDependency>)

fun allowExternalLibraries()

fun applyRule(rule: RestrictModuleRule)

// Not sure this is a good idea, bundles can be updated without seeing what dependencies we are allowing
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ abstract class ProjectGuardExtension @Inject constructor(
ModuleRestrictionSpec(
modulePath = modulePath,
reason = scope.getReason(),
allowed = scope.getAllowedDependencies()
allowed = scope.getAllowedDependencies(),
allowExternalLibraries = scope.areExternalLibrariesAllowed()
)
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,20 @@

package com.rubensousa.projectguard.plugin.internal

internal sealed interface Dependency {
import java.io.Serializable

internal sealed interface Dependency : Serializable {
val id: String
val isLibrary: Boolean
}

internal data class DirectDependency(
override val id: String,
override val isLibrary: Boolean,
) : Dependency

internal data class TransitiveDependency(
override val id: String,
override val isLibrary: Boolean,
val path: List<String>,
) : Dependency
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,42 @@ import java.io.Serializable
internal class DependencyGraph : Serializable {

private val configurations = mutableMapOf<String, Configuration>()
private val libraries = mutableSetOf<String>()

fun getConfigurations() = configurations.values.toList()

fun addDependency(
module: String,
dependency: DirectDependency,
configurationId: String = DependencyConfiguration.COMPILE,
) {
val configuration = configurations.getOrPut(configurationId) {
Configuration(configurationId)
}
configuration.add(module = module, dependency = dependency)
}

fun addInternalDependency(
module: String,
dependency: String,
configurationId: String = DependencyConfiguration.COMPILE,
) {
addDependency(
module = module,
dependency = dependency,
configurationId = configurationId
dependency = DirectDependency(dependency, isLibrary = false),
configurationId = configurationId,
)
}

fun addExternalDependency(
fun addLibraryDependency(
module: String,
dependency: String,
configurationId: String = DependencyConfiguration.COMPILE,
) {
addDependency(
module = module,
dependency = dependency,
dependency = DirectDependency(dependency, isLibrary = true),
configurationId = configurationId
)
libraries.add(dependency)
}

fun isExternalLibrary(dependency: String): Boolean {
return libraries.contains(dependency)
}

fun getDependencies(module: String): List<Dependency> {
Expand All @@ -64,71 +69,57 @@ internal class DependencyGraph : Serializable {
TraversalState(
configurationId = configuration.id,
dependency = dependency,
path = emptyList()
)
)
}
}
while (queue.isNotEmpty()) {
val currentTraversal = queue.removeFirst()
val currentDependency = currentTraversal.dependency
if (visitedDependencies.contains(currentDependency)) {
if (visitedDependencies.contains(currentDependency.id)) {
continue
}
paths[currentDependency] = if (currentTraversal.path.isEmpty()) {
DirectDependency(currentDependency)
} else {
TransitiveDependency(
currentDependency,
currentTraversal.path + currentDependency
)
}
visitedDependencies.add(currentDependency)
paths[currentDependency.id] = currentDependency
visitedDependencies.add(currentDependency.id)
configurations.values.forEach { configuration ->
// Search only for non-test configurations as they're not considered transitive at this point
if (!DependencyConfiguration.isTestConfiguration(configuration.id)) {
configuration.getDependencies(currentDependency).forEach { nextDependency ->
configuration.getDependencies(currentDependency.id).forEach { nextDependency ->
queue.addFirst(
TraversalState(
configurationId = configuration.id,
dependency = nextDependency,
path = currentTraversal.path + currentDependency
dependency = TransitiveDependency(
id = nextDependency.id,
isLibrary = nextDependency.isLibrary,
path = when (currentDependency) {
is DirectDependency -> listOf(currentDependency.id, nextDependency.id)
is TransitiveDependency -> currentDependency.path + nextDependency.id
},
)
)
)
}
}
}
}
return paths.values.sortedBy { it.id }
}

private fun addDependency(
module: String,
dependency: String,
configurationId: String,
) {
val configuration = configurations.getOrPut(configurationId) {
Configuration(configurationId)
}
configuration.add(module = module, dependency = dependency)
return paths.values.sortedBy { dependency -> dependency.id }
}

private data class TraversalState(
val configurationId: String,
val dependency: String,
val path: List<String>,
val dependency: Dependency,
)

class Configuration(val id: String) : Serializable {

private val nodes = mutableMapOf<String, MutableSet<String>>()
private val nodes = mutableMapOf<String, MutableSet<DirectDependency>>()

fun add(module: String, dependency: String) {
fun add(module: String, dependency: DirectDependency) {
val existingDependencies = nodes.getOrPut(module) { mutableSetOf() }
existingDependencies.add(dependency)
}

fun getDependencies(module: String): Set<String> {
fun getDependencies(module: String): Set<DirectDependency> {
return nodes[module] ?: emptySet()
}

Expand All @@ -148,14 +139,11 @@ internal class DependencyGraph : Serializable {

override fun equals(other: Any?): Boolean {
return other is DependencyGraph
&& other.libraries == this.libraries
&& other.configurations == this.configurations
}

override fun hashCode(): Int {
var result = configurations.hashCode()
result = 31 * result + libraries.hashCode()
return result
return configurations.hashCode()
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,19 +28,14 @@ internal class DependencyGraphBuilder {
projectDump.modules.forEach { report ->
report.configurations.forEach { configuration ->
configuration.dependencies.forEach { dependency ->
if (dependency.isLibrary) {
graph.addExternalDependency(
configurationId = configuration.id,
module = report.module,
dependency = dependency.id,
)
} else {
graph.addInternalDependency(
configurationId = configuration.id,
module = report.module,
dependency = dependency.id,
)
}
graph.addDependency(
module = report.module,
dependency = DirectDependency(
id = dependency.id,
isLibrary = dependency.isLibrary
),
configurationId = configuration.id
)
}
}
}
Expand All @@ -67,7 +62,7 @@ internal class DependencyGraphBuilder {
}

is ExternalModuleDependency -> {
graph.addExternalDependency(
graph.addLibraryDependency(
module = moduleId,
dependency = "${dependency.group}:${dependency.name}",
configurationId = config.name
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,6 @@ internal sealed interface DependencyRestriction {
return "module:$moduleId|dependency:${dependencyId}"
}

fun getText(moduleId: String): String

companion object {

fun from(dependency: Dependency, reason: String): DependencyRestriction {
Expand All @@ -50,36 +48,14 @@ internal sealed interface DependencyRestriction {
internal data class DirectDependencyRestriction(
override val dependencyId: String,
override val reason: String = UNSPECIFIED_REASON,
) : DependencyRestriction {

override fun getText(moduleId: String): String {
return """
| Dependency restriction found!
| Module -> $moduleId
| Match -> $dependencyId
| Module '$moduleId' cannot depend on '$dependencyId'
| Reason: $reason
""".trimMargin()
}

}
) : DependencyRestriction

internal data class TransitiveDependencyRestriction(
override val dependencyId: String,
val pathToDependency: List<String>,
override val reason: String = UNSPECIFIED_REASON,
) : DependencyRestriction {

override fun getText(moduleId: String): String {
return """
| Transitive dependency restriction found!
| Module -> $moduleId
| Match -> $dependencyId from ${getPathToDependencyText()}
| Module '$moduleId' cannot depend on '$dependencyId'
| Reason: $reason
""".trimMargin()
}

fun getPathToDependencyText(): String {
return pathToDependency.joinToString(separator = " -> ") { it }
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,10 @@ internal class DependencyRestrictionFinder(
dependency: Dependency,
spec: ProjectGuardSpec,
) {
spec.moduleRestrictionSpecs.forEach { restriction ->
for (restriction in spec.moduleRestrictionSpecs) {
if (dependency.isLibrary && restriction.allowExternalLibraries) {
continue
}
val matchesModule = hasModuleMatch(
modulePath = moduleId,
referencePath = restriction.modulePath
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ internal class ModuleRestrictionScopeImpl : ModuleRestrictionScope {

private val allowed = mutableListOf<ModuleAllowSpec>()
private var restrictionReason = UNSPECIFIED_REASON
private var allowExternalLibraries = false

override fun reason(reason: String) {
restrictionReason = reason
Expand All @@ -41,6 +42,10 @@ internal class ModuleRestrictionScopeImpl : ModuleRestrictionScope {
allow(modulePath = moduleDelegation.map { module -> module.path }.toTypedArray())
}

override fun allowExternalLibraries() {
allowExternalLibraries = true
}

override fun allow(vararg library: Provider<MinimalExternalModuleDependency>) {
allow(modulePath = library.map { lib ->
lib.getDependencyPath()
Expand All @@ -55,5 +60,7 @@ internal class ModuleRestrictionScopeImpl : ModuleRestrictionScope {

fun getReason() = restrictionReason

fun areExternalLibrariesAllowed() = allowExternalLibraries


}
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ internal data class ModuleRestrictionSpec(
val modulePath: String,
val reason: String,
val allowed: List<ModuleAllowSpec>,
val allowExternalLibraries: Boolean,
): Serializable
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,11 @@ internal class DependencyDumpExecutor(
configurations = dependencyGraph.getConfigurations().map { configuration ->
ConfigurationDependencies(
id = configuration.id,
dependencies = configuration.getDependencies(moduleId).toList().map { dependencyId ->
DependencyReferenceDump(dependencyId, dependencyGraph.isExternalLibrary(dependencyId))
dependencies = configuration.getDependencies(moduleId).map { dependency ->
DependencyReferenceDump(
id = dependency.id,
isLibrary = dependency.isLibrary
)
}
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,37 @@ class ProjectGuardExtensionTest {
assertThat(restrictions.first().allowed.first().modulePath).isEqualTo("kotlin")
}

@Test
fun `module restriction does not allow all external libraries by default`() {
// given
val extension = createExtension()

// when
extension.restrictModule(":domain")

// then
val spec = extension.getSpec()
val restrictions = spec.moduleRestrictionSpecs
assertThat(restrictions.first().allowExternalLibraries).isFalse()
}

@Test
fun `module restriction can allow all external libraries`() {
// given
val extension = createExtension()

// when
extension.restrictModule(":domain") {
allow(":feature")
allowExternalLibraries()
}

// then
val spec = extension.getSpec()
val restrictions = spec.moduleRestrictionSpecs
assertThat(restrictions.first().allowExternalLibraries).isTrue()
}

@Test
fun `extension correctly configures dependency restrictions`() {
// given
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ class TaskAggregateDependencyDumpTest {
addInternalDependency(firstModule, firstDependency)
}
plugin.dumpDependencies(secondModule) {
addExternalDependency(module = secondModule, dependency = secondDependency)
addLibraryDependency(module = secondModule, dependency = secondDependency)
}

// when
Expand Down
Loading