Skip to content

Commit b9cf7f2

Browse files
authored
Merge pull request #471 from scala-js/topic/mima
Poor Man's MiMa
2 parents 014eedd + 69df48c commit b9cf7f2

File tree

15 files changed

+49318
-3
lines changed

15 files changed

+49318
-3
lines changed

.github/pull_request_template.md

+8
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
Hi there!
2+
3+
Before submitting a PR containing any Scala changes, please make sure you...
4+
5+
* run `sbt prePR`
6+
* commit changes to `api-reports`
7+
8+
Thanks for contributing!

.github/workflows/ci.yml

+14-1
Original file line numberDiff line numberDiff line change
@@ -21,20 +21,33 @@ jobs:
2121
env:
2222
SCALAJS_VERSION: "${{ matrix.scalajsversion == '0.6.x' && '0.6.28' || '' }}"
2323
steps:
24+
2425
- uses: actions/checkout@v2
2526
- uses: olafurpg/setup-scala@v13
2627
with:
2728
java-version: "[email protected]"
2829
- uses: coursier/cache-action@v6
30+
31+
- name: Hacks for Scala 2.10
32+
if: matrix.scalaversion == '2.10.7'
33+
run: ./prepareForScala210.sh
34+
2935
- name: Build
3036
run: sbt "++${{ matrix.scalaversion }}" package
37+
3138
- name: Test generate documentation
3239
run: sbt "++${{ matrix.scalaversion }}" doc
40+
3341
- name: Build examples
3442
run: sbt "++${{ matrix.scalaversion }}" example/compile
35-
- name: scalafmt
43+
44+
- name: Validate formatting
3645
run: sbt "++${{ matrix.scalaversion }}" scalafmtCheck
3746

47+
- name: Validate api report
48+
if: matrix.scalajsversion == '1.x' && matrix.scalaversion != '2.11.12'
49+
run: ./api-reports/validate "${{ matrix.scalaversion }}"
50+
3851
readme:
3952
runs-on: ubuntu-latest
4053
steps:

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
.idea_modules
77
.metals
88
.project
9+
.sbtboot
910
.settings/
1011
.vscode
1112
metals.sbt

.scalafix.conf

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
rules = [
2+
GenerateApiReport,
3+
]

api-reports/2_12.txt

+24,453
Large diffs are not rendered by default.

api-reports/2_13.txt

+24,453
Large diffs are not rendered by default.

