Created
February 23, 2016 13:59
-
-
Save jrudolph/432298193b071604b997 to your computer and use it in GitHub Desktop.
Skeleton for a cleanup unused imports plugin
This file contains hidden or 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
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") | |
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(pos: Position, msg: String, sev: Severity): Unit = | |
if (sev == Severity.Warn && msg.startsWith("Unused import")) { | |
println(s"Found unused import at: $pos ${pos.offset().get()} '${pos.lineContent()}'") | |
allProblems :+= Logger.problem("", pos, msg, sev) | |
} else delegate.foreach(_.log(pos, msg, sev)) | |
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, AutoImportFixerKeys.applyFixes in Test).map((a, b) => a ++ b) | |
) | |
/*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 <<= (Access.compilerReporter in compile, compile).map { (rep0, _) => | |
rep0 match { | |
case Some(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 <<= AutoImportFixerKeys.collectFixes map { fixes => | |
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 = | |
if (end == nextClosingBrace) prefix.reverse.dropWhile(_ != ',').drop(1).reverse | |
else prefix | |
val suffix = rest.drop(end + 1) | |
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 ~= (old => Some(new MyReporter(old))), | |
compileInputs in compile <<= compileInputs in compile in oldConfig map { inputs => | |
inputs.copy(config = inputs.config.copy(options = inputs.config.options ++ 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