lightbend / genjavadoc

A compiler plugin for generating doc’able Java source from Scala source
Other
58 stars 32 forks source link
javadoc sbt-plugin scala scaladoc supported

This project’s goal is the creation of real Javadoc for Scala projects. While Scaladoc—the native API documentation format of Scala—has several benefits over Javadoc, Java programmers are very much used to having access to API documentation in a syntax matching their programming language of choice. Another motivating factor may be that javadoc-JARs are supported within IDE, e.g. showing tooltip help texts.

How to Use It

GenJavadoc is a Scala compiler plugin which emits structurally equivalent Java code for all Scala sources of a project, keeping the Scaladoc comments (with a few format adaptions). Integration into an SBT build is quite simple:

lazy val Javadoc = config("genjavadoc") extend Compile

lazy val javadocSettings = inConfig(Javadoc)(Defaults.configSettings) ++ Seq(
  addCompilerPlugin("com.typesafe.genjavadoc" %% "genjavadoc-plugin" % "0.18" cross CrossVersion.full),
  scalacOptions += s"-P:genjavadoc:out=${target.value}/java",
  Compile / packageDoc := (Javadoc / packageDoc).value,
  Javadoc / sources :=
    (target.value / "java" ** "*.java").get ++
    (Compile / sources).value.filter(_.getName.endsWith(".java")),
  Javadoc / javacOptions := Seq(),
  Javadoc / packageDoc / artifactName := ((sv, mod, art) =>
    "" + mod.name + "_" + sv.binary + "-" + mod.revision + "-javadoc.jar")
)

To make it work, you must add the config and the settings to your project. One way to do this is to place the following line in your build.sbt file:

lazy val root = project.in(file(".")).configs(Javadoc).settings(javadocSettings: _*)

Adding javadocSettings to a Project this way will replace the packaging of the API docs to use the Javadoc instead of the Scaladoc (i.e. the XY-javadoc.jar will then contain Javadoc). The Scaladoc can still be generated using the normal doc task, whereas the Javadoc can now be generated using genjavadoc:doc.

GenJavadoc can also be integrated into a Maven build (inspired by this answer on StackOverflow):

<profile>
  <id>javadoc</id>
  <build>
    <plugins>
      <plugin>
        <groupId>net.alchim31.maven</groupId>
        <artifactId>scala-maven-plugin</artifactId>
        <executions>
          <execution>
            <id>doc</id>
            <phase>generate-sources</phase>
            <goals>
              <goal>compile</goal>
            </goals>
          </execution>
        </executions>
        <configuration>
          <args>
            <arg>-P:genjavadoc:out=${project.build.directory}/genjavadoc</arg>
          </args>
          <compilerPlugins>
            <compilerPlugin>
              <groupId>com.typesafe.genjavadoc</groupId>
              <artifactId>genjavadoc-plugin_${scala.binary.full.version}</artifactId>
              <version>0.11</version>
            </compilerPlugin>
          </compilerPlugins>
        </configuration>
      </plugin>
      <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>build-helper-maven-plugin</artifactId>
        <executions>
          <execution>
            <phase>generate-sources</phase>
            <goals>
              <goal>add-source</goal>
            </goals>
            <configuration>
              <sources>
                <source>${project.build.directory}/genjavadoc</source>
              </sources>
            </configuration>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-javadoc-plugin</artifactId>
        <version>2.9</version>
        <configuration>
          <minmemory>64m</minmemory>
          <maxmemory>2g</maxmemory>
          <outputDirectory>${project.build.directory}</outputDirectory>
          <detectLinks>true</detectLinks>
        </configuration>
      </plugin>
    </plugins>
  </build>
</profile>

You can integrate genjavadoc with gradle build:

apply plugin: 'scala'

configurations {
  scalaCompilerPlugin
}

dependencies {
  // ...
  scalaCompilerPlugin "com.typesafe.genjavadoc:genjavadoc-plugin_${scalaFullVersion}:0.13"

}

tasks.withType(ScalaCompile) {
  scalaCompileOptions.with {
    additionalParameters = [
        "-Xplugin:" + configurations.scalaCompilerPlugin.asPath,
        "-P:genjavadoc:out=$buildDir/generated/java".toString()
    ]
  }
}

tasks.withType(Javadoc) {
  dependsOn("compileScala")
  source = [sourceSets.main.allJava, "$buildDir/generated/java"]
}

Translation of Scaladoc comments

Comments found within the Scala sources are transferred to the corresponding Java sources including some modifications. These are necessary since Scaladoc supports different mark-up elements than Javadoc. The modifications are:

Additional transformations

Some additional transformations are applied in order to make the generated Javadoc more useful:

How it Works

Scaladoc generation is done by a special variant of the Scala compiler, which can in principle emit different output, but the syntax parsed by the Scaladoc code is the Scala one: the compiler phases which adapt the AST to be more Java-like (to emit JVM byte-code in the end) are not run. On the other hand source comments cannot easily be associated with method signatures parsed from class files, and generating corresponding Java code to be fed into the javadoc tool is also no small task.

The approach taken here is to use the Scala compiler’s support as far as possible and then generate mostly valid Java code corresponding to the AST—but only the class and method structur without implementations. Since the Javadoc shall contain generic type information, and shall also not be confused by artifacts of Scala’s encoding of traits and other things, the AST must be inspected before the “erasure” phase; due to Java’s flat method parameter lists the other bound on where to hook into the transformation is that it should be after the “uncurry” phase (which transforms def f(x: Int)(s: String) into def f(x: int, s: String)). Luckily there is a gap between those two phases which is just wide enough to squeeze some code in.

One drawback of this choice is that the flattening of classes and companion objects or the mixing-in of method implementations from traits into derived classes happens much later in the transformation pipeline, meaning that the compiler plugin has to do that transformation itself; for this it has the advantage that it does not need to be 100% Scala-correct since the goal is just to document method signatures, not to implement all the intricacies of access widening and so on.

Known Limitations

Reporting Bugs

If you find errors in the generation process or have suggestions on how to improve the quality of the emitted Javadoc, please report issues on this GitHub repo’s issue tracker.

Sponsored by Lightbend

Originally responsible: Dr. Roland Kuhn (@rkuhn)

Community maintenance is overseen by Lightbend staff.

Pull requests are very welcome. Thanks in advance!