Skip to content

Commit

Permalink
Merge pull request #471 from scala-js/topic/mima
Browse files Browse the repository at this point in the history
Poor Man's MiMa
  • Loading branch information
japgolly authored Aug 12, 2021
2 parents 014eedd + 69df48c commit b9cf7f2
Show file tree
Hide file tree
Showing 15 changed files with 49,318 additions and 3 deletions.
8 changes: 8 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
Hi there!

Before submitting a PR containing any Scala changes, please make sure you...

* run `sbt prePR`
* commit changes to `api-reports`

Thanks for contributing!
15 changes: 14 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,20 +21,33 @@ jobs:
env:
SCALAJS_VERSION: "${{ matrix.scalajsversion == '0.6.x' && '0.6.28' || '' }}"
steps:

- uses: actions/checkout@v2
- uses: olafurpg/setup-scala@v13
with:
java-version: "[email protected]"
- uses: coursier/cache-action@v6

- name: Hacks for Scala 2.10
if: matrix.scalaversion == '2.10.7'
run: ./prepareForScala210.sh

- name: Build
run: sbt "++${{ matrix.scalaversion }}" package

- name: Test generate documentation
run: sbt "++${{ matrix.scalaversion }}" doc

- name: Build examples
run: sbt "++${{ matrix.scalaversion }}" example/compile
- name: scalafmt

- name: Validate formatting
run: sbt "++${{ matrix.scalaversion }}" scalafmtCheck

- name: Validate api report
if: matrix.scalajsversion == '1.x' && matrix.scalaversion != '2.11.12'
run: ./api-reports/validate "${{ matrix.scalaversion }}"

readme:
runs-on: ubuntu-latest
steps:
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
.idea_modules
.metals
.project
.sbtboot
.settings/
.vscode
metals.sbt
Expand Down
3 changes: 3 additions & 0 deletions .scalafix.conf
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
rules = [
GenerateApiReport,
]
24,453 changes: 24,453 additions & 0 deletions api-reports/2_12.txt

Large diffs are not rendered by default.

24,453 changes: 24,453 additions & 0 deletions api-reports/2_13.txt

Large diffs are not rendered by default.

21 changes: 21 additions & 0 deletions api-reports/validate
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"

