Skip to content

Commit 84ff47e

Browse files
committed
Generate an api.txt report on compile
1 parent 293419b commit 84ff47e

File tree

9 files changed

+24492
-17
lines changed

9 files changed

+24492
-17
lines changed

.scalafix.conf

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ rules = []
22

33
// Run automatically via (scalafixOnCompile := true)
44
triggered.rules = [
5-
Mima,
5+
GenerateApiReport,
66
]

api.txt

Lines changed: 24192 additions & 0 deletions
Large diffs are not rendered by default.

build.sbt

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
import scalatex.ScalatexReadme
22

3-
ThisBuild / shellPrompt := ((s: State) => Project.extract(s).currentRef.project + "> ")
3+
ThisBuild / shellPrompt := ((s: State) => Project.extract(s).currentRef.project + "> ")
44

55
lazy val root = project
66
.in(file("."))
77
.enablePlugins(ScalaJSPlugin)
88
.enablePlugins(ScalafixPlugin)
9-
.dependsOn(mima % ScalafixConfig)
9+
.dependsOn(scalafixRules % ScalafixConfig)
1010
.settings(scalafixOnCompile := true)
1111

1212
name := "Scala.js DOM"
@@ -121,8 +121,8 @@ lazy val example = project.
121121

122122
import _root_.scalafix.sbt.BuildInfo.scalafixVersion
123123

