Skip to content

Commit 5b617a4

Browse files
Preserve more information about unreachable symbols (scala-native#3537)
* Preserve more information about unreachable symbols including arg types and result type * Adapt logging unreachable symbol stackreaces and improve their formatting * Respect NO_COLOR env var when printing backtrace
1 parent 2e4b6d5 commit 5b617a4

File tree

3 files changed

+161
-56
lines changed

3 files changed

+161
-56
lines changed

tools/src/main/scala/scala/scalanative/build/ScalaNative.scala

Lines changed: 30 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -107,26 +107,49 @@ private[scalanative] object ScalaNative {
107107
analysis: ReachabilityAnalysis.Failure
108108
): Unit = {
109109
val log = config.logger
110+
// see https://no-color.org/
111+
val noColor = sys.env.contains("NO_COLOR")
110112
def appendBackTrace(
111113
buf: StringBuilder,
112114
backtrace: List[Reach.BackTraceElement]
113115
): Unit = {
116+
import scala.io.AnsiColor._
114117
// Build stacktrace in memory to prevent its spliting when logging asynchronously
115-
val padding = backtrace.foldLeft(0)(_ max _.kind.length())
116-
backtrace.foreach {
117-
case Reach.BackTraceElement(_, kind, name, filename, line) =>
118-
val pad = " " * (padding - kind.length())
119-
buf.append(s" $pad$kind at $name($filename:$line)\n")
118+
val elems = backtrace.map {
119+
case elem @ Reach.BackTraceElement(_, symbol, filename, line) =>
120+
import symbol.argTypes
121+
val rendered = symbol.toString
122+
val descriptorStart = rendered.indexOf(symbol.name)
123+
val uncolored @ (modifiers, descriptor) =
124+
rendered.splitAt(descriptorStart)
125+
126+
if (noColor) uncolored
127+
else {
128+
val (name, typeInfo) =
129+
if (argTypes.nonEmpty)
130+
descriptor.splitAt(descriptor.indexOf("("))
131+
else (descriptor, "")
132+
modifiers -> s"$BOLD$YELLOW$name$RESET$typeInfo at $BOLD$filename:$line"
133+
}
134+
}
135+
val padding = elems
136+
.map(_._1.length)
137+
.max
138+
.min(14) + 2
139+
elems.foreach {
140+
case (modifiers, tracedDescriptor) =>
141+
val pad = " " * (padding - modifiers.length)
142+
buf.append(s"$pad$modifiers$tracedDescriptor\n")
120143
}
121144
buf.append("\n")
122145
}
123146

124147
if (analysis.unreachable.nonEmpty) {
125148
log.error(s"Found ${analysis.unreachable.size} unreachable symbols!")
126149
analysis.unreachable.foreach {
127-
case Reach.UnreachableSymbol(_, kind, name, backtrace) =>
150+
case Reach.UnreachableSymbol(_, symbol, backtrace) =>
128151
val buf = new StringBuilder()
129-
buf.append(s"Found unknown $kind $name, referenced from:\n")
152+
buf.append(s"Unknown $symbol, referenced from:\n")
130153
appendBackTrace(buf, backtrace)
131154
log.error(buf.toString())
132155
}

tools/src/main/scala/scala/scalanative/linker/Reach.scala

Lines changed: 116 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -917,44 +917,113 @@ class Reach(
917917
unsupported.getOrElseUpdate(global, details)
918918
case _ =>
919919
unreachable.getOrElseUpdate(
920-
global, {
921-
val (kind, symbol) = parseSymbol(global)
922-
UnreachableSymbol(
923-
name = global,
924-
kind = kind,
925-
symbol = symbol,
926-
backtrace = getBackTrace(global)
927-
)
928-
}
920+
global,
921+
UnreachableSymbol(
922+
name = global,
923+
symbol = parseSymbol(global),
924+
backtrace = getBackTrace(global)
925+
)
929926
)
930927
}
931928

932-
private def parseSymbol(name: Global): (String, String) = {
933-
def parseSig(owner: String, sig: Sig): (String, String) =
929+
private def parseSymbol(name: Global): SymbolDescriptor = {
930+
def renderType(tpe: nir.Type): String = tpe match {
931+
case arr: Type.Array => s"${renderType(arr.ty)}[]"
932+
case ref: Type.RefKind => ref.className.id
933+
case ty => ty.show
934+
}
935+
def parseArgTypes(
936+
types: Seq[nir.Type],
937+
isCtor: Boolean = false
938+
): Some[Seq[String]] = Some {
939+
val args = types match {
940+
case _ if isCtor => types
941+
case args :+ retty => args
942+
case _ => Nil
943+
}
944+
args.map(renderType)
945+
}
946+
947+
val Private = "private"
948+
val Static = "static"
949+
950+
def parseResultType(types: Seq[nir.Type]): Option[String] =
951+
types.lastOption.map(renderType)
952+
953+
def parseModifiers(scope: nir.Sig.Scope): List[String] = scope match {
954+
case Sig.Scope.Public => Nil
955+
case Sig.Scope.Private(_) => List(Private)
956+
case Sig.Scope.PublicStatic => List(Static)
957+
case Sig.Scope.PrivateStatic(_) => List(Static, Private)
958+
}
959+
960+
def parseSig(owner: String, sig: Sig): SymbolDescriptor =
934961
sig.unmangled match {
935-
case Sig.Method(name, _, _) => "method" -> s"$owner.${name}"
936-
case Sig.Ctor(tys) =>
937-
val ctorTys = tys
938-
.map {
939-
case ty: Type.RefKind => ty.className.id
940-
case ty => ty.show
941-
}
942-
.mkString(",")
943-
"constructor" -> s"$owner($ctorTys)"
944-
case Sig.Clinit => "static constructor" -> owner
945-
case Sig.Field(name, _) => "field" -> s"$owner.$name"
962+
case Sig.Method(name, types, scope) =>
963+
SymbolDescriptor(
964+
"method",
965+
s"$owner.$name",
966+
parseArgTypes(types),
967+
parseResultType(types),
968+
parseModifiers(scope)
969+
)
970+
case Sig.Ctor(types) =>
971+
SymbolDescriptor(
972+
"constructor",
973+
owner,
974+
parseArgTypes(types, isCtor = true)
975+
)
976+
case Sig.Clinit =>
977+
SymbolDescriptor(
978+
"constructor",
979+
owner,
980+
modifiers = List(Static)
981+
)
982+
case Sig.Field(name, scope) =>
983+
SymbolDescriptor(
984+
"field",
985+
owner,
986+
modifiers = parseModifiers(scope)
987+
)
946988
case Sig.Generated(name) =>
947-
"generated method" -> s"$owner.${name}"
948-
case Sig.Proxy(name, _) => "proxy method" -> s"$owner.$name"
949-
case Sig.Duplicate(sig, _) =>
950-
val (kind, name) = parseSig(owner, sig)
951-
s"duplicate $kind" -> s"$owner.name"
952-
case Sig.Extern(name) => s"extern method" -> s"$owner.$name"
989+
SymbolDescriptor(
990+
"symbol",
991+
s"$owner.$name",
992+
modifiers = List("generated")
993+
)
994+
case Sig.Proxy(name, types) =>
995+
SymbolDescriptor(
996+
"method",
997+
s"$owner.$name",
998+
parseArgTypes(types),
999+
parseResultType(types),
1000+
modifiers = List("proxy")
1001+
)
1002+
case Sig.Duplicate(sig, types) =>
1003+
val original = parseSig(owner, sig)
1004+
original.copy(
1005+
argTypes = parseArgTypes(types),
1006+
resultType = parseResultType(types),
1007+
modifiers = List("duplicate") ++ original.modifiers
1008+
)
1009+
SymbolDescriptor(
1010+
"method",
1011+
s"$owner.$name",
1012+
parseArgTypes(types),
1013+
parseResultType(types),
1014+
modifiers = List("duplicate")
1015+
)
1016+
case Sig.Extern(name) =>
1017+
SymbolDescriptor(
1018+
"symbol",
1019+
s"$owner.$name",
1020+
modifiers = List("extern")
1021+
)
9531022
}
9541023

9551024
name match {
9561025
case Global.Member(owner, sig) => parseSig(owner.id, sig)
957-
case Global.Top(id) => "type" -> id
1026+
case Global.Top(id) => SymbolDescriptor("type", id)
9581027
case _ => util.unreachable
9591028
}
9601029
}
@@ -969,11 +1038,9 @@ class Reach(
9691038
else {
9701039
val file = current.srcPosition.filename.getOrElse("unknown")
9711040
val line = current.srcPosition.line
972-
val (kind, symbol) = parseSymbol(current.referencedBy)
9731041
buf += BackTraceElement(
9741042
name = current.referencedBy,
975-
kind = kind,
976-
symbol = symbol,
1043+
symbol = parseSymbol(current.referencedBy),
9771044
filename = file,
9781045
line = line + 1
9791046
)
@@ -1087,17 +1154,30 @@ object Reach {
10871154
object ReferencedFrom {
10881155
final val Root = ReferencedFrom(nir.Global.None, nir.Position.NoPosition)
10891156
}
1157+
case class SymbolDescriptor(
1158+
kind: String,
1159+
name: String,
1160+
argTypes: Option[Seq[String]] = None,
1161+
resultType: Option[String] = None,
1162+
modifiers: Seq[String] = Nil
1163+
) {
1164+
override def toString(): String = {
1165+
val mods =
1166+
if (modifiers.isEmpty) "" else modifiers.distinct.mkString("", " ", " ")
1167+
val argsList = argTypes.fold("")(_.mkString("(", ", ", ")"))
1168+
val resType = resultType.fold("")(tpe => s": $tpe")
1169+
s"$mods$kind $name$argsList$resType"
1170+
}
1171+
}
10901172
case class BackTraceElement(
10911173
name: Global,
1092-
kind: String,
1093-
symbol: String,
1174+
symbol: SymbolDescriptor,
10941175
filename: String,
10951176
line: Int
10961177
)
10971178
case class UnreachableSymbol(
10981179
name: Global,
1099-
kind: String,
1100-
symbol: String,
1180+
symbol: SymbolDescriptor,
11011181
backtrace: List[BackTraceElement]
11021182
)
11031183

tools/src/test/scala/scala/scalanative/linker/MissingSymbolsTest.scala

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -45,11 +45,13 @@ class MissingSymbolsTest extends LinkerSpec {
4545
assertContainsAll(
4646
"kind-symbols",
4747
Seq(
48-
"type" -> "java.sql.Time",
49-
"constructor" -> "java.sql.Time(long)",
50-
"method" -> "java.sql.Time.valueOf"
48+
("type", "java.sql.Time", None),
49+
("constructor", "java.sql.Time", Some(Seq("long"))),
50+
("method", "java.sql.Time.valueOf", Some(Seq("java.lang.String")))
5151
),
52-
result.unreachable.map(v => (v.kind, v.symbol))
52+
result.unreachable
53+
.map(_.symbol)
54+
.map(v => (v.kind, v.name, v.argTypes))
5355
)
5456
val TimeType = Global.Top("java.sql.Time")
5557
val TimeCtor = TimeType.member(Sig.Ctor(Seq(Type.Long)))
@@ -68,7 +70,9 @@ class MissingSymbolsTest extends LinkerSpec {
6870

6971
result.unreachable.foreach { symbol =>
7072
val backtrace =
71-
symbol.backtrace.map(v => (v.kind, v.symbol, v.filename, v.line))
73+
symbol.backtrace.map(v =>
74+
(v.symbol.kind, v.symbol.name, v.filename, v.line)
75+
)
7276
// format: off
7377
assertEquals("backtrace", List(
7478
("method", "Bar$.getTimeString", sourceFile, if(symbol.name == TimeCtor) 9 else 8),
@@ -94,23 +98,22 @@ class MissingSymbolsTest extends LinkerSpec {
9498
""".stripMargin)
9599
) {
96100
case (config, result) =>
97-
scala.scalanative.build.ScalaNative.logLinked(config, result, "test")
98101
assertEquals("unreachable", 2, result.unreachable.size)
99102
assertContainsAll(
100103
"kind-symbols",
101104
Seq(
102105
"type" -> "java.sql.Time",
103-
"constructor" -> "java.sql.Time(long)"
106+
"constructor" -> "java.sql.Time"
104107
),
105-
result.unreachable.map(v => (v.kind, v.symbol))
108+
result.unreachable.map(_.symbol).map(v => (v.kind, v.name))
106109
)
107110

108111
result.unreachable
109-
.find(_.symbol == "java.sql.Time")
112+
.find(_.symbol.name == "java.sql.Time")
110113
.map { symbol =>
111114
val from = symbol.backtrace.head
112-
assertEquals("type", from.kind)
113-
assertEquals("Test$Foo$1", from.symbol)
115+
assertEquals("type", from.symbol.kind)
116+
assertEquals("Test$Foo$1", from.symbol.name)
114117
}
115118
.getOrElse(fail("Not found required unreachable symbol"))
116119
}
@@ -134,7 +137,7 @@ class MissingSymbolsTest extends LinkerSpec {
134137
// Testing if is able to get non-empty backtrace.
135138
// If reference tacking of delayed methods is invalid we would get empty list here
136139
result.unreachable
137-
.find(_.symbol == "java.lang.Class.getDeclaredFields")
140+
.find(_.symbol.name == "java.lang.Class.getDeclaredFields")
138141
.map { symbol =>
139142
assertTrue("no-backtrace", symbol.backtrace.nonEmpty)
140143
}
@@ -170,7 +173,6 @@ class MissingSymbolsTest extends LinkerSpec {
170173
Reach.UnsupportedFeature.SystemThreads,
171174
kind
172175
)
173-
println(backtrace)
174176
assertTrue("no-backtrace", backtrace.nonEmpty)
175177
}
176178
.getOrElse(fail("Not found required unreachable symbol"))

0 commit comments

Comments
 (0)