-
Notifications
You must be signed in to change notification settings - Fork 161
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
Poor Man's MiMa #471
Changes from all commits
31b8a81
293419b
84ff47e
31ef32e
978e4a5
804c11b
0548f7f
90622a5
fb7ecc2
3744649
69df48c
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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! |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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: | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,6 +6,7 @@ | |
.idea_modules | ||
.metals | ||
.project | ||
.sbtboot | ||
.settings/ | ||
.vscode | ||
metals.sbt | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
rules = [ | ||
GenerateApiReport, | ||
] |
Large diffs are not rendered by default.
Large diffs are not rendered by default.
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 |
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 😩 |
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)) | ||
} | ||
} |
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) | ||
} | ||
} |
There was a problem hiding this comment.
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