Skip to content

Backport "Ensure to escape characters before constructing JSON profile trace" to 3.3 LTS #98

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

Closed
wants to merge 6 commits into from
Closed
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
1 change: 0 additions & 1 deletion compiler/src/dotty/tools/dotc/core/TypeErasure.scala
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ package core
import Symbols.*, Types.*, Contexts.*, Flags.*, Names.*, StdNames.*, Phases.*
import Flags.JavaDefined
import Uniques.unique
import TypeOps.makePackageObjPrefixExplicit
import backend.sjs.JSDefinitions
import transform.ExplicitOuter.*
import transform.ValueClasses.*
Expand Down
30 changes: 0 additions & 30 deletions compiler/src/dotty/tools/dotc/core/TypeOps.scala
Original file line number Diff line number Diff line change
Expand Up @@ -573,36 +573,6 @@ object TypeOps:
widenMap(tp)
}

/** If `tpe` is of the form `p.x` where `p` refers to a package
* but `x` is not owned by a package, expand it to
*
* p.package.x
*/
def makePackageObjPrefixExplicit(tpe: NamedType)(using Context): Type = {
def tryInsert(pkgClass: SymDenotation): Type = pkgClass match {
case pkg: PackageClassDenotation =>
var sym = tpe.symbol
if !sym.exists && tpe.denot.isOverloaded then
// we know that all alternatives must come from the same package object, since
// otherwise we would get "is already defined" errors. So we can take the first
// symbol we see.
sym = tpe.denot.alternatives.head.symbol
val pobj = pkg.packageObjFor(sym)
if (pobj.exists) tpe.derivedSelect(pobj.termRef)
else tpe
case _ =>
tpe
}
if (tpe.symbol.isRoot)
tpe
else
tpe.prefix match {
case pre: ThisType if pre.cls.is(Package) => tryInsert(pre.cls)
case pre: TermRef if pre.symbol.is(Package) => tryInsert(pre.symbol.moduleClass)
case _ => tpe
}
}

/** An argument bounds violation is a triple consisting of
* - the argument tree
* - a string "upper" or "lower" indicating which bound is violated
Expand Down
36 changes: 35 additions & 1 deletion compiler/src/dotty/tools/dotc/core/TypeUtils.scala
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ package dotc
package core

import TypeErasure.ErasedValueType
import Types.*, Contexts.*, Symbols.*, Flags.*, Decorators.*
import Types.*, Contexts.*, Symbols.*, Flags.*, Decorators.*, SymDenotations.*
import Names.{Name, TermName}
import Constants.Constant

import Names.Name
import StdNames.nme

Expand Down Expand Up @@ -127,6 +130,37 @@ class TypeUtils:
case mt: MethodType => mt.isImplicitMethod || mt.resType.takesImplicitParams
case _ => false


/** If `self` is of the form `p.x` where `p` refers to a package
* but `x` is not owned by a package, expand it to
*
* p.package.x
*/
def makePackageObjPrefixExplicit(using Context): Type =
def tryInsert(tpe: NamedType, pkgClass: SymDenotation): Type = pkgClass match
case pkg: PackageClassDenotation =>
var sym = tpe.symbol
if !sym.exists && tpe.denot.isOverloaded then
// we know that all alternatives must come from the same package object, since
// otherwise we would get "is already defined" errors. So we can take the first
// symbol we see.
sym = tpe.denot.alternatives.head.symbol
val pobj = pkg.packageObjFor(sym)
if pobj.exists then tpe.derivedSelect(pobj.termRef)
else tpe
case _ =>
tpe
self match
case tpe: NamedType =>
if tpe.symbol.isRoot then
tpe
else
tpe.prefix match
case pre: ThisType if pre.cls.is(Package) => tryInsert(tpe, pre.cls)
case pre: TermRef if pre.symbol.is(Package) => tryInsert(tpe, pre.symbol.moduleClass)
case _ => tpe
case tpe => tpe

