Last active
January 17, 2018 13:07
-
-
Save jrudolph/0c347fa00a9ebe1f10aca85aac2c4022 to your computer and use it in GitHub Desktop.
Cleanup Imports for sbt 1.0
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
* Copyright (C) 2009-2017 Lightbend Inc. <http://www.lightbend.com> | |
*/ | |
import sbt._ | |
import Keys._ | |
package sbt { | |
object Access { | |
def compilerReporter = sbt.Keys.compilerReporter | |
} | |
} | |
package autoimport { | |
import java.io.FileWriter | |
import autoimport.AutoImportFixerKeys.DeleteLine | |
import sbt.plugins.JvmPlugin | |
import xsbti.{Severity, Position, Problem} | |
object AutoImportFixerKeys { | |
sealed trait Fix | |
case class DeleteLine(file: File, lineNo: Int, column: Int, lineContent: String) extends Fix | |
val collectFixes = taskKey[Seq[DeleteLine]]("Collects fixes that could be applied automatically") | |
val applyFixes = taskKey[Seq[DeleteLine]]("Apply fixes to files") | |
} | |
object AutoImportFixer extends AutoPlugin { | |
val AutoImports = config("auto-imports") | |
val AutoImportsTest = config("auto-imports-test") | |
val EndsWithOpenBraceAndOneElement = """(.*)\{\s*([^\s]+)\s*""".r | |
val ClosingBraceAndWhiteSpace = """\s*\}\s*""".r | |
class MyReporter(delegate: Option[xsbti.Reporter]) extends xsbti.Reporter { | |
var allProblems = Vector.empty[Problem] | |
def hasWarnings: Boolean = delegate.map(_.hasWarnings).getOrElse(false) | |
def comment(pos: Position, msg: String): Unit = delegate.foreach(_.comment(pos, msg)) | |
def log(p: Problem): Unit = | |
if (p.severity == Severity.Warn && p.message.startsWith("Unused import")) { | |
val pos = p.position | |
println(s"Found unused import at: $pos ${pos.offset().get()} '${pos.lineContent()}'") | |
allProblems :+= p | |
} else delegate.foreach(_.log(p)) | |
def problems(): Array[Problem] = delegate.map(_.problems).getOrElse(Array.empty[Problem]) | |
def hasErrors: Boolean = delegate.map(_.hasErrors).getOrElse(false) | |
def printSummary(): Unit = delegate.foreach(_.printSummary()) | |
def reset(): Unit = { | |
allProblems = Vector.empty | |
delegate.foreach(_.reset()) | |
} | |
} | |
override def trigger: PluginTrigger = allRequirements | |
override def requires: Plugins = JvmPlugin | |
override def projectSettings: Seq[Def.Setting[_]] = | |
inConfig(Compile)(settings(Compile)) ++ inConfig(Test)(settings(Test)) ++ Seq( | |
AutoImportFixerKeys.applyFixes := { | |
(AutoImportFixerKeys.applyFixes in Compile).value ++ (AutoImportFixerKeys.applyFixes in Test).value | |
} | |
) | |
/*forConfig(Compile, Compile, Defaults.compileSettings) ++ | |
forConfig(Test, Test, Defaults.testSettings)*/ | |
def forConfig(config: Configuration, oldConfig: Configuration, defaultSettings: Seq[Setting[_]]) = | |
inConfig(config)(defaultSettings ++ settings(oldConfig)) | |
def settings(oldConfig: Configuration): Seq[Setting[_]] = Seq( | |
AutoImportFixerKeys.collectFixes := { | |
val rep0 = (Access.compilerReporter in compile).value | |
val _ = compile.value | |
rep0 match { | |
case rep: MyReporter => | |
println(s"Got ${rep.allProblems.size} problems") | |
rep.allProblems | |
.filterNot { p => | |
val line = p.position.lineContent | |
line.contains("language.existentials") // it seems to be a bug in 2.11 that those are needed more often than in 2.12, keep them for now | |
// || | |
//line.contains("import language") || | |
//line.endsWith("collection.immutable") | |
} | |
.map { p => | |
DeleteLine(p.position.sourceFile.get, p.position.line.get, p.position.pointer.get, p.position.lineContent) | |
} | |
case _ => Nil | |
} | |
}, | |
AutoImportFixerKeys.applyFixes := { | |
val fixes = AutoImportFixerKeys.collectFixes.value | |
fixes.groupBy(_.file.getCanonicalPath).foreach { | |
case (file, fixes) => | |
val newFile = java.io.File.createTempFile("out", ".scala") | |
val out = new FileWriter(newFile) | |
val lines = fixes.groupBy(_.lineNo) | |
scala.io.Source.fromFile(file, "utf8").getLines().zipWithIndex.foreach { | |
case (line, idx) => | |
def deleteAt(column: Int): Unit = { | |
val prefix = line.take(column - 1) | |
val rest = line.drop(column - 1) | |
def find(char: Char): Int = { | |
val res = rest.indexOf(char) | |
if (res == -1) Int.MaxValue | |
else res | |
} | |
val nextComma = find(',') | |
val nextClosingBrace = find('}') - 1 | |
require(nextComma < Int.MaxValue || nextClosingBrace < Int.MaxValue) | |
val end = math.min(nextComma, nextClosingBrace) | |
val realPrefix: String = | |
if (end == nextClosingBrace) prefix.reverse.dropWhile(_ != ',').drop(1).reverse | |
else prefix | |
val suffix: String = { | |
val rem = rest.drop(end + 1) | |
if (rem.startsWith(" ")) rem | |
else " "+rem | |
} | |
// don't leave braces with single element | |
(realPrefix, suffix) match { | |
case (EndsWithOpenBraceAndOneElement(textBeforeBrace, oneElement), ClosingBraceAndWhiteSpace()) => | |
// matches prefix: "import xyz.abc.{ Blib" suffix: " }" and changes it to "import xyz.abc.Blib" | |
out.write(textBeforeBrace) | |
out.write(oneElement) | |
case _ => | |
out.write(realPrefix) | |
out.write(suffix) | |
} | |
out.write('\n') | |
} | |
lines.get(idx + 1) match { | |
case None => | |
out.write(line) | |
out.write('\n') | |
case Some(Seq(single)) => | |
if (single.lineContent contains ",") deleteAt(single.column) | |
else () // drop line completely | |
case Some(several) => | |
val occs = line.count(_ == ',') | |
if (occs == several.size - 1) () // delete all | |
else { | |
// delete just the first and depend on recompilation to find others | |
deleteAt(several.head.column) | |
} | |
} | |
} | |
out.close() | |
newFile.renameTo(new File(file)) | |
} | |
fixes | |
}, | |
// this has the bad side-effect that MyReporter is now responsible for all error reporting | |
// currently, you will have to enable / disable this line manually to get either compilation | |
// with normal error reporting or import cleanup functionality | |
Access.compilerReporter in compile := new MyReporter(Some((Access.compilerReporter in compile).value)), | |
compileInputs in compile := { | |
val inputs = (compileInputs in compile in oldConfig).value | |
inputs.withOptions(inputs.options.withScalacOptions(inputs.options.scalacOptions ++ Seq("-Ywarn-unused-import", "-Ywarn-dead-code", "-Ywarn-unused"))) | |
}) | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment