Merge pull request #5153 from eed3si9n/wip/lint

build linting to warn on unused settings during reload
This commit is contained in:
eugene yokota 2019-10-30 11:36:43 -04:00 committed by GitHub
commit e17c64dfb6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 188 additions and 6 deletions

View File

@ -351,7 +351,7 @@ object Defaults extends BuildCommon {
sys.env.contains("CI") || SysProp.ci,
// watch related settings
pollInterval :== Watch.defaultPollInterval,
)
) ++ LintUnused.lintSettings
)
def defaultTestTasks(key: Scoped): Seq[Setting[_]] =

View File

@ -492,6 +492,9 @@ object Keys {
private[sbt] val postProgressReports = settingKey[Unit]("Internally used to modify logger.").withRank(DTask)
@deprecated("No longer used", "1.3.0")
private[sbt] val executeProgress = settingKey[State => TaskProgress]("Experimental task execution listener.").withRank(DTask)
val lintUnused = inputKey[Unit]("Check for keys unused by other settings and tasks.")
val excludeLintKeys = settingKey[Set[Def.KeyedInitialize[_]]]("Keys excluded from lintUnused task")
val includeLintKeys = settingKey[Set[Def.KeyedInitialize[_]]]("Task keys that are included into lintUnused task")
val stateStreams = AttributeKey[Streams]("stateStreams", "Streams manager, which provides streams for different contexts. Setting this on State will override the default Streams implementation.")
val resolvedScoped = Def.resolvedScoped

View File

@ -833,10 +833,10 @@ object BuiltinCommands {
checkSBTVersionChanged(s0)
val (s1, base) = Project.loadAction(SessionVar.clear(s0), action)
IO.createDirectory(base)
val s = if (s1 has Keys.stateCompilerCache) s1 else registerCompilerCache(s1)
val s2 = if (s1 has Keys.stateCompilerCache) s1 else registerCompilerCache(s1)
val (eval, structure) =
try Load.defaultLoad(s, base, s.log, Project.inPluginProject(s), Project.extraBuilds(s))
try Load.defaultLoad(s2, base, s2.log, Project.inPluginProject(s2), Project.extraBuilds(s2))
catch {
case ex: compiler.EvalException =>
s0.log.debug(ex.getMessage)
@ -846,12 +846,13 @@ object BuiltinCommands {
}
val session = Load.initialSession(structure, eval, s0)
SessionSettings.checkSession(session, s)
addCacheStoreFactoryFactory(
SessionSettings.checkSession(session, s2)
val s3 = addCacheStoreFactoryFactory(
Project
.setProject(session, structure, s)
.setProject(session, structure, s2)
.put(sbt.nio.Keys.hasCheckedMetaBuild, new AtomicBoolean(false))
)
LintUnused.lintUnusedFunc(s3)
}
private val addCacheStoreFactoryFactory: State => State = (s: State) => {

View File

@ -0,0 +1,160 @@
/*
* sbt
* Copyright 2011 - 2018, Lightbend, Inc.
* Copyright 2008 - 2010, Mark Harrah
* Licensed under Apache License 2.0 (see LICENSE)
*/
package sbt
package internal
import Keys._
import Def.{ Setting, ScopedKey }
import sbt.internal.util.{ FilePosition, NoPosition, SourcePosition }
import java.io.File
import Scope.Global
import sbt.Def._
object LintUnused {
lazy val lintSettings: Seq[Setting[_]] = Seq(
excludeLintKeys := Set(
aggregate,
concurrentRestrictions,
commands,
crossScalaVersions,
onLoadMessage,
sbt.nio.Keys.watchTriggers,
),
includeLintKeys := Set(
scalacOptions,
javacOptions,
javaOptions,
incOptions,
compileOptions,
packageOptions,
mainClass,
mappings,
testOptions,
classpathConfiguration,
ivyConfiguration,
),
Keys.lintUnused := lintUnusedTask.evaluated,
)
// input task version of the lintUnused
def lintUnusedTask: Def.Initialize[InputTask[Unit]] = Def.inputTask {
val _ = Def.spaceDelimited().parsed // not used yet
val state = Keys.state.value
val log = streams.value.log
val includeKeys = (includeLintKeys in Global).value map { _.scopedKey.key.label }
val excludeKeys = (excludeLintKeys in Global).value map { _.scopedKey.key.label }
val result = lintUnused(state, includeKeys, excludeKeys)
if (result.isEmpty) log.success("ok")
else lintResultLines(result) foreach { log.warn(_) }
}
// function version of the lintUnused, based on just state
def lintUnusedFunc(s: State): State = {
val log = s.log
val extracted = Project.extract(s)
val includeKeys = extracted.get(includeLintKeys in Global) map { _.scopedKey.key.label }
val excludeKeys = extracted.get(excludeLintKeys in Global) map { _.scopedKey.key.label }
val result = lintUnused(s, includeKeys, excludeKeys)
lintResultLines(result) foreach { log.warn(_) }
s
}
def lintResultLines(
result: Seq[(ScopedKey[_], String, Vector[SourcePosition])]
): Vector[String] = {
import scala.collection.mutable.ListBuffer
val buffer = ListBuffer.empty[String]
val size = result.size
if (size == 1) buffer.append("there's a key that's not used by any other settings/tasks:")
else buffer.append(s"there are $size keys that are not used by any other settings/tasks:")
buffer.append(" ")
result foreach {
case (_, str, positions) =>
buffer.append(s"* $str")
positions foreach {
case pos: FilePosition => buffer.append(s" +- ${pos.path}:${pos.startLine}")
case _ => ()
}
}
buffer.append(" ")
buffer.append(
"note: a setting might still be used by a command; to exclude a key from this `lintUnused` check"
)
buffer.append(
"either append it to `Global / excludeLintKeys` or call .withRank(KeyRanks.Invisible) on the key"
)
buffer.toVector
}
def lintUnused(
state: State,
includeKeys: Set[String],
excludeKeys: Set[String]
): Seq[(ScopedKey[_], String, Vector[SourcePosition])] = {
val extracted = Project.extract(state)
val structure = extracted.structure
val display = Def.showShortKey(None) // extracted.showKey
val actual = true
val comp =
Def.compiled(structure.settings, actual)(structure.delegates, structure.scopeLocal, display)
val cMap = Def.flattenLocals(comp)
val used: Set[ScopedKey[_]] = cMap.values.flatMap(_.dependencies).toSet
val unused: Seq[ScopedKey[_]] = cMap.keys.filter(!used.contains(_)).toSeq
val withDefinedAts: Seq[UnusedKey] = unused map { u =>
val definingScope = structure.data.definingScope(u.scope, u.key)
val definingScoped = definingScope match {
case Some(sc) => ScopedKey(sc, u.key)
case _ => u
}
val definedAt = comp.get(definingScoped) match {
case Some(c) => definedAtString(c.settings.toVector)
case _ => Vector.empty
}
val data = Project.scopedKeyData(structure, u.scope, u.key)
UnusedKey(u, definedAt, data)
}
def isIncludeKey(u: UnusedKey): Boolean = includeKeys(u.scoped.key.label)
def isExcludeKey(u: UnusedKey): Boolean = excludeKeys(u.scoped.key.label)
def isSettingKey(u: UnusedKey): Boolean = u.data match {
case Some(data) => data.settingValue.isDefined
case _ => false
}
def isLocallyDefined(u: UnusedKey): Boolean = u.positions exists {
case pos: FilePosition => pos.path.contains(File.separator)
case _ => false
}
def isInvisible(u: UnusedKey): Boolean = u.scoped.key.rank == KeyRanks.Invisible
val unusedKeys = withDefinedAts collect {
case u
if !isExcludeKey(u) && !isInvisible(u)
&& (isSettingKey(u) || isIncludeKey(u))
&& isLocallyDefined(u) =>
u
}
(unusedKeys map { u =>
(u.scoped, display.show(u.scoped), u.positions)
}).sortBy(_._2)
}
private[this] case class UnusedKey(
scoped: ScopedKey[_],
positions: Vector[SourcePosition],
data: Option[ScopedKeyData[_]]
)
private def definedAtString(settings: Vector[Setting[_]]): Vector[SourcePosition] = {
settings flatMap { setting =>
setting.pos match {
case NoPosition => Vector.empty
case pos => Vector(pos)
}
}
}
}

View File

@ -0,0 +1,17 @@
ThisBuild / doc / scalacOptions += "-Xsomething"
lazy val lintBuildTest = taskKey[Unit]("")
lazy val root = (project in file("."))
.settings(
lintBuildTest := {
val state = Keys.state.value
val includeKeys = (includeLintKeys in Global).value map { _.scopedKey.key.label }
val excludeKeys = (excludeLintKeys in Global).value map { _.scopedKey.key.label }
val result = sbt.internal.LintUnused.lintUnused(state, includeKeys, excludeKeys)
assert(result.size == 1)
assert(result(0)._2 == "ThisBuild / doc / scalacOptions", result(0)._2)
}
)
lazy val app = project

View File

@ -0,0 +1 @@
> lintBuildTest