[lint] Mostly remove GradleCoordinate from GradleDetector

Its primary replacement in this context is Dependency, representing a
declaration.  In some cases we perpetuate the "pun" of a particular
Component to a Dependency, in particular when handling Dependencies
with a textual declaration involving a non-compile-time-constant
string (e.g. variable interpolation), where Lint requests the resolved
component from the project models and then treats that as if it were
the user's depdendency declaration.

Even after accounting for this pun, this implementation is not correct
because the mapping of identifiers to Dependency entities is not
one-to-one: there are multiple ways of writing certain RichVersion
declaration identifiers, for example "[,]" and "+" both mean "any
version", but the entities themselves do not preserve the original
notation.  This means that attempts to perform string replacement of
the version identifier for a new one may fail.

Bug: 257726238
Bug: 259279612
Bug: 242691473
Bug: 279886738
Test: existing tests adjusted
Change-Id: Ie21b0add2075875080d582b1f167ef836d0e42c9
diff --git a/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/LintClient.kt b/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/LintClient.kt
index d88056b..d3d15eb 100644
--- a/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/LintClient.kt
+++ b/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/LintClient.kt
@@ -32,6 +32,7 @@
 import com.android.SdkConstants.PLATFORM_LINUX
 import com.android.SdkConstants.RES_FOLDER
 import com.android.SdkConstants.SRC_FOLDER
+import com.android.ide.common.gradle.Dependency
 import com.android.ide.common.gradle.Version
 import com.android.ide.common.repository.AgpVersion
 import com.android.ide.common.repository.GradleCoordinate
@@ -1483,14 +1484,18 @@
   protected val sourceNodeCache: MutableMap<Node, Pair<File, out Node>> =
     Maps.newIdentityHashMap<Node, Pair<File, out Node>>()
 
+  @Suppress("DeprecatedCallableAddReplaceWith")
+  @Deprecated("Use the Dependency version")
+  open fun getHighestKnownVersion(
+    coordinate: GradleCoordinate,
+    filter: Predicate<Version>?
+  ): Version? = getHighestKnownVersion(Dependency.parse(coordinate.toString()), filter)
+
   /**
    * Looks up the highest known version of the given library if possible, possibly applying the
    * given [filter]
    */
