Autofix directory structure based on package in scala

228 views Asked by At

I have a file src/main/scala/foo.scala which needs to be inside package bar. Ideally the file should be inside src/main/scala/bar/foo.scala.

// src/main/scala/foo.scala


package bar

// ... 

How can I auto-fix this issue throughout my project such that the folder structure matches the package structure?

Is there any SBT plugin etc that can help me fix this issue?

2

There are 2 answers

2
Mateusz Kubuszok On

As far as I am aware there are not such tools, though AFAIR IntelliJ can warn about package-directory mismatch.

Best I can think if is custom scalafix (https://scalacenter.github.io/scalafix/) rule - scalafix/scalameta would be used to check file's actual package, translate it to an expected directory and if they differ, move file.

I suggest scalafix/scalameta because there are corner cases like:

  • you are allowed to write your packages like:

    package a
    package b
    package c
    

    and it almost like package a.b.c except that it automatically imports everything from a and b

  • you can have package object in your file and then if you have

package a.b

package object c

this file should be in a/b/c directory

so I would prefer to check if file didn't fall under any of those using some existing tooling.

If you are certain that you don't have such cases (I wouldn't without checking) you could:

  • match the first line with regexp (^package (.*))
  • translate a.b.c into a/b/c (matched.split('.').map(_.trim).mkString(File.separator))
  • compare generated location to an actual location ( I suggest resolving absolute file locations)
  • move file if necessary

If there is a possibility of having more complex case than that, I could replace first step by querying scalafix/scalameta utilities.

1
Mario Galic On

Here is an sbt plugin providing packageStructureToDirectoryStructure task that reads package statements from source files, creates corresponding directories, and then moves files to them

import sbt._
import sbt.Keys._
import better.files._

object PackagesToDirectories extends AutoPlugin {
  object autoImport {
    val packageStructureToDirectoryStructure = taskKey[Unit]("Make directory structure match package structure")
  }

  import autoImport._

  override def trigger = allRequirements

  override lazy val projectSettings = Seq(
    packageStructureToDirectoryStructure := {
      val log = streams.value.log
      log.info(s"Refactoring directory structure to match package structure...")
      val sourceFiles = (Compile / sources).value
      val sourceBase = (Compile / scalaSource).value

      def packageStructure(lines: Traversable[String]): String = {
        val packageObjectRegex = """package object\s(.+)\s\{""".r
        val packageNestingRegex = """package\s(.+)\s\{""".r
        val packageRegex = """package\s(.+)""".r
        lines
          .collect {
            case packageObjectRegex(name) => name
            case packageNestingRegex(name) => name
            case packageRegex(name) => name
          }
          .flatMap(_.split('.'))
          .mkString("/")
      }

      sourceFiles.foreach { sourceFile =>
        val packagePath = packageStructure(sourceFile.toScala.lines)
        val destination = file"$sourceBase/$packagePath"
        destination.createDirectoryIfNotExists(createParents = true)
        val result = sourceFile.toScala.moveToDirectory(destination)
        log.info(s"$sourceFile moved to $result")
      }
    }
  )

}

WARNING: Make sure to backup the project before running it.