Repository: scala-steward-org/scala-steward (scala)

Issue:
Document how to integrate with scala projects that use Maven
scala-steward is capable of running in a project that uses maven for project definition by using https://github.com/sbt/sbt-pom-reader to start sbt and get dependencies. If this can't be done reliably, it may need to be opt-in on a project-by-project basis.

Once the dependencies are enumerated, applying a different set of heuristics to the sbt version replacements seems straightforward enough.

Pull request:
This contains @stanislav-chetvertkov's code from #1193 for supporting Maven. I accidentally closed #1193 by merging #1429 that contained all commits of #1193. Therefore this PR is basically a revert of 584b500e4a6fe2aac5d97709bcd576b9c6b1a8b7 and a few changes to bring the Maven support in sync with current master.

Patch:
diff --git a/build.sbt b/build.sbt
index 650e3e1e64..fc282f9984 100644
--- a/build.sbt
+++ b/build.sbt
@@ -28,6 +28,7 @@ lazy val core = myCrossProject("core")
     libraryDependencies ++= Seq(
       compilerPlugin(Dependencies.betterMonadicFor),
       compilerPlugin(Dependencies.kindProjector.cross(CrossVersion.full)),
+      Dependencies.attoCore,
       Dependencies.betterFiles,
       Dependencies.caseApp,
       Dependencies.catsEffect,
@@ -185,7 +186,7 @@ lazy val dockerSettings = Def.settings(
       Cmd("USER", "root"),
       Cmd(
         "RUN",
-        s"apk --no-cache add bash git ca-certificates && wget $sbtUrl && tar -xf $sbtTgz && rm -f $sbtTgz"
+        s"apk --no-cache add bash git ca-certificates maven && wget $sbtUrl && tar -xf $sbtTgz && rm -f $sbtTgz"
       )
     )
   },
