diff options
Diffstat (limited to 'main/src/main/Resolve.scala')
-rw-r--r-- | main/src/main/Resolve.scala | 443 |
1 files changed, 443 insertions, 0 deletions
diff --git a/main/src/main/Resolve.scala b/main/src/main/Resolve.scala new file mode 100644 index 00000000..a2c186ed --- /dev/null +++ b/main/src/main/Resolve.scala @@ -0,0 +1,443 @@ +package mill.main + +import mill.define._ +import mill.define.TaskModule +import ammonite.util.Res +import mill.main.ResolveMetadata.singleModuleMeta +import mill.util.Router.EntryPoint +import mill.util.Scripts + +import scala.reflect.ClassTag + +object ResolveMetadata extends Resolve[String]{ + def singleModuleMeta(obj: Module, discover: Discover[_], isRootModule: Boolean) = { + val modules = obj.millModuleDirectChildren.map(_.toString) + val targets = + obj + .millInternal + .reflectAll[Target[_]] + .map(_.toString) + val commands = for{ + (cls, entryPoints) <- discover.value + if cls.isAssignableFrom(obj.getClass) + ep <- entryPoints + } yield { + if (isRootModule) ep._2.name + else obj + "." + ep._2.name + } + modules ++ targets ++ commands + } + + def endResolveLabel(obj: Module, + revSelectorsSoFar: List[Segment], + last: String, + discover: Discover[_], + rest: Seq[String]): Either[String, List[String]] = { + + val direct = singleModuleMeta(obj, discover, revSelectorsSoFar.isEmpty) + last match{ + case "__" => + Right( + // Filter out our own module in + obj.millInternal.modules + .filter(_ != obj) + .flatMap(m => singleModuleMeta(m, discover, m != obj)) + .toList + ) + case "_" => Right(direct.toList) + case _ => + direct.find(_.split('.').last == last) match{ + case None => Resolve.errorMsgLabel(direct, last, revSelectorsSoFar) + case Some(s) => Right(List(s)) + } + } + } + + def endResolveCross(obj: Module, + revSelectorsSoFar: List[Segment], + last: List[String], + discover: Discover[_], + rest: Seq[String]): Either[String, List[String]] = { + obj match{ + case c: Cross[Module] => + last match{ + case List("__") => Right(c.items.map(_._2.toString)) + case items => + c.items + .filter(_._1.length == items.length) + .filter(_._1.zip(last).forall{case (a, b) => b == "_" || a.toString == b}) + .map(_._2.toString) match{ + case Nil => + Resolve.errorMsgCross( + c.items.map(_._1.map(_.toString)), + last, + revSelectorsSoFar + ) + case res => Right(res) + } + + } + case _ => + Left( + Resolve.unableToResolve(Segment.Cross(last), revSelectorsSoFar) + + Resolve.hintListLabel(revSelectorsSoFar) + ) + } + } +} + +object ResolveSegments extends Resolve[Segments] { + + override def endResolveCross(obj: Module, + revSelectorsSoFar: List[Segment], + last: List[String], + discover: Discover[_], + rest: Seq[String]): Either[String, Seq[Segments]] = { + obj match{ + case c: Cross[Module] => + last match{ + case List("__") => Right(c.items.map(_._2.millModuleSegments)) + case items => + c.items + .filter(_._1.length == items.length) + .filter(_._1.zip(last).forall{case (a, b) => b == "_" || a.toString == b}) + .map(_._2.millModuleSegments) match { + case Nil => + Resolve.errorMsgCross( + c.items.map(_._1.map(_.toString)), + last, + revSelectorsSoFar + ) + case res => Right(res) + } + } + case _ => + Left( + Resolve.unableToResolve(Segment.Cross(last), revSelectorsSoFar) + + Resolve.hintListLabel(revSelectorsSoFar) + ) + } + } + + def endResolveLabel(obj: Module, + revSelectorsSoFar: List[Segment], + last: String, + discover: Discover[_], + rest: Seq[String]): Either[String, Seq[Segments]] = { + val target = + obj + .millInternal + .reflectSingle[Target[_]](last) + .map(t => Right(t.ctx.segments)) + + val command = + Resolve + .invokeCommand(obj, last, discover, rest) + .headOption + .map(_.map(_.ctx.segments)) + + val module = + obj.millInternal + .reflectNestedObjects[Module] + .find(_.millOuterCtx.segment == Segment.Label(last)) + .map(m => Right(m.millModuleSegments)) + + command orElse target orElse module match { + case None => + Resolve.errorMsgLabel( + singleModuleMeta(obj, discover, revSelectorsSoFar.isEmpty), + last, + revSelectorsSoFar + ) + + case Some(either) => either.right.map(Seq(_)) + } + } +} + +object ResolveTasks extends Resolve[NamedTask[Any]]{ + + + def endResolveCross(obj: Module, + revSelectorsSoFar: List[Segment], + last: List[String], + discover: Discover[_], + rest: Seq[String])= { + + obj match{ + case c: Cross[Module] => + + Resolve.runDefault(obj, Segment.Cross(last), discover, rest).flatten.headOption match{ + case None => + Left( + "Cannot find default task to evaluate for module " + + Segments((Segment.Cross(last) :: revSelectorsSoFar).reverse:_*).render + ) + case Some(v) => v.map(Seq(_)) + } + case _ => + Left( + Resolve.unableToResolve(Segment.Cross(last), revSelectorsSoFar) + + Resolve.hintListLabel(revSelectorsSoFar) + ) + } + } + + def endResolveLabel(obj: Module, + revSelectorsSoFar: List[Segment], + last: String, + discover: Discover[_], + rest: Seq[String]) = last match{ + case "__" => + Right( + obj.millInternal.modules + .filter(_ != obj) + .flatMap(m => m.millInternal.reflectAll[Target[_]]) + ) + case "_" => Right(obj.millInternal.reflectAll[Target[_]]) + + case _ => + val target = + obj + .millInternal + .reflectSingle[Target[_]](last) + .map(Right(_)) + + val command = Resolve.invokeCommand(obj, last, discover, rest).headOption + + command orElse target orElse Resolve.runDefault(obj, Segment.Label(last), discover, rest).flatten.headOption match { + case None => + Resolve.errorMsgLabel( + singleModuleMeta(obj, discover, revSelectorsSoFar.isEmpty), + last, + revSelectorsSoFar + ) + + // Contents of `either` *must* be a `Task`, because we only select + // methods returning `Task` in the discovery process + case Some(either) => either.right.map(Seq(_)) + } + } +} + +object Resolve{ + def minimum(i1: Int, i2: Int, i3: Int)= math.min(math.min(i1, i2), i3) + + /** + * Short Levenshtein distance algorithm, based on + * + * https://rosettacode.org/wiki/Levenshtein_distance#Scala + */ + def editDistance(s1: String, s2: String) = { + val dist = Array.tabulate(s2.length+1, s1.length+1){(j, i) => if(j==0) i else if (i==0) j else 0} + + for(j <- 1 to s2.length; i <- 1 to s1.length) + dist(j)(i) = if(s2(j - 1) == s1(i-1)) dist(j - 1)(i-1) + else minimum(dist(j - 1)(i) + 1, dist(j)(i - 1) + 1, dist(j - 1)(i - 1) + 1) + + dist(s2.length)(s1.length) + } + + def unableToResolve(last: Segment, revSelectorsSoFar: List[Segment]): String = { + unableToResolve(Segments((last :: revSelectorsSoFar).reverse: _*).render) + } + + def unableToResolve(segments: String): String = "Cannot resolve " + segments + "." + + def hintList(revSelectorsSoFar: List[Segment]) = { + val search = Segments(revSelectorsSoFar.reverse: _*).render + s" Try `mill resolve $search` to see what's available." + } + + def hintListLabel(revSelectorsSoFar: List[Segment]) = { + hintList(Segment.Label("_") :: revSelectorsSoFar) + } + + def hintListCross(revSelectorsSoFar: List[Segment]) = { + hintList(Segment.Cross(Seq("__")) :: revSelectorsSoFar) + } + + def errorMsgBase[T](direct: Seq[T], + last0: T, + revSelectorsSoFar: List[Segment], + editSplit: String => String, + defaultErrorMsg: String) + (strings: T => Seq[String], + render: T => String): Left[String, Nothing] = { + val last = strings(last0) + val similar = + direct + .map(x => (x, strings(x))) + .filter(_._2.length == last.length) + .map{ case (d, s) => (d, s.zip(last).map{case (a, b) => Resolve.editDistance(editSplit(a), b)}.sum)} + .filter(_._2 < 3) + .sortBy(_._2) + + if (similar.headOption.exists(_._1 == last0)){ + // Special case: if the most similar segment is the desired segment itself, + // this means we are trying to resolve a module where a task is present. + // Special case the error message to make it something meaningful + Left("Task " + last0 + " is not a module and has no children.") + }else{ + + val hint = similar match{ + case Nil => defaultErrorMsg + case items => " Did you mean " + render(items.head._1) + "?" + } + Left(unableToResolve(render(last0)) + hint) + } + } + + def errorMsgLabel(direct: Seq[String], last: String, revSelectorsSoFar: List[Segment]) = { + errorMsgBase( + direct, + Segments((Segment.Label(last) :: revSelectorsSoFar).reverse:_*).render, + revSelectorsSoFar, + _.split('.').last, + hintListLabel(revSelectorsSoFar) + )( + rendered => Seq(rendered.split('.').last), + x => x + ) + } + + def errorMsgCross(crossKeys: Seq[Seq[String]], + last: Seq[String], + revSelectorsSoFar: List[Segment]) = { + errorMsgBase( + crossKeys, + last, + revSelectorsSoFar, + x => x, + hintListCross(revSelectorsSoFar) + )( + crossKeys => crossKeys, + crossKeys => Segments((Segment.Cross(crossKeys) :: revSelectorsSoFar).reverse:_*).render + ) + } + + def invokeCommand(target: Module, + name: String, + discover: Discover[_], + rest: Seq[String]) = for { + (cls, entryPoints) <- discover.value + if cls.isAssignableFrom(target.getClass) + ep <- entryPoints + if ep._2.name == name + } yield Scripts.runMainMethod( + target, + ep._2.asInstanceOf[EntryPoint[Module]], + ammonite.main.Scripts.groupArgs(rest.toList) + ) match { + case Res.Success(v: Command[_]) => Right(v) + case Res.Failure(msg) => Left(msg) + case Res.Exception(ex, msg) => + val sw = new java.io.StringWriter() + ex.printStackTrace(new java.io.PrintWriter(sw)) + val prefix = if (msg.nonEmpty) msg + "\n" else msg + Left(prefix + sw.toString) + + } + + def runDefault(obj: Module, last: Segment, discover: Discover[_], rest: Seq[String]) = for { + child <- obj.millInternal.reflectNestedObjects[Module] + if child.millOuterCtx.segment == last + res <- child match { + case taskMod: TaskModule => + Some(invokeCommand(child, taskMod.defaultCommandName(), discover, rest).headOption) + case _ => None + } + } yield res + +} +abstract class Resolve[R: ClassTag] { + def endResolveCross(obj: Module, + revSelectorsSoFar: List[Segment], + last: List[String], + discover: Discover[_], + rest: Seq[String]): Either[String, Seq[R]] + def endResolveLabel(obj: Module, + revSelectorsSoFar: List[Segment], + last: String, + discover: Discover[_], + rest: Seq[String]): Either[String, Seq[R]] + + def resolve(remainingSelector: List[Segment], + obj: mill.Module, + discover: Discover[_], + rest: Seq[String], + remainingCrossSelectors: List[List[String]], + revSelectorsSoFar: List[Segment]): Either[String, Seq[R]] = { + + remainingSelector match{ + case Segment.Cross(last) :: Nil => + endResolveCross(obj, revSelectorsSoFar, last.map(_.toString).toList, discover, rest) + case Segment.Label(last) :: Nil => + endResolveLabel(obj, revSelectorsSoFar, last, discover, rest) + + case head :: tail => + val newRevSelectorsSoFar = head :: revSelectorsSoFar + + def recurse(searchModules: Seq[Module], resolveFailureMsg: => Left[String, Nothing]) = { + val matching = searchModules + .map(resolve(tail, _, discover, rest, remainingCrossSelectors, newRevSelectorsSoFar)) + + matching match{ + case Seq(Left(err)) => Left(err) + case items => + items.collect{case Right(v) => v} match{ + case Nil => resolveFailureMsg + case values => Right(values.flatten) + } + } + } + head match{ + case Segment.Label(singleLabel) => + recurse( + if (singleLabel == "__") obj.millInternal.modules + else if (singleLabel == "_") obj.millModuleDirectChildren.toSeq + else{ + obj.millInternal.reflectNestedObjects[mill.Module] + .find(_.millOuterCtx.segment == Segment.Label(singleLabel)) + .toSeq + }, + if (singleLabel != "_") Resolve.errorMsgLabel( + singleModuleMeta(obj, discover, revSelectorsSoFar.isEmpty), + singleLabel, + revSelectorsSoFar + ) + else Left( + "Cannot resolve " + Segments((remainingSelector.reverse ++ revSelectorsSoFar).reverse:_*).render + + ". Try `mill resolve " + Segments((Segment.Label("_") :: revSelectorsSoFar).reverse:_*).render + "` to see what's available" + ) + ) + case Segment.Cross(cross) => + obj match{ + case c: Cross[Module] => + recurse( + if(cross == Seq("__")) for ((k, v) <- c.items) yield v + else if (cross.contains("_")){ + for { + (k, v) <- c.items + if k.length == cross.length + if k.zip(cross).forall { case (l, r) => l == r || r == "_" } + } yield v + }else c.itemMap.get(cross.toList).toSeq, + Resolve.errorMsgCross( + c.items.map(_._1.map(_.toString)), + cross.map(_.toString), + revSelectorsSoFar + ) + ) + case _ => + Left( + Resolve.unableToResolve(Segment.Cross(cross.map(_.toString)), tail) + + Resolve.hintListLabel(tail) + ) + } + } + + case Nil => Left("Selector cannot be empty") + } + } +} |