22// SPDX-License-Identifier: Apache-2.0
33package com.autonomousapps.internal
44
5+ import com.autonomousapps.ProjectType
56import com.autonomousapps.extension.DependenciesHandler.SerializableBundles
67import com.autonomousapps.graph.Graphs.children
78import com.autonomousapps.graph.Graphs.reachableNodes
9+ import com.autonomousapps.internal.utils.filterToOrderedSet
810import com.autonomousapps.model.*
911import com.autonomousapps.model.Coordinates.Companion.copy
1012import com.autonomousapps.model.internal.DependencyGraphView
1113import com.autonomousapps.model.internal.declaration.Bucket
14+ import com.autonomousapps.model.internal.declaration.ConfigurationNames
15+ import com.autonomousapps.model.internal.declaration.Declaration
1216import com.autonomousapps.model.internal.intermediates.Usage
1317
1418/* *
@@ -19,7 +23,11 @@ import com.autonomousapps.model.internal.intermediates.Usage
1923 * C -> used as API, part of bundle with B. Should not be declared!
2024 */
2125@Suppress(" UnstableApiUsage" )
22- internal class Bundles private constructor(private val dependencyUsages : Map <Coordinates , Set <Usage >>) {
26+ internal class Bundles private constructor(
27+ private val dependencyUsages : Map <Coordinates , Set <Usage >>,
28+ private val declarations : Set <Declaration >,
29+ private val configurationNames : ConfigurationNames ,
30+ ) {
2331
2432 // a sort of adjacency-list structure
2533 private val parentKeyedBundle = mutableMapOf<Coordinates , MutableSet <Coordinates >>()
@@ -44,8 +52,103 @@ internal class Bundles private constructor(private val dependencyUsages: Map<Coo
4452 }
4553
4654 fun hasParentInBundle (coordinates : Coordinates ): Boolean = parentPointers[coordinates] != null
55+
4756 fun findParentInBundle (coordinates : Coordinates ): Coordinates ? = parentPointers[coordinates]
4857
58+ /* *
59+ * Requirements for calling this method:
60+ * 1. [hasParentInBundle] has already been called and it returns true. Otherwise this method will throw.
61+ * 2. [addAdvice] is add-advice.
62+ *
63+ * Can return either [addAdvice] exactly, or a change-advice. Will do the latter when the following is true:
64+ * 1. [originalCoordinates] is in a bundle with a declared parent.
65+ * 2. That declared parent is declared on another source set (currently either `commonMain` or `commonTest`) in a
66+ * Kotlin Multiplatform (KMP) project.
67+ * 3. The parent declaration is implementation-scoped and [addAdvice] is api-scoped (that is, we'd like to upgrade).
68+ *
69+ * In this case, we suggest changing the parent declaration from `commonMainImplementation` or
70+ * `commonTestImplementation` to `commonMainApi` or `commonTestApi`, respectively.
71+ *
72+ * We do this because KMP is a special case where we know a priori that the commonX source sets are upstream from
73+ * target-specific source sets. We're being very conservative by only targeting the `implementation` -> `api` upgrade;
74+ * that is, the goal is to provide _safe_ advice even more than maximally-correct advice. Part of the problem here is
75+ * we're in an intermediate state where we only support JVM targets: we can't know how the other targets use the
76+ * common dependencies.
77+ *
78+ * @see [maybePrimary]
79+ */
80+ fun maybeParent (addAdvice : Advice , originalCoordinates : Coordinates ): Advice {
81+ check(addAdvice.isAdd()) { " Must be add-advice" }
82+
83+ val parent = findParentInBundle(originalCoordinates)
84+ ? : error(" No parent for $originalCoordinates . Check 'hasParentInBundle()' before calling this method." )
85+ val preferredCoordinatesNotation = preferredCoordinates(parent, addAdvice)
86+
87+ val preferredBucket = Bucket .of(addAdvice.toConfiguration!! , configurationNames)
88+
89+ // Get the source set name for the addAdvice. E.g., "jvmMain".
90+ val adviceSourceSetName = DependencyScope .sourceSetName(addAdvice.toConfiguration)
91+
92+ // Find all declarations for this dependency. E.g., ["commonMainImplementation", "jvmMainImplementation"].
93+ val parentDeclarations = declarations
94+ .filterToOrderedSet { decl -> decl.identifier == preferredCoordinatesNotation.identifier }
95+
96+ // Find all declarations for the advice source set. E.g., ["jvmMainImplementation", "jvmMainApi"]. It's been known
97+ // to happen.
98+ var parentDeclaration = parentDeclarations
99+ .filter { declaration -> DependencyScope .sourceSetName(declaration.configurationName) == adviceSourceSetName }
100+ // Pick the "highest" one (api > implementation > everything else)
101+ .maxByOrNull { declaration ->
102+ when (declaration.bucket(configurationNames)) {
103+ Bucket .API -> 10
104+ Bucket .IMPL -> 1
105+ else -> - 1
106+ }
107+ }
108+
109+ // If there are no declarations within the same source set
110+ if (parentDeclaration == null ) {
111+ parentDeclaration = parentDeclarations
112+ .filter { declaration -> DependencyScope .sourceSetName(declaration.configurationName) != adviceSourceSetName }
113+ // Pick the "highest" one (api > implementation > everything else)
114+ .maxBy { declaration ->
115+ when (declaration.bucket(configurationNames)) {
116+ Bucket .API -> 10
117+ Bucket .IMPL -> 1
118+ else -> - 1
119+ }
120+ }
121+ }
122+
123+ val parentBucket = parentDeclaration.bucket(configurationNames)
124+
125+ // Only change the advice if it's from implementation -> api. We don't change compileOnly or runtimeOnly advice.
126+ return if (preferredBucket == Bucket .API && parentBucket == Bucket .IMPL ) {
127+ // TODO(tsr): there's a bug that probably impacts all project types, but I've only just noticed while working on KMP.
128+ val toConfiguration = if (configurationNames.projectType == ProjectType .KMP ) {
129+ when (parentDeclaration.configurationName) {
130+ // Handle the `commonX` cases
131+ " commonMainImplementation" -> " commonMainApi"
132+ " commonTestImplementation" -> " commonTestApi"
133+
134+ // This means the parent is probably in the same source set?
135+ else -> addAdvice.toConfiguration
136+ }
137+ } else {
138+ // This means the parent is probably in the same source set?
139+ addAdvice.toConfiguration
140+ }
141+
142+ Advice .ofChange(
143+ coordinates = preferredCoordinatesNotation.withoutDefaultCapability(),
144+ fromConfiguration = parentDeclaration.configurationName,
145+ toConfiguration = toConfiguration,
146+ )
147+ } else {
148+ addAdvice
149+ }
150+ }
151+
49152 fun hasUsedChild (coordinates : Coordinates ): Boolean {
50153 val children = parentKeyedBundle[coordinates] ? : return false
51154
@@ -65,25 +168,30 @@ internal class Bundles private constructor(private val dependencyUsages: Map<Coo
65168 fun maybePrimary (addAdvice : Advice , originalCoordinates : Coordinates ): Advice {
66169 check(addAdvice.isAdd()) { " Must be add-advice" }
67170 return primaryPointers[originalCoordinates]?.let { primary ->
68- val preferredCoordinatesNotation =
69- if (primary is IncludedBuildCoordinates && addAdvice.coordinates is ProjectCoordinates ) {
70- primary.resolvedProject
71- } else {
72- primary
73- }
171+ val preferredCoordinatesNotation = preferredCoordinates(primary, addAdvice)
74172 addAdvice.copy(coordinates = preferredCoordinatesNotation.withoutDefaultCapability())
75173 } ? : addAdvice
76174 }
77175
176+ private fun preferredCoordinates (coordinates : Coordinates , advice : Advice ): Coordinates {
177+ return if (coordinates is IncludedBuildCoordinates && advice.coordinates is ProjectCoordinates ) {
178+ coordinates.resolvedProject
179+ } else {
180+ coordinates
181+ }
182+ }
183+
78184 companion object {
79185 fun of (
80186 projectPath : String ,
81187 dependencyGraph : Map <String , DependencyGraphView >,
82188 bundleRules : SerializableBundles ,
83189 dependencyUsages : Map <Coordinates , Set <Usage >>,
190+ declarations : Set <Declaration >,
191+ configurationNames : ConfigurationNames ,
84192 ignoreKtx : Boolean ,
85193 ): Bundles {
86- val bundles = Bundles (dependencyUsages)
194+ val bundles = Bundles (dependencyUsages, declarations, configurationNames )
87195
88196 // Handle bundles with primary entry points
89197 bundleRules.primaries.forEach { (name, primaryId) ->
0 commit comments