api-reports/validate

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
cd "$(dirname "$0")"
4+
5+
[ $# -ne 1 ] && echo "Usage: $0 <scala version>" && exit 1
6+
7+
series="${1%.*}"
8+
file="${series/./_}.txt"
9+
echo -n "Validating $file ... "
10+
11+
help='Run `sbt +compile` and check in the differences to the '"$(basename "$0") directory"
12+
13+
if [ ! -e "$file" ]; then
14+
echo "file not found. $help"
15+
exit 2
16+
elif [ -n "$(git status --porcelain -- "$file")" ]; then
17+
echo "out-of-date. $help"
18+
exit 3
19+
else
20+
echo "ok"
21+
fi

build.sbt

+26-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1+
import _root_.scalafix.sbt.BuildInfo.scalafixVersion // delete if Scala 2.10
12
import scalatex.ScalatexReadme
23

3-
lazy val root = project.in(file(".")).
4-
enablePlugins(ScalaJSPlugin)
4+
ThisBuild / shellPrompt := ((s: State) => Project.extract(s).currentRef.project + "> ")
5+
6+
lazy val scalafixRules = project
7+
.in(file("scalafix"))
8+
.settings(
9+
libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % scalafixVersion, // delete if Scala 2.10
10+
)
11+
12+
lazy val root = project
13+
.in(file("."))
14+
.enablePlugins(ScalaJSPlugin)
15+
.enablePlugins(ScalafixPlugin) // delete if Scala 2.10
16+
.dependsOn(scalafixRules % ScalafixConfig) // delete if Scala 2.10
517

618
name := "Scala.js DOM"
719

@@ -110,3 +122,15 @@ lazy val example = project.
110122
settings(commonSettings: _*).
111123
settings(noPublishSettings: _*).
112124
dependsOn(root)
125+
126+
addCommandAlias("prePR", "+prePR_nonCross")
127+
128+
val prePR_nonCross = taskKey[Unit]("Performs all necessary work required before submitting a PR, for a single version of Scala.")
129+
130+
ThisBuild / prePR_nonCross := Def.sequential(
131+
root / clean,
132+
root / Compile / scalafmt,
133+
root / Compile / compile,
134+
(root / Compile / scalafix).toTask(""), // delete if Scala 2.10
135+
example / Compile / compile,
136+
).value

prepareForScala210.sh

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/bin/bash
2+
set -euo pipefail
3+
cd "$(dirname "$0")"
4+
5+
sed -i -e '/delete if Scala 2.10/d' *.sbt project/*.sbt
6+
rm scalafix.sbt

project/plugins.sbt

+2
Original file line numberDiff line numberDiff line change
@@ -8,3 +8,5 @@ addSbtPlugin("com.lihaoyi" % "scalatex-sbt-plugin" % "0.3.11")
88
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0")
99

1010
addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7")
11+
12+
addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.29") // delete if Scala 2.10

scalafix.sbt

+10
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
ThisBuild / semanticdbEnabled := true
2+
ThisBuild / semanticdbVersion := "4.4.27"
3+
ThisBuild / scalafixScalaBinaryVersion := CrossVersion.binaryScalaVersion(scalaVersion.value)
4+
5+
ThisBuild / scalacOptions ++= {
6+
if (scalaVersion.value startsWith "2")
7+
"-Yrangepos" :: Nil
8+
else
9+
Nil
10+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
org.scalajs.dom.scalafix.GenerateApiReport
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
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+
private[this] def enabled = state ne null
12+
13+
override def beforeStart(): Unit = {
14+
Util.scalaSeriesVer match {
15+
case "2.11" => // disabled - can't read classfiles
16+
case _ => MutableState.global = new MutableState // can't set state= in early Scala versions
17+
}
18+
}
19+
20+
override def fix(implicit doc: SemanticDocument): Patch = {
21+
22+
if (enabled)
23+
doc.tree.traverse {
24+
case a: Defn.Class => process(a.symbol, a.templ, ScopeType.Class)
25+
case a: Defn.Object => process(a.symbol, a.templ, ScopeType.Object)
26+
case a: Defn.Trait => process(a.symbol, a.templ, ScopeType.Trait)
27+
case a: Pkg.Object => process(a.symbol, a.templ, ScopeType.Object)
28+
case _ =>
29+
}
30+
31+
Patch.empty
32+
}
33+
34+
private def process(sym: Symbol, body: Template, typ: ScopeType)(implicit doc: SemanticDocument): Unit = {
35+
// Skip non-public scopes
36+
val info = sym.info.get
37+
if (!info.isPublic && !info.isPackageObject)
38+
return
39+
40+
val parents = Util.parents(sym).iterator.map(Util.typeSymbol).toList
41+
val domParents = parents.iterator.filter(isScalaJsDom).toSet
42+
val isJsType = parents.exists(isScalaJs)
43+
val s = state.register(sym, isJsType, typ, domParents)
44+
45+
def letsSeeHowLazyWeCanBeLol(t: Tree): Unit = {
46+
// Skip non-public members
47+
if (!t.symbol.info.get.isPublic)
48+
return
49+
50+
// Remove definition bodies
51+
val t2: Tree =
52+
t match {
53+
case Defn.Def(mods, name, tparams, paramss, Some(tpe), _) => Decl.Def(mods, name, tparams, paramss, tpe)
54+
case Defn.Val(mods, pats, Some(tpe), _) => Decl.Val(mods, pats, tpe)
55+
case Defn.Var(mods, pats, Some(tpe), _) => Decl.Var(mods, pats, tpe)
56+
case _ => t
57+
}
58+
59+
val desc =
60+
t2
61+
.toString
62+
.replace('\n', ' ')
63+
.replace("=", " = ")
64+
.replace("@inline ", "")
65+
.replaceAll(", *", ", ")
66+
.replaceAll(" {2,}", " ")
67+
.trim
68+
.stripSuffix(" = js.native")
69+
.replaceAll(" = js.native(?=[^\n])", "?")
70+
71+
s.add(desc)
72+
}
73+
74+
body.traverse {
75+
76+
// Skip inner members that we collect at the outer scope
77+
case _: Defn.Class =>
78+
case _: Defn.Object =>
79+
case _: Defn.Trait =>
80+
case _: Pkg.Object =>
81+
82+
case d: Decl => letsSeeHowLazyWeCanBeLol(d)
83+
case d: Defn => letsSeeHowLazyWeCanBeLol(d)
84+
85+
case _ =>
86+
}
87+
}
88+
89+
private def isScalaJs(sym: Symbol): Boolean =
90+
sym.toString startsWith "scala/scalajs/js/"
91+
92+
private def isScalaJsDom(sym: Symbol): Boolean =
93+
sym.toString startsWith "org/scalajs/dom/"
94+
95+
override def afterComplete(): Unit =
96+
if (enabled) {
97+
saveReport()
98+
MutableState.global = null // can't set state= in early Scala versions
99+
}
100+
101+
private def saveReport(): Unit = {
102+
val scalaVer = Util.scalaSeriesVer.replace('.', '_')
103+
val projectRoot = System.getProperty("user.dir")
104+
val reportFile = Paths.get(s"$projectRoot/api-reports/$scalaVer.txt")
105+
val api = state.result().iterator.map(_.stripPrefix("org/scalajs/dom/")).mkString("\n")
106+
107+
val content =
108+
s"""|scala-js-dom API
109+
|================
110+
|
111+
|This is generated automatically on compile via custom Scalafix rule '${name.value}'.
112+
|
113+
|Flags:
114+
| [J-] = JavaScript type
115+
| [S-] = Scala type
116+
| [-${ScopeType.Class.id}] = Class
117+
| [-${ScopeType.Trait.id}] = Trait
118+
| [-${ScopeType.Object.id}] = Object
119+
|
120+
|
121+
|$api
122+
|""".stripMargin
123+
124+
println(s"[info] Generating $reportFile")
125+
Files.write(reportFile, content.getBytes(StandardCharsets.UTF_8))
126+
}
127+
}
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 name = root.symbol.value.stripSuffix("#").stripSuffix(".")
59+
val prefix = {
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 && !name.endsWith("/package"))
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+
}

0 commit comments

Comments
 (0)