/** The constructors of this type that are applicable to `argTypes`, without needing
* an implicit conversion. Curried constructors are always excluded.
* @param adaptVarargs if true, allow a constructor with just a varargs argument to
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1189,7 +1189,7 @@ class TreeUnpickler(reader: TastyReader,
val tpe0 = name match
case name: TypeName => TypeRef(qualType, name, denot)
case name: TermName => TermRef(qualType, name, denot)
val tpe = TypeOps.makePackageObjPrefixExplicit(tpe0)
val tpe = tpe0.makePackageObjPrefixExplicit
ConstFold.Select(untpd.Select(qual, name).withType(tpe))

def completeSelect(name: Name, sig: Signature, target: Name): Select =
Expand Down
46 changes: 46 additions & 0 deletions compiler/src/dotty/tools/dotc/profile/JsonNameTransformer.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package dotty.tools.dotc.profile

import scala.annotation.internal.sharable

// Based on NameTransformer but dedicated for JSON encoding rules
object JsonNameTransformer {
private val nops = 128

@sharable private val op2code = new Array[String](nops)
private def enterOp(op: Char, code: String) = op2code(op.toInt) = code

enterOp('\"', "\\\"")
enterOp('\\', "\\\\")
// enterOp('/', "\\/") // optional, no need for escaping outside of html context
enterOp('\b', "\\b")
enterOp('\f', "\\f")
enterOp('\n', "\\n")
enterOp('\r', "\\r")
enterOp('\t', "\\t")

def encode(name: String): String = {
var buf: StringBuilder = null.asInstanceOf
val len = name.length
var i = 0
while (i < len) {
val c = name(i)
if (c < nops && (op2code(c.toInt) ne null)) {
if (buf eq null) {
buf = new StringBuilder()
buf.append(name.subSequence(0, i))
}
buf.append(op2code(c.toInt))
} else if (c <= 0x1F || c >= 0x7F) {
if (buf eq null) {
buf = new StringBuilder()
buf.append(name.subSequence(0, i))
}
buf.append("\\u%04X".format(c.toInt))
} else if (buf ne null) {
buf.append(c)
}
i += 1
}
if (buf eq null) name else buf.toString
}
}
15 changes: 10 additions & 5 deletions compiler/src/dotty/tools/dotc/profile/Profiler.scala
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,7 @@ private [profile] class RealProfiler(reporter : ProfileReporter)(using Context)
override def beforePhase(phase: Phase): (TracedEventId, ProfileSnap) = {
assert(mainThread eq Thread.currentThread())
traceThreadSnapshotCounters()
val eventId = traceDurationStart(Category.Phase, phase.phaseName)
val eventId = traceDurationStart(Category.Phase, escapeSpecialChars(phase.phaseName))
if (ctx.settings.YprofileRunGcBetweenPhases.value.contains(phase.toString))
doGC()
if (ctx.settings.YprofileExternalTool.value.contains(phase.toString)) {
Expand All @@ -287,7 +287,7 @@ private [profile] class RealProfiler(reporter : ProfileReporter)(using Context)
assert(mainThread eq Thread.currentThread())
if chromeTrace != null then
traceThreadSnapshotCounters()
traceDurationStart(Category.File, unit.source.name)
traceDurationStart(Category.File, escapeSpecialChars(unit.source.name))
else TracedEventId.Empty
}

Expand Down Expand Up @@ -325,7 +325,7 @@ private [profile] class RealProfiler(reporter : ProfileReporter)(using Context)
then EmptyCompletionEvent
else
val completionName = this.completionName(root, associatedFile)
val event = TracedEventId(associatedFile.name)
val event = TracedEventId(escapeSpecialChars(associatedFile.name))
chromeTrace.traceDurationEventStart(Category.Completion.name, "↯", colour = "thread_state_sleeping")
chromeTrace.traceDurationEventStart(Category.File.name, event)
chromeTrace.traceDurationEventStart(Category.Completion.name, completionName)
Expand All @@ -350,8 +350,13 @@ private [profile] class RealProfiler(reporter : ProfileReporter)(using Context)
if chromeTrace != null then
chromeTrace.traceDurationEventEnd(category.name, event, colour)

private def symbolName(sym: Symbol): String = s"${sym.showKind} ${sym.showName}"
private def completionName(root: Symbol, associatedFile: AbstractFile): String =
private inline def escapeSpecialChars(value: String): String =
JsonNameTransformer.encode(value)

private def symbolName(sym: Symbol): String = escapeSpecialChars:
s"${sym.showKind} ${sym.showName}"

private def completionName(root: Symbol, associatedFile: AbstractFile): String = escapeSpecialChars:
def isTopLevel = root.owner != NoSymbol && root.owner.is(Flags.Package)
if root.is(Flags.Package) || isTopLevel
then root.javaBinaryName
Expand Down
2 changes: 1 addition & 1 deletion compiler/src/dotty/tools/dotc/typer/TypeAssigner.scala
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ trait TypeAssigner {
defn.FromJavaObjectType
else tpe match
case tpe: NamedType =>
val tpe1 = TypeOps.makePackageObjPrefixExplicit(tpe)
val tpe1 = tpe.makePackageObjPrefixExplicit
if tpe1 ne tpe then
accessibleType(tpe1, superAccess)
else
Expand Down
4 changes: 2 additions & 2 deletions compiler/src/dotty/tools/dotc/typer/Typer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -306,7 +306,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
// so we ignore that import.
if reallyExists(denot) && !isScalaJsPseudoUnion then
if unimported.isEmpty || !unimported.contains(pre.termSymbol) then
return pre.select(name, denot)
return pre.select(name, denot).makePackageObjPrefixExplicit
case _ =>
if imp.importSym.isCompleting then
report.warning(i"cyclic ${imp.importSym}, ignored", pos)
Expand Down Expand Up @@ -466,7 +466,7 @@ class Typer(@constructorOnly nestingLevel: Int = 0) extends Namer
defDenot.symbol.owner
else
curOwner
effectiveOwner.thisType.select(name, defDenot)
effectiveOwner.thisType.select(name, defDenot).makePackageObjPrefixExplicit
}
if !curOwner.is(Package) || isDefinedInCurrentUnit(defDenot) then
result = checkNewOrShadowed(found, Definition) // no need to go further out, we found highest prec entry
Expand Down
2 changes: 1 addition & 1 deletion compiler/test/dotty/tools/DottyTest.scala
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ trait DottyTest extends ContextEscapeDetection {

protected def defaultCompiler: Compiler = new Compiler()

private def compilerWithChecker(phase: String)(assertion: (tpd.Tree, Context) => Unit) = new Compiler {
protected def compilerWithChecker(phase: String)(assertion: (tpd.Tree, Context) => Unit) = new Compiler {

private val baseCompiler = defaultCompiler

Expand Down
133 changes: 133 additions & 0 deletions compiler/test/dotty/tools/dotc/profile/TraceNameManglingTest.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package dotty.tools.dotc.profile

import org.junit.Assert.*
import org.junit.*

import scala.annotation.tailrec
import dotty.tools.DottyTest
import dotty.tools.dotc.util.SourceFile
import dotty.tools.dotc.core.Contexts.FreshContext
import java.nio.file.Files
import java.util.Locale

class TraceNameManglingTest extends DottyTest {

override protected def initializeCtx(fc: FreshContext): Unit = {
super.initializeCtx(fc)
val tmpDir = Files.createTempDirectory("trace_name_mangling_test").nn
fc.setSetting(fc.settings.YprofileEnabled, true)
fc.setSetting(
fc.settings.YprofileTrace,
tmpDir.resolve("trace.json").nn.toAbsolutePath().toString()
)
fc.setSetting(
fc.settings.YprofileDestination,
tmpDir.resolve("profiler.out").nn.toAbsolutePath().toString()
)
}

@Test def escapeBackslashes(): Unit = {
val isWindows = sys.props("os.name").toLowerCase(Locale.ROOT) == "windows"
val filename = if isWindows then "/.scala" else "\\.scala"
checkTraceEvents(
"""
|class /\ :
| var /\ = ???
|object /\{
| def /\ = ???
|}""".stripMargin,
filename = filename
)(
Set(
raw"class /\\",
raw"object /\\",
raw"method /\\",
raw"variable /\\",
raw"setter /\\_="
).map(TraceEvent("typecheck", _))
++ Set(
TraceEvent("file", if isWindows then "/.scala" else "\\\\.scala")
)
)
}

@Test def escapeDoubleQuotes(): Unit = {
val filename = "\"quoted\".scala"
checkTraceEvents(
"""
|class `"QuotedClass"`:
| var `"quotedVar"` = ???
|object `"QuotedObject"` {
| def `"quotedMethod"` = ???
|}""".stripMargin,
filename = filename
):
Set(
raw"class \"QuotedClass\"",
raw"object \"QuotedObject\"",
raw"method \"quotedMethod\"",
raw"variable \"quotedVar\""
).map(TraceEvent("typecheck", _))
++ Set(TraceEvent("file", "\\\"quoted\\\".scala"))
}
@Test def escapeNonAscii(): Unit = {
val filename = "unic😀de.scala"
checkTraceEvents(
"""
|class ΩUnicodeClass:
| var `中文Var` = ???
|object ΩUnicodeObject {
| def 中文Method = ???
|}""".stripMargin,
filename = filename
):
Set(
"class \\u03A9UnicodeClass",
"object \\u03A9UnicodeObject",
"method \\u4E2D\\u6587Method",
"variable \\u4E2D\\u6587Var"
).map(TraceEvent("typecheck", _))
++ Set(TraceEvent("file", "unic\\uD83D\\uDE00de.scala"))
}

case class TraceEvent(category: String, name: String)
private def compileWithTracer(
code: String,
filename: String,
afterPhase: String = "typer"
)(checkEvents: Seq[TraceEvent] => Unit) = {
val runCtx = locally:
val source = SourceFile.virtual(filename, code)
val c = compilerWithChecker(afterPhase) { (_, _) => () }
val run = c.newRun
run.compileSources(List(source))
run.runContext
assert(!runCtx.reporter.hasErrors, "compilation failed")
val outfile = ctx.settings.YprofileTrace.value
checkEvents:
scala.io.Source
.fromFile(outfile)
.getLines()
.collect:
case s"""${_}"cat":"${category}","name":${name},"ph":${_}""" =>
TraceEvent(category, name.stripPrefix("\"").stripSuffix("\""))
.distinct.toSeq
}

private def checkTraceEvents(code: String, filename: String = "test")(expected: Set[TraceEvent]): Unit = {
compileWithTracer(code, filename = filename, afterPhase = "typer"){ events =>
val missing = expected.diff(events.toSet)
def showFound = events
.groupBy(_.category)
.collect:
case (category, events)
if expected.exists(_.category == category) =>
s"- $category: [${events.map(_.name).mkString(", ")}]"
.mkString("\n")
assert(
missing.isEmpty,
s"""Missing ${missing.size} names [${missing.mkString(", ")}] in events, got:\n${showFound}"""
)
}
}
}
22 changes: 22 additions & 0 deletions tests/pos/i18097.1.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
opaque type Pos = Double

object Pos:
extension (x: Pos)
def mult1(y: Pos): Pos = x * y

extension (x: Pos)
def mult2(y: Pos): Pos = x * y

class Test:
def test(key: String, a: Pos, b: Pos): Unit =
val tup1 = new Tuple1(Pos.mult1(a)(b))
val res1: Pos = tup1._1

val tup2 = new Tuple1(a.mult1(b))
val res2: Pos = tup2._1

val tup3 = new Tuple1(mult2(a)(b))
val res3: Pos = tup3._1

val tup4 = new Tuple1(a.mult2(b))
val res4: Pos = tup4._1 // was error: Found: (tup4._4 : Double) Required: Pos
Loading