Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Load Scalafix migrations from this repository #1650

Merged
merged 10 commits into from
Oct 10, 2020
12 changes: 6 additions & 6 deletions docs/scalafix-migrations.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,21 +31,21 @@ which resolves those rules and edits the source code accordingly.
See the [Scalafix Developer Guide][scalafix-dev-guide] for more information
about writing rewrite rules or have a look at the existing
[migration rules][migrations] for inspiration. Rules in Scala Steward must be
accessible via the [github:][using-github], the [http:][using-http] or the [dependency:][using-dependency] schemes.
accessible via the [github:][using-github], the [http:][using-http] or the
[dependency:][using-dependency] schemes.

## Adding migration rules to Scala Steward

After you have written a new migration rule for a new version of your project,
Scala Steward needs to be made aware of it. Creating a pull request that adds
the new rule to the list of [migrations][migrations] is enough for that. Once
that pull request is merged, Scala Steward will start using this migration.
When running Scala Steward you can specify a file containing extra migrations
that might not be present in the [default list][migrations].
You can also specify if you want the default list to be disabled.

The file is in [HOCON][HOCON] and should look like this:
When running Scala Steward you can also specify files via the
fthomas marked this conversation as resolved.
Show resolved Hide resolved
`--scalafix-migrations` command-line option that contain additional
fthomas marked this conversation as resolved.
Show resolved Hide resolved
migrations which are not be present in the [default list][migrations].
fthomas marked this conversation as resolved.
Show resolved Hide resolved
These files are in [HOCON][HOCON] format and should look like this:
```hocon
disableDefaults = true
migrations = [
{
groupId: "org.http4s",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ object Cli {
ignoreOptsFiles: Boolean = false,
envVar: List[EnvVar] = Nil,
processTimeout: FiniteDuration = 10.minutes,
scalafixMigrations: Option[String] = None,
scalafixMigrations: List[Uri] = Nil,
disableDefaultScalafixMigrations: Boolean = false,
groupMigrations: Option[String] = None,
cacheTtl: FiniteDuration = 2.hours,
cacheMissDelay: FiniteDuration = 0.milliseconds,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import cats.effect.Sync
import org.http4s.Uri
import org.http4s.Uri.UserInfo
import org.scalasteward.core.application.Cli.EnvVar
import org.scalasteward.core.application.Config.Scalafix
import org.scalasteward.core.git.Author
import org.scalasteward.core.util
import org.scalasteward.core.vcs.data.AuthenticatedUser
Expand Down Expand Up @@ -68,7 +69,7 @@ final case class Config(
ignoreOptsFiles: Boolean,
envVars: List[EnvVar],
processTimeout: FiniteDuration,
scalafixMigrations: Option[File],
scalafix: Scalafix,
groupMigrations: Option[File],
cacheTtl: FiniteDuration,
cacheMissDelay: FiniteDuration,
Expand All @@ -86,6 +87,11 @@ final case class Config(
}

object Config {
final case class Scalafix(
migrations: List[Uri],
disableDefaults: Boolean
)

def create[F[_]](args: Cli.Args)(implicit F: Sync[F]): F[Config] =
F.delay {
Config(
Expand All @@ -105,7 +111,7 @@ object Config {
ignoreOptsFiles = args.ignoreOptsFiles,
envVars = args.envVar,
processTimeout = args.processTimeout,
scalafixMigrations = args.scalafixMigrations.map(_.toFile),
scalafix = Scalafix(args.scalafixMigrations, args.disableDefaultScalafixMigrations),
groupMigrations = args.groupMigrations.map(_.toFile),
cacheTtl = args.cacheTtl,
cacheMissDelay = args.cacheMissDelay,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,14 +48,15 @@ object Context {
): Resource[F, StewardAlg[F]] =
for {
blocker <- Blocker[F]
implicit0(logger: Logger[F]) <- Resource.liftF(Slf4jLogger.create[F])
_ <- Resource.liftF(printBanner[F])
implicit0(config: Config) <- Resource.liftF(Config.create[F](args))
implicit0(client: Client[F]) <- AsyncHttpClient.resource[F]()
implicit0(logger: Logger[F]) <- Resource.liftF(Slf4jLogger.create[F])
implicit0(httpExistenceClient: HttpExistenceClient[F]) <- HttpExistenceClient.create[F]
implicit0(user: AuthenticatedUser) <- Resource.liftF(config.vcsUser[F])
implicit0(fileAlg: FileAlg[F]) = FileAlg.create[F]
implicit0(migrationAlg: MigrationAlg) <- Resource.liftF(
MigrationAlg.create[F](config.scalafixMigrations)
MigrationAlg.create[F](config.scalafix)
)
implicit0(groupMigration: GroupMigrations) <- Resource.liftF(GroupMigrations.create[F])
} yield {
Expand Down Expand Up @@ -93,4 +94,16 @@ object Context {
implicit val pruningAlg: PruningAlg[F] = new PruningAlg[F]
new StewardAlg[F]
}

private def printBanner[F[_]](implicit logger: Logger[F]): F[Unit] = {
val banner =
"""| ____ _ ____ _ _
| / ___| ___ __ _| | __ _ / ___|| |_ _____ ____ _ _ __ __| |
| \___ \ / __/ _` | |/ _` | \___ \| __/ _ \ \ /\ / / _` | '__/ _` |
| ___) | (_| (_| | | (_| | ___) | || __/\ V V / (_| | | | (_| |
| |____/ \___\__,_|_|\__,_| |____/ \__\___| \_/\_/ \__,_|_| \__,_|""".stripMargin
val msg = List(" ", banner, s" v${org.scalasteward.core.BuildInfo.version}", " ")
.mkString(System.lineSeparator())
logger.info(msg)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,18 +47,6 @@ final class StewardAlg[F[_]](implicit
workspaceAlg: WorkspaceAlg[F],
F: BracketThrowable[F]
) {
private def printBanner: F[Unit] = {
val banner =
"""| ____ _ ____ _ _
| / ___| ___ __ _| | __ _ / ___|| |_ _____ ____ _ _ __ __| |
| \___ \ / __/ _` | |/ _` | \___ \| __/ _ \ \ /\ / / _` | '__/ _` |
| ___) | (_| (_| | | (_| | ___) | || __/\ V V / (_| | | | (_| |
| |____/ \___\__,_|_|\__,_| |____/ \__\___| \_/\_/ \__,_|_| \__,_|""".stripMargin
val msg = List(" ", banner, s" v${org.scalasteward.core.BuildInfo.version}", " ")
.mkString(System.lineSeparator())
logger.info(msg)
}

private def readRepos(reposFile: File): Stream[F, Repo] =
Stream.evals {
fileAlg.readFile(reposFile).map { maybeContent =>
Expand Down Expand Up @@ -88,7 +76,6 @@ final class StewardAlg[F[_]](implicit
def runF: F[ExitCode] =
logger.infoTotalTime("run") {
for {
_ <- printBanner
_ <- selfCheckAlg.checkAll
_ <- workspaceAlg.cleanWorkspace
exitCode <- sbtAlg.addGlobalPlugins {
Expand Down
17 changes: 14 additions & 3 deletions modules/core/src/main/scala/org/scalasteward/core/io/FileAlg.scala
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ import cats.{Functor, Traverse}
import fs2.Stream
import io.chrisdavenport.log4cats.Logger
import org.apache.commons.io.FileUtils
import org.http4s.Uri
import org.http4s.implicits.http4sLiteralsSyntax
import org.scalasteward.core.util.MonadThrowable
import scala.io.Source

Expand All @@ -43,6 +45,8 @@ trait FileAlg[F[_]] {

def readResource(resource: String): F[String]

def readUri(uri: Uri): F[String]

def walk(dir: File): Stream[F, File]

def writeFile(file: File, content: String): F[Unit]
Expand Down Expand Up @@ -125,9 +129,16 @@ object FileAlg {
F.delay(if (file.exists) Some(file.contentAsString) else None)

override def readResource(resource: String): F[String] =
Resource
.fromAutoCloseable(F.delay(Source.fromResource(resource)))
.use(src => F.delay(src.mkString))
readSource(Source.fromResource(resource))

override def readUri(uri: Uri): F[String] = {
val scheme = uri.scheme.getOrElse(scheme"file")
val withScheme = uri.copy(scheme = Some(scheme))
readSource(Source.fromURL(withScheme.renderString))
}

private def readSource(source: => Source): F[String] =
Resource.fromAutoCloseable(F.delay(source)).use(src => F.delay(src.mkString))

override def walk(dir: File): Stream[F, File] =
Stream.eval(F.delay(dir.walk())).flatMap(Stream.fromIterator(_))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,12 @@

package org.scalasteward.core.scalafix

import better.files.File
import cats.data.OptionT
import cats.effect.Sync
import cats.syntax.all._
import io.chrisdavenport.log4cats.Logger
import io.circe.config.parser.decode
import org.http4s.Uri
import org.http4s.implicits.http4sLiteralsSyntax
import org.scalasteward.core.application.Config
import org.scalasteward.core.data.{Update, Version}
import org.scalasteward.core.io.FileAlg
import org.scalasteward.core.util.{ApplicativeThrowable, MonadThrowable}
Expand All @@ -30,40 +31,46 @@ trait MigrationAlg {
}

object MigrationAlg {
def create[F[_]](extraMigrations: Option[File])(implicit
def create[F[_]](config: Config.Scalafix)(implicit
fileAlg: FileAlg[F],
F: Sync[F]
logger: Logger[F],
F: MonadThrowable[F]
): F[MigrationAlg] =
loadMigrations(extraMigrations).map { migrations =>
new MigrationAlg {
override def findMigrations(update: Update): List[Migration] =
findMigrationsImpl(migrations, update)
loadMigrations(config).flatMap { migrations =>
logger.info(s"Loaded ${migrations.size} Scalafix migrations").as {
new MigrationAlg {
override def findMigrations(update: Update): List[Migration] =
findMigrationsImpl(migrations, update)
}
}
}

def loadMigrations[F[_]](
extraMigrations: Option[File]
)(implicit fileAlg: FileAlg[F], F: MonadThrowable[F]): F[List[Migration]] =
for {
default <-
fileAlg
.readResource("scalafix-migrations.conf")
.flatMap(decodeMigrations[F](_, "default"))
maybeExtra <- OptionT(extraMigrations.flatTraverse(fileAlg.readFile))
.semiflatMap(decodeMigrations[F](_, "extra"))
.value
migrations = maybeExtra match {
case Some(extra) if extra.disableDefaults => extra.migrations
case Some(extra) => default.migrations ++ extra.migrations
case None => default.migrations
}
} yield migrations
val defaultScalafixMigrationsUrl: Uri =
uri"https://raw.githubusercontent.com/scala-steward-org/scala-steward/master/modules/core/src/main/resources/scalafix-migrations.conf"

def loadMigrations[F[_]](config: Config.Scalafix)(implicit
fileAlg: FileAlg[F],
logger: Logger[F],
F: MonadThrowable[F]
): F[List[Migration]] = {
val maybeDefaultMigrationsUrl =
Option.unless(config.disableDefaults)(defaultScalafixMigrationsUrl)
(maybeDefaultMigrationsUrl.toList ++ config.migrations).flatTraverse(loadMigrationsFrom[F])
}

private def loadMigrationsFrom[F[_]](uri: Uri)(implicit
fileAlg: FileAlg[F],
logger: Logger[F],
F: MonadThrowable[F]
): F[List[Migration]] =
logger.debug(s"Loading Scalafix migrations from $uri") >>
fileAlg.readUri(uri).flatMap(decodeMigrations(_, uri)).map(_.migrations)

private def decodeMigrations[F[_]](content: String, tpe: String)(implicit
private def decodeMigrations[F[_]](content: String, uri: Uri)(implicit
F: ApplicativeThrowable[F]
): F[ScalafixMigrations] =
F.fromEither(decode[ScalafixMigrations](content))
.adaptErr(new Throwable(s"Failed to load $tpe Scalafix migrations", _))
.adaptErr(new Throwable(s"Failed to load Scalafix migrations from ${uri.renderString}", _))

private def findMigrationsImpl(
givenMigrations: List[Migration],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import io.circe.generic.extras.Configuration
import io.circe.generic.extras.semiauto._

final case class ScalafixMigrations(
disableDefaults: Boolean = false,
migrations: List[Migration] = List.empty
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class MavenAlgTest extends AnyFunSuite with Matchers {
val state =
mavenAlg.getDependencies(repo).runS(MockState.empty.copy(files = files)).unsafeRunSync()

state shouldBe MockState(
state shouldBe MockState.empty.copy(
files = files,
logs = Vector.empty,
commands = Vector(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.scalasteward.core.io

import better.files.File
import cats.effect.IO
import org.http4s.Uri
import org.scalacheck.Arbitrary
import org.scalasteward.core.TestInstances.ioLogger
import org.scalasteward.core.io.FileAlgTest.ioFileAlg
Expand Down Expand Up @@ -101,6 +102,16 @@ class FileAlgTest extends AnyFunSuite with Matchers {
} yield symlinkExists
p.unsafeRunSync() shouldBe false
}

test("readUri: local file without scheme") {
val file = File.temp / "steward" / "readUri.txt"
val content = "42"
val p = for {
_ <- ioFileAlg.writeFile(file, content)
read <- ioFileAlg.readUri(Uri.unsafeFromString(file.toString))
} yield read
p.unsafeRunSync() shouldBe content
}
}

object FileAlgTest {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import better.files.File
import cats.data.StateT
import cats.effect.IO
import fs2.Stream
import org.http4s.Uri
import org.scalasteward.core.mock.{applyPure, MockEff, MockState}

class MockFileAlg extends FileAlg[MockEff] {
Expand Down Expand Up @@ -42,6 +43,9 @@ class MockFileAlg extends FileAlg[MockEff] {
content <- StateT.liftF(FileAlgTest.ioFileAlg.readResource(resource))
} yield content

override def readUri(uri: Uri): MockEff[String] =
applyPure(s => (s.exec(List("read", uri.renderString)), s.uris.getOrElse(uri, "")))

override def walk(dir: File): Stream[MockEff, File] = {
val dirAsString = dir.pathAsString
val state: MockEff[List[File]] = StateT.inspect {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ object MockContext {
EnvVar("ANOTHER_TEST_VAR", "ALSO_GREAT")
),
processTimeout = 10.minutes,
scalafixMigrations = None,
scalafix = Config.Scalafix(Nil, disableDefaults = false),
groupMigrations = None,
cacheTtl = 1.hour,
cacheMissDelay = 0.milliseconds,
Expand All @@ -73,7 +73,7 @@ object MockContext {
implicit val vcsRepoAlg: VCSRepoAlg[MockEff] = VCSRepoAlg.create(config, gitAlg)
implicit val scalafmtAlg: ScalafmtAlg[MockEff] = ScalafmtAlg.create
implicit val migrationAlg: MigrationAlg =
MigrationAlg.create[MockEff](config.scalafixMigrations).runA(MockState.empty).unsafeRunSync()
MigrationAlg.create[MockEff](config.scalafix).runA(MockState.empty).unsafeRunSync()
implicit val cacheRepository: RepoCacheRepository[MockEff] =
new RepoCacheRepository[MockEff](new JsonKeyValueStore("repo_cache", "1"))
implicit val filterAlg: FilterAlg[MockEff] = new FilterAlg[MockEff]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,23 @@
package org.scalasteward.core.mock

import better.files.File
import org.http4s.Uri

final case class MockState(
commands: Vector[List[String]],
logs: Vector[(Option[Throwable], String)],
files: Map[File, String]
files: Map[File, String],
uris: Map[Uri, String]
) {
def add(file: File, content: String): MockState =
copy(files = files + (file -> content))

def addFiles(newFiles: Map[File, String]): MockState =
copy(files = files ++ newFiles)

def addUri(uri: Uri, content: String): MockState =
copy(uris = uris + (uri -> content))

def rm(file: File): MockState =
copy(files = files - file)

Expand All @@ -25,5 +30,5 @@ final case class MockState(

object MockState {
def empty: MockState =
MockState(commands = Vector.empty, logs = Vector.empty, files = Map.empty)
MockState(commands = Vector.empty, logs = Vector.empty, files = Map.empty, uris = Map.empty)
}
Loading