Skip to content

Poor Man's MiMa #471

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Aug 12, 2021
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
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 }}"
Comment on lines +47 to +49
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is gonna be great!! :D


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
Comment on lines +1 to +6
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So terrible!! Do we even need to publish for 2.10 other than to be nice?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nah just being nice and preserving compat across the 1.x series. Really looking forward to the 2.x branch 😩

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