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

scalapb.compiler.ProtoValidation.scala Maven / Gradle / Ivy

The newest version!
package scalapb.compiler

import com.google.protobuf.Descriptors._
import scala.jdk.CollectionConverters._

class ProtoValidation(implicits: DescriptorImplicits) {
  import implicits._

  def validateFiles(files: Seq[FileDescriptor]): Unit = {
    files.foreach(validateFile)
    implicits.fileOptionsCache
    ()
  }

  def validateFile(fd: FileDescriptor): Unit = {
    fd.getEnumTypes.asScala.foreach(validateEnum)
    fd.getMessageTypes.asScala.foreach(validateMessage)
    if (fd.scalaOptions.hasPrimitiveWrappers && fd.scalaOptions.getNoPrimitiveWrappers) {
      throw new GeneratorException(
        s"${fd.getFullName}: primitive_wrappers and no_primitive_wrappers must not be used at the same time."
      )
    }
    if (fd.javaConversions && fd.javaPackage.isEmpty) {
      throw new GeneratorException(
        s"${fd.getFullName}: java_conversions require either a package statement or setting the java_package option."
      )
    }
    val allSealedOneofCases = for {
      msg   <- fd.getMessageTypes.asScala
      cases <- msg.sealedOneofCases.getOrElse(Seq.empty)
    } yield cases
    allSealedOneofCases
      .groupBy(identity)
      .collect {
        case (d, occurences) if occurences.length > 1 => d
      }
      .foreach { d =>
        throw new GeneratorException(
          s"${d.getFullName}: message may belong to at most one sealed oneof"
        )
      }
  }

  def validateEnum(e: EnumDescriptor): Unit = {
    if (e.getValues.asScala.exists(_.getName.toUpperCase == "UNRECOGNIZED")) {
      throw new GeneratorException(
        s"The enum value 'UNRECOGNIZED' in ${e.getName} is not allowed due to conflict with the catch-all " +
          "Unrecognized(v: Int) value."
      )
    }
  }

  def validateMessage(m: Descriptor): Unit = {
    m.getEnumTypes.asScala.foreach(validateEnum)
    m.getNestedTypes.asScala.foreach(validateMessage)
    m.getFields.asScala.foreach(validateField)
    if (m.isSealedOneofType) {
      val oneof = m.getRealOneofs.get(0)
      if (m.getFields.size() != oneof.getFields.size()) {
        throw new GeneratorException(
          s"${m.getFullName}: sealed oneofs must have all their fields inside a single oneof"
        )
      }
      val fields = oneof.getFields.asScala
      fields.find(!_.isMessage).foreach { field =>
        throw new GeneratorException(
          s"${m.getFullName}.${field.getName}: sealed oneofs must have all their fields be message types"
        )
      }
      fields.find(_.getMessageType.isSealedOneofType).foreach { field =>
        throw new GeneratorException(
          s"${m.getFullName}.${field.getName}: sealed oneofs may not be a case within another sealed oneof"
        )
      }
      fields.find(_.getMessageType.getContainingType != m.getContainingType).foreach { field =>
        throw new GeneratorException(
          s"${m.getFullName}.${field.getName}: sealed oneofs must be in the same containing message (if any) as all case messages"
        )
      }
      fields.find(_.getMessageType.getFile != m.getFile()).foreach { field =>
        throw new GeneratorException(
          s"${m.getFullName}.${field.getName}: all sealed oneof cases must be defined in the same file as the sealed oneof field."
        )
      }
      fields.find(_.customSingleScalaTypeName.isDefined).foreach { field =>
        throw new GeneratorException(
          s"${m.getFullName}.${field.getName}: sealed oneof cases may not have custom types."
        )
      }
      val distinctTypes = fields.map(_.getMessageType).toSet
      if (distinctTypes.size != fields.size) {
        throw new GeneratorException(
          s"${m.getFullName}: all sealed oneof cases must be of a distinct message type"
        )
      }
      if (!m.getNestedTypes.isEmpty) {
        throw new GeneratorException(
          s"${m.getFullName}: sealed oneofs may not contain nested messages"
        )
      }
      if (!m.getEnumTypes.isEmpty) {
        throw new GeneratorException(
          s"${m.getFullName}: sealed oneofs may not contain nested enums"
        )
      }
    } else {
      if (m.sealedOneOfExtendsCount > 0) {
        throw new GeneratorException(
          s"${m.getFullName}: `sealed_oneof_extends` option can only be used on a sealed oneof. Did you mean `extends`?"
        )
      }
      if (m.sealedOneofCompanionExtendsCount > 0) {
        throw new GeneratorException(
          s"${m.getFullName}: `sealed_oneof_companion_extends` option can only be used on a sealed oneof. Did you mean `companion_extends`?"
        )
      }
    }
  }