124-
lazy val mima = project
125-
.in(file("mima"))
124+
lazy val scalafixRules = project
125+
.in(file("scalafix"))
126126
.enablePlugins(ScalaJSPlugin)
127127
.settings(
128128
libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % scalafixVersion,

mima/src/main/resources/META-INF/services/scalafix.v1.Rule

Lines changed: 0 additions & 1 deletion
This file was deleted.

mima/src/main/scala/mima/Mima.scala

Lines changed: 0 additions & 11 deletions
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.scalajs.dom.scalafix.GenerateApiReport
Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
package org.scalajs.dom.scalafix
2+
3+
import java.nio.charset.StandardCharsets
4+
import java.nio.file.{Paths, Files}
5+
import scala.meta._
6+
import scalafix.v1._
7+
8+
class GenerateApiReport extends SemanticRule("GenerateApiReport") {
9+
import MutableState.{global => state, ScopeType}
10+
11+
override def beforeStart(): Unit = {
12+
state = new MutableState
13+
}
14+
15+
override def fix(implicit doc: SemanticDocument): Patch = {
16+
17+
doc.tree.traverse {
18+
// case a: Defn.Object if a.name.value.contains("AesCbcParams") => process(a.symbol, a.templ, ScopeType.Object)
19+
20+
case a: Defn.Class => process(a.symbol, a.templ, ScopeType.Class)
21+
case a: Defn.Object => process(a.symbol, a.templ, ScopeType.Object)
22+
case a: Defn.Trait => process(a.symbol, a.templ, ScopeType.Trait)
23+
case a: Pkg.Object => process(a.symbol, a.templ, ScopeType.Object)
24+
case _ =>
25+
}
26+
27+
Patch.empty
28+
}
29+
30+
private def process(sym: Symbol, body: Template, typ: ScopeType)(implicit doc: SemanticDocument): Unit = {
31+
// Skip non-public API
32+
if (!sym.info.get.isPublic)
33+
return
34+
35+
val parents = Util.parents(sym).iterator.map(Util.typeSymbol).toList
36+
val domParents = parents.iterator.filter(isScalaJsDom).toSet
37+
val isJsType = parents.exists(isScalaJs)
38+
val s = state.register(sym, isJsType, typ, domParents)
39+
40+
def letsSeeHowLazyWeCanBeLol(t: Tree): Unit = {
41+
42+
// Skip non-public API
43+
if (!t.symbol.info.get.isPublic)
44+
return
45+
46+
// Remove definition bodies
47+
val t2: Tree =
48+
t match {
49+
case Defn.Def(mods, name, tparams, paramss, Some(tpe), _) => Decl.Def(mods, name, tparams, paramss, tpe)
50+
case Defn.Val(mods, pats, Some(tpe), _) => Decl.Val(mods, pats, tpe)
51+
case Defn.Var(mods, pats, Some(tpe), _) => Decl.Var(mods, pats, tpe)
52+
case _ => t
53+
}
54+
55+
val desc =
56+
t2
57+
.toString
58+
.replace('\n', ' ')
59+
.replace("=", " = ")
60+
.replace("@inline ", "")
61+
.replaceAll(", *", ", ")
62+
.replaceAll(" {2,}", " ")
63+
.trim
64+
.stripSuffix(" = js.native")
65+
.replaceAll(" = js.native(?=[^\n])", "?")
66+
67+
s.add(desc)
68+
}
69+
70+
body.traverse {
71+
72+
// Skip inner members that we collect at the outer scope
73+
case _: Defn.Class =>
74+
case _: Defn.Object =>
75+
case _: Defn.Trait =>
76+
case _: Pkg.Object =>
77+
78+
case d: Decl => letsSeeHowLazyWeCanBeLol(d)
79+
case d: Defn => letsSeeHowLazyWeCanBeLol(d)
80+
81+
case _ =>
82+
}
83+
}
84+
85+
private def isScalaJs(sym: Symbol): Boolean =
86+
sym.toString startsWith "scala/scalajs/js/"
87+
88+
private def isScalaJsDom(sym: Symbol): Boolean =
89+
sym.toString startsWith "org/scalajs/dom/"
90+
91+
override def afterComplete(): Unit = {
92+
saveReport()
93+
state = null
94+
}
95+
96+
private def saveReport(): Unit = {
97+
val projectRoot = System.getProperty("user.dir")
98+
val reportFile = Paths.get(s"$projectRoot/api.txt")
99+
val api = state.result().iterator.map(_.stripPrefix("org/scalajs/dom/")).mkString("\n")
100+
101+
val content =
102+
s"""|scala-js-dom API
103+
|================
104+
|
105+
|This is generated automatically on compile via custom Scalafix rule '${name.value}'.
106+
|
107+
|Flags:
108+
| [J-] = JavaScript type
109+
| [S-] = Scala type
110+
| [-${ScopeType.Class.id}] = Class
111+
| [-${ScopeType.Trait.id}] = Trait
112+
| [-${ScopeType.Object.id}] = Object
113+
|
114+
|
115+
|$api
116+
|""".stripMargin
117+
118+
Files.write(reportFile, content.getBytes(StandardCharsets.UTF_8))
119+
}
120+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
package org.scalajs.dom.scalafix
2+
3+
import scala.annotation.tailrec
4+
import scala.collection.immutable.SortedSet
5+
import scala.collection.mutable
6+
import scala.meta._
7+
import scalafix.v1._
8+
9+
final class MutableState {
10+
import MutableState._
11+
12+
private[this] val scopes = mutable.Map.empty[Symbol, Scope]
13+
14+
def register(sym: Symbol, isJsType: Boolean, scopeType: ScopeType, parents: Set[Symbol]): Scope = synchronized {
15+
scopes.get(sym) match {
16+
case None =>
17+
val s = Scope(sym)(scopeType, parents)
18+
scopes.update(sym, s)
19+
s.isJsType = isJsType
20+
s
21+
case Some(s) =>
22+
s
23+
}
24+
}
25+
26+
private def scopeParents(root: Scope): List[Scope] = {
27+
@tailrec
28+
def go(s: Scope, seen: Set[Symbol], queue: Set[Symbol], results: List[Scope]): List[Scope] =
29+
if (!seen.contains(s.symbol))
30+
go(s, seen + s.symbol, queue ++ s.parents, s :: results)
31+
else if (queue.nonEmpty) {
32+
val sym = queue.head
33+
val nextQueue = queue - sym
34+
scopes.get(sym) match {
35+
case Some(scope) => go(scope, seen, nextQueue, results)
36+
case None => go(s, seen, nextQueue, results)
37+
}
38+
} else
39+
results
40+
41+
go(root, Set.empty, Set.empty, Nil)
42+
}
43+
44+
def result(): Array[String] = synchronized {
45+
// Because - comes before . in ASCII this little hack affects the ordering so that A[X] comes before A.B[X]
46+
val sortHack = "-OMG-"
47+
48+
val b = SortedSet.newBuilder[String]
49+
50+
// Pass 1
51+
for (root <- scopes.valuesIterator) {
52+
if (!root.isJsType && scopeParents(root).exists(_.isJsType))
53+
root.isJsType = true
54+
}
55+
56+
// Pass 2
57+
for (root <- scopes.valuesIterator) {
58+
val prefix = {
59+
val name = root.symbol.value.stripSuffix("#").stripSuffix(".")
60+
val lang = if (root.isJsType) "J" else "S"
61+
val typ = root.scopeType.id
62+
s"$name$sortHack[$lang$typ] "
63+
}
64+
65+
var membersFound = false
66+
for {
67+
s <- root :: scopeParents(root)
68+
v <- s.directMembers
69+
} {
70+
membersFound = true
71+
b += prefix + v
72+
}
73+
74+
if (!membersFound)
75+
b += prefix.trim
76+
}
77+
78+
val array = b.result().toArray
79+
for (i <- array.indices)
80+
array(i) = array(i).replace(sortHack, "")
81+
array
82+
}
83+
}
84+
85+
object MutableState {
86+
var global: MutableState = null
87+
88+
sealed abstract class ScopeType(final val id: String)
89+
object ScopeType {
90+
case object Class extends ScopeType("C")
91+
case object Trait extends ScopeType("T")
92+
case object Object extends ScopeType("O")
93+
}
94+
95+
final case class Scope(symbol: Symbol)
96+
(val scopeType: ScopeType,
97+
val parents: Set[Symbol]) {
98+
99+
private[MutableState] val directMembers = mutable.Set.empty[String]
100+
private[MutableState] var isJsType = false
101+
102+
def add(ov: Option[String]): Unit =
103+
ov.foreach(add(_))
104+
105+
def add(v: String): Unit =
106+
synchronized(directMembers += v)
107+
}
108+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package org.scalajs.dom.scalafix
2+
3+
import scala.meta._
4+
import scalafix.v1._
5+
6+
object Util {
7+
8+
def parents(sym: Symbol)(implicit doc: SemanticDocument): List[SemanticType] =
9+
sym.info match {
10+
case Some(i) => parents(i.signature)
11+
case None => Nil
12+
}
13+
14+
def parents(sig: Signature)(implicit doc: SemanticDocument): List[SemanticType] =
15+
sig match {
16+
case x: ClassSignature => x.parents
17+
case _ => Nil
18+
}
19+
20+
// ===================================================================================================================
21+
22+
def termName(pats: Iterable[Pat]): Option[Term.Name] =
23+
pats.iterator.flatMap(termName(_)).nextOption()
24+
25+
def termName(pat: Pat): Option[Term.Name] =
26+
pat match {
27+
case x: Pat.Var => Some(x.name)
28+
case _ => None
29+
}
30+
31+
def termName(t: Tree): Option[Term.Name] =
32+
t match {
33+
case d: Decl.Def => Some(d.name)
34+
case d: Decl.Val => termName(d.pats)
35+
case d: Decl.Var => termName(d.pats)
36+
case d: Defn.Def => Some(d.name)
37+
case d: Defn.Val => termName(d.pats)
38+
case d: Defn.Var => termName(d.pats)
39+
// case d: Defn.Object => termName(d.pats) // ?
40+
case _ => None
41+
}
42+
43+
// ===================================================================================================================
44+
45+
def typeSymbol(t: SemanticType): Symbol =
46+
t match {
47+
case x: TypeRef => x.symbol
48+
case x: SingleType => x.symbol
49+
case x: ThisType => x.symbol
50+
case x: SuperType => x.symbol
51+
// case x: ConstantType => xxx // (constant) =>
52+
// case x: IntersectionType => xxx // (types) =>
53+
// case x: UnionType => xxx // (types) =>
54+
// case x: WithType => xxx // (types) =>
55+
// case x: StructuralType => xxx // (tpe, declarations) =>
56+
// case x: AnnotatedType => xxx // (annotations, tpe) =>
57+
// case x: ExistentialType => xxx // (tpe, declarations) =>
58+
// case x: UniversalType => xxx // (typeParameters, tpe) =>
59+
// case x: ByNameType => xxx // (tpe) =>
60+
// case x: RepeatedType => xxx // (tpe) =>
61+
// case NoType => Symbol.None
62+
case _ => Symbol.None
63+
}
64+
65+
66+
}

0 commit comments

Comments
 (0)