Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,40 @@ It comes as a standalone library, a CLI tool and an sbt plugin.
Scala2PlantUML consumes [SemanticDB] files so you will need to know how to create those or simply follow the sbt setup
instructions below.

## Example

```shell
scala2plantuml \
--url 'https://repo1.maven.org/maven2/nz/co/bottech/scala2plantuml-example_2.13/0.2.0/scala2plantuml-example_2.13-0.2.0.jar'\
--project example \
"nz/co/bottech/scala2plantuml/example/Main."
```

```text
@startuml
class A extends B {
+ {method} <init>
+ {method} b
}
A o-- C
interface B {
+ {abstract} {method} b
}
B o-- C
class C {
+ {method} <init>
+ {field} value
}
C o-- A
class Main {
+ {static} {field} a
}
Main o-- A
@enduml
```

![Example Class Diagram](http://www.plantuml.com/plantuml/proxy?cache=no&src=https://raw.github.com/BotTech/scala2plantuml/main/example/example.md)

## sbt

### Enable SemanticDB
Expand Down Expand Up @@ -40,6 +74,21 @@ Create `~/.sbt/1.0/plugins/scala2PlantUML.sbt` containing:
addSbtPlugin("nz.co.bottech" % "sbt-scala2plantuml" % "0.2.0")
```

### Generate the Diagram

Run the `scala2PlantUML` task from sbt:

```sbt
scala2PlantUML "com/example/Foo#"
```

This accepts the following arguments:
- `--include`
- `--exclude`
- `--output`

Refer to the [CLI Usage](#usage) for the definition of these arguments.

## CLI

### Install
Expand Down Expand Up @@ -179,6 +228,18 @@ Each of these can be provided multiple times. The result will be all combination

> 🚧 TODO: Document Library.

## Limitations

- Only class diagrams are supported.
- Only inheritance or aggregations are supported, compositions are shown as aggregations.
- Aggregations are shown between types not between fields. There is a [bug][namespaced field links] in PlantUML which
prevents us from being able to do this reliably.
- There is no reliable way to determine the path to a SemanticDB file from any symbol.
If Scala2PlantUML is unable to find your symbols then the following may help:
- Only have a single top level type in each file.
- Ensure that the file name matches the type name.
- Nest any subclasses of a sealed class within the companion object of the sealed class.

[coursier]: https://get-coursier.io/docs/cli-install
[plantuml]: https://plantuml.com/
[semanticdb]: https://scalameta.org/docs/semanticdb/guide.html
30 changes: 21 additions & 9 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -82,16 +82,15 @@ inThisBuild(
// This needs to be set otherwise the GitHub workflow plugin gets confused about which
// version to use for the publish job.
scalaVersion := scala212,
versionPolicyIntention := Compatibility.BinaryAndSourceCompatible,
// TODO: Revert this after releasing 0.3.0.
//versionPolicyIntention := Compatibility.BinaryAndSourceCompatible,
versionPolicyIntention := Compatibility.None,
versionScheme := Some("early-semver")
)
)

val commonProjectSettings = List(
isScala213 := isScala213Setting.value,
// Who cares about these. Forwards binary compatibility is used as an approximation for source
// backwards compatibility and missing classes isn't a problem.
mimaForwardIssueFilters += "0.2.0" -> List(ProblemFilters.exclude[MissingClassProblem]("nz.co.bottech.scala2plantuml.*")),
name := s"${(LocalRootProject / name).value}-${name.value}",
scalastyleFailOnError := true,
scalastyleFailOnWarning := true,
Expand All @@ -110,14 +109,14 @@ val commonProjectSettings = List(
val metaProjectSettings = List(
mimaFailOnNoPrevious := false,
mimaPreviousArtifacts := Set.empty,
publish / skip := true
publish / skip := true,
versionPolicyCheck := Def.unit(())
)

lazy val root = (project in file("."))
.aggregate(cli, core, docs, example, integrationTests, sbtProject)
.settings(metaProjectSettings)
.settings(
crossScalaVersions := supportedScalaVersions,
name := "scala2plantuml",
// Workaround for https://github.com/olafurpg/sbt-ci-release/issues/181
// These have to go on the root project.
Expand Down Expand Up @@ -158,6 +157,10 @@ lazy val cli = project
"ch.qos.logback" % "logback-core" % logbackVersion,
"com.github.scopt" %% "scopt" % scoptVersion,
"org.slf4j" % "slf4j-api" % slf4jVersion
),
mimaForwardIssueFilters += "0.2.0" -> List(
// This is private so no harm done.
ProblemFilters.exclude[MissingClassProblem]("nz.co.bottech.scala2plantuml.ConfigParser$Terminated$")
)
)

Expand Down Expand Up @@ -211,7 +214,7 @@ lazy val sbtProject = (project in file("sbt"))
lazy val docs = (project in (file("meta") / "docs"))
// Include build info here so that we can override the version.
.enablePlugins(BuildInfoPlugin, MdocPlugin)
.dependsOn(cli)
.dependsOn(cli, example)
.settings(metaProjectSettings)
.settings(
// We use a different version setting so that it may depend on versionPolicyPreviousVersions
Expand All @@ -226,7 +229,8 @@ lazy val docs = (project in (file("meta") / "docs"))
},
mdocOut := (ThisBuild / baseDirectory).value,
mdocVariables := Map(
"VERSION" -> (mdoc / version).value
"VERSION" -> (mdoc / version).value,
"SCALA_VERSION" -> scalaMajorMinorVersion.value
),
unusedCompileDependenciesFilter -= moduleFilter("org.scalameta", "mdoc*"),
mdoc / version := versionPolicyPreviousVersions.value.lastOption.getOrElse(version.value)
Expand All @@ -237,9 +241,17 @@ lazy val example = project
.settings(
semanticdbEnabled := true,
semanticdbIncludeInJar := true,
semanticdbVersion := sdbVersion
semanticdbVersion := sdbVersion,
versionPolicyCheck := Def.unit(())
)

def scalaMajorMinorVersion: Def.Initialize[String] = Def.setting {
CrossVersion.partialVersion(scala213) match {
case Some((major, minor)) => s"$major.$minor"
case _ => throw new IllegalArgumentException("scalaVersion is malformed.")
}
}

def isScala213Setting: Def.Initialize[Boolean] = Def.setting {
CrossVersion.partialVersion(scalaVersion.value) match {
case Some((2, n)) if n == 13 => true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package nz.co.bottech.scala2plantuml
import scopt._

import java.io.File
import java.net.URI

object ConfigParser {

Expand Down Expand Up @@ -104,7 +105,7 @@ object ConfigParser {
opt[File]('j', "jar")
.valueName("<jar>")
.unbounded()
.action((jar, config) => config.addDirectory(jar))
.action((jar, config) => config.addFile(jar))
.text(
"""JAR containing META-INF/semanticdb/**/*.semanticdb files.
|
Expand All @@ -113,10 +114,10 @@ object ConfigParser {
|""".stripMargin
),
note(""),
opt[File]('u', "url")
opt[URI]('u', "url")
.valueName("<url>")
.unbounded()
.action((url, config) => config.addDirectory(url))
.action((url, config) => config.addURL(url.toURL))
.text(
"""A URL to a JAR containing META-INF/semanticdb/**/*.semanticdb files.
|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,15 @@ object ClassDiagramGenerator {
classloader: ClassLoader,
maxLevel: Option[Int] = None
): Seq[ClassDiagramElement] = {
val loader = new SemanticdbLoader(prefixes, classloader)
val loader = new SemanticDBLoader(prefixes, classloader)
val symbolTable = aggregateSymbolTable(loader)
val symbolIndex = new SymbolIndex(ignore, symbolTable)
val typeIndex = new TypeIndex(symbolIndex)
val definitionIndex = new DefinitionIndex(loader)
SymbolProcessor.processSymbol(symbol, maxLevel, symbolIndex, typeIndex, definitionIndex)
}

private def aggregateSymbolTable(loader: SemanticdbLoader) =
private def aggregateSymbolTable(loader: SemanticDBLoader) =
AggregateSymbolTable(
List(
new LazySymbolTable(loader),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ object ClassDiagramRenderer {
def render(elements: Seq[ClassDiagramElement], options: Options, writer: Writer): Unit = {
writer.write("@startuml\n")
renderSnippet(elements, options, writer)
writer.write("@enduml\n")
writer.write("@enduml")
}

def renderSnippetString(elements: Seq[ClassDiagramElement], options: Options): String =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import org.slf4j.LoggerFactory
import scala.collection.concurrent.TrieMap
import scala.meta.internal.semanticdb.SymbolOccurrence

private[scala2plantuml] class DefinitionIndex(loader: SemanticdbLoader) {
private[scala2plantuml] class DefinitionIndex(loader: SemanticDBLoader) {

private val logger = LoggerFactory.getLogger(classOf[DefinitionIndex])
private val cache = TrieMap.empty[String, Option[SymbolOccurrence]]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import scala.collection.concurrent.TrieMap
import scala.meta.internal.semanticdb.SymbolInformation
import scala.meta.internal.symtab.SymbolTable

private[scala2plantuml] class LazySymbolTable(loader: SemanticdbLoader) extends SymbolTable {
private[scala2plantuml] class LazySymbolTable(loader: SemanticDBLoader) extends SymbolTable {

private val logger = LoggerFactory.getLogger(classOf[LazySymbolTable])
private val cache = TrieMap.empty[String, SymbolInformation]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package nz.co.bottech.scala2plantuml

import nz.co.bottech.scala2plantuml.SemanticDBLoader._

import java.io.{File, FileInputStream, FilenameFilter, InputStream}
import java.net.URL
import java.nio.file.Paths
Expand All @@ -9,10 +11,7 @@ import scala.meta.internal.semanticdb.Scala._
import scala.meta.internal.semanticdb.{TextDocument, TextDocuments}
import scala.util.Using

private[scala2plantuml] class SemanticdbLoader(prefixes: Seq[String], classLoader: ClassLoader) {

type Errors = Vector[String]
type Result = Either[Errors, Seq[TextDocument]]
private[scala2plantuml] class SemanticDBLoader(prefixes: Seq[String], classLoader: ClassLoader) {

private val cache = TrieMap.empty[String, Result]

Expand Down Expand Up @@ -78,10 +77,23 @@ private[scala2plantuml] class SemanticdbLoader(prefixes: Seq[String], classLoade
TextDocuments.parseFrom(semanticdb).documents
}.toEither.left.map(error => Vector(error.getLocalizedMessage))

private def semanticdbPath(symbol: String): Either[Errors, String] =
if (symbol.isGlobal)
Right(s"${symbol.dropRight(1).takeWhile(_ != '#')}.scala.semanticdb")
else
}

private[scala2plantuml] object SemanticDBLoader {

private type Errors = Vector[String]
private type Result = Either[Errors, Seq[TextDocument]]

private[scala2plantuml] def semanticdbPath(symbol: String): Either[Errors, String] =
if (symbol.isGlobal) {
val a = symbol.lastIndexOf('/')
val (start, end) = symbol.splitAt(a + 1)
val prefix = if (start == "_empty" || start == "_root_") "" else start
val name = end.takeWhile(c => c != '#' && c != '.')
if (name == "package") {
Right(s"${prefix.init}.scala.semanticdb")
} else Right(s"$prefix$name.scala.semanticdb")
} else
Left(Vector(s"Symbol is not global: $symbol"))

private def packagePath(path: String): Option[String] = {
Expand All @@ -91,9 +103,10 @@ private[scala2plantuml] class SemanticdbLoader(prefixes: Seq[String], classLoade
}

private def findSemanticdbs(directory: File): Array[File] =
directory.listFiles(new FilenameFilter {
directory
.listFiles(new FilenameFilter {

override def accept(dir: File, name: String): Boolean =
name.endsWith(".semanticdb")
})
override def accept(dir: File, name: String): Boolean =
name.endsWith(".semanticdb")
})
}
Loading