Skip to content

Commit

Permalink
Add CliException for cleaner error reporting (#1345)
Browse files Browse the repository at this point in the history
  • Loading branch information
johnynek authored Jan 8, 2025
1 parent 5cc4256 commit a262f8b
Show file tree
Hide file tree
Showing 9 changed files with 116 additions and 43 deletions.
10 changes: 10 additions & 0 deletions cli/src/main/scala/org/bykn/bosatsu/IOPlatformIO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -187,4 +187,14 @@ object IOPlatformIO extends PlatformIO[IO, JPath] {
System.out.println("")
}

def writeError(doc: Doc): IO[Unit] =
IO.blocking {
doc
.renderStreamTrim(80)
.iterator
.foreach(System.err.print)

System.out.println("")
}

}
6 changes: 6 additions & 0 deletions cliJS/src/main/scala/org/bykn/bosatsu/Fs2PlatformIO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,12 @@ object Fs2PlatformIO extends PlatformIO[IO, Path] {
.compile
.drain

def writeError(doc: Doc): IO[Unit] =
docStream(doc)
.evalMapChunk(part => IO.consoleForIO.error(part))
.compile
.drain

def println(str: String): IO[Unit] =
IO.println(str)

Expand Down
53 changes: 32 additions & 21 deletions core/src/main/scala/org/bykn/bosatsu/MainModule.scala
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import cats.parse.{Parser => P}
import org.typelevel.paiges.Doc
import scala.util.{Failure, Success, Try}
import org.bykn.bosatsu.Parser.argFromParser
import org.bykn.bosatsu.tool.{ExitCode, FileKind, GraphOutput, Output, PackageResolver, PathParseError}
import org.bykn.bosatsu.tool.{CliException, ExitCode, FileKind, GraphOutput, Output, PackageResolver, PathParseError}
import org.typelevel.paiges.Document

import codegen.Transpiler
Expand Down Expand Up @@ -40,11 +40,22 @@ class MainModule[IO[_], Path](val platformIO: PlatformIO[IO, Path]) {
final def runAndReport(args: List[String]): Either[Help, IO[ExitCode]] =
run(args).map(report)

sealed abstract class MainException extends Exception {
sealed abstract class MainException extends Exception with CliException {
def command: MainCommand
def messageString: String
}
object MainException {
case class NoInputs(command: MainCommand) extends MainException
case class NoInputs(command: MainCommand) extends MainException {
def messageString: String = {
val name = command.name
s"no inputs given to $name"
}

def exitCode: ExitCode = ExitCode.Error
def errDoc = Doc.text(messageString)
def stdOutDoc: Doc = Doc.empty
}

case class ParseErrors(
command: MainCommand,
errors: NonEmptyList[PathParseError[Path]],
Expand Down Expand Up @@ -75,6 +86,11 @@ class MainModule[IO[_], Path](val platformIO: PlatformIO[IO, Path]) {
)
}
}

def messageString: String = messages.mkString("\n")
def errDoc = Doc.intercalate(Doc.hardLine, messages.map(Doc.text(_)))
def stdOutDoc: Doc = Doc.empty
def exitCode: ExitCode = ExitCode.Error
}
case class PackageErrors(
command: MainCommand,
Expand All @@ -85,23 +101,18 @@ class MainModule[IO[_], Path](val platformIO: PlatformIO[IO, Path]) {
def messages: List[String] =
errors.toList.distinct
.map(_.message(sourceMap, color))

def messageString: String = messages.mkString("\n")
def errDoc = Doc.intercalate(Doc.hardLine, messages.map(Doc.text(_)))
def stdOutDoc: Doc = Doc.empty
def exitCode: ExitCode = ExitCode.Error
}
}

def mainExceptionToString(ex: Throwable): Option[String] =
ex match {
case me: MainException =>
me match {
case MainException.NoInputs(cmd) =>
val name = cmd.name
Some(s"no inputs given to $name")
case pe @ MainException.ParseErrors(_, _, _) =>
Some(pe.messages.mkString("\n"))
case pe @ MainException.PackageErrors(_, _, _, _) =>
Some(pe.messages.mkString("\n"))
}
case _ =>
None
case me: MainException => Some(me.messageString)
case _ => None
}

sealed abstract class MainCommand(val name: String) {
Expand Down Expand Up @@ -1245,18 +1256,18 @@ class MainModule[IO[_], Path](val platformIO: PlatformIO[IO, Path]) {
stringWriter.toString
}

def reportException(ex: Throwable): IO[Unit] =
mainExceptionToString(ex) match {
case Some(msg) =>
platformIO.errorln(msg)
case None =>
def reportException(ex: Throwable): IO[ExitCode] =
ex match {
case ce: CliException => ce.report(platformIO)
case _ =>
platformIO.errorln("unknown error:\n") *>
platformIO.errorln(stackTraceToString(ex))
.as(ExitCode.Error)
}

def report(io: IO[Output[Path]]): IO[ExitCode] =
io.attempt.flatMap {
case Right(out) => reportOutput(out)
case Left(err) => reportException(err).as(ExitCode.Error)
case Left(err) => reportException(err)
}
}
9 changes: 6 additions & 3 deletions core/src/main/scala/org/bykn/bosatsu/MemoryMain.scala
Original file line number Diff line number Diff line change
Expand Up @@ -312,13 +312,16 @@ object MemoryMain {
state.copy(stdOut = state.stdOut + (doc + Doc.hardLine))
}

def writeError(doc: Doc): F[Unit] =
StateT.modify { state =>
state.copy(stdErr = state.stdErr + (doc + Doc.hardLine))
}

def println(str: String): F[Unit] =
writeStdout(Doc.text(str))

def errorln(str: String): F[Unit] =
StateT.modify { state =>
state.copy(stdErr = state.stdErr + (Doc.text(str) + Doc.hardLine))
}
writeError(Doc.text(str))

override def resolve(base: Path, parts: List[String]): Path =
base ++ Chain.fromSeq(parts)
Expand Down
5 changes: 4 additions & 1 deletion core/src/main/scala/org/bykn/bosatsu/PackageName.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package org.bykn.bosatsu

import cats.Order
import cats.{Order, Show}
import cats.data.NonEmptyList
import cats.implicits._
import cats.parse.{Parser => P}
Expand Down Expand Up @@ -50,4 +50,7 @@ object PackageName {
"package name",
"Must be capitalized strings separated by /"
)

implicit val showPackageName: Show[PackageName] =
Show(_.asString)
}
1 change: 1 addition & 0 deletions core/src/main/scala/org/bykn/bosatsu/PlatformIO.scala
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ trait PlatformIO[F[_], Path] {

def writeDoc(p: Path, d: Doc): F[Unit]
def writeStdout(doc: Doc): F[Unit]
def writeError(doc: Doc): F[Unit]

def system(command: String, args: List[String]): F[Unit]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import com.monovore.decline.{Argument, Opts}
import java.util.regex.{Pattern => RegexPat}
import org.bykn.bosatsu.codegen.Transpiler
import org.bykn.bosatsu.{BuildInfo, Identifier, Json, MatchlessFromTypedExpr, Package, PackageName, PackageMap, Par, PlatformIO, TypedExpr, TypeName}
import org.bykn.bosatsu.tool.{CliException, ExitCode}
import org.bykn.bosatsu.rankn.Type
import org.typelevel.paiges.Doc
import scala.util.{Failure, Success, Try}
Expand Down Expand Up @@ -97,7 +98,7 @@ case object ClangTranspiler extends Transpiler {
ccConf <- CcConf.parse(json) match {
case Right(cc) => moduleIOMonad.pure(cc)
case Left((str, j, path)) =>
moduleIOMonad.raiseError(new Exception(show"when parsing $confPath got error: $str at $path to json: $j"))
moduleIOMonad.raiseError(CliException.Basic(show"when parsing $confPath got error: $str at $path to json: $j"))
}
} yield ccConf

Expand All @@ -112,7 +113,7 @@ case object ClangTranspiler extends Transpiler {
case Some(PlatformIO.FSDataType.File) => moduleIOMonad.unit
case res @ (None | Some(PlatformIO.FSDataType.Dir)) =>
moduleIOMonad.raiseError[Unit](
new Exception(show"expected a CcConf json file at $confPath but found: ${res.toString}.\n\n" +
CliException.Basic(show"expected a CcConf json file at $confPath but found: ${res.toString}.\n\n" +
"Perhaps you need to `make install` the c_runtime")
)
}
Expand Down Expand Up @@ -142,28 +143,40 @@ case object ClangTranspiler extends Transpiler {
}
}

case class GenError(error: ClangGen.Error) extends Exception(s"clang gen error: ${error.display.render(80)}")
case class GenError(error: ClangGen.Error) extends Exception(s"clang gen error: ${error.display.render(80)}") with CliException {
def errDoc: Doc = error.display
def stdOutDoc: Doc = Doc.empty
def exitCode: ExitCode = ExitCode.Error
}

private def spacePackList(ps: Iterable[PackageName]): Doc =
(Doc.line + Doc.intercalate(Doc.comma + Doc.line, ps.map(p => Doc.text(p.asString))))
.nested(4)
.grouped

case class CircularPackagesFound(loop: NonEmptyList[PackageName])
extends Exception(
(Doc.text("circular dependencies found in packages:") + spacePackList(loop.toList)).render(80)
)
case class CircularPackagesFound(loop: NonEmptyList[PackageName]) extends Exception("circular deps in packages") with CliException {
def errDoc: Doc =
(Doc.text("circular dependencies found in packages:") + spacePackList(loop.toList))
def stdOutDoc: Doc = Doc.empty
def exitCode: ExitCode = ExitCode.Error
}

case class InvalidMainValue(pack: PackageName, message: String) extends
Exception(s"invalid main ${pack.asString}: $message.")
case class InvalidMainValue(pack: PackageName, message: String) extends Exception(s"invalid main ${pack.asString}: $message.") with CliException {
def errDoc = Doc.text(getMessage())
def stdOutDoc: Doc = Doc.empty
def exitCode: ExitCode = ExitCode.Error
}

case class NoTestsFound(packs: List[PackageName], regex: NonEmptyList[String]) extends
Exception(
(Doc.text("no tests found in:") + spacePackList(packs) + Doc.hardLine +
case class NoTestsFound(packs: List[PackageName], regex: NonEmptyList[String]) extends Exception(show"no tests found in $packs with regex $regex") with CliException {
def errDoc: Doc =
(Doc.text("no tests found in:") + spacePackList(packs) + Doc.hardLine +
Doc.text("using regexes:") +
(Doc.line + Doc.intercalate(Doc.line, regex.toList.map(Doc.text(_))).nested(4)).grouped
).render(80)
)
)

def stdOutDoc: Doc = Doc.empty
def exitCode: ExitCode = ExitCode.Error
}

def externalsFor(pm: PackageMap.Typed[Any]): ClangGen.ExternalResolver =
ClangGen.ExternalResolver.stdExternals(pm)
Expand Down
8 changes: 4 additions & 4 deletions core/src/main/scala/org/bykn/bosatsu/library/Command.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package org.bykn.bosatsu.library

import cats.MonoidK
import org.bykn.bosatsu.{Json, PlatformIO}
import org.bykn.bosatsu.tool.Output
import org.bykn.bosatsu.tool.{CliException, Output}
import com.monovore.decline.Opts
import cats.syntax.all._

Expand Down Expand Up @@ -30,12 +30,12 @@ object Command {
case Right(lib) => moduleIOMonad.pure(lib)
case Left((msg, j, p)) =>
moduleIOMonad.raiseError[Libraries](
new Exception(show"$msg: from json = $j at $p")
CliException.Basic(show"$msg: from json = $j at $p")
)
}
}
case Some(PlatformIO.FSDataType.Dir) =>
moduleIOMonad.raiseError[Libraries](new Exception(show"expected $path to be a file, not directory."))
moduleIOMonad.raiseError[Libraries](CliException.Basic(show"expected $path to be a file, not directory."))
}

val initCommand =
Expand All @@ -57,7 +57,7 @@ object Command {
relDir <- platformIO.relativize(gitRoot, rootDir) match {
case Some(value) => moduleIOMonad.pure(value)
case None =>
moduleIOMonad.raiseError(new Exception(show"$rootDir is not a subdir of $gitRoot"))
moduleIOMonad.raiseError(CliException.Basic(show"$rootDir is not a subdir of $gitRoot"))
}
lib1 = lib0.updated(name, show"$relDir")
out1 = Output.JsonOutput(Json.Writer.write(lib1), Some(path))
Expand Down
26 changes: 26 additions & 0 deletions core/src/main/scala/org/bykn/bosatsu/tool/CliException.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package org.bykn.bosatsu.tool

import org.bykn.bosatsu.PlatformIO
import org.typelevel.paiges.Doc

import cats.syntax.all._

trait CliException { self: Exception =>
def errDoc: Doc
def stdOutDoc: Doc
def exitCode: ExitCode

def report[F[_], P](platform: PlatformIO[F, P]): F[ExitCode] = {
import platform.moduleIOMonad

platform.writeStdout(stdOutDoc) *>
platform.writeError(errDoc).as(exitCode)
}
}

object CliException {
case class Basic(summary: String, exitCode: ExitCode = ExitCode.Error) extends Exception(summary) with CliException {
def stdOutDoc: Doc = Doc.empty
lazy val errDoc: Doc = Doc.text(summary)
}
}

0 comments on commit a262f8b

Please sign in to comment.