@@ -230,6 +231,7 @@ moduleRootPkg := rootPkg
 lazy val runSteward = taskKey[Unit]("")
 runSteward := Def.taskDyn {
   val home = System.getenv("HOME")
+  val myJavaHome = System.getenv("JAVA_HOME")
   val projectDir = (LocalRootProject / baseDirectory).value
   val args = Seq(
     Seq("--workspace", s"$projectDir/workspace"),
@@ -238,9 +240,11 @@ runSteward := Def.taskDyn {
     Seq("--vcs-login", projectName),
     Seq("--git-ask-pass", s"$home/.github/askpass/$projectName.sh"),
     Seq("--whitelist", s"$home/.cache/coursier"),
-    Seq("--whitelist", s"$home/.coursier"),
+    Seq("--whitelist", s"$home/.cache/JNA"),
     Seq("--whitelist", s"$home/.ivy2"),
-    Seq("--whitelist", s"$home/.sbt")
+    Seq("--whitelist", s"$home/.sbt"),
+    Seq("--whitelist", myJavaHome),
+    Seq("--read-only", myJavaHome)
   ).flatten.mkString(" ", " ", "")
   (core.jvm / Compile / run).toTask(args)
 }.value
diff --git a/docs/repo-specific-configuration.md b/docs/repo-specific-configuration.md
index ce1698db16..18cf1ecc42 100644
--- a/docs/repo-specific-configuration.md
+++ b/docs/repo-specific-configuration.md
@@ -64,7 +64,7 @@ updates.limit = 5
 updates.includeScala = true
 
 # The extensions of files that should be updated.
-# Default: [".scala", ".sbt", ".sbt.shared", ".sc", ".yml"]
+# Default: [".scala", ".sbt", ".sbt.shared", ".sc", ".yml", "pom.xml"]
 updates.fileExtensions = [".scala", ".sbt", ".sbt.shared", ".sc", ".yml", ".md", ".markdown", ".txt"]
 
 # If "on-conflicts", Scala Steward will update the PR it created to resolve conflicts as
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 4d9794036a..466548938d 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
@@ -23,6 +23,7 @@ import io.chrisdavenport.log4cats.slf4j.Slf4jLogger
 import org.http4s.client.Client
 import org.http4s.client.asynchttpclient.AsyncHttpClient
 import org.scalasteward.core.buildsystem.BuildSystemDispatcher
+import org.scalasteward.core.buildsystem.maven.MavenAlg
 import org.scalasteward.core.buildsystem.sbt.SbtAlg
 import org.scalasteward.core.coursier.{CoursierAlg, VersionsCache}
 import org.scalasteward.core.edit.EditAlg
@@ -80,6 +81,7 @@ object Context {
       implicit val versionsCache: VersionsCache[F] =
         new VersionsCache[F](config.cacheTtl, new JsonKeyValueStore("versions", "2"))
       implicit val updateAlg: UpdateAlg[F] = new UpdateAlg[F]
+      implicit val mavenAlg: MavenAlg[F] = MavenAlg.create[F]
       implicit val sbtAlg: SbtAlg[F] = SbtAlg.create[F]
       implicit val buildSystemDispatcher: BuildSystemDispatcher[F] = BuildSystemDispatcher.create[F]
       implicit val refreshErrorAlg: RefreshErrorAlg[F] =
diff --git a/modules/core/src/main/scala/org/scalasteward/core/buildsystem/BuildSystemDispatcher.scala b/modules/core/src/main/scala/org/scalasteward/core/buildsystem/BuildSystemDispatcher.scala
index e076290c86..9f614979d6 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/buildsystem/BuildSystemDispatcher.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/buildsystem/BuildSystemDispatcher.scala
@@ -18,6 +18,7 @@ package org.scalasteward.core.buildsystem
 
 import cats.Monad
 import cats.implicits._
+import org.scalasteward.core.buildsystem.maven.MavenAlg
 import org.scalasteward.core.buildsystem.sbt.SbtAlg
 import org.scalasteward.core.data.Scope
 import org.scalasteward.core.scalafix.Migration
@@ -28,10 +29,11 @@ trait BuildSystemDispatcher[F[_]] extends BuildSystemAlg[F]
 
 object BuildSystemDispatcher {
   def create[F[_]](implicit
+      mavenAlg: MavenAlg[F],
       sbtAlg: SbtAlg[F],
       F: Monad[F]
   ): BuildSystemDispatcher[F] = {
-    val allBuildSystems = List(sbtAlg)
+    val allBuildSystems = List(sbtAlg, mavenAlg)
     val fallbackBuildSystem = sbtAlg
 
     new BuildSystemDispatcher[F] {
diff --git a/modules/core/src/main/scala/org/scalasteward/core/buildsystem/maven/MavenAlg.scala b/modules/core/src/main/scala/org/scalasteward/core/buildsystem/maven/MavenAlg.scala
new file mode 100644
index 0000000000..443b9da700
--- /dev/null
+++ b/modules/core/src/main/scala/org/scalasteward/core/buildsystem/maven/MavenAlg.scala
@@ -0,0 +1,68 @@
+/*
+ * Copyright 2018-2020 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.buildsystem.maven
+
+import better.files.File
+import cats.Monad
+import cats.implicits._
+import org.scalasteward.core.application.Config
+import org.scalasteward.core.buildsystem.BuildSystemAlg
+import org.scalasteward.core.data._
+import org.scalasteward.core.io.{FileAlg, ProcessAlg, WorkspaceAlg}
+import org.scalasteward.core.scalafix.Migration
+import org.scalasteward.core.util.Nel
+import org.scalasteward.core.vcs.data.Repo
+
+trait MavenAlg[F[_]] extends BuildSystemAlg[F]
+
+object MavenAlg {
+  def create[F[_]](implicit
+      config: Config,
+      fileAlg: FileAlg[F],
+      processAlg: ProcessAlg[F],
+      workspaceAlg: WorkspaceAlg[F],
+      F: Monad[F]
+  ): MavenAlg[F] =
+    new MavenAlg[F] {
+      override def containsBuild(repo: Repo): F[Boolean] =
+        workspaceAlg.repoDir(repo).flatMap(repoDir => fileAlg.isRegularFile(repoDir / "pom.xml"))
+
+      override def getDependencies(repo: Repo): F[List[Scope.Dependencies]] =
+        for {
+          repoDir <- workspaceAlg.repoDir(repo)
+          dependenciesRaw <- exec(mvnCmd(command.listDependencies), repoDir)
+          repositoriesRaw <- exec(mvnCmd(command.listRepositories), repoDir)
+          dependencies = parser.parseDependencies(dependenciesRaw)
+          resolvers = parser.parseResolvers(repositoriesRaw)
+        } yield List(Scope(dependencies.distinct, resolvers))
+
+      override def runMigrations(repo: Repo, migrations: Nel[Migration]): F[Unit] =
+        F.unit
+
+      def exec(command: Nel[String], repoDir: File): F[List[String]] =
+        maybeIgnoreOptsFiles(repoDir)(processAlg.execSandboxed(command, repoDir))
+
+      def mvnCmd(commands: String*): Nel[String] =
+        Nel("mvn", "--batch-mode" :: commands.toList)
+
+      def maybeIgnoreOptsFiles[A](dir: File)(fa: F[A]): F[A] =
+        if (config.ignoreOptsFiles) ignoreOptsFiles(dir)(fa) else fa
+
+      def ignoreOptsFiles[A](dir: File)(fa: F[A]): F[A] =
+        fileAlg.removeTemporarily(dir / ".jvmopts")(fa)
+    }
+}
diff --git a/modules/core/src/main/scala/org/scalasteward/core/buildsystem/maven/command.scala b/modules/core/src/main/scala/org/scalasteward/core/buildsystem/maven/command.scala
new file mode 100644
index 0000000000..b2b0e53af4
--- /dev/null
+++ b/modules/core/src/main/scala/org/scalasteward/core/buildsystem/maven/command.scala
@@ -0,0 +1,22 @@
+/*
+ * Copyright 2018-2020 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.buildsystem.maven
+
+object command {
+  val listDependencies = "dependency:list"
+  val listRepositories = "dependency:list-repositories"
+}
diff --git a/modules/core/src/main/scala/org/scalasteward/core/buildsystem/maven/parser.scala b/modules/core/src/main/scala/org/scalasteward/core/buildsystem/maven/parser.scala
new file mode 100644
index 0000000000..bb4721a9fe
--- /dev/null
+++ b/modules/core/src/main/scala/org/scalasteward/core/buildsystem/maven/parser.scala
@@ -0,0 +1,74 @@
+/*
+ * Copyright 2018-2020 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.buildsystem.maven
+
+import atto.Atto._
+import atto._
+import cats.implicits._
+import org.scalasteward.core.data._
+
+object parser {
+  private val colon: Parser[Char] = char(':')
+
+  private val underscore: Parser[Char] = char('_')
+
+  private val stringNoSpace: Parser[String] =
+    many1(noneOf(" ")).map(_.mkString_(""))
+
+  private val stringNoSpaceNoColon: Parser[String] =
+    many1(noneOf(" :")).map(_.mkString_(""))
+
+  private val stringNoSpaceNoColonNoUnderscore: Parser[String] =
+    many1(noneOf(" :_")).map(_.mkString_(""))
+
+  private val artifactId: Parser[ArtifactId] =
+    for {
+      name <- stringNoSpaceNoColonNoUnderscore
+      suffix <- opt(underscore ~ stringNoSpaceNoColon)
+    } yield ArtifactId(name, suffix.map { case (c, str) => name + c + str })
+
+  private val configurations: Parser[Option[String]] =
+    opt(stringNoSpaceNoColon).map(_.filterNot(_ === "compile"))
+
+  private val dependency: Parser[Dependency] =
+    for {
+      _ <- opt(string("[INFO]") ~ many(whitespace))
+      groupId <- stringNoSpaceNoColon.map(GroupId.apply) <~ colon
+      artifactId <- artifactId <~ colon <~ string("jar") <~ colon
+      version <- stringNoSpaceNoColon <~ colon
+      configurations <- configurations
+    } yield Dependency(
+      groupId = groupId,
+      artifactId = artifactId,
+      version = version,
+      configurations = configurations
+    )
+
+  def parseDependencies(input: List[String]): List[Dependency] =
+    input.flatMap(line => dependency.parse(line).done.option)
+
+  private val resolver: Parser[Resolver] =
+    for {
+      _ <- many(whitespace) ~ string("id:") ~ whitespace
+      id <- stringNoSpace
+      _ <- many(whitespace) ~ string("url:") ~ whitespace
+      url <- stringNoSpace
+    } yield Resolver.MavenRepository(id, url, None)
+
+  def parseResolvers(input: List[String]): List[Resolver] =
+    input.mkString.split("""\[INFO\]""").toList.flatMap(line => resolver.parse(line).done.option)
+}
diff --git a/modules/core/src/main/scala/org/scalasteward/core/io/package.scala b/modules/core/src/main/scala/org/scalasteward/core/io/package.scala
index 73cb73b0d7..aa4ea18dda 100644
--- a/modules/core/src/main/scala/org/scalasteward/core/io/package.scala
+++ b/modules/core/src/main/scala/org/scalasteward/core/io/package.scala
@@ -35,10 +35,8 @@ package object io {
       case _                                 => isGenericSourceFile(file, fileExtensions)
     }
 
-  private def isGenericSourceFile(file: File, fileExtensions: Set[String]): Boolean = {
-    val name = file.name
-    fileExtensions.exists(suffix => name.endsWith(suffix) && !name.startsWith(suffix))
-  }
+  private def isGenericSourceFile(file: File, fileExtensions: Set[String]): Boolean =
+    fileExtensions.exists(file.name.endsWith)
 
   private def isSbtUpdate(update: Update): Boolean =
     update.groupId === GroupId("org.scala-sbt") &&
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 5d87624e6a..72c9b7760d 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
@@ -86,7 +86,8 @@ object UpdatesConfig {
 
   val defaultIncludeScala: Boolean = false
 
-  val defaultFileExtensions: Set[String] = Set(".scala", ".sbt", ".sbt.shared", ".sc", ".yml")
+  val defaultFileExtensions: Set[String] =
+    Set(".scala", ".sbt", ".sbt.shared", ".sc", ".yml", "pom.xml")
 
   // prevent IntelliJ from removing the import of io.circe.refined._
   locally(refinedDecoder: Decoder[PosInt])
diff --git a/project/Dependencies.scala b/project/Dependencies.scala
index b7e4875e6c..6b87126b15 100644
--- a/project/Dependencies.scala
+++ b/project/Dependencies.scala
@@ -1,6 +1,7 @@
 import sbt._
 
 object Dependencies {
+  val attoCore = "org.tpolecat" %% "atto-core" % "0.7.0"
   val betterFiles = "com.github.pathikrit" %% "better-files" % "3.8.0"
   val betterMonadicFor = "com.olegpy" %% "better-monadic-for" % "0.3.1"
   val caseApp = "com.github.alexarchambault" %% "case-app" % "2.0.0-M16"
diff --git a/repos.md b/repos.md
index 9012313f2c..6ec76d191c 100644
--- a/repos.md
+++ b/repos.md
@@ -13,3 +13,4 @@ If you want Scala Steward to keep your project up-to-date, please add it there.
 - fthomas/scala-steward
 - fthomas/singleton-ops
 - fthomas/status-page
+- scala-steward-org/test-repo-2


Test patch:
diff --git a/modules/core/src/test/scala/org/scalasteward/core/buildsystem/BuildSystemDispatcherTest.scala b/modules/core/src/test/scala/org/scalasteward/core/buildsystem/BuildSystemDispatcherTest.scala
index 3e2c1ba524..e99a476d8b 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/buildsystem/BuildSystemDispatcherTest.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/buildsystem/BuildSystemDispatcherTest.scala
@@ -17,6 +17,7 @@ class BuildSystemDispatcherTest extends AnyFunSuite with Matchers {
     state shouldBe initial.copy(commands =
       Vector(
         List("test", "-f", s"$repoDir/build.sbt"),
+        List("test", "-f", s"$repoDir/pom.xml"),
         List(
           "TEST_VAR=GREAT",
           "ANOTHER_TEST_VAR=ALSO_GREAT",
diff --git a/modules/core/src/test/scala/org/scalasteward/core/buildsystem/maven/MavenAlgTest.scala b/modules/core/src/test/scala/org/scalasteward/core/buildsystem/maven/MavenAlgTest.scala
new file mode 100644
index 0000000000..73e1c7428b
--- /dev/null
+++ b/modules/core/src/test/scala/org/scalasteward/core/buildsystem/maven/MavenAlgTest.scala
@@ -0,0 +1,49 @@
+package org.scalasteward.core.buildsystem.maven
+
+import better.files.File
+import org.scalasteward.core.mock.MockContext._
+import org.scalasteward.core.mock.MockState
+import org.scalasteward.core.vcs.data.Repo
+import org.scalatest.funsuite.AnyFunSuite
+import org.scalatest.matchers.should.Matchers
+
+class MavenAlgTest extends AnyFunSuite with Matchers {
+  val var1 = "TEST_VAR=GREAT"
+  val var2 = "ANOTHER_TEST_VAR=ALSO_GREAT"
+
+  test("getDependencies") {
+    val repo = Repo("namespace", "repo-name")
+    val repoDir = config.workspace / repo.show
+    val files: Map[File, String] = Map.empty
+
+    val state =
+      mavenAlg.getDependencies(repo).runS(MockState.empty.copy(files = files)).unsafeRunSync()
+
+    state shouldBe MockState(
+      files = files,
+      logs = Vector.empty,
+      commands = Vector(
+        List(
+          var1,
+          var2,
+          repoDir.toString,
+          "firejail",
+          s"--whitelist=$repoDir",
+          "mvn",
+          "--batch-mode",
+          command.listDependencies
+        ),
+        List(
+          var1,
+          var2,
+          repoDir.toString,
+          "firejail",
+          s"--whitelist=$repoDir",
+          "mvn",
+          "--batch-mode",
+          command.listRepositories
+        )
+      )
+    )
+  }
+}
diff --git a/modules/core/src/test/scala/org/scalasteward/core/buildsystem/maven/parserTest.scala b/modules/core/src/test/scala/org/scalasteward/core/buildsystem/maven/parserTest.scala
new file mode 100644
index 0000000000..449db81236
--- /dev/null
+++ b/modules/core/src/test/scala/org/scalasteward/core/buildsystem/maven/parserTest.scala
@@ -0,0 +1,73 @@
+package org.scalasteward.core.buildsystem.maven
+
+import org.scalasteward.core.TestSyntax._
+import org.scalasteward.core.data.ArtifactId
+import org.scalasteward.core.data.Resolver.MavenRepository
+import org.scalatest.funsuite.AnyFunSuite
+import org.scalatest.matchers.should.Matchers
+
+class parserTest extends AnyFunSuite with Matchers {
+  test("parseDependencies") {
+    val input =
+      """|[INFO] Scanning for projects...
+         |[INFO]
+         |[INFO] ----------------------< com.mycompany.app:my-app >----------------------
+         |[INFO] Building my-app 1.0-SNAPSHOT
+         |[INFO] --------------------------------[ jar ]---------------------------------
+         |[INFO]
+         |[INFO] --- maven-dependency-plugin:2.8:list (default-cli) @ my-app ---
+         |[INFO]
+         |[INFO] The following files have been resolved:
+         |[INFO]    org.typelevel:cats-core_2.12:jar:1.5.0:compile
+         |[INFO]    org.hamcrest:hamcrest-core:jar:1.3:test
+         |[INFO]    junit:junit:jar:4.12:test
+         |[INFO]    ch.qos.logback:logback-core:jar:1.1.11:compile
+         |[INFO]
+         |[INFO] ------------------------------------------------------------------------
+         |[INFO] BUILD SUCCESS
+         |[INFO] ------------------------------------------------------------------------
+         |[INFO] Total time:  1.150 s
+         |[INFO] Finished at: 2020-05-14T09:40:06+02:00
+         |[INFO] ------------------------------------------------------------------------
+         |""".stripMargin.linesIterator.toList
+    val dependencies = parser.parseDependencies(input)
+    dependencies shouldBe List(
+      "org.typelevel" % ArtifactId("cats-core", "cats-core_2.12") % "1.5.0",
+      "org.hamcrest" % "hamcrest-core" % "1.3" % "test",
+      "junit" % "junit" % "4.12" % "test",
+      "ch.qos.logback" % "logback-core" % "1.1.11"
+    )
+  }
+
+  test("parseResolvers") {
+    val input =
+      """|[INFO]        id: sonatype-nexus-snapshots
+         |      url: https://oss.sonatype.org/content/repositories/snapshots
+         |   layout: default
+         |snapshots: [enabled => true, update => daily]
+         | releases: [enabled => false, update => daily]
+         |
+         |[INFO]        id: bintrayakkamaven
+         |      url: https://dl.bintray.com/akka/maven/
+         |   layout: default
+         |snapshots: [enabled => true, update => daily]
+         | releases: [enabled => true, update => daily]
+         |
+         |[INFO]        id: apache.snapshots
+         |      url: http://repository.apache.org/snapshots
+         |   layout: default
+         |snapshots: [enabled => true, update => daily]
+         | releases: [enabled => false, update => daily]
+         |""".stripMargin.linesIterator.toList
+    val resolvers = parser.parseResolvers(input)
+    resolvers shouldBe List(
+      MavenRepository(
+        "sonatype-nexus-snapshots",
+        "https://oss.sonatype.org/content/repositories/snapshots",
+        None
+      ),
+      MavenRepository("bintrayakkamaven", "https://dl.bintray.com/akka/maven/", None),
+      MavenRepository("apache.snapshots", "http://repository.apache.org/snapshots", None)
+    )
+  }
+}
diff --git a/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala b/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala
index cc96028ac9..a9139bbdcc 100644
--- a/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala
+++ b/modules/core/src/test/scala/org/scalasteward/core/mock/MockContext.scala
@@ -8,6 +8,7 @@ import org.scalasteward.core.TestInstances.ioContextShift
 import org.scalasteward.core.application.Cli.EnvVar
 import org.scalasteward.core.application.{Config, SupportedVCS}
 import org.scalasteward.core.buildsystem.BuildSystemDispatcher
+import org.scalasteward.core.buildsystem.maven.MavenAlg
 import org.scalasteward.core.buildsystem.sbt.SbtAlg
 import org.scalasteward.core.coursier.{CoursierAlg, VersionsCache}
 import org.scalasteward.core.edit.EditAlg
@@ -77,6 +78,7 @@ object MockContext {
   implicit val groupMigrations: GroupMigrations =
     GroupMigrations.create[MockEff].runA(MockState.empty).unsafeRunSync()
   implicit val updateAlg: UpdateAlg[MockEff] = new UpdateAlg[MockEff]
+  implicit val mavenAlg: MavenAlg[MockEff] = MavenAlg.create
   implicit val sbtAlg: SbtAlg[MockEff] = SbtAlg.create
   implicit val buildSystemDispatcher: BuildSystemDispatcher[MockEff] = BuildSystemDispatcher.create
   implicit val editAlg: EditAlg[MockEff] = new EditAlg[MockEff]