  def validateField(fd: FieldDescriptor): Unit = {
    if (ProtoValidation.ForbiddenFieldNames.contains(fd.scalaName))
      throw new GeneratorException(
        s"Field named '${fd.getName}' in message '${fd.getFullName}' is not allowed. See https://scalapb.github.io/docs/customizations#custom-names"
      )
    if (!fd.isRepeated && fd.fieldOptions.hasCollectionType)
      throw new GeneratorException(
        s"${fd.getFullName}: Field ${fd.getName} has collection_type set but is not a repeated field."
      )
    if (!fd.isMapField && (fd.fieldOptions.hasKeyType || fd.fieldOptions.hasValueType)) {
      throw new GeneratorException(
        s"${fd.getFullName}: Field ${fd.getName} is not a map but specifies key_type or value_type."
      )
    }
    if (fd.isMapField && fd.fieldOptions.hasCollectionType) {
      throw new GeneratorException(
        s"${fd.getFullName}: Field ${fd.getName} is a map but has collection_type specified. Use map_type instead."
      )
    }
    if (!fd.isMapField && fd.fieldOptions.hasMapType) {
      throw new GeneratorException(
        s"${fd.getFullName}: Field ${fd.getName} has map_type specified, but it is not a map field"
      )
    }
    if (fd.isMapField && fd.fieldOptions.hasType)
      throw new GeneratorException(
        s"${fd.getFullName}: Field ${fd.getName} is a map and has type specified. Use key_type or value_type instead."
      )
    if (!fd.isOptional && fd.fieldOptions.hasNoBox)
      throw new GeneratorException(
        s"${fd.getFullName}: Field ${fd.getName} has no_box set but is not an optional field."
      )
    if (fd.isMessage && fd.getMessageType.isSealedOneofType && fd.fieldOptions.hasType) {
      throw new GeneratorException(
        s"${fd.getFullName}: Sealed oneofs can not be type mapped. Use regular oneofs instead."
      )
    }
    if (fd.isMessage && fd.isInOneof && fd.getMessageType().messageOptions.getNoBox()) {
      throw new GeneratorException(
        s"${fd.getFullName}: message fields in oneofs are not allowed to have no_box set."
      )
    }
    if (fd.noBoxRequired) {
      if (fd.fieldOptions.hasNoBox && !fd.fieldOptions.getNoBox)
        throw new GeneratorException(
          s"${fd.getFullName}: setting no_box to false is not allowed while setting required to true."
        )
      if (fd.isInOneof)
        throw new GeneratorException(
          s"${fd.getFullName}: setting required is not allowed on oneof fields."
        )
      if (fd.isRepeated() || fd.isMapField())
        throw new GeneratorException(
          s"${fd.getFullName}: required is not allowed on repeated fields."
        )
      if (!fd.isMessage) {
        throw new GeneratorException(
          s"${fd.getFullName}: required can only be applied to message fields."
        )
      }
    }
  }
}

object ProtoValidation {
  val ForbiddenFieldNames = Set(
    "hashCode",
    "equals",
    "class",
    "clone",
    "copy",
    "finalize",
    "getClass",
    "notify",
    "notifyAll",
    "toString",
    "wait",
    "productArity",
    "productElementName",
    "productElementNames",
    "productIterator",
    "productPrefix"
  )
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy