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

Add Cats RemoveInstancesImport rule #31

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,12 @@ lazy val `typelevel-scalafix-rules` = project
// typelevel/cats Scalafix rules
lazy val cats = scalafixProject("cats")
.inputSettings(
semanticdbOptions += "-P:semanticdb:synthetics:on",
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % CatsVersion
)
)
.outputSettings(
libraryDependencies ++= Seq(
"org.typelevel" %% "cats-core" % CatsVersion
)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
rule = TypelevelCatsRemoveInstanceImports
*/
package fix

import cats.Semigroup
import scala.concurrent.Future

object RemoveInstanceImportsTests1 {
{
import cats.instances.option._
import cats.instances.int._
Semigroup[Option[Int]].combine(Some(1), Some(2))
}

{
import cats.instances.all._
Semigroup[Option[Int]].combine(Some(1), Some(2))
}

{
import cats.implicits._
Semigroup[Option[Int]].combine(Some(1), Some(2))
}

{
import cats.instances.option._
import cats.instances.int._
import cats.syntax.semigroup._
Option(1) |+| Option(2)
}

{
import cats.implicits._
1.some |+| 2.some
}

{
import cats.instances.future._
import cats.instances.int._
import scala.concurrent.ExecutionContext.Implicits.global
Semigroup[Future[Int]]
}

{
import cats.instances.all._
import scala.concurrent.ExecutionContext.Implicits.global
Semigroup[Future[Int]]
}

{
import cats.implicits._
import scala.concurrent.ExecutionContext.Implicits.global
Semigroup[Future[Int]]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
rule = TypelevelCatsRemoveInstanceImports
*/
package fix

import cats.implicits._

object RemoveInstanceImportsTests2 {
val x = "hello".some
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
/*
rule = TypelevelCatsRemoveInstanceImports
*/
package fix

import cats._
import cats.implicits._

object RemoveInstanceImportsTests3 {

def doSomethingMonadic[F[_]: Monad](x: F[Int]): F[String] =
for {
a <- x
b <- Monad[F].pure("hi")
c <- Monad[F].pure("hey")
} yield a.toString + b + c

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
rule = TypelevelCatsRemoveInstanceImports
*/
package fix

import cats.implicits._
import cats.Order

sealed trait Resolver extends Product with Serializable {
import Resolver._

val path: String = {
val url = this match {
case MavenRepository(_, location, _) => location
case IvyRepository(_, pattern, _) => pattern.takeWhile(!Set('[', '(')(_))
}
url.replace(":", "")
}
}

object Resolver {
final case class Credentials(user: String, pass: String)

final case class MavenRepository(name: String, location: String, credentials: Option[Credentials])
extends Resolver
final case class IvyRepository(name: String, pattern: String, credentials: Option[Credentials])
extends Resolver

val mavenCentral: MavenRepository =
MavenRepository("public", "https://repo1.maven.org/maven2/", None)

implicit val resolverOrder: Order[Resolver] =
Order.by {
case MavenRepository(name, location, _) => (1, name, location)
case IvyRepository(name, pattern, _) => (2, name, pattern)
}
}

final case class Scope[A](value: A, resolvers: List[Resolver])

object Scope {

def combineByResolvers[A: Order](scopes: List[Scope[List[A]]]): List[Scope[List[A]]] =
scopes.groupByNel(_.resolvers).toList.map { case (resolvers, group) =>
Scope(group.reduceMap(_.value).distinct.sorted, resolvers)
}

implicit def scopeOrder[A: Order]: Order[Scope[A]] =
Order.by((scope: Scope[A]) => (scope.value, scope.resolvers))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package fix

import cats.Semigroup
import scala.concurrent.Future

object RemoveInstanceImportsTests1 {
{
Semigroup[Option[Int]].combine(Some(1), Some(2))
}

{
Semigroup[Option[Int]].combine(Some(1), Some(2))
}

{
Semigroup[Option[Int]].combine(Some(1), Some(2))
}

{
import cats.syntax.semigroup._
Option(1) |+| Option(2)
}

{
import cats.syntax.all._
1.some |+| 2.some
}

{
import scala.concurrent.ExecutionContext.Implicits.global
Semigroup[Future[Int]]
}

{
import scala.concurrent.ExecutionContext.Implicits.global
Semigroup[Future[Int]]
}

{
import scala.concurrent.ExecutionContext.Implicits.global
Semigroup[Future[Int]]
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package fix

import cats.syntax.all._

object RemoveInstanceImportsTests2 {
val x = "hello".some
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package fix

import cats._
import cats.syntax.all._

object RemoveInstanceImportsTests3 {

def doSomethingMonadic[F[_]: Monad](x: F[Int]): F[String] =
for {
a <- x
b <- Monad[F].pure("hi")
c <- Monad[F].pure("hey")
} yield a.toString + b + c

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package fix

import cats.implicits._
import cats.Order

sealed trait Resolver extends Product with Serializable {
import Resolver._

val path: String = {
val url = this match {
case MavenRepository(_, location, _) => location
case IvyRepository(_, pattern, _) => pattern.takeWhile(!Set('[', '(')(_))
}
url.replace(":", "")
}
}

object Resolver {
final case class Credentials(user: String, pass: String)

final case class MavenRepository(name: String, location: String, credentials: Option[Credentials])
extends Resolver
final case class IvyRepository(name: String, pattern: String, credentials: Option[Credentials])
extends Resolver

val mavenCentral: MavenRepository =
MavenRepository("public", "https://repo1.maven.org/maven2/", None)

implicit val resolverOrder: Order[Resolver] =
Order.by {
case MavenRepository(name, location, _) => (1, name, location)
case IvyRepository(name, pattern, _) => (2, name, pattern)
}
}

final case class Scope[A](value: A, resolvers: List[Resolver])

object Scope {

def combineByResolvers[A: Order](scopes: List[Scope[List[A]]]): List[Scope[List[A]]] =
scopes.groupByNel(_.resolvers).toList.map { case (resolvers, group) =>
Scope(group.reduceMap(_.value).distinct.sorted, resolvers)
}

implicit def scopeOrder[A: Order]: Order[Scope[A]] =
Order.by((scope: Scope[A]) => (scope.value, scope.resolvers))
}
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
org.typelevel.fix.MapSequence
org.typelevel.fix.UnusedShowInterpolator
org.typelevel.fix.As
org.typelevel.fix.CatsRemoveInstanceImports
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
/*
* Copyright 2022 Typelevel
*
* 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.typelevel.fix

import scalafix.v1._
import scala.meta._

class CatsRemoveInstanceImports extends SemanticRule("TypelevelCatsRemoveInstanceImports") {

override def fix(implicit doc: SemanticDocument): Patch = doc.tree.collect {
// e.g. "import cats.instances.int._" or "import cats.instances.all._"
case CatsInstancesImport(i) => removeImportLine(doc)(i)

// "import cats.implicits._"
case CatsImplicitsImport(i) =>
try {
// These are all the sibling trees of the import
val siblings: List[Tree] = i.parent.fold(List.empty[Tree])(_.children).filterNot(_ == i)

def usesImplicitConversion: Boolean =
siblings.exists(treeContainsFunctionApplicationSymbolMatching(catsKernelConversion))

def usesSyntax: Boolean =
siblings.exists(treeContainsFunctionApplicationSymbolMatching(catsSyntax))

if (usesImplicitConversion) {
// the import is used to enable an implicit conversion,
// so we have to keep it
Patch.empty
} else if (usesSyntax) {
// the import is used to enable an extension method,
// so replace it with "import cats.syntax.all._"
Patch.replaceTree(i, "import cats.syntax.all._")
} else {
// the import is only used to import instances,
// so it's safe to remove
removeImportLine(doc)(i)
}
} catch {
case e: scalafix.v1.MissingSymbolException =>
// see https://github.com/typelevel/cats/pull/3566#issuecomment-684007028
// and https://github.com/scalacenter/scalafix/issues/1123
doc.input match {
case Input.File(path, _) =>
println(
s"Skipping rewrite of 'import cats.implicits._' in file ${path.syntax} because we ran into a Scalafix bug. $e"
)
case _ =>
println(
s"Skipping rewrite of 'import cats.implicits._' because we ran into a Scalafix bug. $e"
)
}
e.printStackTrace()
Patch.empty
}
}.asPatch

// Recursively searches for a function symbol matching the predicate
// in all the function applications among all the children of the Tree t
private def treeContainsFunctionApplicationSymbolMatching(
f: Symbol => Boolean
)(t: Tree)(implicit doc: SemanticDocument): Boolean =
t match {
case t: Term =>
t.synthetics.exists {
case ApplyTree(fn, _) => fn.symbol.fold(false)(f)
case _ => false
} || t.children.exists(treeContainsFunctionApplicationSymbolMatching(f))
case t => t.children.exists(treeContainsFunctionApplicationSymbolMatching(f))
}

private def catsKernelConversion(symbol: Symbol): Boolean =
symbol.value.contains("cats/kernel") // && symbol.value.contains("Conversion")

private def catsSyntax(symbol: Symbol): Boolean = symbol.value
.contains("cats") && (symbol.value.contains("syntax") || symbol.value.contains("Ops"))

private def removeImportLine(doc: SemanticDocument)(i: Import): Patch =
Patch.removeTokens(i.tokens) + removeWhitespaceAndNewlineBefore(doc)(i.tokens.start)

private def removeWhitespaceAndNewlineBefore(doc: SemanticDocument)(index: Int): Patch =
Patch.removeTokens(
doc.tokens
.take(index)
.takeRightWhile(t => t.is[Token.Space] || t.is[Token.Tab] || t.is[Token.LF])
)
}

object CatsInstancesImport {
def unapply(t: Tree): Option[Import] = t match {
case i @ Import(
Importer(Term.Select(Term.Select(Name("cats"), Name("instances")), _), _) :: _
) =>
Some(i)
case _ => None
}
}

object CatsImplicitsImport {
def unapply(t: Tree): Option[Import] = t match {
case i @ Import(Importer(Term.Select(Name("cats"), Name("implicits")), _) :: _) =>
Some(i)
case _ => None
}
}