Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Hacky build-lint and new-help commands
package com.typesafe.sbthelp
import _root_.sbt._
import Project.Initialize
import Keys._
import Defaults._
import Scope.GlobalScope
import Load.BuildStructure
import scala.annotation.tailrec
import scala.collection.generic.CanBuildFrom
import scala.xml
/** This plugin is a temporary holding place to develop a
* lint command and improved help/inspect commands.
*/
object HelpAndLintPlugin extends Plugin {
import Markup.{ List => _, _ }
private lazy val defaultKeysThatCanBeUnused: Seq[ScopedKey[_]] =
// The way this works is that these act as requested
// dependencies (anything that would be a delegate of
// these is counted as used, unless it's overridden by a
// definition in which case it is not)
Seq(silenceUnusedWarning,
// 'commands' is special-cased because we set
// *:commands below as part of the plugin and it's
// then unused. it seems like setting commands in
// Global below would be right, but it doesn't work.
// anyway on integration into sbt this would go away.
commands,
// TODO I don't understand why these need to be
// special-cased; I can't find where sbt even
// sets them.
configuration in Optional,
configuration in Provided,
// the global scope version of this is in the
// settings list, but the cross build command
// also uses the project scope version
crossScalaVersions
).map(_.scopedKey) ++
(Project.defaultSettings ++ Defaults.buildCore).map(_.key)
override lazy val settings = Seq(commands ++= Seq(lintCommand,
helpCommand,
helpBrowserCommand),
silenceUnusedWarning := defaultKeysThatCanBeUnused)
// debug examples
val someKey = TaskKey[String]("some-key")
val otherKey = TaskKey[String]("other-key")
val thirdKey = TaskKey[String]("third-key")
val fourthKey = TaskKey[String]("fourth-key")
// cut-and-paste from Settings.scala to add selfDeps,
// should move into sbt proper
def compiled(init: Seq[Setting[_]], actual: Boolean = true, selfDeps: Boolean = false)(implicit delegates: Scope => Seq[Scope], scopeLocal: Project.ScopeLocal, display: Show[ScopedKey[_]]): Project.CompiledMap =
{
// prepend per-scope settings
val withLocal = Project.addLocal(init)(scopeLocal)
// group by Scope/Key, dropping dead initializations
val sMap: Project.ScopedMap = Project.grouped(withLocal)
// delegate references to undefined values according to 'delegates'
val dMap: Project.ScopedMap = if(actual) Project.delegate(sMap)(delegates, display) else sMap
// merge Seq[Setting[_]] into Compiled
compile(dMap, selfDeps)
}
// cut-and-paste from Settings.scala to add selfDeps,
// should move into sbt proper
def compile(sMap: Project.ScopedMap, selfDeps: Boolean = false): Project.CompiledMap =
sMap.toTypedSeq.map { case sMap.TPair(k, ss) =>
val deps = ss flatMap { ss => if (selfDeps) ss.init.dependencies else ss.dependencies } toSet;
(k, new Project.Compiled(k, deps, ss))
} toMap;
/** Analysis of a project used for presenting help and lint information.
* All lazy-computed since it may not all be needed in a given run
* of the command.
*/
class ProjectAnalysis(state: State) {
lazy val extracted: Extracted = Project.extract(state)
lazy val structure: BuildStructure = extracted.structure
lazy implicit val display =
Project.showContextKey(extracted.session, extracted.structure)
def markup(sk: ScopedKey[_]): Span = {
val withScope = display(sk)
ScopedKeyLink(withScope, sk.key.label, withScope, sk)
}
private def hyphenToCamel(s: String): String = {
val sb = new java.lang.StringBuilder()
s.foldLeft(false)({
(justSawHyphen, c) =>
if (c == '-') {
true
} else if (justSawHyphen) {
sb.appendCodePoint(Character.toUpperCase(c))
false
} else {
sb.appendCodePoint(c)
false
}
})
sb.toString
}
// returns a Scala version of the key if we can come up with
// one that seems likely to work.
private def displayScala(key: ScopedKey[_]): Option[String] = {
val current = extracted.session.current
val scopesOption = try {
val projectScope =
key.scope.project match {
case Global =>
Seq("GlobalScope")
case Select(BuildRef(current.build)) =>
Seq("ThisBuild")
case Select(current) =>
Nil
case This =>
Nil
}
val configScope =
key.scope.config match {
case Select(c) if Configurations.default.exists(_.name == c.name) =>
Seq(c.name.substring(0, 1).toUpperCase + c.name.substring(1))
case Global =>
Nil
case This =>
Nil
}
val taskScope =
key.scope.task match {
case Select(t) =>
Seq(hyphenToCamel(t.label))
case Global =>
Nil
case This =>
Nil
}
Some(projectScope ++ configScope ++ taskScope)
} catch {
case ex: MatchError =>
None
}
scopesOption map {
scopes =>
(hyphenToCamel(key.key.label) +: scopes).mkString(" in ")
}
}
def markupAkaInScala(key: ScopedKey[_]): Option[Span] = {
displayScala(key).map(s => Code(Seq(Text(s))))
}
// Flattened(key:ScopedKey,dependencies:Iterable[ScopedKey])
lazy val actualCMap: Map[ScopedKey[_],Project.Flattened] =
Project.flattenLocals(Project.compiled(structure.settings, actual=true)(structure.delegates, structure.scopeLocal, display))
lazy val requestedCMap: Map[ScopedKey[_],Project.Flattened] =
Project.flattenLocals(compiled(structure.settings, actual=false, selfDeps=true)(structure.delegates, structure.scopeLocal, display))
// these are both just lists of all known keys I guess
private lazy val actualKeys = actualCMap.keys
private lazy val requestedKeys = requestedCMap.keys
case class TriggerKey(cause: ScopedKey[_],
effects: Seq[ScopedKey[_]])
lazy val triggers: Seq[TriggerKey] = {
def key(task: Task[_]): ScopedKey[_] =
structure.index.taskToKey.get(task).get
val result =
for ((cause, effects) <- structure.index.triggers.injectFor)
yield TriggerKey(key(cause), effects map(key(_)))
result.toSeq
}
// map causes to effects
private lazy val causeToEffectsMap: Map[ScopedKey[_], Seq[ScopedKey[_]]] =
triggers.foldLeft(Map.empty[ScopedKey[_], Seq[ScopedKey[_]]])({
(sofar, next) => sofar + (next.cause -> next.effects)
})
def triggerEffects(cause: ScopedKey[_]): Seq[ScopedKey[_]] = {
causeToEffectsMap.get(cause).getOrElse(Nil)
}
private lazy val effectToCausesMap: Map[ScopedKey[_], Seq[ScopedKey[_]]] =
triggers.foldLeft(Map.empty[ScopedKey[_], Seq[ScopedKey[_]]])({
(sofar, next) =>
next.effects.foldLeft(sofar)({
(sofar, nextEffect) =>
val olderCauses =
sofar.get(nextEffect).getOrElse(Nil)
sofar + (nextEffect -> (olderCauses :+ next.cause))
})
})
def triggerCauses(effect: ScopedKey[_]): Seq[ScopedKey[_]] = {
effectToCausesMap.get(effect).getOrElse(Nil)
}
lazy val allTriggerCauses: Set[ScopedKey[_]] = {
causeToEffectsMap.keys.toSet
}
lazy val allTriggerEffects: Set[ScopedKey[_]] = {
effectToCausesMap.keys.toSet
}
def isTriggerCause(key: ScopedKey[_]) =
allTriggerCauses.contains(key)
lazy val actualUsed: Set[ScopedKey[_]] =
actualCMap.values.flatMap(_.dependencies).toSet ++
allTriggerEffects ++ allTriggerCauses
lazy val actualUnused: Set[ScopedKey[_]] =
actualKeys.filter(!actualUsed.contains(_)).toSet
lazy val requestedUsed: Set[ScopedKey[_]] =
requestedCMap.values.flatMap(_.dependencies).toSet ++
allTriggerEffects ++ allTriggerCauses
lazy val requestedUnused: Set[ScopedKey[_]] =
requestedKeys.filter(!requestedUsed.contains(_)).toSet
def keyIsDefined(key: ScopedKey[_]) =
actualCMap.contains(key)
def actualScopedKeys(key: AttributeKey[_]): Set[ScopedKey[_]] =
actualKeys.filter(_.key == key).toSet
def requestedScopedKeys(key: AttributeKey[_]): Set[ScopedKey[_]] =
requestedKeys.filter(_.key == key).toSet
def scopedKeys(key: AttributeKey[_]): Set[ScopedKey[_]] =
actualScopedKeys(key) ++ requestedScopedKeys(key)
def requesters(key: ScopedKey[_]): Set[ScopedKey[_]] =
requestedCMap.values.filter(_.dependencies.exists(_ == key)).map(_.key).toSet
def users(key: ScopedKey[_]): Set[ScopedKey[_]] =
actualCMap.values.filter(_.dependencies.exists(_ == key)).map(_.key).toSet ++
triggerCauses(key) ++ triggerEffects(key)
def actualDependencies(key: ScopedKey[_]): Iterable[ScopedKey[_]] =
actualCMap.get(key).toSeq.flatMap(_.dependencies)
def requestedDependencies(key: ScopedKey[_]): Iterable[ScopedKey[_]] =
requestedCMap.get(key).toSeq.flatMap(_.dependencies)
def delegates(key: ScopedKey[_]): Iterable[ScopedKey[_]] =
Project.delegates(structure, key.scope, key.key)
def guessIntended(key: ScopedKey[_]): Option[ScopedKey[_]] = {
Project.guessIntendedScope((actualUsed ++ requestedUsed).toSeq,
structure.delegates,
key)
}
}
private def analyze(state: State): ProjectAnalysis = {
return new ProjectAnalysis(state)
}
/** Convert a collection of A into a collection of Seq[A], where
* each Seq[A] is a list of adjacent elements that a predicate
* says to group together.
*/
private def condense[A, Coll[A] <: Traversable[A]](c: Coll[A], groupTogether: (A, A) => Boolean)(implicit bf: CanBuildFrom[Coll[A], Seq[A], Coll[Seq[A]]]): Coll[Seq[A]] = {
val b = bf(c)
// acc is built up backward then reversed
@tailrec
def makeGroups(acc: List[A], remaining: Traversable[A]): Unit = {
if (remaining.isEmpty) {
b += acc.reverse
} else if (acc.isEmpty ||
groupTogether(acc.head, remaining.head)) {
makeGroups(remaining.head :: acc, remaining.tail)
} else {
b += acc.reverse
makeGroups(Nil, remaining)
}
}
makeGroups(Nil, c)
b.result
}
private def spansChainToBlock(chain: Seq[Seq[Span]]): Block = {
BlockList(chain.zipWithIndex.map({
spansAndIndex =>
Indented(IndentedAfterFirst(Paragraph(spansAndIndex._1, verticalSpaceAfter=false)),
count = spansAndIndex._2)
}))
}
private def markupDepChain(a: ProjectAnalysis, user: ScopedKey[_], requested: ScopedKey[_], defined: ScopedKey[_]): Block = {
val initialLinks: Seq[Seq[Span]] =
Seq(Seq(a.markup(user)),
Seq(Text("depends on "), a.markup(requested)))
// it should be true that either "defined" is in this
// list of delegates, or "defined == requested"
val delegates = a.delegates(requested)
val condensed = condense(delegates, {
(first: ScopedKey[_], second: ScopedKey[_]) =>
if (first == defined || second == defined)
// "defined" is always by itself
false
else if (first == requested || second == requested)
// "requested" is always by itself
false
else
// group undefined and defined keys
a.keyIsDefined(first) == a.keyIsDefined(second)
})
def markupDelegates(delegates: Seq[ScopedKey[_]]): Seq[Span] = {
val markups = delegates.map(a.markup(_))
if (markups.size > 1) {
// this has unfortunate algorithmic complexity,
// fix it if the lists get long ...
markups.tail.foldLeft(Seq(markups.head))({
(sofar, next) =>
sofar ++ Seq(Text(", "), next)
})
} else {
markups
}
}
val chain: Seq[Seq[Span]] = initialLinks ++ condensed.map({
delegates =>
val delegate = delegates.head
if (delegate == defined) {
Seq(Bold(Text("delegates to "), a.markup(delegate)))
} else if (a.keyIsDefined(delegate)) {
if (delegate == requested)
Seq(Text("skips circular dependency "), a.markup(delegate))
else
Text("overrides ") +: markupDelegates(delegates)
} else {
Text("skips undefined ") +: markupDelegates(delegates)
}
})
spansChainToBlock(chain)
}
private def markupTriggeredBy(a: ProjectAnalysis, cause: ScopedKey[_], effect: ScopedKey[_]): Block = {
spansChainToBlock(Seq(Seq(a.markup(effect)), Seq(Text("triggered by "), a.markup(cause))))
}
private def markupTriggers(a: ProjectAnalysis, cause: ScopedKey[_], effect: ScopedKey[_]): Block = {
spansChainToBlock(Seq(Seq(a.markup(cause)), Seq(Text("triggers "), a.markup(effect))))
}
private def markupSummary(a: ProjectAnalysis, key: ScopedKey[_]): Block = {
Paragraph(Seq(Text(key.key.label)) ++ key.key.description.map(desc => Seq(Text(" " + desc))).getOrElse(Seq.empty))
}
private def markupUsers(a: ProjectAnalysis, defined: ScopedKey[_]): Block = {
val users = a.users(defined).toSeq.sortBy(a.display(_))
val blocksBuilder = Seq.newBuilder[Block]
if (users.nonEmpty) {
for (u <- users) {
// we only want to display one way that u makes use
// of the key; the main time we have two ways is
// that if u is triggered by us, we're also a
// dependency of u
val howUsed = {
for (cause <- a.triggerCauses(u)
if cause == defined)
yield markupTriggeredBy(a, cause, u)
} ++ {
for (effect <- a.triggerEffects(u)
if effect == defined)
yield markupTriggers(a, u, effect)
} ++ {
for (requestedDep <- a.requestedDependencies(u).find(_.key == defined.key))
yield markupDepChain(a, u, requestedDep, defined)
} head
blocksBuilder += howUsed
}
} else {
blocksBuilder += spansChainToBlock(Seq(Seq(a.markup(defined)),
Seq(Text("is defined, but isn't used anywhere"))))
}
BlockList(blocksBuilder.result)
}
private def markupHelp(a: ProjectAnalysis, key: ScopedKey[_]): Block = {
val blockBuilder = Seq.newBuilder[Block]
blockBuilder += markupSummary(a, key)
val actualScopes = a.actualScopedKeys(key.key).toSeq.sortBy(a.display(_))
blockBuilder += Paragraph(ScopedKeyLink(key.key.label,
key.key.label,
key.key.label,
key),
Text(" is defined in these scopes:"))
def spansToTable(pairs: Iterable[Seq[Span]]): Table = {
val rows = pairs.zipWithIndex.toSeq.map({
rowWithIndex =>
rowWithIndex._1.map(col =>
IndentedAfterFirst(Paragraph(Seq(col),
verticalSpaceAfter=(rowWithIndex._2 == 0))))
})
Table(rows)
}
def keyTableFromSpans(pairs: Iterable[Seq[Span]]): Table = {
spansToTable(Seq(Seq(Bold(Text("Key")),
Bold(Text("Project.settings syntax")))) ++ pairs)
}
val definedTable =
for (k <- actualScopes)
yield Seq(a.markup(k),
a.markupAkaInScala(k).getOrElse(Text("")))
blockBuilder += Indented(keyTableFromSpans(definedTable))
blockBuilder += Indented(Paragraph(Text("(to override these, a setting must use the same or a more specific scope)")))
val requested = a.requestedUsed filter {
k => k.key == key.key
}
if (requested.nonEmpty) {
blockBuilder += Paragraph(Text("Other settings depend on '" + key.key.label + "' in these scopes:"))
val requestedTable =
for (i <- requested.toSeq.sortBy(a.display(_)))
yield Seq(a.markup(i),
a.markupAkaInScala(i).getOrElse(Text("")))
blockBuilder += Indented(keyTableFromSpans(requestedTable))
blockBuilder += Indented(Paragraph(Text("(to be used, a setting's scope must be identical to or less-specific than these)")))
}
blockBuilder += Paragraph(ScopedKeyLink(key.key.label,
key.key.label,
key.key.label,
key),
Text(" is used in these ways:"))
for (defined <- actualScopes) {
blockBuilder += Indented(markupUsers(a, defined))
}
BlockList(blockBuilder.result)
}
private def help(state: State, log: Logger, key: ScopedKey[_]): Unit = {
val a = analyze(state)
val doc = markupHelp(a, key)
log.info(renderText(doc,
// TODO get actual terminal properties
RenderTextOptions(wrapWidth = 100,
ansiCodes=true)))
}
// the Command here doesn't really matter, we'll move the guts
// of it over to the actual help command
lazy val helpCommand =
Command("new-help", ("help <key>", "Help!"), "")(BuiltinCommands.optSpacedKeyParser) {
(state: State, maybeKey: Option[ScopedKey[_]]) =>
val log = CommandSupport.logger(state)
if (maybeKey.isDefined) {
help(state, log, maybeKey.get)
} else {
log.info("Use help <key name> for help on a key")
}
state
}
case class KeyHelpFiles(docs: Map[String, Block]) {
def add(label: String, doc: Block): KeyHelpFiles = {
KeyHelpFiles(docs + (label -> doc))
}
def add(a: ProjectAnalysis, key: ScopedKey[_]): KeyHelpFiles = {
if (docs.contains(key.key.label)) {
this
} else {
val doc = markupHelp(a, key)
add(key.key.label, doc).addDeps(a, doc)
}
}
def addDeps(a: ProjectAnalysis, node: Node): KeyHelpFiles = {
node match {
case ScopedKeyLink(_, _, _, key) =>
add(a, key)
case _ =>
node.children.foldLeft(this)({
(sofar, child) =>
sofar.addDeps(a, child)
})
}
}
}
def helpBrowser(state: State, log: Logger, key: ScopedKey[_]): Unit = {
val a = analyze(state)
val targetDir = a.extracted.get(target)
val helpDir = targetDir / "help"
helpDir.mkdir()
val startDoc = markupHelp(a, key)
val files = KeyHelpFiles(Map(key.key.label -> startDoc)).addDeps(a, startDoc)
// TODO this generates a whole lot of files sometimes,
// need to be smarter about when to regenerate, etc.
// or just switch to rendering on-demand instead
// of generating files, of course.
for ((label, doc) <- files.docs) {
val helpFile = helpDir / (label + ".html")
val options = RenderHtmlOptions(currentKey=Some(label))
IO.write(helpFile,
renderHtmlDocument(title=label, block=doc,
options=options))
}
val uri = (helpDir / (key.key.label + ".html")).toURI
log.info("Opening " + uri.toASCIIString)
// this API is from Java 6.
java.awt.Desktop.getDesktop().browse(uri)
}
lazy val helpBrowserCommand =
Command("help-browser", ("help-browser <key>", "Show help about a key in your web browser"), "")(BuiltinCommands.optSpacedKeyParser) {
(state: State, maybeKey: Option[ScopedKey[_]]) =>
val log = CommandSupport.logger(state)
if (maybeKey.isDefined) {
helpBrowser(state, log, maybeKey.get)
} else {
log.info("Use help-browser <key name> for help on a key")
}
state
}
private def projectsAreRelated(key: ScopedKey[_], other: ScopedKey[_]): Boolean = {
other.scope.project match {
case Select(x) =>
Select(x) == key.scope.project
case _ =>
true
}
}
// try to chop down a list of keys to the most-related ones
private def fewerKeys(a: ProjectAnalysis, key: ScopedKey[_], more: Iterable[ScopedKey[_]]): Seq[ScopedKey[_]] = {
val desiredSize = 5
// "not showing 1 other keys" is both bad grammar
// and seems kind of aggravating (why hide just one key?).
// so we try to avoid that.
if (more.size < desiredSize + 2) {
more.toSeq
} else {
// we first sort alphabetically, which nicely puts the
// global keys at the front since they start with *
// and then we partition so we show different-project
// keys only after showing global, build, or same-project
// keys.
val (fewer, others) =
more.toSeq.sortBy(a.display(_)).partition(projectsAreRelated(key, _))
if (fewer.size < desiredSize) {
fewer ++ others.take(desiredSize - fewer.size)
} else {
// note we don't truncate this; always show all related keys.
// this can be a lot if a key is in a bunch of configuration
// or task scopes, but in those cases knowing those scopes
// is important. It's only long lists of project scopes
// that we can feel pretty confident aren't useful.
fewer
}
}
}
private def markupFewerNote(fewer: Iterable[ScopedKey[_]],
more: Iterable[ScopedKey[_]]): Option[Block] = {
val omitted = more.size - fewer.size
if (omitted > 0)
Some(Paragraph(Seq(Text("(not showing " + omitted + " other related keys)")),
verticalSpaceAfter=false))
else
None
}
private def markupUnused(a: ProjectAnalysis, unused: ScopedKey[_]): Block = {
val blockBuilder = Seq.newBuilder[Block]
blockBuilder += Warning(Paragraph(Seq(a.markup(unused),
Text(" is defined but not used"))))
val actual = a.actualUsed filter { _.key == unused.key }
val (overriders, allNonOverriders) = actual partition { sk => a.delegates(sk).exists(_ == unused) }
val nonOverriders =
allNonOverriders.filter(a.actualUsed.contains(_))
val intendedScopeOption = a.guessIntended(unused)
for (intendedScope <- intendedScopeOption) {
// the "overrides" message is better if it applies
// so skip this one in that case.
if (!overriders.contains(intendedScope)) {
blockBuilder +=
Indented(Paragraph(Seq(Text("You may have intended '"),
a.markup(intendedScope),
Text("' rather than '"),
a.markup(unused),
Text("'?"))))
}
}
for (o <- overriders) {
blockBuilder +=
Indented(Paragraph(Seq(Text("The more-specific definition '"),
a.markup(o),
Text("' may override '"),
a.markup(unused),
Text("'"))))
}
val fewerNonOverriders = fewerKeys(a, unused, nonOverriders)
if (fewerNonOverriders.nonEmpty) {
blockBuilder +=
Indented(Paragraph(Seq(Text("These other definitions of '"),
ScopedKeyLink(unused.key.label,
unused.key.label,
unused.key.label,
unused),
Text("' are used:"))))
val nonOverridersBuilder = Seq.newBuilder[Block]
for (i <- fewerNonOverriders) {
nonOverridersBuilder +=
Indented(Paragraph(Seq(a.markup(i)), verticalSpaceAfter=false),
count = 2)
}
for (note <- markupFewerNote(fewerNonOverriders, nonOverriders)) {
nonOverridersBuilder += Indented(note, count = 2)
}
blockBuilder += BlockList(nonOverridersBuilder.result,
verticalSpaceAfter = true)
blockBuilder +=
Indented(Paragraph(Text("(perhaps you meant to set one of those instead?)")),
count = 2)
}
val requested = a.requestedUsed filter {
k => k.key == unused.key && k != unused
}
val fewerRequested = fewerKeys(a, unused, requested)
if (fewerRequested.nonEmpty) {
blockBuilder +=
Indented(Paragraph(Seq(Text("Other settings depend on '"),
ScopedKeyLink(unused.key.label,
unused.key.label,
unused.key.label,
unused),
Text("' using these scopes:"))))
val requestedBuilder = Seq.newBuilder[Block]
for (i <- fewerRequested) {
requestedBuilder +=
Indented(Paragraph(Seq(a.markup(i)),
verticalSpaceAfter = false),
count = 2)
}
for (note <- markupFewerNote(fewerRequested, requested)) {
requestedBuilder += Indented(note, count = 2)
}
blockBuilder += BlockList(requestedBuilder.result,
verticalSpaceAfter = true)
blockBuilder +=
Indented(Paragraph(Seq(Text("(to be used, a setting's scope must be identical to or less-specific than these requested dependencies)"))),
count = 2)
}
blockBuilder +=
Indented(Paragraph(Seq(Text("Try 'new-help "),
ScopedKeyLink(unused.key.label,
unused.key.label,
unused.key.label,
unused),
Text("' for more information."))))
BlockList(blockBuilder.result)
}
val silenceUnusedWarning =
SettingKey[Seq[ScopedKey[_]]]("silence-unused-warning",
"List of scoped keys build-lint should not warn about if they are unused; use This scopes as wildcards")
// scope the ignoreable-keys list to a project
private def ignoreableForScope(a: ProjectAnalysis,
ignoreableWithThisScopes: Set[ScopedKey[_]],
projectScope: ScopeAxis[Reference]): Set[ScopedKey[_]] = {
projectScope match {
case Select(ref) => {
val resolved = Scope.resolveReference(a.extracted.currentRef.build,
a.extracted.rootProject,
ref)
val scoper = Scope.resolveScope(Load.projectScope(resolved),
a.extracted.currentRef.build,
a.extracted.rootProject)
ignoreableWithThisScopes.map({
ignoreableWithThis =>
Project.ScopedKey(scoper(ignoreableWithThis.scope),
ignoreableWithThis.key)
})
}
case _ => {
ignoreableWithThisScopes
}
}
}
// for each key we can ignore if it's unused, also ignore its
// delegates up to the first _defined_ delegate. Do not ignore
// anything after the defined delegate. So the ignore-list
// works as a "user" that is using keys it might delegate to.
private def addDelegatesToIgnoreable(a: ProjectAnalysis,
ignoreables: Set[ScopedKey[_]]): Set[ScopedKey[_]] = {
ignoreables.foldLeft(ignoreables)({
(sofar, sk) =>
val delegates = a.delegates(sk)
sofar ++ delegates.takeWhile(!a.keyIsDefined(_))
})
}
private def markupLint(state: State): Option[Block] = {
val a = analyze(state)
// sanity checks (TODO remove later)
for (u <- a.actualUsed) {
require(a.actualUsed.contains(u))
require(!a.actualUnused.contains(u))
require(a.actualCMap.contains(u))
}
for (u <- a.actualUnused) {
require(!a.actualUsed.contains(u))
require(a.actualUnused.contains(u))
require(a.actualCMap.contains(u))
}
// the silence-unused-warning list is allowed to have
// unresolved This scopes in it in order to silence
// keys in any project in a multi-project build.
val ignoreableWithThisScopes =
a.extracted.getOpt(silenceUnusedWarning).getOrElse(Nil).toSet
val unusedByProjectScope = a.actualUnused.groupBy(_.scope.project)
// Ignore warnings on keys that are in the list
val ignored = {
for ((projectScope, keys) <- unusedByProjectScope)
yield {
val scopedIgnoreable =
addDelegatesToIgnoreable(a,
ignoreableForScope(a,
ignoreableWithThisScopes,
projectScope))
keys.filter(scopedIgnoreable.contains(_))
}
}.reduce(_ ++ _)
// and we will warn about these
val warnAbout = a.actualUnused.filter(!ignored.contains(_))
if (warnAbout.nonEmpty) {
val blockBuilder = Seq.newBuilder[Block]
for (u <- warnAbout.toSeq.sortBy(a.display(_))) {
blockBuilder += markupUnused(a, u)
}
if (warnAbout.size > 1)
blockBuilder += Warning(Paragraph(Text(warnAbout.size + " settings were not used.")))
blockBuilder += Warning(Paragraph(Text("You can ignore these warnings by adding to the " + silenceUnusedWarning.key + " setting.")))
Some(BlockList(blockBuilder.result))
} else {
None
}
}
lazy val lintCommand =
Command.command("build-lint") {
(state: State) =>
val log = CommandSupport.logger(state)
val docOption = markupLint(state)
docOption.foreach({
doc =>
log.warn(renderText(doc,
// TODO get actual terminal properties
RenderTextOptions(wrapWidth = 100,
ansiCodes=true)))
})
state
}
}
object Markup {
// TODO all the nodes should have id and class fields
// that pass through as HTML id and class for CSS purposes.
sealed trait Node {
def children: Seq[Node]
}
sealed trait Block extends Node {
/** This is a hint for plain text rendering, HTML can usually
* do something that looks nice without forcing whitespace.
* Should be set to false for "decorator" block types that
* will have child block(s) that set it or not.
*/
def verticalSpaceAfter: Boolean = true
}
sealed trait Span extends Node
case class Heading(spans: Seq[Span]) extends Block {
override def children = spans
}
case class Paragraph(spans: Seq[Span],
override val verticalSpaceAfter: Boolean = true) extends Block {
override def children = spans
}
object Paragraph {
def apply(firstSpan: Span, spans: Span*): Paragraph = {
Paragraph(firstSpan +: spans.toSeq)
}
}
// in general BlockList(BlockList(a,b),BlockList(c,d)) should be
// equivalent to BlockList(a,b,c,d)
case class BlockList(blocks: Seq[Block] = Seq.empty,
override val verticalSpaceAfter: Boolean = false) extends Block {
override def children = blocks
def ++(list: BlockList): BlockList = {
++(list.blocks)
}
def ++(list: Seq[Block]): BlockList = {
BlockList(blocks ++ list)
}
def :+(block: Block): BlockList = {
block match {
case list: BlockList =>
++(list)
case _ =>
BlockList(blocks :+ block)
}
}
}
object BlockList {
def apply(firstBlock: Block, blocks: Block*): BlockList = BlockList(firstBlock +: blocks.toSeq)
}
case class Warning(block: Block) extends Block {
override def children = Seq(block)
override def verticalSpaceAfter: Boolean = false
}
case class Error(block: Block) extends Block {
override def children = Seq(block)
override def verticalSpaceAfter: Boolean = false
}
// raw text such as output from a markup-unaware command
case class NotMarkedUp(text: String) extends Block {
override def children = Seq.empty
}
// same idea as NotMarkedUp but for a different reason
case class Preformatted(text: String) extends Block {
override def children = Seq.empty
}
case class Text(text: String) extends Span {
override def children = Seq.empty
}
case class Code(spans: Seq[Span]) extends Span {
override def children = spans
}
object Code {
def apply(firstSpan: Span, spans: Span*): Code = Code(firstSpan +: spans.toSeq)
}
case class Bold(spans: Seq[Span]) extends Span {
override def children = Seq.empty
}
object Bold {
def apply(firstSpan: Span, spans: Span*): Bold = Bold(firstSpan +: spans.toSeq)
}
case class ScopedKeyLink(text: String, unscoped: String, scoped: String,
key: ScopedKey[_]) extends Span {
override def children = Seq.empty
}
sealed trait ListStyle
case object ListBullet extends ListStyle
case object ListNumbered extends ListStyle
case class List(style: ListStyle, items: Seq[Paragraph]) extends Block {
override def children = items
}
case class Indented(block: Block, count: Int = 1) extends Block {
override def children = Seq(block)
override def verticalSpaceAfter: Boolean = false
}
case class IndentedAfterFirst(block: Block, count: Int = 1) extends Block {
override def children = Seq(block)
override def verticalSpaceAfter: Boolean = false
}
case class Table(rows: Seq[Seq[Block]]) extends Block {
override def children = rows.flatMap(identity)
}
def noAnsiLength(s: String): Int = {
// this only handles the "SGR" (color/bold) sequences
// which are all we use anyway
s.replaceAll("\033\\[[0-9]m", "").length
}
private val indentPrefix = " "
private def indent(s: String, count: Int, skipFirstLine: Boolean = false): String = {
if (count == 0) {
s
} else {
val indented =
if (s.endsWith("\n")) {
indent(indentPrefix +
s.substring(0, s.length - 1)
.replaceAll("\n", "\n" + indentPrefix) +
"\n",
count - 1)
} else {
indent(indentPrefix +
s
.replaceAll("\n", "\n" + indentPrefix),
count - 1)
}
if (skipFirstLine)
indented.substring(indentPrefix.length)
else
indented
}
}
private def wordWrap(s: String, wrapWidth: Int): String = {
// this is lame and ASCII-only and lame
// get it all on one line, nuking any existing line breaks
val words = s.split("\\s")
val paraBuilder = new StringBuilder()
val lineBuilder = new StringBuilder()
for (w <- words) {
if (lineBuilder.length > 0 &&
(lineBuilder.length + noAnsiLength(w)) > wrapWidth) {
lineBuilder.append('\n')
paraBuilder.append(lineBuilder.toString)
lineBuilder.setLength(0)
} else if (lineBuilder.length > 0) {
lineBuilder.append(' ')
}
lineBuilder.append(w)
}
paraBuilder.append(lineBuilder.toString)
paraBuilder.toString
}
private def renderTextSpans(spans: Seq[Span], options: RenderTextOptions): String = {
import scala.{ Console => ansi }
val sb = new StringBuilder()
for (span <- spans) {
span match {
case Text(text) =>
sb.append(text)
case Code(childSpans) =>
sb.append(renderTextSpans(childSpans, options))
case Bold(childSpans) => {
val needsBold = renderTextSpans(childSpans, options)
if (options.ansiCodes)
sb.append(ansi.BOLD + needsBold + ansi.RESET)
else
sb.append(needsBold)
}
case ScopedKeyLink(text, unscoped, scoped, key) =>
sb.append(text)
}
}
wordWrap(sb.toString, options.wrapWidth)
}
private def renderTextList(style: ListStyle, items: Seq[Paragraph], options: RenderTextOptions): String = {
val sb = new StringBuilder()
for (item <- items) {
val indented = indent(renderText(item, options), 1)
sb.append(" - " + indented.substring(2))
sb.append("\n")
}
sb.toString
}
@tailrec
private def appendSplitParas(sb: StringBuilder,
columnWidths: IndexedSeq[Int],
remaining: Seq[Seq[String]]): Unit = {
if (remaining.exists(_.nonEmpty)) {
val firstLines = remaining.map(_.head)
for ((line, col) <- firstLines.zipWithIndex) {
sb.append(line)
for (i <- 0 until (columnWidths(col) - noAnsiLength(line)))
sb.append(' ')
// gap between columns
sb.append(indentPrefix)
}
// remove trailing whitespace on the line
while (sb.length > 0 &&
sb.charAt(sb.length - 1) == ' ') {
sb.setLength(sb.length - 1)
}
// end the line
sb.append('\n')
// next line
appendSplitParas(sb,
columnWidths,
remaining.map(_.tail))
}
}
private def renderTextTable(rows: Seq[Seq[Block]], options: RenderTextOptions): String = {
val sb = new StringBuilder()
val numColumns = rows.maxBy(_.length).length
// 12 is "minimum reasonable"
val columnWidth = math.max(12, (options.wrapWidth - ((numColumns - 1) * indentPrefix.length)) / numColumns)
val rowsAsSplitParas = for (row <- rows) yield {
// render each paragraph word-wrapped to max column width
val paras = row.map(renderText(_, options.copy(wrapWidth = columnWidth)))
// now change each paragraph from a single string to
// a Seq[String] one string per line
val splitParas = paras.map(_.split("\n").toSeq)
// now make all paragraphs on the row have same number
// of lines by adding blanks
val maxLines = splitParas.map(_.size).max
splitParas.map({
lines =>
lines.padTo(maxLines, "")
})
}
val columnWidthsPerRow = for (row <- rowsAsSplitParas) yield {
row.map({
lines =>
lines.map(noAnsiLength(_)).max
})
}
val zeros = Iterator.fill(numColumns)(0).toIndexedSeq
val columnWidths = columnWidthsPerRow.foldLeft(zeros)({
(sofar, rowWidths) =>
sofar.zipAll(rowWidths, 0, 0).map({
widthPair =>
math.max(widthPair._1, widthPair._2)
})
})
for (row <- rowsAsSplitParas) {
appendSplitParas(sb, columnWidths, row)
}
sb.toString
}
case class RenderTextOptions(wrapWidth: Int = 80,
ansiCodes: Boolean = false)
private def ensureNewline(s: String): String = {
if (s.endsWith("\n"))
s
else
s + "\n"
}
def renderText(block: Block, options: RenderTextOptions = RenderTextOptions()): String = {
val rendered = block match {
case BlockList(blocks, _) => {
val sb = new StringBuilder()
for (b <- blocks) {
val text = renderText(b, options)
sb.append(text)
}
sb.toString()
}
case Indented(child, count) => {
val newWrapWidth = options.wrapWidth - indentPrefix.length
indent(renderText(child,
options.copy(wrapWidth = newWrapWidth)), count)
}
case IndentedAfterFirst(child, count) => {
val newWrapWidth = options.wrapWidth - indentPrefix.length
indent(renderText(child,
options.copy(wrapWidth = newWrapWidth)), count,
skipFirstLine = true)
}
case Heading(spans) =>
renderTextSpans(spans, options.copy(wrapWidth=Int.MaxValue))
case Paragraph(spans, _) =>
renderTextSpans(spans, options)
case NotMarkedUp(text) =>
text
case Preformatted(text) =>
text
case List(style, items) =>
renderTextList(style, items, options)
case Table(rows) =>
renderTextTable(rows, options)
case Warning(block) =>
// handling this properly needs us to take over the
// [error] [warn] etc. prefixes from the logger,
// or else return some kind of (loglevel, string)
// tuple
renderText(block, options)
case Error(block) =>
// same issue as Warning
renderText(block, options)
}
// add another newline if we want a blank line after
if (block.verticalSpaceAfter)
ensureNewline(rendered) + "\n"
else
ensureNewline(rendered)
}
private def renderHtmlSpans(spans: Seq[Span], options: RenderHtmlOptions): xml.NodeSeq = {
val nodes = xml.NodeSeq.newBuilder
for (span <- spans) {
span match {
case Text(text) =>
nodes += xml.Text(text)
case Code(childSpans) =>
val codeNodes = xml.NodeSeq.newBuilder
for (child <- renderHtmlSpans(childSpans, options))
codeNodes += child
nodes += <code>{ codeNodes.result }</code>
case Bold(childSpans) => {
val boldNodes = xml.NodeSeq.newBuilder
for (child <- renderHtmlSpans(childSpans, options))
boldNodes += child
nodes += <strong>{ boldNodes.result }</strong>
}
case ScopedKeyLink(text, unscoped, scoped, key) =>
if (options.currentKey == Some(unscoped)) {
// don't link to self.
nodes += <code>{ text }</code>
} else {
val href =
xml.Attribute("href", xml.Text(unscoped + ".html"),
xml.Null)
nodes += <a><code>{ text }</code></a> % href
}
}
}
nodes.result
}
private def renderHtmlList(style: ListStyle, items: Seq[Paragraph], options: RenderHtmlOptions): xml.NodeSeq = {
val liNodes = xml.NodeSeq.newBuilder
for (item <- items) {
val itemNodes = xml.NodeSeq.newBuilder
for (node <- renderHtmlFragment(item, options))
itemNodes += node
liNodes += <li> { itemNodes.result } </li>
}
style match {
case ListBullet =>
<ul>
{ liNodes }
</ul>
case ListNumbered =>
<ol>
{ liNodes }
</ol>
}
}
private def renderHtmlTable(rows: Seq[Seq[Block]], options: RenderHtmlOptions): xml.NodeSeq = {
val numColumns = rows.maxBy(_.length).length
val rowNodes = xml.NodeSeq.newBuilder
for (row <- rows) {
val paras = row.map(renderHtmlFragment(_, options))
rowNodes += <tr>
{ paras.map(nodes => <td>{ nodes }</td>) }
</tr>
}
// TODO set a class and fix this up in css
<table border="0">
{ rowNodes.result }
</table>
}
private def renderHtmlIndented(block: Block, count: Int, options: RenderHtmlOptions): xml.NodeSeq = {
val childNodes = renderHtmlFragment(block, options)
// TODO instead of hardcoding this, set class=""
// and deal with it in css
val px = 40 * count
val attr = xml.Attribute("style", xml.Text("margin-left: " + px + "px;"), xml.Null)
<div>{ childNodes }</div> % attr
}
case class RenderHtmlOptions(currentKey: Option[String])
def renderHtmlFragment(block: Block, options: RenderHtmlOptions): xml.NodeSeq = {
val rendered = block match {
case BlockList(blocks, _) => {
val nodes = xml.NodeSeq.newBuilder
for (b <- blocks) {
for (node <- renderHtmlFragment(b, options))
nodes += node
}
nodes.result
}
case Indented(child, count) =>
renderHtmlIndented(child, count, options)
case IndentedAfterFirst(child, count) => {
// we make this the same as Indented for now,
// it's really mostly useful for plain text rendering.
renderHtmlIndented(child, count, options)
}
case Heading(spans) =>
val content = renderHtmlSpans(spans, options)
<h2>{ content }</h2>
case Paragraph(spans, _) =>
val content = renderHtmlSpans(spans, options)
<p>
{ content }
</p>
case NotMarkedUp(text) =>
// TODO really don't want <pre> with
// monotype font, just want to put in
// a forced newline for every newline
// and otherwise be a regular paragraph.
<pre>
{ text }
</pre>
case Preformatted(text) =>
<pre>
{ text }
</pre>
case List(style, items) =>
renderHtmlList(style, items, options)
case Table(rows) =>
renderHtmlTable(rows, options)
case Warning(block) =>
// TODO put this in a nice yellow box with icon
// or something like that
renderHtmlFragment(block, options)
case Error(block) =>
// TODO put this in a nice red box with icon
// or something like that
renderHtmlFragment(block, options)
}
rendered
}
def wrapHtmlDocument(title: String, fragment: xml.NodeSeq): String = {
"""<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
"http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
""" +
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title>{ title }</title>
</head>
<body>
{ fragment }
</body>
</html>
}
def renderHtmlDocument(title: String, block: Block, options: RenderHtmlOptions): String = {
wrapHtmlDocument(title, renderHtmlFragment(block, options))
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment