Skip to content

Commit

Permalink
SeparateEachFile
Browse files Browse the repository at this point in the history
  • Loading branch information
xuwei-k committed Jul 23, 2024
1 parent 8f60ce2 commit 3dd6712
Show file tree
Hide file tree
Showing 14 changed files with 251 additions and 0 deletions.
7 changes: 7 additions & 0 deletions input/src/main/scala/fix/SeparateEachFileWarnTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
/*
rule = SeparateEachFileWarn
*/
package fix

trait SeparateEachFileWarnTest // assert: SeparateEachFileWarn
class SeparateEachFileWarnTest1
10 changes: 10 additions & 0 deletions input/src/main/scala/fix/SeparateEachFileWarnTest2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
rule = SeparateEachFileWarn
SeparateEachFileWarn.limit = 5
*/
package fix

trait SeparateEachFileWarnTest2_1
trait SeparateEachFileWarnTest2_2
trait SeparateEachFileWarnTest2_3
trait SeparateEachFileWarnTest2_4
4 changes: 4 additions & 0 deletions output/src/main/scala/fix/SeparateEachFileWarnTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package fix

trait SeparateEachFileWarnTest
class SeparateEachFileWarnTest1
6 changes: 6 additions & 0 deletions output/src/main/scala/fix/SeparateEachFileWarnTest2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package fix

trait SeparateEachFileWarnTest2_1
trait SeparateEachFileWarnTest2_2
trait SeparateEachFileWarnTest2_3
trait SeparateEachFileWarnTest2_4
36 changes: 36 additions & 0 deletions rules/src/main/scala/fix/SeparateEachFileConfig.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package fix

import scalafix.lint.LintSeverity
import java.util.Locale
import metaconfig.ConfDecoder
import metaconfig.generic.Surface

case class SeparateEachFileConfig(
limit: Int,
severity: LintSeverity
)

object SeparateEachFileConfig {

val default: SeparateEachFileConfig = SeparateEachFileConfig(
limit = 2,
severity = LintSeverity.Warning,
)

implicit val surface: Surface[SeparateEachFileConfig] =
metaconfig.generic.deriveSurface[SeparateEachFileConfig]

private implicit val lintSeverityDecoderInstance: ConfDecoder[LintSeverity] = { conf =>
conf.as[String].map(_.toUpperCase(Locale.ROOT)).map {
case "ERROR" =>
LintSeverity.Error
case "INFO" =>
LintSeverity.Info
case _ =>
LintSeverity.Warning
}
}

implicit val decoder: ConfDecoder[SeparateEachFileConfig] =
metaconfig.generic.deriveDecoder(default)
}
84 changes: 84 additions & 0 deletions rules/src/main/scala/fix/SeparateEachFileRewrite.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package fix

import metaconfig.Configured
import scala.meta._
import scalafix.v1._
import java.io.File
import java.nio.charset.StandardCharsets
import java.nio.file.Files
import scala.meta.inputs.Input

class SeparateEachFileRewrite(config: SeparateEachFileConfig) extends SyntacticRule("SeparateEachFileRewrite") {

def this() = this(SeparateEachFileConfig.default)

override def withConfiguration(config: Configuration): Configured[Rule] =
config.conf
.getOrElse("SeparateEachFileRewrite")(this.config)
.map(newConfig => new SeparateEachFileRewrite(newConfig))

private def maxOption[A: Ordering](xs: Seq[A]): Option[A] =
if (xs.isEmpty) None else Option(xs.max)

override def fix(implicit doc: SyntacticDocument): Patch = {
PartialFunction
.condOpt(doc.input) {
case f: Input.File =>
f.path.toFile
case f: Input.VirtualFile =>
new File(f.path)
}
.foreach { input =>
val topLevelValues = doc.tree.collect {
case t: (Stat.WithTemplate & Member & Stat.WithMods)
if isTopLevel(t) && t.templ.inits.isEmpty && !t.is[Defn.Object] =>
t
}

if (
(topLevelValues.lengthCompare(config.limit) >= 0) && topLevelValues.forall(_.mods.forall(!_.is[Mod.Sealed]))
) {
val headerLastPos = {
maxOption(
doc.tree.collect {
case i: Import if isTopLevel(i) => i.pos.end
}
).orElse(
maxOption(
doc.tree.collect { case p: Pkg =>
p.ref.pos.end
}
)
).getOrElse(0)
}
val header = doc.input.text.take(headerLastPos)

assert(input.delete())

val xs = doc.tree.collect {
case t: (Stat.WithTemplate & Member) if isTopLevel(t) =>
t
}.groupBy(_.name.value)

xs.foreach { case (k, v) =>
Files.write(
new File(
input.getParentFile,
s"${k}.scala"
).toPath,
(header + "\n\n" + v
.sortBy(_.getClass.getName)
.map { x =>
(doc.comments.leading(x).toSeq.sortBy(_.pos.start).map(_.toString) :+ x.toString).mkString("\n")
}
.mkString("\n", "\n\n", "\n")).getBytes(StandardCharsets.UTF_8)
)
}
}
}

Patch.empty
}

private def isTopLevel(t: Tree): Boolean = t.parent.forall(_.is[Pkg])
}
38 changes: 38 additions & 0 deletions rules/src/main/scala/fix/SeparateEachFileWarn.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package fix

