All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.kaitai.struct.problems.CompilationProblem.scala Maven / Gradle / Ivy

package io.kaitai.struct.problems

import fastparse.StringReprOps
import io.kaitai.struct.{JSON, Jsonable, Utils}
import io.kaitai.struct.datatype.DataType
import io.kaitai.struct.exprlang.Expressions
import io.kaitai.struct.format.{ClassSpec, Identifier, KSVersion}

/**
  * Abstract top-level trait common to all problems which might be raised during
  * precompilation / compilation process in a Kaitai Struct compiler.
  */
sealed abstract class CompilationProblem extends Jsonable {
  def severity: ProblemSeverity
  def coords: ProblemCoords
  def text: String
  def message = s"${coords.message}:\n\t${severity.message}: $text\n"
  def localizedInFile(fileName: String): CompilationProblem

  /**
    * @param typeSpec type spec this problem is firing about
    * @return copy of exception with problem localized to a file containing specific type, if necessary
    */
  def localizedInType(typeSpec: ClassSpec): CompilationProblem = {
    coords.file match {
      case Some(_) => this
      case None => localizedInFile(typeSpec.fileNameAsStr)
    }
  }

  def toException = CompilationProblemException(this)

  override def toJson: String =
    JSON.mapToJson(
      (coords.toSeq ++ Seq("message" -> text)).toMap
    )
}

/**
  * Unified YAML parser exception which pinpoints the row/col of the problem.
  * Used to re-wrap YAML parser library-specific exceptions into this unified
  * format that neutral codebase understands.
  * @param text user-readable error message
  */
case class YAMLParserError(
  val text: String,
  val coords: ProblemCoords
) extends CompilationProblem {
  override def severity = ProblemSeverity.Error
  override def localizedInFile(fileName: String): CompilationProblem =
    copy(coords = coords.copy(file = Some(fileName)))
}

case class KSYParseError(
  val text: String,
  path: List[String],
  fileName: Option[String] = None
) extends CompilationProblem {
  override def severity: ProblemSeverity = ProblemSeverity.Error
  override val coords: ProblemCoords = ProblemCoords(fileName, Some(path))
  override def localizedInFile(fileName: String): CompilationProblem =
    copy(fileName = Some(fileName))
}

object KSYParseError {
  def withText(text: String, path: List[String]): CompilationProblemException =
    KSYParseError(text, path).toException

  def noKey(path: List[String]) =
    withText(s"missing mandatory argument `${path.last}`", path)

  def noKeys(path: List[String], expectedKeys: Set[String]) =
    withText(s"expected any of ${expectedKeys.toList.sorted.mkString(", ")}, found none", path)

  def badType(expected: String, got: Any, path: List[String]) = {
    val gotStr = got match {
      case null => "null"
      case _ => s"$got (${got.getClass})"
    }
    withText(s"expected $expected, got $gotStr", path)
  }

  def badDictValue(expected: Set[String], got: String, path: List[String]) =
    withText(s"expected ${expected.toList.sorted.mkString(" / ")}, got '$got'", path)

  def incompatibleVersion(expected: KSVersion, got: KSVersion, path: List[String]) =
    withText(
      s"this ksy requires compiler version at least $expected, but you have $got",
      path
    )

  def invalidId(id: String, entity: String, path: List[String]) =
    withText(
      s"invalid $entity ID: '$id', expected /${Identifier.ReIdentifier.toString}/",
      path
    )

  def expression(epe: Expressions.ParseException, path: List[String]) = {
    val f = epe.failure
    val pos = StringReprOps.prettyIndex(f.extra.input, f.index)

    // Try to diagnose most common errors and provide a friendly suggestion
    val lookup2 = Utils.safeLookup(epe.src, f.index, 2)
    val suggestion: String = (if (lookup2 == "&&") {
      Some("and")
    } else if (lookup2 == "||") {
      Some("or")
    } else {
      None
    }).map((x) => s", did you mean '$x'?").getOrElse("")

    f.extra.traced.expected

    withText(
      s"parsing expression '${epe.src}' failed on $pos, " +
        s"expected ${f.extra.traced.expected.replaceAll("\n", "\\n")}$suggestion",
      path
    )
  }

  def exprType(expected: String, got: DataType, path: List[String]) =
    withText(s"invalid type: expected $expected, got $got", path)

  def badProcess(got: String, path: List[String]) =
    withText(s"incorrect process expression `$got`", path)

  def invalidParamCount(paramSize: Int, argSize: Int, path: List[String]) =
    withText(s"parameter count mismatch: $paramSize declared, but $argSize used", path)
}

/**
  * Container for a real exception that happened due to some known problem
  * with input file, and we know where exactly is the culprit (path and file).
  * @param err inner exception
  * @param path YAML path components in file
  * @param file file to report as erroneous, None means "main compilation unit"
  */
case class ErrorInInput(err: Throwable, path: List[String] = List(), fileName: Option[String] = None)
  extends CompilationProblem {

  override def text = Option(err.getMessage).getOrElse (err.toString)
  override val coords: ProblemCoords = ProblemCoords(fileName, Some(path))
  override def localizedInFile(fileName: String): CompilationProblem =
    copy(fileName = Some(fileName))
  override def severity: ProblemSeverity = ProblemSeverity.Error
}

case class ExpressionTypeError(expected: String, got: DataType, path: List[String], fileName: Option[String] = None)
  extends CompilationProblem {

  override def text = s"invalid type: expected $expected, got $got"
  override val coords: ProblemCoords = ProblemCoords(fileName, Some(path))
  override def localizedInFile(fileName: String): CompilationProblem =
    copy(fileName = Some(fileName))
  override def severity: ProblemSeverity = ProblemSeverity.Error
}

case class ParamMismatchError(idx: Int, argType: DataType, paramName: String, paramType: DataType, path: List[String], fileName: Option[String] = None)
  extends CompilationProblem {

  override def text = s"can't pass argument #$idx of type $argType into parameter `$paramName` of type $paramType"
  override val coords: ProblemCoords = ProblemCoords(fileName, Some(path))
  override def localizedInFile(fileName: String): CompilationProblem =
    copy(fileName = Some(fileName))
  override def severity: ProblemSeverity = ProblemSeverity.Error
}

abstract class StyleWarning(val coords: ProblemCoords) extends CompilationProblem {
  /**
    * @return main warning text, without references to the style guide
    */
  def warningText: String

  /**
    * @return reference to a particular anchor in a KSY style guide (without "#" and full URL prepending that)
    */
  def styleGuideAnchor: String

  override def severity: ProblemSeverity = ProblemSeverity.Warning
  override def text = s"$warningText (see https://doc.kaitai.io/ksy_style_guide.html#$styleGuideAnchor)"
}

case class StyleWarningSizeLen(goodName: String, badName: String, becauseOfName: String, override val coords: ProblemCoords) extends StyleWarning(coords) {
  override def warningText = s"use `$goodName` instead of `$badName`, given that it's only used as a byte size of `$becauseOfName`"
  override def styleGuideAnchor = "attr-id"

  override def localizedInFile(fileName: String): CompilationProblem =
    copy(coords = coords.copy(file = Some(fileName)))
}

case class StyleWarningRepeatExprNum(goodName: String, badName: String, becauseOfName: String, override val coords: ProblemCoords) extends StyleWarning(coords) {
  override def warningText = s"use `$goodName` instead of `$badName`, given that it's only used as repeat count of `$becauseOfName`"
  override def styleGuideAnchor = "attr-id"

  override def localizedInFile(fileName: String): CompilationProblem =
    copy(coords = coords.copy(file = Some(fileName)))
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy