Skip to content

Commit

Permalink
Merge pull request #146 from eed3si9n/wip/javaintf
Browse files Browse the repository at this point in the history
Fix Java code generation
  • Loading branch information
eed3si9n authored Mar 6, 2020
2 parents 38c2ef3 + d8b21e0 commit 9d32bf1
Show file tree
Hide file tree
Showing 14 changed files with 613 additions and 322 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ target
/.settings
/RUNNING_PID
log
metals.sbt
21 changes: 21 additions & 0 deletions .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
version = 2.3.2
maxColumn = 140
project.git = true
project.excludeFilters = [ /sbt-test/, /input_sources/, /contraband-scala/ ]

# http://docs.scala-lang.org/style/scaladoc.html recommends the JavaDoc style.
# scala/scala is written that way too https://github.com/scala/scala/blob/v2.12.2/src/library/scala/Predef.scala
docstrings = JavaDoc

# This also seems more idiomatic to include whitespace in import x.{ yyy }
spaces.inImportCurlyBraces = true

# This is more idiomatic Scala.
# http://docs.scala-lang.org/style/indentation.html#methods-with-numerous-arguments
align.openParenCallSite = false
align.openParenDefnSite = false

# For better code clarity
danglingParentheses = true

trailingCommas = preserve
5 changes: 2 additions & 3 deletions .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,6 @@ env: ADOPTOPENJDK=11

matrix:
include:
- env: SBT_VERSION="0.13.18" ADOPTOPENJDK=8
scala: 2.10.7
- env: SBT_VERSION="1.2.8" ADOPTOPENJDK=11
scala: 2.12.10
- scala: 2.13.1
Expand All @@ -18,10 +16,11 @@ before_install:
- "[[ -d $HOME/.sdkman/bin/ ]] || rm -rf $HOME/.sdkman/"
- curl -sL https://get.sdkman.io | bash
- echo sdkman_auto_answer=true > $HOME/.sdkman/etc/config
- echo sdkman_auto_selfupdate=true >> $HOME/.sdkman/etc/config
- source "$HOME/.sdkman/bin/sdkman-init.sh"

install:
- sdk install java $(sdk list java | grep -o "$ADOPTOPENJDK\.[0-9\.]*hs-adpt" | head -1)
- sdk install java $(sdk list java | grep -o "$ADOPTOPENJDK\.[0-9\.]*hs-adpt" | head -1) || true
- unset JAVA_HOME
- java -Xmx32m -version
- javac -J-Xmx32m -version
Expand Down
11 changes: 7 additions & 4 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Dependencies._

ThisBuild / version := "0.4.6-SNAPSHOT"
ThisBuild / organization := "org.scala-sbt"
ThisBuild / crossScalaVersions := Seq(scala213, scala212, scala211, scala210)
ThisBuild / crossScalaVersions := Seq(scala213, scala212, scala211)
ThisBuild / scalaVersion := "2.12.8"
ThisBuild / organizationName := "sbt"
ThisBuild / organizationHomepage := Some(url("http://scala-sbt.org/"))
Expand Down Expand Up @@ -40,7 +40,8 @@ lazy val library = (project in file("library"))
baseDirectory.value / "src/main/scala-2.13+"
}
},
libraryDependencies ++= Seq(parboiled.value) ++ jsonDependencies.value ++ Seq(scalaTest % Test, diffutils % Test)
testFrameworks += new TestFramework("verify.runner.Framework"),
libraryDependencies ++= Seq(parboiled.value) ++ jsonDependencies.value ++ Seq(verify % Test, scalaTest % Test, diffutils % Test)
)

lazy val plugin = (project in file("plugin"))
Expand All @@ -50,8 +51,10 @@ lazy val plugin = (project in file("plugin"))
name := "sbt-contraband",
bintrayPackage := "sbt-contraband",
description := "sbt plugin to generate growable datatypes.",
scriptedLaunchOpts := { scriptedLaunchOpts.value ++
Seq("-Xmx1024M", "-Dplugin.version=" + version.value)
scriptedLaunchOpts := {
scriptedLaunchOpts.value ++
Seq("-Xmx1024M", "-Dplugin.version=" + version.value)
},
crossScalaVersions := Seq(scala212),
publishLocal := (publishLocal dependsOn (publishLocal in library)).value
)
139 changes: 84 additions & 55 deletions library/src/main/scala/sbt/contraband/JavaCodeGen.scala
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,12 @@ import AstUtil._
/**
* Code generator for Java.
*/
class JavaCodeGen(lazyInterface: String, optionalInterface: String,
instantiateJavaOptional: (String, String) => String,
wrapOption: Boolean) extends CodeGenerator {
class JavaCodeGen(
lazyInterface: String,
optionalInterface: String,
instantiateJavaOptional: (String, String) => String,
wrapOption: Boolean
) extends CodeGenerator {

/** Indentation configuration for Java sources. */
implicit object indentationConfiguration extends IndentationConfiguration {
Expand All @@ -34,7 +37,7 @@ class JavaCodeGen(lazyInterface: String, optionalInterface: String,
val parentsInSchema = lookupInterfaces(s, parents)
val parent: Option[InterfaceTypeDefinition] = parentsInSchema.headOption
val allFields = fields filter { _.arguments.isEmpty }
val extendsCode = parent map (p => s"extends ${fullyQualifiedName(p)}") getOrElse "implements java.io.Serializable"
val extendsCode = genExtendsCode(parent, extraParents)
val doc = toDoc(comments)
val extra = toExtra(i)
val msgs = i.fields filter { _.arguments.nonEmpty }
Expand Down Expand Up @@ -65,7 +68,7 @@ class JavaCodeGen(lazyInterface: String, optionalInterface: String,
val parent: Option[InterfaceTypeDefinition] = parentsInSchema.headOption
val doc = toDoc(r.comments)
val extra = toExtra(r)
val extendsCode = parent map (p => s"extends ${fullyQualifiedName(p)}") getOrElse "implements java.io.Serializable"
val extendsCode = genExtendsCode(parent, extraParents)
val toStringImpl: List[String] = toToStringImpl(r)
val lfs = localFields(r, parentsInSchema)
val mod = toModifier(r.directives).getOrElse("public final")
Expand Down Expand Up @@ -93,10 +96,12 @@ class JavaCodeGen(lazyInterface: String, optionalInterface: String,

val valuesCode =
if (values.isEmpty) ""
else (values map { case EnumValueDefinition(name, dir, comments, _) =>
s"""${genDoc(toDoc(comments))}
else
(values map {
case EnumValueDefinition(name, dir, comments, _) =>
s"""${genDoc(toDoc(comments))}
|$name""".stripMargin
}).mkString("", "," + EOL, ";")
}).mkString("", "," + EOL, ";")

val extra = toExtra(e)
val code =
Expand All @@ -111,7 +116,7 @@ class JavaCodeGen(lazyInterface: String, optionalInterface: String,
}

private def genDoc(doc: List[String]) = doc match {
case Nil => ""
case Nil => ""
case l :: Nil => s"/** $l */"
case lines =>
val doc = lines map (l => s" * $l") mkString EOL
Expand All @@ -120,6 +125,14 @@ class JavaCodeGen(lazyInterface: String, optionalInterface: String,
| */""".stripMargin
}

private def genExtendsCode(parent: Option[InterfaceTypeDefinition], extraParents: List[String]): String = {
val parentOpt = parent.map(p => fullyQualifiedName(p))
(parentOpt match {
case Some(p) => s"extends $p "
case _ => ""
}) + "implements " + (extraParents ::: List("java.io.Serializable")).mkString(", ")
}

private def genFile(d: TypeDefinition) = {
val fileName = d.name + ".java"
d.namespace map (ns => new File(ns.replace(".", File.separator), fileName)) getOrElse new File(fileName)
Expand Down Expand Up @@ -158,23 +171,24 @@ class JavaCodeGen(lazyInterface: String, optionalInterface: String,
}

private def genMessages(messages: List[FieldDefinition]) = messages map genMessage mkString EOL
private def genMessage(message: FieldDefinition): String =
{
val FieldDefinition(name, fieldType, arguments, defaultValue, dirs, comments, _) = message
val doc = toDoc(comments)
val argsDoc = arguments flatMap { a: InputValueDefinition =>
toDoc(a.comments) match {
case Nil => Nil
case doc :: Nil => s"@param ${a.name} $doc" :: Nil
case docs =>
val prefix = s"@param ${a.name} "
docs.mkString(prefix, EOL + " " * (prefix.length + 3), "") :: Nil
}
private def genMessage(message: FieldDefinition): String = {
val FieldDefinition(name, fieldType, arguments, defaultValue, dirs, comments, _) = message
val doc = toDoc(comments)
val argsDoc = arguments flatMap { a: InputValueDefinition =>
toDoc(a.comments) match {
case Nil => Nil
case doc :: Nil => s"@param ${a.name} $doc" :: Nil
case docs =>
val prefix = s"@param ${a.name} "
docs.mkString(prefix, EOL + " " * (prefix.length + 3), "") :: Nil
}
val params: List[String] = arguments map { a => s"${genRealTpe(a.valueType)} ${a.name}" }
s"""${genDoc(doc ++ argsDoc)}
|public abstract ${genRealTpe(fieldType)} ${name}(${params mkString ","});"""
}
val params: List[String] = arguments map { a =>
s"${genRealTpe(a.valueType)} ${a.name}"
}
s"""${genDoc(doc ++ argsDoc)}
|public abstract ${genRealTpe(fieldType)} ${name}(${params mkString ","});"""
}

private def renderJavaValue(v: Value, tpe: Type): String =
v match {
Expand All @@ -188,9 +202,11 @@ class JavaCodeGen(lazyInterface: String, optionalInterface: String,
val str =
v match {
case x: ObjectValue =>
val args = x.fields map { f => f.value.renderPretty }
s"""new ${tpe.name}(${ args.mkString(", ") })"""
case _ => v.renderPretty
val args = x.fields map { f =>
f.value.renderPretty
}
s"""new ${tpe.name}(${args.mkString(", ")})"""
case _ => v.renderPretty
}
if (tpe.isListType) s"new Array { $str }"
else if (tpe.isNotNullType) str
Expand All @@ -202,7 +218,7 @@ class JavaCodeGen(lazyInterface: String, optionalInterface: String,
case Some(v) => renderJavaValue(v, f.fieldType)
case None if f.fieldType.isListType || !f.fieldType.isNotNullType =>
renderJavaValue(NullValue(), f.fieldType)
case _ => sys.error(s"Needs a default value for field ${f.name}.")
case _ => sys.error(s"Needs a default value for field ${f.name}.")
}

private def genFactoryMethods(cl: RecordLikeDefinition, parent: Option[InterfaceTypeDefinition]) =
Expand All @@ -212,21 +228,24 @@ class JavaCodeGen(lazyInterface: String, optionalInterface: String,
val args = provided map (f => s"_${f.name}")

val factoryMethodNames = List("create", "of")
val methods = factoryMethodNames map { factory => genStaticFactoryMethod(cl, factory, ctorParameters, args) }
methods.mkString(EOL + EOL) +
{
val methods = factoryMethodNames map { factory =>
genStaticFactoryMethod(cl, factory, ctorParameters, args)
}
methods.mkString(EOL + EOL) + {
if (!containsStrictOptional(provided) || !wrapOption) ""
else {
val ctorParameters2 = provided map { f =>
if (f.fieldType.isOptionalType && !f.fieldType.isLazyType) s"${genRealTpe(f.fieldType.notNull)} _${f.name}"
else s"${genRealTpe(f.fieldType)} _${f.name}"
}
val methods2 = factoryMethodNames map { factory => genStaticFactoryMethod(cl, factory, ctorParameters2, args) }
val methods2 = factoryMethodNames map { factory =>
genStaticFactoryMethod(cl, factory, ctorParameters2, args)
}
EOL + EOL + methods2.mkString(EOL + EOL)
}
}
} mkString (EOL + EOL)

private def genStaticFactoryMethod(cl: RecordLikeDefinition, methodName: String, params: List[String], args: List[String]): String =
s"""public static ${cl.name} ${methodName}(${params.mkString(", ")}) {
| return new ${cl.name}(${args.mkString(", ")});
Expand Down Expand Up @@ -280,12 +299,12 @@ class JavaCodeGen(lazyInterface: String, optionalInterface: String,
| $superCall2
| $assignments2
|}""".stripMargin
}
}
}
} mkString (EOL + EOL)

private def genWith(r: ObjectTypeDefinition, parents: List[InterfaceTypeDefinition]) = {
def capitalize(s: String) = { val (fst, rst) = s.splitAt(1) ; fst.toUpperCase + rst }
def capitalize(s: String) = { val (fst, rst) = s.splitAt(1); fst.toUpperCase + rst }
val allFields = (r.fields filter { _.arguments.isEmpty }).zipWithIndex
val lfs = localFields(r, parents)
def nonParam(f: (FieldDefinition, Int)): String = {
Expand All @@ -299,37 +318,41 @@ class JavaCodeGen(lazyInterface: String, optionalInterface: String,
} else s"${f._1.name}()"
}

allFields map { case (f, idx) =>
val (before, after) = allFields filterNot (_._2 == idx) splitAt idx
val tpe = f.fieldType
val params = (before map nonParam) ::: f.name :: (after map nonParam) mkString ", "
s"""public ${r.name} with${capitalize(f.name)}(${genRealTpe(tpe)} ${f.name}) {
allFields map {
case (f, idx) =>
val (before, after) = allFields filterNot (_._2 == idx) splitAt idx
val tpe = f.fieldType
val params = (before map nonParam) ::: f.name :: (after map nonParam) mkString ", "
s"""public ${r.name} with${capitalize(f.name)}(${genRealTpe(tpe)} ${f.name}) {
| return new ${r.name}($params);
|}""".stripMargin +
( if (tpe.isListType || tpe.isNotNullType) ""
else {
val wrappedParams = (before map nonParam) ::: instantiateJavaOptional(boxedType(tpe.name), f.name) :: (after map nonParam) mkString ", "
s"""
(if (tpe.isListType || tpe.isNotNullType) ""
else {
val wrappedParams = (before map nonParam) ::: instantiateJavaOptional(boxedType(tpe.name), f.name) :: (after map nonParam) mkString ", "
s"""
|public ${r.name} with${capitalize(f.name)}(${genRealTpe(f.fieldType.notNull)} ${f.name}) {
| return new ${r.name}($wrappedParams);
|}""".stripMargin
}
)
})
} mkString (EOL + EOL)
}

private def genEquals(cl: RecordLikeDefinition) = {
val allFields = cl.fields filter { _.arguments.isEmpty }
val body =
if (allFields exists { f => f.fieldType.isLazyType }) {
if (allFields exists { f =>
f.fieldType.isLazyType
}) {
"return this == obj; // We have lazy members, so use object identity to avoid circularity."
} else {
val comparisonCode =
if (allFields.isEmpty) "return true;"
else
allFields.map { f =>
genJavaEquals("this", "o", f, s"${f.name}()", true)
}.mkString("return ", " && ", ";")
allFields
.map { f =>
genJavaEquals("this", "o", f, s"${f.name}()", true)
}
.mkString("return ", " && ", ";")

s"""if (this == obj) {
| return true;
Expand All @@ -351,7 +374,9 @@ class JavaCodeGen(lazyInterface: String, optionalInterface: String,
val fqcn = cl.namespace.fold("")(_ + ".") + cl.name
val seed = s"""37 * (17 + "$fqcn".hashCode())"""
val body =
if (allFields exists { f => f.fieldType.isLazyType }) {
if (allFields exists { f =>
f.fieldType.isLazyType
}) {
"return super.hashCode(); // Avoid evaluating lazy members in hashCode to avoid circularity."
} else {
val computation = (seed /: allFields) { (acc, f) =>
Expand All @@ -369,12 +394,16 @@ class JavaCodeGen(lazyInterface: String, optionalInterface: String,
private def genToString(cl: RecordLikeDefinition, toString: List[String]) = {
val body = if (toString.isEmpty) {
val allFields = cl.fields filter { _.arguments.isEmpty }
if (allFields exists { f => f.fieldType.isLazyType }) {
if (allFields exists { f =>
f.fieldType.isLazyType
}) {
"return super.toString(); // Avoid evaluating lazy members in toString to avoid circularity."
} else {
allFields.map{ f =>
s""" + "${f.name}: " + ${f.name}()"""
}.mkString(s"""return "${cl.name}(" """, " + \", \"", " + \")\";")
allFields
.map { f =>
s""" + "${f.name}: " + ${f.name}()"""
}
.mkString(s"""return "${cl.name}(" """, " + \", \"", " + \")\";")
}
} else toString mkString s"$EOL "

Expand Down
1 change: 1 addition & 0 deletions library/src/test/scala/EqualLines.scala
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ trait EqualLines {
private val emptyLines = Lines(Vector.empty)

implicit class CleanedString(s: String) {
def stripSpace: String = unindent.value.mkString("\n")
def unindent: Lines = Lines(s.linesIterator.map(_.trim).filterNot(_.isEmpty).toVector)
def withoutEmptyLines: Lines = Lines(s.linesIterator.filterNot(_.trim.isEmpty).toVector)
}
Expand Down
Loading

0 comments on commit 9d32bf1

Please sign in to comment.