import metaconfig.Configured
import scala.meta._
import scalafix.v1._

class SeparateEachFileWarn(config: SeparateEachFileConfig) extends SyntacticRule("SeparateEachFileWarn") {

def this() = this(SeparateEachFileConfig.default)

override def withConfiguration(config: Configuration): Configured[Rule] =
config.conf.getOrElse("SeparateEachFileWarn")(this.config).map(newConfig => new SeparateEachFileWarn(newConfig))

override def fix(implicit doc: SyntacticDocument): Patch = {
val topLevelValues = doc.tree.collect {
case t: (Stat.WithTemplate & Stat.WithMods & Member)
if t.parent.forall(_.is[Pkg]) && t.templ.inits.isEmpty && !t.is[Defn.Object] =>
t
}

if ((topLevelValues.lengthCompare(config.limit) >= 0) && topLevelValues.forall(_.mods.forall(!_.is[Mod.Sealed]))) {
Patch.lint(
Diagnostic(
id = "",
message = Seq(
s"too many top level classes. please separate file. ${topLevelValues.size} ",
topLevelValues.map(_.name.value).mkString("[", ", ", "]")
).mkString(""),
position = topLevelValues.head.pos,
severity = config.severity
)
)
} else {
Patch.empty
}
}

}
14 changes: 14 additions & 0 deletions sbt-test/SeparateEachFileRewrite/test-1/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
ThisBuild / scalafixDependencies += "com.github.xuwei-k" %% "scalafix-rules" % sys.props("scalafix-rules.version")

TaskKey[Unit]("check") := {
val names = Set("X1.scala", "X2.scala", "A.scala")
val actualNames = file("src/main/scala/foo").listFiles().map(_.getName).toSet
assert(actualNames == names, actualNames)
names.foreach {
f =>
val actual = IO.read(file(s"src/main/scala/foo/${f}"))
val expect = IO.read(file(s"expect/${f}"))
sys.process.Process(Seq("diff", s"src/main/scala/foo/${f}", s"expect/${f}")).!
assert(actual == expect, actual)
}
}
6 changes: 6 additions & 0 deletions sbt-test/SeparateEachFileRewrite/test-1/expect/A.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package foo

import scala.util.Try


object A
11 changes: 11 additions & 0 deletions sbt-test/SeparateEachFileRewrite/test-1/expect/X1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package foo

import scala.util.Try


/**
* x1
*/
trait X1 {
def y: Try[Int]
}
12 changes: 12 additions & 0 deletions sbt-test/SeparateEachFileRewrite/test-1/expect/X2.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package foo

import scala.util.Try


// x2 class
class X2

/**
* x2 object
*/
object X2
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % sys.props("scalafix.version"))
20 changes: 20 additions & 0 deletions sbt-test/SeparateEachFileRewrite/test-1/src/main/scala/foo/A.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package foo

import scala.util.Try

/**
* x2 object
*/
object X2

/**
* x1
*/
trait X1 {
def y: Try[Int]
}

// x2 class
class X2

object A
2 changes: 2 additions & 0 deletions sbt-test/SeparateEachFileRewrite/test-1/test
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
> scalafix SeparateEachFileRewrite
> check

0 comments on commit 3dd6712

Please sign in to comment.