diff --git a/scalafix-core/shared/src/main/scala/scalafix/internal/rule/ImplicitClassPrivateVal.scala b/scalafix-core/shared/src/main/scala/scalafix/internal/rule/ImplicitClassPrivateVal.scala new file mode 100644 index 0000000000..f6312e1f97 --- /dev/null +++ b/scalafix-core/shared/src/main/scala/scalafix/internal/rule/ImplicitClassPrivateVal.scala @@ -0,0 +1,28 @@ +package scalafix.internal.rule + +import scala.meta.quasiquotes.XtensionQuasiquoteInit +import scala.meta.{Ctor, Defn, Mod, Template, Term} +import scalafix.rule.RuleCtx +import scalafix.{Patch, Rule} + +case object ImplicitClassPrivateVal + extends Rule("ImplicitClassPrivateVal") { + + override def description: String = + "Add private access modifier to val parameters of implicit value classes in order to prevent public access" + + override def fix(ctx: RuleCtx): Patch = { + ctx.tree.collect { + case Defn.Class(cMods, _, _, Ctor.Primary(_, _, (Term.Param(pMods, _, _, _) :: Nil) :: Nil), Template(_, init"AnyVal" :: Nil, _, _)) + if cMods.exists(_.is[Mod.Implicit]) => + val optPatch = for { + anchorMod <- pMods.find(!_.is[Mod.Annot]) if !containsPrivateOrProtected(pMods) + } yield ctx.addLeft(anchorMod, "private ") + optPatch.asPatch + + }.asPatch + } + + private def containsPrivateOrProtected(pMods: List[Mod]) = + pMods.exists(m => m.is[Mod.Private] || m.is[Mod.Protected]) +} diff --git a/scalafix-core/shared/src/main/scala/scalafix/rule/ScalafixRules.scala b/scalafix-core/shared/src/main/scala/scalafix/rule/ScalafixRules.scala index 3fe8166f1c..880a5575ee 100644 --- a/scalafix-core/shared/src/main/scala/scalafix/rule/ScalafixRules.scala +++ b/scalafix-core/shared/src/main/scala/scalafix/rule/ScalafixRules.scala @@ -14,7 +14,8 @@ object ScalafixRules { NoFinalize, DottyKeywords, DottyVarArgPattern, - DisableSyntax() + DisableSyntax(), + ImplicitClassPrivateVal ) def semantic(index: SemanticdbIndex): List[Rule] = List( NoInfer(index, NoInferConfig.default), diff --git a/scalafix-tests/input/src/main/scala/test/ImplicitClassPrivateVal.scala b/scalafix-tests/input/src/main/scala/test/ImplicitClassPrivateVal.scala new file mode 100644 index 0000000000..54936b5f71 --- /dev/null +++ b/scalafix-tests/input/src/main/scala/test/ImplicitClassPrivateVal.scala @@ -0,0 +1,26 @@ +/* +rules = ImplicitClassPrivateVal + */ +package test + +object ImplicitClassPrivateVal { + implicit class XtensionVal(val int: Int) extends AnyVal { + def doubled: Int = int + int + } + + implicit class XtensionFinalVal(final val int: Int) extends AnyVal { + def doubled: Int = int + int + } + + implicit class XtensionAnnotatedVal(@transient val str: String) extends AnyVal { + def doubled: String = str + str + } + + implicit class XtensionAnnotatedFinalVal(@transient final val str: String) extends AnyVal { + def doubled: String = str + str + } + + implicit class XtensionAnnotatedProtectedVal(@transient protected val int: Int) extends AnyVal { + def doubled: Int = int + int + } +} diff --git a/scalafix-tests/output/src/main/scala/test/ImplicitClassPrivateVal.scala b/scalafix-tests/output/src/main/scala/test/ImplicitClassPrivateVal.scala new file mode 100644 index 0000000000..4cb2f80451 --- /dev/null +++ b/scalafix-tests/output/src/main/scala/test/ImplicitClassPrivateVal.scala @@ -0,0 +1,23 @@ +package test + +object ImplicitClassPrivateVal { + implicit class XtensionVal(private val int: Int) extends AnyVal { + def doubled: Int = int + int + } + + implicit class XtensionFinalVal(private final val int: Int) extends AnyVal { + def doubled: Int = int + int + } + + implicit class XtensionAnnotatedVal(@transient private val str: String) extends AnyVal { + def doubled: String = str + str + } + + implicit class XtensionAnnotatedFinalVal(@transient private final val str: String) extends AnyVal { + def doubled: String = str + str + } + + implicit class XtensionAnnotatedProtectedVal(@transient protected val int: Int) extends AnyVal { + def doubled: Int = int + int + } +} diff --git a/website/src/main/resources/microsite/data/rules.yml b/website/src/main/resources/microsite/data/rules.yml index d3eea6c3f9..637fde5fdc 100644 --- a/website/src/main/resources/microsite/data/rules.yml +++ b/website/src/main/resources/microsite/data/rules.yml @@ -13,3 +13,4 @@ rules: - Disable - DisableSyntax - DisableUnless +- ImplicitClassPrivateVal diff --git a/website/src/main/tut/docs/rules/ImplicitClassPrivateVal.md b/website/src/main/tut/docs/rules/ImplicitClassPrivateVal.md new file mode 100644 index 0000000000..30cbd60afd --- /dev/null +++ b/website/src/main/tut/docs/rules/ImplicitClassPrivateVal.md @@ -0,0 +1,25 @@ +--- +layout: docs +title: ImplicitClassPrivateVal +--- + +# ImplicitClassPrivateVal + +`val` fields of implicit classes are accessible as an extension. +This rule adds the `private` access modifier in such cases in order to prevent direct access. + +```scala +// before +implicit class XtensionVal(val str: String) extends AnyVal { + def doubled: String = str + str +} +"message".str // compiles + +// after +implicit class XtensionValFixed(private val str: String) extends AnyVal { + def doubled: String = str + str +} +"message".str // does not compile +``` + +This rule safely ignores all other modifiers.