diff --git a/input/src/main/scala-3/fix/ImplicitConversionsTest.scala b/input/src/main/scala-3/fix/ImplicitConversionsTest.scala new file mode 100644 index 0000000..9259695 --- /dev/null +++ b/input/src/main/scala-3/fix/ImplicitConversionsTest.scala @@ -0,0 +1,21 @@ +/* +rule = ImplicitConversions + */ + +package fix + +import scala.language.implicitConversions + +// format: off +object ImplicitConversionsTest { + implicit def implicitDefWithFunctionArg1(x: Int): String = x.toString + implicit def implicitDefWithFunctionArg2(x: (Int, Long)): String = x.toString + + implicit val implicitValWithFunctionType1: Long => String = _.toString + implicit val implicitValWithFunctionType2: (Int, Long) => String = (i, l ) => i.toString + + def defWithImplicitFunctionType1(x: Int)(implicit conv: Int => String): String = x + def defWithImplicitFunctionType2(x: Int)(implicit conv: (Int, Long) => String): String = x + def defWithImplicitFunctionType3(x: Int)(using conv: (Int, Long) => String): String = x +} +// format: on diff --git a/output/src/main/scala-3/fix/ImplicitConversionsTest.scala b/output/src/main/scala-3/fix/ImplicitConversionsTest.scala new file mode 100644 index 0000000..8546af7 --- /dev/null +++ b/output/src/main/scala-3/fix/ImplicitConversionsTest.scala @@ -0,0 +1,17 @@ +package fix + +import scala.language.implicitConversions + +// format: off +object ImplicitConversionsTest { + implicit val implicitDefWithFunctionArg1: Conversion[Int, String] = (x: Int) => x.toString + implicit val implicitDefWithFunctionArg2: Conversion[(Int, Long), String] = (x: (Int, Long)) => x.toString + + implicit val implicitValWithFunctionType1: Conversion[Long, String] = _.toString + implicit val implicitValWithFunctionType2: Conversion[(Int, Long), String] = (i, l ) => i.toString + + def defWithImplicitFunctionType1(x: Int)(implicit conv: Conversion[Int, String]): String = x + def defWithImplicitFunctionType2(x: Int)(implicit conv: Conversion[(Int, Long), String]): String = x + def defWithImplicitFunctionType3(x: Int)(using conv: Conversion[(Int, Long), String]): String = x +} +// format: on diff --git a/rules/src/main/resources/META-INF/services/scalafix.v1.Rule b/rules/src/main/resources/META-INF/services/scalafix.v1.Rule index caef2e8..017fec0 100644 --- a/rules/src/main/resources/META-INF/services/scalafix.v1.Rule +++ b/rules/src/main/resources/META-INF/services/scalafix.v1.Rule @@ -2,4 +2,5 @@ fix.SemiAuto fix.GivenAndUsing fix.PackageObjectExport fix.DropModThis -fix.WildcardInitializer \ No newline at end of file +fix.WildcardInitializer +fix.ImplicitConversions diff --git a/rules/src/main/scala/fix/ImplicitConversions.scala b/rules/src/main/scala/fix/ImplicitConversions.scala new file mode 100644 index 0000000..4387a16 --- /dev/null +++ b/rules/src/main/scala/fix/ImplicitConversions.scala @@ -0,0 +1,102 @@ +/* + * Copyright 2022 Arktekk + * + * 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 fix + +import fix.ImplicitConversions._ +import scalafix.v1.{Patch, SemanticDocument, SemanticRule} + +import scala.meta._ +import scala.meta.tokens.Token + +class ImplicitConversions extends SemanticRule("ImplicitConversions") { + + override def fix(implicit doc: SemanticDocument): Patch = { + + doc.tree.collect { + + case ImplicitDefWithFunctionArg(d, inType, outType) => + (for { + pathDefToVal <- d.tokens.find(_.is[Token.KwDef]).map(Patch.replaceToken(_, "val")) + eqSignToken <- d.tokens.find(_.is[Token.Equals]) + argTokens = d.tokens.dropWhile(_.isNot[Token.LeftParen]).dropRightWhile(_.isNot[Token.RightParen]) + } yield pathDefToVal + + Patch.removeTokens(argTokens) + + Patch.addRight(eqSignToken, s" ${argTokens.toString()} =>") + + Patch.replaceTree(outType, conversionCode(inType, outType))).asPatch + + case ImplicitValWithFunctionType(ts) => + Patch.replaceTree(ts, conversionCode(ts.params, ts.res)) + + case DefWithImplicitFunctionType(typeFunctions) => + typeFunctions.foldRight(Patch.empty) { case (tf, patches) => + patches + Patch.replaceTree(tf, conversionCode(tf.params, tf.res)) + } + }.asPatch + } + + private def conversionCode(inType: Type, retType: Type): String = + conversionCode(inType :: Nil, retType) + + private def conversionCode(inType: List[Type], retType: Type): String = { + val inStr = inType match { + case one :: Nil => one.toString() + case many => many.map(_.toString()).mkString("(", ", ", ")") + } + s"Conversion[$inStr, $retType]" + } +} + +object ImplicitConversions { + + object ImplicitValWithFunctionType { + def unapply(tree: Tree): Option[Type.Function] = + tree match { + case Defn.Val(mods, _, Some(ts: Type.Function), _) if mods.exists(_.is[Mod.Implicit]) => Some(ts) + case _ => None + } + } + object ImplicitDefWithFunctionArg { + def unapply(tree: Tree): Option[(Defn.Def, Type, Type)] = + tree match { + case d @ Defn.Def(mods, Term.Name(_), _, (firstParam :: Nil) :: Nil, Some(outType: Type.Name), _) + if mods.exists(_.is[Mod.Implicit]) && firstParam.mods.forall(_.isNot[Mod.Implicit]) => + firstParam.decltpe.map(inType => (d, inType, outType)) + case _ => None + } + } + + object DefWithImplicitFunctionType { + def unapply(tree: Tree): Option[List[Type.Function]] = tree match { + case Defn.Def(_, _, _, LastImplicitTypeFunctionParams(typeFunctions), _, _) => Some(typeFunctions) + case _ => None + } + + object LastImplicitTypeFunctionParams { + def unapply(params: List[List[Term.Param]]): Option[List[Type.Function]] = { + val lastParams = params.lastOption + if (lastParams.exists(_.exists(_.mods.exists(a => a.is[Mod.Implicit] || a.is[Mod.Using])))) { + val tfs = lastParams.toList + .flatMap(_.map(_.decltpe)) + .collect { case Some(tf: Type.Function) => tf } + if (tfs.isEmpty) None + else Some(tfs) + } else None + } + } + } + +}