-  open fun getHighestKnownVersion(
-    coordinate: GradleCoordinate,
-    filter: Predicate<Version>?
-  ): Version? {
+  open fun getHighestKnownVersion(dependency: Dependency, filter: Predicate<Version>?): Version? {
     // Overridden in Studio to consult SDK manager's cache
     return null
   }
diff --git a/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/LintDriver.kt b/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/LintDriver.kt
index ee37513..fafef0f 100644
--- a/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/LintDriver.kt
+++ b/lint/libs/lint-api/src/main/java/com/android/tools/lint/client/api/LintDriver.kt
@@ -37,8 +37,8 @@
 import com.android.SdkConstants.SUPPRESS_LINT
 import com.android.SdkConstants.TOOLS_URI
 import com.android.SdkConstants.VALUE_TRUE
+import com.android.ide.common.gradle.Dependency
 import com.android.ide.common.gradle.Version
-import com.android.ide.common.repository.GradleCoordinate
 import com.android.ide.common.resources.ResourceItem
 import com.android.ide.common.resources.ResourceRepository
 import com.android.ide.common.resources.configuration.FolderConfiguration.QUALIFIER_SPLITTER
@@ -3008,10 +3008,10 @@
     }
 
     override fun getHighestKnownVersion(
-      coordinate: GradleCoordinate,
+      dependency: Dependency,
       filter: Predicate<Version>?
     ): Version? {
-      return delegate.getHighestKnownVersion(coordinate, filter)
+      return delegate.getHighestKnownVersion(dependency, filter)
     }
 
     override fun readBytes(resourcePath: PathString): ByteArray {
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/GradleDetector.kt b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/GradleDetector.kt
index ebfa06d..cc7a04e 100644
--- a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/GradleDetector.kt
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/GradleDetector.kt
@@ -28,11 +28,11 @@
 import com.android.SdkConstants.currentPlatform
 import com.android.ide.common.gradle.Component
 import com.android.ide.common.gradle.Dependency
+import com.android.ide.common.gradle.RichVersion
 import com.android.ide.common.gradle.Version
 import com.android.ide.common.repository.GoogleMavenRepository
 import com.android.ide.common.repository.GoogleMavenRepository.Companion.MAVEN_GOOGLE_CACHE_DIR_KEY
 import com.android.ide.common.repository.GradleCoordinate
-import com.android.ide.common.repository.GradleCoordinate.COMPARE_PLUS_HIGHER
 import com.android.ide.common.repository.MavenRepositories
 import com.android.io.CancellableFileIo
 import com.android.sdklib.AndroidTargetHash
@@ -95,6 +95,11 @@
 import java.util.function.Predicate
 import kotlin.text.Charsets.UTF_8
 
+private val Dependency.majorVersion: Int
+  get() =
+    version?.lowerBound?.major
+      ?: if (version == RichVersion.parse("+")) GradleCoordinate.PLUS_REV_VALUE else Int.MIN_VALUE
+
 /** Checks Gradle files for potential errors. */
 open class GradleDetector : Detector(), GradleScanner, TomlScanner {
 
@@ -181,7 +186,7 @@
     val newerVersion: Version,
     val newerVersionIsSafe: Boolean,
     val safeReplacement: Version?,
-    val dependency: GradleCoordinate,
+    val dependency: Dependency,
     val isResolved: Boolean,
     val cookie: Any
   )
@@ -484,20 +489,16 @@
           report(context, valueCookie, PATH, message)
         }
       } else {
-        var dependency = getStringLiteralValue(value, valueCookie)
-        if (dependency == null) {
-          dependency = getNamedDependency(value)
+        var dependencyString = getStringLiteralValue(value, valueCookie)
+        if (dependencyString == null) {
+          dependencyString = getNamedDependency(value)
         }
         // If the dependency is a GString (i.e. it uses Groovy variable substitution,
         // with a $variable_name syntax) then don't try to parse it.
-        if (dependency != null) {
-          dependency =
-            dependency.removeSuffix(
-              "!!"
-            ) // Strip Gradle 'strict' version syntax (see b/257726238 and b/259279612).
-          var gc = GradleCoordinate.parseCoordinateString(dependency)
+        if (dependencyString != null) {
+          var dependency: Dependency? = Dependency.parse(dependencyString)
           var isResolved = false
-          if (gc != null && dependency.contains("$")) {
+          if (dependency != null && dependency.version?.toIdentifier()?.contains("$") == true) {
             if (
               value.startsWith("'") && value.endsWith("'") && context.isEnabled(NOT_INTERPOLATED)
             ) {
@@ -515,19 +516,28 @@
               report(context, statementCookie, NOT_INTERPOLATED, message, fix)
             }
 
-            gc = resolveCoordinate(context, property, gc)
+            dependency = resolveCoordinate(context, property, dependency)
             isResolved = true
-          } else if (gc != null && !value.contains(gc.revision)) {
+          } else if (dependency?.version?.toIdentifier()?.let { !value.contains(it) } != false) {
             isResolved = true
           }
-          if (gc != null) {
-            if (gc.acceptsGreaterRevisions()) {
+          if (dependency != null) {
+            if (
+              dependency.version?.run { require ?: strictly }?.toIdentifier()?.endsWith("+") == true
+            ) {
               val message =
                 "Avoid using + in version numbers; can lead " +
                   "to unpredictable and unrepeatable builds (" +
-                  dependency +
+                  dependencyString +
                   ")"
-              val fix = fix().data(KEY_COORDINATE, gc.toString(), KEY_REVISION, gc.revision)
+              val fix =
+                fix()
+                  .data(
+                    KEY_COORDINATE,
+                    dependency.toString(),
+                    KEY_REVISION,
+                    dependency.version?.toIdentifier()
+                  )
               report(context, valueCookie, PLUS, message, fix)
             }
 
@@ -535,11 +545,12 @@
             if (
               tomlLibraries != null &&
                 !context.file.name.startsWith("settings.gradle") &&
-                !dependency.contains("+") &&
-                (!dependency.contains("$") || isResolved)
+                !dependencyString.contains("+") &&
+                (!dependencyString.contains("$") || isResolved)
             ) {
               val versionVar = getVersionVariable(value)
-              val result = createMoveToTomlFix(context, tomlLibraries, gc, valueCookie, versionVar)
+              val result =
+                createMoveToTomlFix(context, tomlLibraries, dependency, valueCookie, versionVar)
               val message = result?.first ?: "Use version catalog instead"
               val fix = result?.second
               report(context, valueCookie, SWITCH_TO_TOML, message, fix)
@@ -548,10 +559,12 @@
             // Check dependencies without the PSI read lock, because we
             // may need to make network requests to retrieve version info.
             context.driver.runLaterOutsideReadAction {
-              checkDependency(context, gc, isResolved, valueCookie, statementCookie)
+              checkDependency(context, dependency, isResolved, valueCookie, statementCookie)
             }
           }
-          if (hasLifecycleAnnotationProcessor(dependency) && targetJava8Plus(context.project)) {
+          if (
+            hasLifecycleAnnotationProcessor(dependencyString) && targetJava8Plus(context.project)
+          ) {
             report(
               context,
               valueCookie,
@@ -562,25 +575,25 @@
               null
             )
           }
-          checkAnnotationProcessorOnCompilePath(property, dependency, context, propertyCookie)
+          checkAnnotationProcessorOnCompilePath(property, dependencyString, context, propertyCookie)
         }
         checkDeprecatedConfigurations(property, context, propertyCookie)
 
         // If we haven't managed to parse the dependency yet, try getting it from version catalog
         var libTomlValue: LintTomlValue? = null
-        if (dependency == null) {
+        if (dependencyString == null) {
           val dependencyFromVc = getDependencyFromVersionCatalog(value, context)
           if (dependencyFromVc != null) {
-            dependency = dependencyFromVc.coordinates
+            dependencyString = dependencyFromVc.coordinates
             libTomlValue = dependencyFromVc.tomlValue
           }
         }
 
-        if (dependency != null) {
+        if (dependencyString != null) {
           if (property == "kapt") {
-            checkKaptUsage(dependency, libTomlValue, context, statementCookie)
+            checkKaptUsage(dependencyString, libTomlValue, context, statementCookie)
           }
-          checkForBomUsageWithoutPlatform(property, dependency, value, context, valueCookie)
+          checkForBomUsageWithoutPlatform(property, dependencyString, value, context, valueCookie)
         }
       }
     } else if (property == "packageNameSuffix") {
@@ -959,19 +972,19 @@
   // Any interaction with PSI or issue reporting should be wrapped in a read action.
   private fun checkDependency(
     context: Context,
-    dependency: GradleCoordinate,
+    dependency: Dependency,
     isResolved: Boolean,
     cookie: Any,
     statementCookie: Any
   ) {
-    val version = dependency.lowerBoundVersion
-    val groupId = dependency.groupId
-    val artifactId = dependency.artifactId
-    val revision = dependency.revision
+    val version = dependency.version?.lowerBound ?: return
+    val groupId = dependency.group ?: return
+    val artifactId = dependency.name
+    val richVersionIdentifier = dependency.version?.toIdentifier() ?: return
     var safeReplacement: Version? = null
     var newerVersion: Version? = null
 
-    val filter = getUpgradeVersionFilter(context, groupId, artifactId, revision)
+    val filter = getUpgradeVersionFilter(context, groupId, artifactId, version)
 
     when (groupId) {
       GMS_GROUP_ID,
@@ -980,7 +993,7 @@
       ANDROID_WEAR_GROUP_ID -> {
         // Play services
 
-        checkPlayServices(context, dependency, version, revision, cookie, statementCookie)
+        checkPlayServices(context, dependency, version, cookie, statementCookie)
       }
       "com.android.tools.build" -> {
         if ("gradle" == artifactId) {
@@ -1068,24 +1081,14 @@
       }
       "io.fabric.tools" -> {
         if ("gradle" == artifactId) {
-          // TODO(b/242691473): semantically I think this should be close to
-          //    richVersion = RichVersion.parse(revision)
-          //    if (richVersion.run { strictly ?: require }
-          //                   ?.intersection(VersionRange.parse("[1.21.6,]")
-          //                   ?.isEmpty() == true
-          //       ) {
-          //      ...
-          //  and the GradleCoordinate version will fail on dependency expressions using
-          //  RichVersion features.
-          val upper = dependency.upperBoundVersion
-          if (upper < Version.parse("1.21.6")) {
-            val fix = getUpdateDependencyFix(revision, "1.22.1")
+          if (Version.parse("1.21.6").isNewerThan(dependency)) {
+            val fix = getUpdateDependencyFix(richVersionIdentifier, "1.22.1")
             report(
               context,
               statementCookie,
               DEPENDENCY,
               "Use Fabric Gradle plugin version 1.21.6 or later to " +
-                "improve Instant Run performance (was $revision)",
+                "improve Instant Run performance (was $richVersionIdentifier)",
               fix
             )
           } else {
@@ -1098,13 +1101,13 @@
       "com.bugsnag" -> {
         if ("bugsnag-android-gradle-plugin" == artifactId) {
           if (version < Version.parse("2.1.2")) {
-            val fix = getUpdateDependencyFix(revision, "2.4.1")
+            val fix = getUpdateDependencyFix(richVersionIdentifier, "2.4.1")
             report(
               context,
               statementCookie,
               DEPENDENCY,
               "Use BugSnag Gradle plugin version 2.1.2 or later to " +
-                "improve Instant Run performance (was $revision)",
+                "improve Instant Run performance (was $richVersionIdentifier)",
               fix
             )
           } else {
@@ -1119,7 +1122,7 @@
       "org.robolectric" -> {
         if ("robolectric" == artifactId && currentPlatform() == PLATFORM_WINDOWS) {
           if (version < Version.parse("4.2.1")) {
-            val fix = getUpdateDependencyFix(revision, "4.2.1")
+            val fix = getUpdateDependencyFix(richVersionIdentifier, "4.2.1")
             report(
               context,
               cookie,
@@ -1218,7 +1221,12 @@
         !groupId.startsWith("androidx.")
     ) {
       val latest =
-        getLatestVersionFromRemoteRepo(context.client, dependency, filter, dependency.isPreview)
+        getLatestVersionFromRemoteRepo(
+          context.client,
+          dependency,
+          filter,
+          dependency.version?.lowerBound?.isPreview ?: true
+        )
       if (latest != null && version < latest) {
         newerVersion = latest
         issue = REMOTE_VERSION
@@ -1246,19 +1254,18 @@
       val versionString = newerVersion.toString()
       val message =
         if (
-          dependency.groupId == "androidx.slidingpanelayout" &&
-            dependency.artifactId == "slidingpanelayout"
+          dependency.group == "androidx.slidingpanelayout" && dependency.name == "slidingpanelayout"
         ) {
           "Upgrade `androidx.slidingpanelayout` for keyboard and mouse support"
         } else if (
-          dependency.groupId == "androidx.compose.foundation" &&
-            dependency.artifactId == "foundation"
+          dependency.group == "androidx.compose.foundation" && dependency.name == "foundation"
         ) {
           "Upgrade `androidx.compose.foundation` for keyboard and mouse support"
         } else {
           getNewerVersionAvailableMessage(dependency, versionString, null)
         }
-      val fix = if (!isResolved) getUpdateDependencyFix(revision, versionString) else null
+      val fix =
+        if (!isResolved) getUpdateDependencyFix(richVersionIdentifier, versionString) else null
       report(context, cookie, issue, message, fix)
     }
   }
@@ -1271,7 +1278,7 @@
     context: Context,
     groupId: String,
     artifactId: String,
-    revision: String
+    version: Version
   ): Predicate<Version>? {
     // Logic here has to match checkSupportLibraries method to avoid creating contradictory
     // warnings.
@@ -1286,8 +1293,6 @@
       }
     }
 
-    val version = Version.parse(revision)
-
     if (groupId == "com.android.tools.build" && LintClient.isStudio) {
       val clientRevision = context.client.getClientRevision() ?: return null
       val ideVersion = Version.parse(clientRevision)
@@ -1334,16 +1339,15 @@
   }
 
   private fun findCachedNewerVersion(
-    dependency: GradleCoordinate,
+    dependency: Dependency,
     filter: Predicate<Version>?
   ): Version? {
+    val group = dependency.group ?: return null
     val versionDir =
-      getArtifactCacheHome()
-        .toPath()
-        .resolve(dependency.groupId + File.separator + dependency.artifactId)
+      getArtifactCacheHome().toPath().resolve(group + File.separator + dependency.name)
     val f =
       when {
-        dependency.groupId == "commons-io" && dependency.artifactId == "commons-io" -> {
+        dependency.group == "commons-io" && dependency.name == "commons-io" -> {
           // For a (long) while, users could get this spurious recommendation of an "upgrade" to
           // commons-io to this very old version (with a very high version number).  This
           // recommendation is no longer given as of mid-2023, except if a user has previously
@@ -1355,9 +1359,13 @@
         else -> filter
       }
     return if (CancellableFileIo.exists(versionDir)) {
-      val component =
-        Component(dependency.groupId, dependency.artifactId, Version.parse(dependency.revision))
-      MavenRepositories.getHighestVersion(versionDir, f, MavenRepositories.isPreview(component))
+      val isPreview =
+        when (val richVersion = dependency.version) {
+          null -> true
+          else ->
+            MavenRepositories.isPreview(Component(group, dependency.name, richVersion.lowerBound))
+        }
+      MavenRepositories.getHighestVersion(versionDir, f, isPreview)
     } else null
   }
 
@@ -1379,17 +1387,17 @@
   // Any interaction with PSI or issue reporting should be wrapped in a read action.
   private fun checkGradlePluginDependency(
     context: Context,
-    dependency: GradleCoordinate,
+    dependency: Dependency,
     cookie: Any
   ): Boolean {
-    val minimum =
-      GradleCoordinate.parseCoordinateString(
-        SdkConstants.GRADLE_PLUGIN_NAME + GRADLE_PLUGIN_MINIMUM_VERSION
-      )
-    if (minimum != null && COMPARE_PLUS_HIGHER.compare(dependency, minimum) < 0) {
+    val minimum = Version.parse(GRADLE_PLUGIN_MINIMUM_VERSION)
+    val dependencyVersion = dependency.version ?: return false
+    if (dependencyVersion.lowerBound >= minimum) return false
+    if (!dependencyVersion.contains(minimum)) {
+      val query = Dependency("com.android.tools.build", "gradle", RichVersion.require(minimum))
       val recommended =
         Version.parse(GRADLE_PLUGIN_RECOMMENDED_VERSION).let { recommended ->
-          getGoogleMavenRepoVersion(context, minimum, null)?.takeIf { it > recommended }
+          getGoogleMavenRepoVersion(context, query, null)?.takeIf { it > recommended }
             ?: recommended
         }
       val message =
@@ -1406,13 +1414,13 @@
 
   private fun checkSupportLibraries(
     context: Context,
-    dependency: GradleCoordinate,
+    dependency: Dependency,
     version: Version,
     newerVersion: Version?,
     cookie: Any
   ) {
-    val groupId = dependency.groupId
-    val artifactId = dependency.artifactId
+    val groupId = dependency.group ?: return
+    val artifactId = dependency.name
 
     // For artifacts that follow the platform numbering scheme, check that it matches the SDK
     // versions used.
@@ -1437,12 +1445,13 @@
         }
 
         var fix: LintFix? = null
-        if (newerVersion != null) {
+        val richVersionIdentifier = dependency.version?.toIdentifier()
+        if (newerVersion != null && richVersionIdentifier != null) {
           fix =
             fix()
               .name("Replace with $newerVersion")
               .replace()
-              .text(version.toString())
+              .text(richVersionIdentifier)
               .with(newerVersion.toString())
               .build()
         }
@@ -1507,24 +1516,25 @@
 
   private fun checkPlayServices(
     context: Context,
-    dependency: GradleCoordinate,
+    dependency: Dependency,
     version: Version,
-    revision: String,
     cookie: Any,
     statementCookie: Any
   ) {
-    val groupId = dependency.groupId
-    val artifactId = dependency.artifactId
+    val groupId = dependency.group ?: return
+    val artifactId = dependency.name
+    val richVersion = dependency.version ?: return
+    val richVersionIdentifier = richVersion.toIdentifier() ?: return
 
     // 5.2.08 is not supported; special case and warn about this
-    if ("5.2.08" == revision && context.isEnabled(COMPATIBILITY)) {
+    if (Version.parse("5.2.08") == version && context.isEnabled(COMPATIBILITY)) {
       // This specific version is actually a preview version which should
       // not be used (https://code.google.com/p/android/issues/detail?id=75292)
       val maxVersion =
         Version.parse("10.2.1").let { v ->
           getGoogleMavenRepoVersion(context, dependency, null)?.takeIf { it > v } ?: v
         }
-      val fix = getUpdateDependencyFix(revision, maxVersion.toString())
+      val fix = getUpdateDependencyFix(richVersionIdentifier, maxVersion.toString())
       val message =
         "Version `5.2.08` should not be used; the app " +
           "can not be published with this version. Use version `$maxVersion` " +
@@ -1534,8 +1544,10 @@
 
     if (
       context.isEnabled(BUNDLED_GMS) &&
-        PLAY_SERVICES_V650.isSameArtifact(dependency) &&
-        COMPARE_PLUS_HIGHER.compare(dependency, PLAY_SERVICES_V650) >= 0
+        PLAY_SERVICES_V650.group == dependency.group &&
+        PLAY_SERVICES_V650.name == dependency.name &&
+        (richVersion.lowerBound >= PLAY_SERVICES_V650.version ||
+          richVersion.contains(PLAY_SERVICES_V650.version))
     ) {
       // Play services 6.5.0 is the first version to allow un-bundling, so if the user is
       // at or above 6.5.0, recommend un-bundling
@@ -1548,14 +1560,14 @@
         "Deprecated: Replace '" +
           GMS_GROUP_ID +
           ":play-services-appindexing:" +
-          revision +
+          richVersionIdentifier +
           "' with 'com.google.firebase:firebase-appindexing:10.0.0' or above. " +
           "More info: http://firebase.google.com/docs/app-indexing/android/migrate"
       val fix =
         fix()
           .name("Replace with Firebase")
           .replace()
-          .text("$GMS_GROUP_ID:play-services-appindexing:$revision")
+          .text("$GMS_GROUP_ID:play-services-appindexing:$richVersionIdentifier")
           .with("com.google.firebase:firebase-appindexing:10.2.1")
           .build()
       report(context, cookie, DEPRECATED, message, fix)
@@ -1900,11 +1912,11 @@
       val versions = document.getValue(VC_VERSIONS) as? LintTomlMapValue
       for ((_, library) in libraries.getMappedValues()) {
         val (coordinate, versionNode) = getLibraryFromTomlEntry(versions, library) ?: continue
-        val gc = GradleCoordinate.parseCoordinateString(coordinate) ?: return
+        val dependency = Dependency.parse(coordinate) ?: return
         // Check dependencies without the PSI read lock, because we
         // may need to make network requests to retrieve version info.
         context.driver.runLaterOutsideReadAction {
-          checkDependency(context, gc, false, versionNode, versionNode)
+          checkDependency(context, dependency, false, versionNode, versionNode)
         }
       }
     }
@@ -1959,14 +1971,16 @@
     if (mDeclaredGoogleMavenRepository || context is TomlContext) {
       agpVersionCheckInfo?.let {
         val versionString = it.newerVersion.toString()
+        val currentIdentifier = it.dependency.version?.toIdentifier()
         val message =
           getNewerVersionAvailableMessage(it.dependency, versionString, it.safeReplacement)
         val fix =
           when {
             it.isResolved -> null
+            currentIdentifier == null -> null
             else ->
               getUpdateDependencyFix(
-                it.dependency.revision,
+                currentIdentifier,
                 versionString,
                 it.newerVersionIsSafe,
                 it.safeReplacement
@@ -2294,13 +2308,23 @@
     return version
   }
 
+  // TODO(b/279886738): resolving a Dependency against the project's artifacts should
+  //  from a theoretical point of view return a Component.  However, here, we're not
+  //  really *conceptually* resolving a Dependency, because what this is actually used
+  //  for is to guess what the value of a version variable in an interpolated String
+  //  might be, and rather than model variables and their values, we pull the resolved
+  //  version and hope for the best.  For our purposes, that's not completely wrong.
   @SuppressWarnings("ExpensiveAssertion")
   private fun resolveCoordinate(
     context: GradleContext,
     property: String,
-    gc: GradleCoordinate
-  ): GradleCoordinate? {
-    assert(gc.revision.contains("$")) { gc.revision }
+    dependency: Dependency
+  ): Dependency? {
+    fun Component.toDependency() = Dependency(group, name, RichVersion.require(version))
+    assert(dependency.version?.toIdentifier()?.contains("$") ?: false) {
+      dependency.version.toString()
+    }
+
     val project = context.project
     val variant = project.buildVariant
     if (variant != null) {
@@ -2315,12 +2339,9 @@
       for (library in artifact.dependencies.getAll()) {
         if (library is LintModelExternalLibrary) {
           val mc = library.resolvedCoordinates
-          if (mc.groupId == gc.groupId && mc.artifactId == gc.artifactId) {
-            val revisions = GradleCoordinate.parseRevisionNumber(mc.version)
-            if (revisions.isNotEmpty()) {
-              return GradleCoordinate(mc.groupId, mc.artifactId, revisions, null)
-            }
-            break
+          if (mc.groupId == dependency.group && mc.artifactId == dependency.name) {
+            val version = Version.parse(mc.version)
+            return Component(mc.groupId, mc.artifactId, version).toDependency()
           }
         }
       }
@@ -2373,18 +2394,16 @@
   }
 
   private fun getNewerVersionAvailableMessage(
-    dependency: GradleCoordinate,
+    dependency: Dependency,
     version: String,
     stable: Version?
   ): String {
     val message = StringBuilder()
     with(message) {
       append("A newer version of ")
-      append(dependency.groupId)
-      append(":")
-      append(dependency.artifactId)
+      append("${dependency.group}:${dependency.name}")
       append(" than ")
-      append(dependency.revision)
+      append("${dependency.version}")
       append(" is available: ")
       append(version)
       if (stable != null) {
@@ -2500,11 +2519,10 @@
 
   private fun getGoogleMavenRepoVersion(
     context: Context,
-    coordinate: GradleCoordinate,
+    dependency: Dependency,
     filter: Predicate<Version>?
   ): Version? {
     val repository = getGoogleMavenRepository(context.client)
-    val dependency = Dependency.parse(coordinate.toString())
     return repository.findVersion(dependency, filter, dependency.explicitlyIncludesPreview)
   }
 
@@ -3369,8 +3387,7 @@
     const val ANDROID_WEAR_GROUP_ID = "com.google.android.wearable"
     private const val WEARABLE_ARTIFACT_ID = "wearable"
 
-    private val PLAY_SERVICES_V650 =
-      GradleCoordinate.parseCoordinateString("$GMS_GROUP_ID:play-services:6.5.0")!!
+    private val PLAY_SERVICES_V650 = Component.parse("$GMS_GROUP_ID:play-services:6.5.0")
 
     /**
      * Threshold to consider a versionCode very high and issue a warning.
@@ -4006,15 +4023,14 @@
  * getLowerBoundVersion and getUpperBoundVersion (both of which are computable) and to use the
  * appropriate one in the right context (in most of this file, the upper bound).
  */
-private fun Version?.isNewerThan(dependency: GradleCoordinate) =
-  dependency.lowerBoundVersion.let { version ->
-    if (this == null) return false
-    GradleCoordinate(dependency.groupId, dependency.artifactId, this.toString()).let {
-      newerCoordinate ->
-      when {
-        dependency.acceptsGreaterRevisions() ->
-          this > version && COMPARE_PLUS_HIGHER.compare(newerCoordinate, dependency) > 0
-        else -> this > version
-      }
-    }
+private fun Version?.isNewerThan(dependency: Dependency): Boolean {
+  val richVersion = dependency.version
+  val maybeSingleton = dependency.explicitSingletonVersion
+  return when {
+    richVersion == null -> true
+    this == null -> false
+    maybeSingleton != null -> this > maybeSingleton
+    richVersion.lowerBound > this -> false
+    else -> !richVersion.contains(this)
   }
+}
diff --git a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/TomlUtilities.kt b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/TomlUtilities.kt
index 6901a4f..cd09a75 100644
--- a/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/TomlUtilities.kt
+++ b/lint/libs/lint-checks/src/main/java/com/android/tools/lint/checks/TomlUtilities.kt
@@ -17,8 +17,8 @@
 
 package com.android.tools.lint.checks
 
+import com.android.ide.common.gradle.Dependency
 import com.android.ide.common.gradle.Version
-import com.android.ide.common.repository.GradleCoordinate
 import com.android.ide.common.repository.pickLibraryVariableName
 import com.android.ide.common.repository.pickVersionVariableName
 import com.android.tools.lint.client.api.LintTomlDocument
@@ -131,7 +131,7 @@
 fun createMoveToTomlFix(
   context: GradleContext,
   librariesMap: LintTomlValue,
-  gc: GradleCoordinate,
+  dependency: Dependency,
   valueCookie: Any,
   versionVar: String?,
 ): Pair<String?, LintFix>? {
@@ -143,26 +143,29 @@
   var artifactVersionNode: LintTomlValue? = null
   var artifactVersion: Version? = null
 
-  val revision = gc.revision
+  val version = dependency.version
+  val richVersionIdentifier = version?.toIdentifier() ?: return null
   for ((key, library) in librariesMap.getMappedValues()) {
     val (coordinate, versionNode) = getLibraryFromTomlEntry(versionsMap, library) ?: continue
-    val c = GradleCoordinate.parseCoordinateString(coordinate) ?: continue
-    if (c.groupId == gc.groupId && c.artifactId == gc.artifactId) {
+    val c = Dependency.parse(coordinate)
+    if (c.group == null || c.version == null) continue
+    if (c.group == dependency.group && c.name == dependency.name) {
       // We already have this dependency in the TOML file!
-      if (c.revision == revision) {
+      if (c.version == version) {
         // It even matches by version! Just switch the dependency over to it!
         val fix =
           createSwitchToLibraryFix(document, library, key, context, valueCookie, safe = true)
             ?: return null
         return "Use the existing version catalog reference (`${fix.replacement}`) instead" to fix
       } else if (
-        artifactLibrary == null || artifactVersion != null && c.lowerBoundVersion > artifactVersion
+        artifactLibrary == null ||
+          artifactVersion?.let { v -> c.version?.lowerBound?.let { it > v } } == true
       ) {
         // There could be multiple declaration of this library (for different versions); pick the
         // highest one
         artifactLibrary = library
         artifactVersionNode = versionNode
-        artifactVersion = c.lowerBoundVersion
+        artifactVersion = c.version?.lowerBound
       }
     }
   }
@@ -174,7 +177,7 @@
     // (1) Insert new library definition for this exact version
     val addNew =
       createAddNewCatalogLibrary(
-        gc,
+        dependency,
         versionsMap,
         librariesMap,
         document,
@@ -196,7 +199,7 @@
         safe = false
       )
     val fix = LintFix.create().alternatives(addNew, switchToVersion)
-    val artifact = "${gc.groupId}:${gc.artifactId}"
+    val artifact = "${dependency.group}:${dependency.name}"
     val message =
       "Use version catalog instead ($artifact is already available as `$key`, but using version $artifactVersion instead)"
     return message to fix
@@ -205,10 +208,10 @@
   val existingVersionNode = if (versionVar != null) versionsMap?.get(versionVar) else null
   val existingVersion = existingVersionNode?.getActualValue()?.toString()
 
-  val matchExistingVar = findExistingVariable(gc, versionsMap, librariesMap)?.key
+  val matchExistingVar = findExistingVariable(dependency, versionsMap, librariesMap)?.key
 
   // We didn't find this artifact in the version catalog at all; offer to add it.
-  if (existingVersion == null || existingVersion == revision) {
+  if (existingVersion == null || existingVersion == richVersionIdentifier) {
     val fix =
       LintFix.create()
         .alternatives(
@@ -216,7 +219,7 @@
             // One of the existing version variables matches this exact revision; offer to re-use
             // it.
             createAddNewCatalogLibrary(
-              gc,
+              dependency,
               versionsMap,
               librariesMap,
               document,
@@ -231,7 +234,7 @@
             null
           },
           createAddNewCatalogLibrary(
-            gc,
+            dependency,
             versionsMap,
             librariesMap,
             document,
@@ -255,7 +258,7 @@
       LintFix.create()
         .alternatives(
           createAddNewCatalogLibrary(
-            gc,
+            dependency,
             versionsMap,
             librariesMap,
             document,
@@ -267,9 +270,9 @@
               "Replace with new library catalog declaration, reusing version variable $versionVar (version=$existingVersion)",
             autoFix = false
           ),
-          createChangeVersionFix(revision, existingVersionNode),
+          createChangeVersionFix(richVersionIdentifier, existingVersionNode),
           createAddNewCatalogLibrary(
-            gc,
+            dependency,
             versionsMap,
             librariesMap,
             document,
@@ -290,14 +293,14 @@
  * This will be true if (1) the version matches exactly, and (2) if the group matches exactly.
  */
 private fun findExistingVariable(
-  gc: GradleCoordinate,
+  dependency: Dependency,
   versions: LintTomlMapValue?,
   libraries: LintTomlMapValue?
 ): Map.Entry<String, LintTomlValue>? {
   versions ?: return null
   libraries ?: return null
-  val revision = gc.revision
-  val group = gc.groupId
+  val revision = dependency.version?.toIdentifier() ?: return null
+  val group = dependency.group ?: return null
   for (versionEntry in versions.getMappedValues()) {
     val versionNode = versionEntry.value
     val value = versionNode.getActualValue()
@@ -345,10 +348,10 @@
 
 /**
  * Creates fix which creates a new version catalog entry (library and version name) for the given
- * [gc] library
+ * [dependency] library
  */
 private fun createAddNewCatalogLibrary(
-  gc: GradleCoordinate,
+  dependency: Dependency,
   versionsMap: LintTomlMapValue?,
   librariesMap: LintTomlMapValue,
   document: LintTomlDocument,
@@ -366,22 +369,35 @@
   //   (2) for related libraries, offer to "reuse" it? (e.g. for related kotlin libraries. Be
   // careful here.)
   val versionVariable =
-    pickVersionVariableName(gc, versionsMap?.getMappedValues(), versionVar, allowExistingVersionVar)
+    pickVersionVariableName(
+      dependency,
+      versionsMap?.getMappedValues(),
+      versionVar,
+      allowExistingVersionVar
+    )
   val libraryVariable =
-    pickLibraryVariableName(gc, librariesMap.getMappedValues(), includeVersionInKey)
+    pickLibraryVariableName(dependency, librariesMap.getMappedValues(), includeVersionInKey)
 
   val source = document.getSource()
   val versionVariableFix: LintFix?
   val usedVariable: String?
   if (versionsMap == null || !versionsMap.contains(versionVariable)) {
-    versionVariableFix = createAddVersionFix(versionsMap, source, document, versionVariable, gc)
+    versionVariableFix =
+      createAddVersionFix(versionsMap, source, document, versionVariable, dependency)
     usedVariable = if (versionVariableFix == null) null else versionVariable
   } else {
     versionVariableFix = null
     usedVariable = versionVariable
   }
   val insertLibraryFix =
-    createInsertLibraryFix(librariesMap, source, libraryVariable, gc, usedVariable, document)
+    createInsertLibraryFix(
+      librariesMap,
+      source,
+      libraryVariable,
+      dependency,
+      usedVariable,
+      document
+    )
       ?: return null
   val gradleFix =
     createReplaceWithLibraryReferenceFix(document, context, valueCookie, libraryVariable, true)
@@ -533,10 +549,11 @@
   source: CharSequence,
   document: LintTomlDocument,
   versionVariable: String,
-  gc: GradleCoordinate
+  dependency: Dependency
 ): LintFix? {
+  val versionIdentifier = dependency.version?.toIdentifier() ?: return null
   val separator = if (spaceAroundEquals(versionsMap)) " = " else "="
-  val variableDeclaration = "$versionVariable$separator\"${gc.revision}\"\n"
+  val variableDeclaration = "$versionVariable$separator\"$versionIdentifier\"\n"
 
   // Is the list already in alphabetical order?
   val independent = true
@@ -584,7 +601,7 @@
   librariesMap: LintTomlMapValue,
   source: CharSequence,
   libraryVariable: String,
-  gc: GradleCoordinate,
+  dependency: Dependency,
   versionVariable: String?,
   document: LintTomlDocument
 ): LintFix? {
@@ -643,9 +660,10 @@
 
   val version =
     if (versionVariable != null) "version.ref = \"$versionVariable\""
-    else "version = \"${gc.revision}\""
+    else dependency.version?.toIdentifier()?.let { "version = \"$it\"" } ?: return null
+  val group = dependency.group ?: return null
   val moduleDeclaration =
-    "$prefix$libraryVariable = { module = \"${gc.groupId}:${gc.artifactId}\", $version }$suffix"
+    "$prefix$libraryVariable = { module = \"$group:${dependency.name}\", $version }$suffix"
   return LintFix.create()
     .replace()
     .range(Location.create(document.getFile(), source, libraryInsertOffset, libraryInsertOffset))
@@ -670,7 +688,7 @@
  */
 @VisibleForTesting
 fun pickLibraryVariableName(
-  gc: GradleCoordinate,
+  dependency: Dependency,
   libraries: Map<String, LintTomlValue>,
   includeVersionInKey: Boolean
 ): String {
@@ -684,7 +702,7 @@
     reserved.add(key)
   }
 
-  val suggestion = pickLibraryVariableName(gc, includeVersionInKey, reserved)
+  val suggestion = pickLibraryVariableName(dependency, includeVersionInKey, reserved)
   reservedQuickfixNames.add(suggestion)
 
   return suggestion
@@ -700,16 +718,16 @@
 }
 
 /**
- * Picks a suitable name for the version variable to use for [gc] which is unique and ideally
- * follows the existing naming style. Ensures that the suggested name does not conflict with an
- * existing versions listed in the [versionsMap].
+ * Picks a suitable name for the version variable to use for [dependency] which is unique and
+ * ideally follows the existing naming style. Ensures that the suggested name does not conflict with
+ * an existing versions listed in the [versionsMap].
  *
  * If the optional [versionVar] name is provided, this is a preferred name to use. It will only use
  * that name if it is not already in use, **or**, if [allowExistingVersionVar] is set to true.
  */
 @VisibleForTesting
 fun pickVersionVariableName(
-  gc: GradleCoordinate,
+  dependency: Dependency,
   versionsMap: Map<String, LintTomlValue>?,
   versionVar: String? = null,
   allowExistingVersionVar: Boolean = false
@@ -739,7 +757,7 @@
     return versionVar
   }
 
-  val suggestion = pickVersionVariableName(gc, reserved)
+  val suggestion = pickVersionVariableName(dependency, reserved)
   reservedQuickfixNames.add(suggestion)
 
   return suggestion
diff --git a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/GradleDetectorTest.kt b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/GradleDetectorTest.kt
index e725e37..feed208 100644
--- a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/GradleDetectorTest.kt
+++ b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/GradleDetectorTest.kt
@@ -17,9 +17,9 @@
 
 import com.android.SdkConstants.GRADLE_PLUGIN_MINIMUM_VERSION
 import com.android.SdkConstants.GRADLE_PLUGIN_RECOMMENDED_VERSION
+import com.android.ide.common.gradle.Dependency
 import com.android.ide.common.gradle.Version
 import com.android.ide.common.repository.GoogleMavenRepository.Companion.MAVEN_GOOGLE_CACHE_DIR_KEY
-import com.android.ide.common.repository.GradleCoordinate
 import com.android.sdklib.AndroidVersion
 import com.android.sdklib.IAndroidTarget
 import com.android.sdklib.SdkVersionInfo.LOWEST_ACTIVE_API
@@ -2431,8 +2431,9 @@
                 testRunner = { module= "com.android.support.test:runner", version= { prefer = "0.1" } }
                 testRunner2 = { module = "com.android.support.test:runner", version = { strictly ="0.3" } }
                 testRunner3 = { module = "com.android.support.test:runner", version.ref ="testRunnerVersion" }
-                # TODO: Support more complex version constraints ([] syntax)
-                testRunner4 = { module = "com.android.support.test:runner", version = { strictly = "[0.3, 0.4[", prefer="0.35" } }
+                testRunner4 = { module = "com.android.support.test:runner", version = { strictly = "[0.3,0.4)", prefer="0.35" } }
+                # TODO: support rich versions expressed in non-canonical form (e.g. [0.3,0.4[ or [,])
+                # testRunner5 = { module = "com.android.support.test:runner", version = "[0.3,0.4["}
                 """
           )
           .indented()
@@ -7297,15 +7298,15 @@
           }
 
           override fun getHighestKnownVersion(
-            coordinate: GradleCoordinate,
+            dependency: Dependency,
             filter: Predicate<Version>?
           ): Version? {
             // Hardcoded for unit test to ensure stable data
             return if (
-              "com.android.support.constraint" == coordinate.groupId &&
-                "constraint-layout" == coordinate.artifactId
+              "com.android.support.constraint" == dependency.group &&
+                "constraint-layout" == dependency.name
             ) {
-              if (coordinate.isPreview) {
+              if (dependency.version?.lowerBound?.isPreview != false) {
                 Version.parse("1.0.3-alpha8")
               } else {
                 Version.parse("1.0.2")
diff --git a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/TomlUtilitiesTest.kt b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/TomlUtilitiesTest.kt
index 7690d42..f41de87 100644
--- a/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/TomlUtilitiesTest.kt
+++ b/lint/libs/lint-tests/src/test/java/com/android/tools/lint/checks/TomlUtilitiesTest.kt
@@ -15,7 +15,7 @@
  */
 package com.android.tools.lint.checks
 
-import com.android.ide.common.repository.GradleCoordinate
+import com.android.ide.common.gradle.Dependency
 import com.android.tools.lint.client.api.LintTomlValue
 import org.junit.Assert.assertEquals
 import org.junit.Test
@@ -113,7 +113,9 @@
     check(
       expected,
       coordinateString,
-      { gc, libraries, include, _, _ -> pickLibraryVariableName(gc, libraries, include) },
+      { dependency, libraries, include, _, _ ->
+        pickLibraryVariableName(dependency, libraries, include)
+      },
       includeVersions,
       null,
       false,
@@ -147,7 +149,7 @@
     coordinateString: String,
     suggestName:
       (
-        gc: GradleCoordinate,
+        dependency: Dependency,
         libraryMap: Map<String, LintTomlValue>,
         includeVersionInKey: Boolean,
         preferred: String?,
@@ -158,13 +160,13 @@
     allowExisting: Boolean,
     vararg variableNames: String
   ) {
-    val gc = GradleCoordinate.parseCoordinateString(coordinateString)!!
+    val dependency = Dependency.parse(coordinateString)
     val map = LinkedHashMap<String, LintTomlValue>()
     val value = Mockito.mock(LintTomlValue::class.java) // map values are unused here
     for (variable in variableNames) {
       map[variable] = value
     }
-    val name = suggestName(gc, map, includeVersions, versionVariable, allowExisting)
+    val name = suggestName(dependency, map, includeVersions, versionVariable, allowExisting)
     assertEquals(expected, name)
     GradleDetector.reservedQuickfixNames = null
   }
diff --git a/sdk-common/src/main/java/com/android/ide/common/repository/VersionCatalogNamingUtil.kt b/sdk-common/src/main/java/com/android/ide/common/repository/VersionCatalogNamingUtil.kt
index b91a0e0..be52c5a 100644
--- a/sdk-common/src/main/java/com/android/ide/common/repository/VersionCatalogNamingUtil.kt
+++ b/sdk-common/src/main/java/com/android/ide/common/repository/VersionCatalogNamingUtil.kt
@@ -15,11 +15,12 @@
  */
 package com.android.ide.common.repository
 
+import com.android.ide.common.gradle.Dependency
 import com.google.common.base.CaseFormat
 import java.util.TreeSet
 import com.google.common.annotations.VisibleForTesting
 
-private fun GradleCoordinate.isAndroidX(): Boolean = groupId.startsWith("androidx.")
+private fun Dependency.isAndroidX(): Boolean = group?.startsWith("androidx.") ?: false
 
 /**
  * Pick name with next steps. Each step generates name and check whether it
@@ -39,17 +40,19 @@
  *   "jetbrains-kotlin-reflect" => "jetbrains-kotlin-reflect2".
  */
 fun pickLibraryVariableName(
-    gc: GradleCoordinate,
+    dependency: Dependency,
     includeVersionInKey: Boolean,
     caseSensitiveReserved: Set<String>
 ): String {
     val reserved = TreeSet(String.CASE_INSENSITIVE_ORDER)
     reserved.addAll(caseSensitiveReserved)
     val versionSuffix =
-        if (includeVersionInKey) "-v" + gc.revision.replace("[-.+_]".toRegex(), "").toSafeKey() else ""
+        if (includeVersionInKey) "-v" + (dependency.version?.toIdentifier()
+            ?.replace("[^A-Za-z0-9]".toRegex(), "")
+            ?.toSafeKey() ?: "") else ""
 
-    if (gc.isAndroidX() && (reserved.isEmpty() || reserved.any { it.startsWith("androidx-") })) {
-        val key = "androidx-${gc.artifactId.toSafeKey()}$versionSuffix"
+    if (dependency.isAndroidX() && (reserved.isEmpty() || reserved.any { it.startsWith("androidx-") })) {
+        val key = "androidx-${dependency.name.toSafeKey()}$versionSuffix"
         if (!reserved.contains(key)) {
             return key
         }
@@ -57,7 +60,7 @@
 
     // Try a few combinations: just the artifact name, just the group-suffix and the artifact name,
     // just the group-prefix and the artifact name, etc.
-    val artifactId = gc.artifactId.toSafeKey()
+    val artifactId = dependency.name.toSafeKey()
     val artifactKey = artifactId + versionSuffix
     if (!reserved.contains(artifactKey)) {
         return artifactKey
@@ -65,7 +68,7 @@
 
     // Normally the groupId suffix plus artifact is used if it's not similar to artifact, e.g.
     // "com.google.libraries:guava" => "libraries-guava"
-    val groupSuffix = gc.groupId.substringAfterLast('.').toSafeKey()
+    val groupSuffix = dependency.group?.substringAfterLast('.')?.toSafeKey() ?: "nogroup"
     val withGroupSuffix = "$groupSuffix-$artifactId$versionSuffix"
     if (!(artifactId.startsWith(groupSuffix))) {
         if (!reserved.contains(withGroupSuffix)) {
@@ -73,13 +76,13 @@
         }
     }
 
-    val groupPrefix = getGroupPrefix(gc)
+    val groupPrefix = getGroupPrefix(dependency)
     val withGroupPrefix = "$groupPrefix-$artifactId$versionSuffix"
     if (!reserved.contains(withGroupPrefix)) {
         return withGroupPrefix
     }
 
-    val groupId = gc.groupId.toSafeKey()
+    val groupId = dependency.group?.toSafeKey() ?: "nogroup"
     val full = "$groupId-$artifactId$versionSuffix"
     if (!reserved.contains(full)) {
         return full
@@ -111,11 +114,12 @@
     return sb.toString()
 }
 
-private fun getGroupPrefix(gc: GradleCoordinate): String {
+private fun getGroupPrefix(dependency: Dependency): String {
     // For com.google etc., use "google" instead of "com"
-    val groupPrefix = gc.groupId.substringBefore('.').toSafeKey()
+    val group = dependency.group ?: return "nogroup"
+    val groupPrefix = group.substringBefore('.').toSafeKey()
     if (groupPrefix == "com" || groupPrefix == "org" || groupPrefix == "io") {
-        return gc.groupId.substringAfter('.').substringBefore('.').toSafeKey()
+        return group.substringAfter('.').substringBefore('.').toSafeKey()
     }
     return groupPrefix.toSafeKey()
 }
@@ -136,9 +140,9 @@
  * - use group name + "-" + artifactId;
  * - use group name + "-" + artifactId + /number/.
  */
- fun pickVersionVariableName(gc: GradleCoordinate, caseSensitiveReserved: Set<String>): String {
+ fun pickVersionVariableName(dependency: Dependency, caseSensitiveReserved: Set<String>): String {
     // If using the artifactVersion convention, follow that
-    val artifact = gc.artifactId.toSafeKey()
+    val artifact = dependency.name.toSafeKey()
     val reserved = TreeSet(String.CASE_INSENSITIVE_ORDER)
     reserved.addAll(caseSensitiveReserved)
 
@@ -187,7 +191,7 @@
         }
     }
 
-    val groupPrefix = getGroupPrefix(gc)
+    val groupPrefix = getGroupPrefix(dependency)
     val withGroupIdPrefix = "$groupPrefix-$artifactName"
     if (!reserved.contains(withGroupIdPrefix)) {
         return withGroupIdPrefix
@@ -201,7 +205,7 @@
     }
 
     // With full group
-    val groupId = gc.groupId.toSafeKey()
+    val groupId = dependency.group?.toSafeKey() ?: "nogroup"
     val withGroupId = "$groupId-$artifactName"
     if (!reserved.contains(withGroupId)) {
         return withGroupId
diff --git a/sdk-common/src/test/java/com/android/ide/common/repository/VersionCatalogNamingUtilTest.kt b/sdk-common/src/test/java/com/android/ide/common/repository/VersionCatalogNamingUtilTest.kt
index 7a41db6..e0befd7 100644
--- a/sdk-common/src/test/java/com/android/ide/common/repository/VersionCatalogNamingUtilTest.kt
+++ b/sdk-common/src/test/java/com/android/ide/common/repository/VersionCatalogNamingUtilTest.kt
@@ -15,9 +15,9 @@
  */
 package com.android.ide.common.repository
 
+import com.android.ide.common.gradle.Dependency
 import org.junit.Test
 import org.junit.Assert.assertEquals
-import kotlin.test.assertNotNull
 
 class VersionCatalogNamingUtilTest {
 
@@ -160,10 +160,6 @@
 
     }
 
-    /**
-     * Need to test it separately as now GradleCoordinates cannot parse some coordinates
-     * that toSafeKey can safely transform.
-     */
     @Test
     fun testToSafeKey() {
         assertEquals("org-company_all","org.company+all".toSafeKey())
@@ -179,7 +175,8 @@
         check(
             expected,
             coordinateString,
-            { gc, libraries, include -> pickLibraryVariableName(gc, include, libraries) },
+            { dependency, libraries, include ->
+                pickLibraryVariableName(dependency, include, libraries) },
             includeVersions,
             *variableNames
         )
@@ -194,8 +191,8 @@
         check(
             expected,
             coordinateString,
-            { gc, set, _ ->
-                pickVersionVariableName(gc, set)
+            { dependency, set, _ ->
+                pickVersionVariableName(dependency, set)
             },
             includeVersions,
             *variableNames
@@ -207,17 +204,16 @@
         coordinateString: String,
         suggestName:
             (
-            gc: GradleCoordinate,
+            dependency: Dependency,
             reserved: Set<String>,
             allowExisting: Boolean
         ) -> String,
         includeVersions: Boolean,
         vararg variableNames: String
     ) {
-        val gc = GradleCoordinate.parseCoordinateString(coordinateString)
-        assertNotNull(gc) { "$coordinateString is not valid GradleCoordinate" }
+        val dependency = Dependency.parse(coordinateString)
         val reserved = setOf(*variableNames)
-        val name = suggestName(gc, reserved, includeVersions)
+        val name = suggestName(dependency, reserved, includeVersions)
         assertEquals(expected, name)
     }
 }