diff --git a/build.sbt b/build.sbt index b053eda9bd..a13e7c59a2 100644 --- a/build.sbt +++ b/build.sbt @@ -149,6 +149,7 @@ lazy val core = myCrossProject("core") Dependencies.monocleCore, Dependencies.refined, Dependencies.scalacacheCaffeine, + Dependencies.tomlj, Dependencies.logbackClassic % Runtime, Dependencies.catsLaws % Test, Dependencies.circeLiteral % Test, diff --git a/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala b/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala index 09ed1ac883..0c76bfa787 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/application/Context.scala @@ -25,6 +25,7 @@ import org.http4s.client.Client import org.http4s.headers.`User-Agent` import org.scalasteward.core.application.Config.ForgeCfg import org.scalasteward.core.buildtool.BuildToolDispatcher +import org.scalasteward.core.buildtool.gradle.GradleAlg import org.scalasteward.core.buildtool.maven.MavenAlg import org.scalasteward.core.buildtool.mill.MillAlg import org.scalasteward.core.buildtool.sbt.SbtAlg @@ -61,6 +62,7 @@ final class Context[F[_]](implicit val filterAlg: FilterAlg[F], val forgeRepoAlg: ForgeRepoAlg[F], val gitAlg: GitAlg[F], + val gradleAlg: GradleAlg[F], val hookExecutor: HookExecutor[F], val httpJsonClient: HttpJsonClient[F], val logger: Logger[F], @@ -176,6 +178,7 @@ object Context { implicit val versionsCache: VersionsCache[F] = new VersionsCache[F](config.cacheTtl, versionsStore) implicit val updateAlg: UpdateAlg[F] = new UpdateAlg[F] + implicit val gradleAlg: GradleAlg[F] = new GradleAlg[F](config.defaultResolver) implicit val mavenAlg: MavenAlg[F] = new MavenAlg[F](config) implicit val sbtAlg: SbtAlg[F] = new SbtAlg[F](config) implicit val scalaCliAlg: ScalaCliAlg[F] = new ScalaCliAlg[F] diff --git a/modules/core/src/main/scala/org/scalasteward/core/buildtool/BuildToolDispatcher.scala b/modules/core/src/main/scala/org/scalasteward/core/buildtool/BuildToolDispatcher.scala index 6635bbb170..be868f6f09 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/buildtool/BuildToolDispatcher.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/buildtool/BuildToolDispatcher.scala @@ -18,6 +18,7 @@ package org.scalasteward.core.buildtool import cats.Monad import cats.syntax.all.* +import org.scalasteward.core.buildtool.gradle.GradleAlg import org.scalasteward.core.buildtool.maven.MavenAlg import org.scalasteward.core.buildtool.mill.MillAlg import org.scalasteward.core.buildtool.sbt.SbtAlg @@ -29,6 +30,7 @@ import org.scalasteward.core.scalafmt.ScalafmtAlg import org.typelevel.log4cats.Logger final class BuildToolDispatcher[F[_]](implicit + gradleAlg: GradleAlg[F], logger: Logger[F], mavenAlg: MavenAlg[F], millAlg: MillAlg[F], @@ -53,7 +55,7 @@ final class BuildToolDispatcher[F[_]](implicit buildTools.traverse_(_.runMigration(buildRoot, migration)) }) - private val allBuildTools = List(mavenAlg, millAlg, sbtAlg, scalaCliAlg) + private val allBuildTools = List(gradleAlg, mavenAlg, millAlg, sbtAlg, scalaCliAlg) private val fallbackBuildTool = List(sbtAlg) private def findBuildTools(buildRoot: BuildRoot): F[(BuildRoot, List[BuildToolAlg[F]])] = diff --git a/modules/core/src/main/scala/org/scalasteward/core/buildtool/gradle/GradleAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/buildtool/gradle/GradleAlg.scala new file mode 100644 index 0000000000..648d8378f9 --- /dev/null +++ b/modules/core/src/main/scala/org/scalasteward/core/buildtool/gradle/GradleAlg.scala @@ -0,0 +1,48 @@ +/* + * Copyright 2018-2025 Scala Steward contributors + * + * 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. + */ + +package org.scalasteward.core.buildtool.gradle + +import better.files.File +import cats.Monad +import cats.syntax.all.* +import org.scalasteward.core.buildtool.{BuildRoot, BuildToolAlg} +import org.scalasteward.core.data.Scope.Dependencies +import org.scalasteward.core.data.{Resolver, Scope} +import org.scalasteward.core.io.{FileAlg, WorkspaceAlg} +import org.typelevel.log4cats.Logger + +final class GradleAlg[F[_]](defaultResolver: Resolver)(implicit + fileAlg: FileAlg[F], + override protected val logger: Logger[F], + workspaceAlg: WorkspaceAlg[F], + F: Monad[F] +) extends BuildToolAlg[F] { + override def name: String = "Gradle" + + override def containsBuild(buildRoot: BuildRoot): F[Boolean] = + libsVersionsToml(buildRoot).flatMap(fileAlg.isRegularFile) + + override def getDependencies(buildRoot: BuildRoot): F[List[Dependencies]] = + libsVersionsToml(buildRoot) + .flatMap(fileAlg.readFile) + .map(_.getOrElse("")) + .map(gradleParser.parseDependencies) + .map(ds => List(Scope(ds, List(defaultResolver)))) + + private def libsVersionsToml(buildRoot: BuildRoot): F[File] = + workspaceAlg.buildRootDir(buildRoot).map(_ / "gradle" / "libs.versions.toml") +} diff --git a/modules/core/src/main/scala/org/scalasteward/core/buildtool/gradle/gradleParser.scala b/modules/core/src/main/scala/org/scalasteward/core/buildtool/gradle/gradleParser.scala new file mode 100644 index 0000000000..5438607f67 --- /dev/null +++ b/modules/core/src/main/scala/org/scalasteward/core/buildtool/gradle/gradleParser.scala @@ -0,0 +1,75 @@ +/* + * Copyright 2018-2025 Scala Steward contributors + * + * 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. + */ + +package org.scalasteward.core.buildtool.gradle + +import org.scalasteward.core.data.{ArtifactId, Dependency, GroupId, Version} +import org.tomlj.{Toml, TomlTable} +import scala.jdk.CollectionConverters.* + +object gradleParser { + def parseDependencies(s: String): List[Dependency] = { + val parsed = Toml.parse(s) + val versions = parsed.getTable(key.versions) + val libraries = parsed.getTable(key.libraries) + libraries + .entrySet() + .asScala + .map(_.getValue) + .flatMap { + case lib: TomlTable => parseDependency(lib, versions) + case _ => None + } + .toList + } + + private def parseDependency(lib: TomlTable, versions: TomlTable): Option[Dependency] = + parseVersion(lib, versions).flatMap { version => + if (lib.contains(key.module)) { + val module = lib.getString(key.module) + module.split(':') match { + case Array(g, a) => + val groupId = GroupId(g) + val artifactId = ArtifactId(a) + Some(Dependency(groupId, artifactId, version)) + case _ => None + } + } else if (lib.contains(key.group)) { + val groupId = GroupId(lib.getString(key.group)) + val artifactId = ArtifactId(lib.getString(key.name)) + Some(Dependency(groupId, artifactId, version)) + } else None + } + + private def parseVersion(lib: TomlTable, versions: TomlTable): Option[Version] = + if (lib.isTable(key.version) && lib.contains(key.versionRef)) { + val ref = lib.getString(key.versionRef) + Option.when(versions.isString(ref))(Version(versions.getString(ref))) + } else if (lib.isString(key.version)) + Some(Version(lib.getString(key.version))) + else + None + + object key { + val group = "group" + val libraries = "libraries" + val module = "module" + val name = "name" + val version = "version" + val versions = "versions" + val versionRef = "version.ref" + } +} diff --git a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala index fa5246787a..6c31bb7963 100644 --- a/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala +++ b/modules/core/src/main/scala/org/scalasteward/core/repoconfig/UpdatesConfig.scala @@ -114,6 +114,7 @@ object UpdatesConfig { ".scala", scalafmtConfName, ".sdkmanrc", + ".toml", ".yml", buildPropertiesName, pomXmlName diff --git a/modules/core/src/test/scala/org/scalasteward/core/buildtool/gradle/GradleAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/buildtool/gradle/GradleAlgTest.scala new file mode 100644 index 0000000000..f43586cdde --- /dev/null +++ b/modules/core/src/test/scala/org/scalasteward/core/buildtool/gradle/GradleAlgTest.scala @@ -0,0 +1,26 @@ +package org.scalasteward.core.buildtool.gradle + +import munit.CatsEffectSuite +import org.scalasteward.core.TestSyntax.* +import org.scalasteward.core.buildtool.BuildRoot +import org.scalasteward.core.data.Repo +import org.scalasteward.core.mock.MockContext.context.* +import org.scalasteward.core.mock.{MockEffOps, MockState} + +class GradleAlgTest extends CatsEffectSuite { + test("getDependencies") { + val repo = Repo("gradle-alg", "test-getDependencies") + val buildRoot = BuildRoot(repo, ".") + val buildRootDir = workspaceAlg.buildRootDir(buildRoot).unsafeRunSync() + + val initial = MockState.empty.addFiles( + buildRootDir / "gradle" / "libs.versions.toml" -> + """|[libraries] + |tomlj = { group = "org.tomlj", name = "tomlj", version = "1.1.1" } + |""".stripMargin + ) + val obtained = initial.flatMap(gradleAlg.getDependencies(buildRoot).runA) + val expected = List(List("org.tomlj".g % "tomlj".a % "1.1.1").withMavenCentral) + assertIO(obtained, expected) + } +} diff --git a/modules/core/src/test/scala/org/scalasteward/core/buildtool/gradle/gradleParserTest.scala b/modules/core/src/test/scala/org/scalasteward/core/buildtool/gradle/gradleParserTest.scala new file mode 100644 index 0000000000..dad7f8cef2 --- /dev/null +++ b/modules/core/src/test/scala/org/scalasteward/core/buildtool/gradle/gradleParserTest.scala @@ -0,0 +1,27 @@ +package org.scalasteward.core.buildtool.gradle + +import munit.FunSuite + +class gradleParserTest extends FunSuite { + test("parseDependencies") { + val input = + """|[versions] + |groovy = "3.0.5" + |checkstyle = "8.37" + | + |[libraries] + |groovy-core = { module = "org.codehaus.groovy:groovy", version.ref = "groovy" } + |groovy-json = { module = "org.codehaus.groovy:groovy-json", version.ref = "groovy" } + |groovy-nio = { module = "org.codehaus.groovy:groovy-nio", version.ref = "groovy" } + |commons-lang3 = { group = "org.apache.commons", name = "commons-lang3", version = { strictly = "[3.8, 4.0[", prefer="3.9" } } + |tomlj = { group = "org.tomlj", name = "tomlj", version = "1.1.1" } + | + |[bundles] + |groovy = ["groovy-core", "groovy-json", "groovy-nio"] + | + |[plugins] + |versions = { id = "com.github.ben-manes.versions", version = "0.45.0" } + |""".stripMargin + gradleParser.parseDependencies(input) + } +} diff --git a/modules/core/src/test/scala/org/scalasteward/core/edit/RewriteTest.scala b/modules/core/src/test/scala/org/scalasteward/core/edit/RewriteTest.scala index 5034112012..8cb61dd63c 100644 --- a/modules/core/src/test/scala/org/scalasteward/core/edit/RewriteTest.scala +++ b/modules/core/src/test/scala/org/scalasteward/core/edit/RewriteTest.scala @@ -947,6 +947,23 @@ class RewriteTest extends FunSuite { runApplyUpdate(update, original, expected) } + test("Gradle Version Catalog") { + val update = ("org.tomlj".g % "tomlj".a % "1.0.0" %> "1.1.1").single + val original = Map( + "gradle/libs.version.toml" -> + """|[libraries] + |tomlj = { group = "org.tomlj", name = "tomlj", version = "1.0.0" } + |""".stripMargin + ) + val expected = Map( + "gradle/libs.version.toml" -> + """|[libraries] + |tomlj = { group = "org.tomlj", name = "tomlj", version = "1.1.1" } + |""".stripMargin + ) + runApplyUpdate(update, original, expected) + } + private def runApplyUpdate( update: Update.Single, files: Map[String, String], diff --git a/project/Dependencies.scala b/project/Dependencies.scala index 320df96b0a..d457d6e213 100644 --- a/project/Dependencies.scala +++ b/project/Dependencies.scala @@ -44,4 +44,5 @@ object Dependencies { val scalaStewardMillPluginArtifactName = "scala-steward-mill-plugin" val scalaStewardMillPlugin = "org.scala-steward" % s"${scalaStewardMillPluginArtifactName}_mill0.10_2.13" % "0.18.0" + val tomlj = "org.tomlj" % "tomlj" % "1.1.1" }