Skip to content

Commit d01f86c

Browse files
authored
Detect link to class (where object is intended) (#155)
* Detect link to class (where object is intended) * Quilify forwarding only classes more
1 parent 6802bf2 commit d01f86c

File tree

5 files changed

+62
-9
lines changed

5 files changed

+62
-9
lines changed

src/main/scala/com/lightbend/paradox/apidoc/ApidocDirective.scala

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,20 +20,38 @@ import com.lightbend.paradox.ParadoxError
2020
import com.lightbend.paradox.ParadoxException
2121
import com.lightbend.paradox.markdown.{Url => ParadoxUrl}
2222
import com.lightbend.paradox.markdown.{InlineDirective, Writer}
23+
import io.github.classgraph.ScanResult
2324
import org.pegdown.Printer
2425
import org.pegdown.ast.DirectiveNode.Source
2526
import org.pegdown.ast.{DirectiveNode, Visitor}
2627

28+
import scala.collection.JavaConverters._
29+
2730
import scala.util.matching.Regex
2831

29-
class ApidocDirective(allClassesAndObjects: IndexedSeq[String], ctx: Writer.Context) extends InlineDirective("apidoc") {
32+
class ApidocDirective(scanner: ScanResult, allClassesAndObjects: IndexedSeq[String], ctx: Writer.Context)
33+
extends InlineDirective("apidoc") {
3034
final val JavadocProperty = raw"""javadoc\.(.*)\.base_url""".r
3135
final val JavadocBaseUrls = ctx.properties.collect {
3236
case (JavadocProperty(pkg), url) => pkg -> url
3337
}
3438

3539
val allClasses = allClassesAndObjects.filterNot(_.endsWith("$"))
3640

41+
def containsOnlyStaticForwarders(className: String): Boolean = {
42+
val info = scanner.getClassInfo(className)
43+
info != null && info.isFinal && info.getMethodInfo.asScala.forall(_.isStatic)
44+
}
45+
46+
private def errorForStaticForwardersOnly(query: Query, node: DirectiveNode, classname: String) =
47+
if (!query.linkToObject && containsOnlyStaticForwarders(classname) &&
48+
allClassesAndObjects.contains(classname + "$")) {
49+
ctx.error(
50+
s"Class `$classname` matches @apidoc[$query], but is empty, did you intend to link to the object?",
51+
node
52+
)
53+
}
54+
3755
private case class Query(label: Option[String], pattern: String, generics: String, linkToObject: Boolean) {
3856
def scalaLabel(matched: String): String =
3957
label match {
@@ -111,10 +129,11 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], ctx: Writer.Cont
111129
renderMatches(query, results, node, visitor, printer)
112130
}
113131
}
114-
} else {
132+
} else { // only a classname
115133
val className = '.' + query.pattern
116134
val classMatches = allClasses.filter(_.endsWith(className))
117135
if (classMatches.size == 1 && classMatches(0).contains(".javadsl.")) {
136+
errorForStaticForwardersOnly(query, node, classMatches(0))
118137
val objectName = className + '$'
119138
val allMatches = allClassesAndObjects.filter(name => name.endsWith(className) || name.endsWith(objectName))
120139
renderMatches(query, allMatches, node, visitor, printer)
@@ -209,6 +228,7 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], ctx: Writer.Cont
209228
)
210229
case 1 =>
211230
val pkg = matches(0)
231+
errorForStaticForwardersOnly(query, node, query.scalaFqcn(pkg))
212232
scaladocNode("scala", query.scalaLabel(pkg), query.scalaFqcn(pkg) + scalaClassSuffix, sAnchor, node)
213233
.accept(visitor)
214234
if (hasJavadocUrl(pkg)) {
@@ -218,9 +238,11 @@ class ApidocDirective(allClassesAndObjects: IndexedSeq[String], ctx: Writer.Cont
218238
.accept(visitor)
219239
case 2 if matches.forall(_.contains("adsl")) =>
220240
matches.foreach { pkg =>
221-
if (!pkg.contains("javadsl"))
241+
if (!pkg.contains("javadsl")) {
242+
errorForStaticForwardersOnly(query, node, query.scalaFqcn(pkg))
222243
scaladocNode("scala", query.scalaLabel(pkg), query.scalaFqcn(pkg) + scalaClassSuffix, sAnchor, node)
223244
.accept(visitor)
245+
}
224246
if (!pkg.contains("scaladsl")) {
225247
if (hasJavadocUrl(pkg))
226248
javadocNode(query.javaLabel(pkg), query.javaFqcn(pkg), jAnchor, node).accept(visitor)

src/main/scala/com/lightbend/paradox/apidoc/ApidocPlugin.scala

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ object ApidocPlugin extends AutoPlugin {
5050
val scanner = new ClassGraph()
5151
.whitelistPackages(apidocRootPackage.value)
5252
.addClassLoader(classLoader)
53+
.enableMethodInfo()
5354
.scan()
5455
val allClasses = scanner.getAllClasses.getNames.asScala.toVector
5556
Def.task {
56-
Seq((ctx: Writer.Context) => new ApidocDirective(allClasses, ctx))
57+
Seq((ctx: Writer.Context) => new ApidocDirective(scanner, allClasses, ctx))
5758
}
5859
}.value
5960
)
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
<p>Api link to a class that is both in scaladsl and javadsl packages, but has no javadoc configured: <span class="group-java"><a href="akka-stream-scaladoc/akka/stream/javadsl/Flow.html" title="akka.stream.javadsl.Flow"><code>Flow</code></a></span><span class="group-scala"><a href="akka-stream-scaladoc/akka/stream/scaladsl/Flow.html" title="akka.stream.scaladsl.Flow"><code>Flow</code></a></span></p>
22
<p>Api link to a class that is in common package, but has no javadoc configured <span class="group-scala"><a href="akka-stream-scaladoc/akka/stream/stage/GraphStage.html" title="akka.stream.stage.GraphStage"><code>GraphStage</code></a></span><span class="group-java"><a href="akka-stream-scaladoc/akka/stream/stage/GraphStage.html" title="akka.stream.stage.GraphStage"><code>GraphStage</code></a></span></p>
3-
<p>Api link to a class that is both in scaladsl and javadsl packages, and has javadoc configured: <span class="group-java"><a href="akka-javadoc/?akka/actor/typed/javadsl/Behaviors.html" title="akka.actor.typed.javadsl.Behaviors"><code>Behaviors</code></a></span><span class="group-scala"><a href="akka-scaladoc/akka/actor/typed/scaladsl/Behaviors.html" title="akka.actor.typed.scaladsl.Behaviors"><code>Behaviors</code></a></span></p>
3+
<p>Api link to a class that is both in scaladsl and javadsl packages, and has javadoc configured: <span class="group-java"><a href="akka-javadoc/?akka/actor/typed/javadsl/Behaviors.html" title="akka.actor.typed.javadsl.Behaviors"><code>Behaviors</code></a></span><span class="group-scala"><a href="akka-scaladoc/akka/actor/typed/scaladsl/Behaviors$.html" title="akka.actor.typed.scaladsl.Behaviors"><code>Behaviors</code></a></span></p>

src/sbt-test/apidoc/scaladoc-for-java-class/src/main/paradox/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,4 +2,4 @@ Api link to a class that is both in scaladsl and javadsl packages, but has no ja
22

33
Api link to a class that is in common package, but has no javadoc configured @apidoc[GraphStage]
44

5-
Api link to a class that is both in scaladsl and javadsl packages, and has javadoc configured: @apidoc[Behaviors]
5+
Api link to a class that is both in scaladsl and javadsl packages, and has javadoc configured: @apidoc[Behaviors$]

src/test/scala/com/lightbend/paradox/apidoc/ApidocDirectiveSpec.scala

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,14 +20,14 @@ import java.io.IOException
2020

2121
import com.lightbend.paradox.ParadoxException
2222
import com.lightbend.paradox.markdown.{MarkdownTestkit, Writer}
23-
23+
import io.github.classgraph.ScanResult
2424
import org.scalatest.matchers.should.Matchers
2525
import org.scalatest.flatspec.AnyFlatSpecLike
2626

2727
class ApidocDirectiveSpec extends MarkdownTestkit with Matchers with AnyFlatSpecLike {
2828
val rootPackage = "akka"
2929

30-
val allClasses = Array(
30+
val allClasses = Vector(
3131
"akka.actor.ActorRef",
3232
"akka.actor.typed.ActorRef",
3333
"akka.cluster.client.ClusterClient",
@@ -54,6 +54,12 @@ class ApidocDirectiveSpec extends MarkdownTestkit with Matchers with AnyFlatSpec
5454
"akka.stream.scaladsl.Flow$",
5555
"akka.stream.scaladsl.JavaFlowSupport$",
5656
"akka.stream.javadsl.JavaFlowSupport",
57+
"akka.kafka.Metadata",
58+
"akka.kafka.Metadata$",
59+
"akka.kafka.javadsl.Producer",
60+
"akka.kafka.javadsl.Producer$",
61+
"akka.kafka.scaladsl.Producer",
62+
"akka.kafka.scaladsl.Producer$",
5763
"akka.kafka.scaladsl.Consumer$Control",
5864
"akka.kafka.javadsl.Consumer$Control",
5965
"akka.kafka.scaladsl.Consumer$Control$$anonfun$drainAndShutdown$2",
@@ -64,7 +70,13 @@ class ApidocDirectiveSpec extends MarkdownTestkit with Matchers with AnyFlatSpec
6470
linkRenderer = Writer.defaultLinks,
6571
verbatimSerializers = Writer.defaultVerbatims,
6672
serializerPlugins = Writer.defaultPlugins(
67-
Writer.defaultDirectives ++ Seq((ctx: Writer.Context) => new ApidocDirective(allClasses, ctx))
73+
Writer.defaultDirectives ++ Seq((ctx: Writer.Context) =>
74+
new ApidocDirective(null: ScanResult, allClasses, ctx) {
75+
override def containsOnlyStaticForwarders(classname: String): Boolean =
76+
"akka.kafka.Metadata" == classname ||
77+
"akka.kafka.scaladsl.Producer" == classname
78+
}
79+
)
6880
)
6981
)
7082

@@ -94,6 +106,24 @@ class ApidocDirectiveSpec extends MarkdownTestkit with Matchers with AnyFlatSpec
94106
"No matches found for apidoc query [ThereIsNoSuchClass]"
95107
}
96108

109+
it should "fail when pointing to a static-forwarders-only class" in {
110+
val thrown = the[ParadoxException] thrownBy markdown("@apidoc[Metadata]")
111+
thrown.getMessage shouldEqual
112+
"Class `akka.kafka.Metadata` matches @apidoc[Metadata], but is empty, did you intend to link to the object?"
113+
}
114+
115+
it should "fail when pointing to a static-forwarders-only class (in scaladsl/javadsl)" in {
116+
val thrown = the[ParadoxException] thrownBy markdown("@apidoc[Producer]")
117+
thrown.getMessage shouldEqual
118+
"Class `akka.kafka.scaladsl.Producer` matches @apidoc[Producer], but is empty, did you intend to link to the object?"
119+
}
120+
121+
it should "fail when pointing to a static-forwarders-only class (with pattern)" in {
122+
val thrown = the[ParadoxException] thrownBy markdown("@apidoc[akka.kafka.(java|scala)dsl.Producer]")
123+
thrown.getMessage shouldEqual
124+
"Class `akka.kafka.scaladsl.Producer` matches @apidoc[akka.kafka.(java|scala)dsl.Producer], but is empty, did you intend to link to the object?"
125+
}
126+
97127
it should "generate markdown correctly when 2 matches found and their package names include javadsl/scaladsl" in {
98128
markdown("@apidoc[Flow]") shouldEqual
99129
html(

0 commit comments

Comments
 (0)