[ $# -ne 1 ] && echo "Usage: $0 <scala version>" && exit 1

series="${1%.*}"
file="${series/./_}.txt"
echo -n "Validating $file ... "

help='Run `sbt +compile` and check in the differences to the '"$(basename "$0") directory"

if [ ! -e "$file" ]; then
echo "file not found. $help"
exit 2
elif [ -n "$(git status --porcelain -- "$file")" ]; then
echo "out-of-date. $help"
exit 3
else
echo "ok"
fi
28 changes: 26 additions & 2 deletions build.sbt
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
import _root_.scalafix.sbt.BuildInfo.scalafixVersion // delete if Scala 2.10
import scalatex.ScalatexReadme

lazy val root = project.in(file(".")).
enablePlugins(ScalaJSPlugin)
ThisBuild / shellPrompt := ((s: State) => Project.extract(s).currentRef.project + "> ")

lazy val scalafixRules = project
.in(file("scalafix"))
.settings(
libraryDependencies += "ch.epfl.scala" %% "scalafix-core" % scalafixVersion, // delete if Scala 2.10
)

lazy val root = project
.in(file("."))
.enablePlugins(ScalaJSPlugin)
.enablePlugins(ScalafixPlugin) // delete if Scala 2.10
.dependsOn(scalafixRules % ScalafixConfig) // delete if Scala 2.10

name := "Scala.js DOM"

Expand Down Expand Up @@ -110,3 +122,15 @@ lazy val example = project.
settings(commonSettings: _*).
settings(noPublishSettings: _*).
dependsOn(root)

addCommandAlias("prePR", "+prePR_nonCross")

val prePR_nonCross = taskKey[Unit]("Performs all necessary work required before submitting a PR, for a single version of Scala.")

ThisBuild / prePR_nonCross := Def.sequential(
root / clean,
root / Compile / scalafmt,
root / Compile / compile,
(root / Compile / scalafix).toTask(""), // delete if Scala 2.10
example / Compile / compile,
).value
6 changes: 6 additions & 0 deletions prepareForScala210.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/bin/bash
set -euo pipefail
cd "$(dirname "$0")"

sed -i -e '/delete if Scala 2.10/d' *.sbt project/*.sbt
rm scalafix.sbt
2 changes: 2 additions & 0 deletions project/plugins.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ addSbtPlugin("com.lihaoyi" % "scalatex-sbt-plugin" % "0.3.11")
addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.0.0")

addSbtPlugin("com.geirsson" % "sbt-ci-release" % "1.5.7")

addSbtPlugin("ch.epfl.scala" % "sbt-scalafix" % "0.9.29") // delete if Scala 2.10
10 changes: 10 additions & 0 deletions scalafix.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
ThisBuild / semanticdbEnabled := true
ThisBuild / semanticdbVersion := "4.4.27"
ThisBuild / scalafixScalaBinaryVersion := CrossVersion.binaryScalaVersion(scalaVersion.value)

ThisBuild / scalacOptions ++= {
if (scalaVersion.value startsWith "2")
"-Yrangepos" :: Nil
else
Nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
org.scalajs.dom.scalafix.GenerateApiReport
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package org.scalajs.dom.scalafix

import java.nio.charset.StandardCharsets
import java.nio.file.{Paths, Files}
import scala.meta._
import scalafix.v1._

class GenerateApiReport extends SemanticRule("GenerateApiReport") {
import MutableState.{global => state, ScopeType}

private[this] def enabled = state ne null

override def beforeStart(): Unit = {
Util.scalaSeriesVer match {
case "2.11" => // disabled - can't read classfiles
case _ => MutableState.global = new MutableState // can't set state= in early Scala versions
}
}

override def fix(implicit doc: SemanticDocument): Patch = {

if (enabled)
doc.tree.traverse {
case a: Defn.Class => process(a.symbol, a.templ, ScopeType.Class)
case a: Defn.Object => process(a.symbol, a.templ, ScopeType.Object)
case a: Defn.Trait => process(a.symbol, a.templ, ScopeType.Trait)
case a: Pkg.Object => process(a.symbol, a.templ, ScopeType.Object)
case _ =>
}

Patch.empty
}

private def process(sym: Symbol, body: Template, typ: ScopeType)(implicit doc: SemanticDocument): Unit = {
// Skip non-public scopes
val info = sym.info.get
if (!info.isPublic && !info.isPackageObject)
return

val parents = Util.parents(sym).iterator.map(Util.typeSymbol).toList
val domParents = parents.iterator.filter(isScalaJsDom).toSet
val isJsType = parents.exists(isScalaJs)
val s = state.register(sym, isJsType, typ, domParents)

def letsSeeHowLazyWeCanBeLol(t: Tree): Unit = {
// Skip non-public members
if (!t.symbol.info.get.isPublic)
return

// Remove definition bodies
val t2: Tree =
t match {
case Defn.Def(mods, name, tparams, paramss, Some(tpe), _) => Decl.Def(mods, name, tparams, paramss, tpe)
case Defn.Val(mods, pats, Some(tpe), _) => Decl.Val(mods, pats, tpe)
case Defn.Var(mods, pats, Some(tpe), _) => Decl.Var(mods, pats, tpe)
case _ => t
}

val desc =
t2
.toString
.replace('\n', ' ')
.replace("=", " = ")
.replace("@inline ", "")
.replaceAll(", *", ", ")
.replaceAll(" {2,}", " ")
.trim
.stripSuffix(" = js.native")
.replaceAll(" = js.native(?=[^\n])", "?")

s.add(desc)
}

body.traverse {

// Skip inner members that we collect at the outer scope
case _: Defn.Class =>
case _: Defn.Object =>
case _: Defn.Trait =>
case _: Pkg.Object =>

case d: Decl => letsSeeHowLazyWeCanBeLol(d)
case d: Defn => letsSeeHowLazyWeCanBeLol(d)

case _ =>
}
}

private def isScalaJs(sym: Symbol): Boolean =
sym.toString startsWith "scala/scalajs/js/"

private def isScalaJsDom(sym: Symbol): Boolean =
sym.toString startsWith "org/scalajs/dom/"

override def afterComplete(): Unit =
if (enabled) {
saveReport()
MutableState.global = null // can't set state= in early Scala versions
}

private def saveReport(): Unit = {
val scalaVer = Util.scalaSeriesVer.replace('.', '_')
val projectRoot = System.getProperty("user.dir")
val reportFile = Paths.get(s"$projectRoot/api-reports/$scalaVer.txt")
val api = state.result().iterator.map(_.stripPrefix("org/scalajs/dom/")).mkString("\n")

val content =
s"""|scala-js-dom API
|================
|
|This is generated automatically on compile via custom Scalafix rule '${name.value}'.
|
|Flags:
| [J-] = JavaScript type
| [S-] = Scala type
| [-${ScopeType.Class.id}] = Class
| [-${ScopeType.Trait.id}] = Trait
| [-${ScopeType.Object.id}] = Object
|
|
|$api
|""".stripMargin

println(s"[info] Generating $reportFile")
Files.write(reportFile, content.getBytes(StandardCharsets.UTF_8))
}
}
108 changes: 108 additions & 0 deletions scalafix/src/main/scala/org/scalajs/dom/scalafix/MutableState.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
package org.scalajs.dom.scalafix

import scala.annotation.tailrec
import scala.collection.immutable.SortedSet
import scala.collection.mutable
import scala.meta._
import scalafix.v1._

final class MutableState {
import MutableState._

private[this] val scopes = mutable.Map.empty[Symbol, Scope]

def register(sym: Symbol, isJsType: Boolean, scopeType: ScopeType, parents: Set[Symbol]): Scope = synchronized {
scopes.get(sym) match {
case None =>
val s = Scope(sym)(scopeType, parents)
scopes.update(sym, s)
s.isJsType = isJsType
s
case Some(s) =>
s
}
}

private def scopeParents(root: Scope): List[Scope] = {
@tailrec
def go(s: Scope, seen: Set[Symbol], queue: Set[Symbol], results: List[Scope]): List[Scope] =
if (!seen.contains(s.symbol))
go(s, seen + s.symbol, queue ++ s.parents, s :: results)
else if (queue.nonEmpty) {
val sym = queue.head
val nextQueue = queue - sym
scopes.get(sym) match {
case Some(scope) => go(scope, seen, nextQueue, results)
case None => go(s, seen, nextQueue, results)
}
} else
results

go(root, Set.empty, Set.empty, Nil)
}

def result(): Array[String] = synchronized {
// Because - comes before . in ASCII this little hack affects the ordering so that A[X] comes before A.B[X]
val sortHack = "-OMG-"

val b = SortedSet.newBuilder[String]

// Pass 1
for (root <- scopes.valuesIterator) {
if (!root.isJsType && scopeParents(root).exists(_.isJsType))
root.isJsType = true
}

// Pass 2
for (root <- scopes.valuesIterator) {
val name = root.symbol.value.stripSuffix("#").stripSuffix(".")
val prefix = {
val lang = if (root.isJsType) "J" else "S"
val typ = root.scopeType.id
s"$name$sortHack[$lang$typ] "
}

var membersFound = false
for {
s <- root :: scopeParents(root)
v <- s.directMembers
} {
membersFound = true
b += prefix + v
}

if (!membersFound && !name.endsWith("/package"))
b += prefix.trim
}

val array = b.result().toArray
for (i <- array.indices)
array(i) = array(i).replace(sortHack, "")
array
}
}

object MutableState {
var global: MutableState = null

sealed abstract class ScopeType(final val id: String)
object ScopeType {
case object Class extends ScopeType("C")
case object Trait extends ScopeType("T")
case object Object extends ScopeType("O")
}

final case class Scope(symbol: Symbol)
(val scopeType: ScopeType,
val parents: Set[Symbol]) {

private[MutableState] val directMembers = mutable.Set.empty[String]
private[MutableState] var isJsType = false

def add(ov: Option[String]): Unit =
ov.foreach(add(_))

def add(v: String): Unit =
synchronized(directMembers += v)
}
}
Loading

0 comments on commit b9cf7f2

Please sign in to comment.