ScalaIO 2022
Chris Kipp
@ckipp01
Message
Severity
Position
Message
Severity
Position
Are you in the REPL?
Are you using X build tool?
Are you using a build server?
Are you using IntelliJ? Metals? Ensime?
Or what combination of all these?
Compiler
Build Tool
Build Server
Metals
Editor
if (param.is(Erased))
report.error("value class first parameter cannot be `erased`", param.srcPos)
else
for (p <- params if !p.is(Erased))
report.error("value class can only have one non `erased` parameter", p.srcPos)
An error just reported on the fly
def fail(msg: Message) = report.error(msg, sym.srcPos)
def checkWithDeferred(flag: FlagSet) =
if (sym.isOneOf(flag))
fail(AbstractMemberMayNotHaveModifier(sym, flag))
Reporting a Message
abstract class Message(val errorId: ErrorMessageID) { self =>
protected def msg: String
def kind: MessageKind
protected def explain: String
protected def msgSuffix: String = ""
def canExplain: Boolean = explain.nonEmpty
private var myMsg: String | Null = null
private var myIsNonSensical: Boolean = false
private def dropNonSensical(msg: String): String = ???
def rawMessage = message
@threadUnsafe lazy val message: String = dropNonSensical(msg + msgSuffix)
@threadUnsafe lazy val explanation: String = dropNonSensical(explain)
def isNonSensical: Boolean = { message; myIsNonSensical }
def persist: Message = ???
def append(suffix: => String): Message = mapMsg(_ ++ suffix)
def mapMsg(f: String => String): Message = ???
def appendExplanation(suffix: => String): Message = ???
def showAlways = false
override def toString = msg
}
class Diagnostic(
val msg: Message,
val pos: SourcePosition,
val level: Int
) extends Exception with interfaces.Diagnostic:
private var verbose: Boolean = false
def isVerbose: Boolean = verbose
def setVerbose(): this.type =
verbose = true
this
override def position: Optional[interfaces.SourcePosition] =
if (pos.exists && pos.source.exists) Optional.of(pos) else Optional.empty()
override def message: String =
msg.message.replaceAll("\u001B\\[[;\\d]*m", "")
override def toString: String = s"$getClass at $pos: $message"
override def getMessage(): String = message
end Diagnostic
❯ tree -L 5 sbt-bridge/
sbt-bridge
├── resources
│ └── META-INF
│ └── services
│ └── xsbti.compile.CompilerInterface2
└── src
├── dotty
│ └── tools
│ └── xsbt
│ ├── CompilerBridge.java
│ ├── CompilerBridgeDriver.java
│ ├── DelegatingReporter.java
│ ├── DiagnosticCode.java
│ ├── InterfaceCompileFailed.java
│ ├── PositionBridge.java
│ ├── Problem.java
│ ├── ZincPlainFile.java
│ └── ZincVirtualFile.java
└── xsbt
├── CachedCompilerImpl.java
├── CompilerClassLoader.java
├── CompilerInterface.java
├── ConsoleInterface.java
├── DottydocRunner.java
└── ScaladocInterface.java
The compiler bridge classes are loaded using
java.util.ServiceLoader
. In other words, the class implementingxsbti.compile.CompilerInterface2
must be mentioned in a file named:/META-INF/services/xsbti.compile.CompilerInterface2
.
Old Problem.java in sbt-interfaces
public interface Problem {
String category();
Severity severity();
String message();
Position position();
// Default value to avoid breaking binary compatibility
/**
* If present, the string shown to the user when displaying this Problem. Otherwise, the Problem
* will be shown in an implementation-defined way based on the values of its other fields.
*/
default Optional<String> rendered() {
return Optional.empty();
}
}
Old DelegatingReporter.java in Dotty
public void doReport(Diagnostic dia, Context ctx) {
Severity severity = severityOf(dia.level());
Position position = positionOf(dia.pos().nonInlined());
StringBuilder rendered = new StringBuilder();
rendered.append(messageAndPos(dia, ctx));
Message message = dia.msg();
boolean shouldExplain = Diagnostic.shouldExplain(dia, ctx);
if (shouldExplain && !message.explanation().isEmpty()) {
rendered.append(explanation(message, ctx));
}
delegate.log(new Problem(position, message.msg(), severity, rendered.toString()));
}
Compiler
Build Tool
Build Server
Metals
Editor
Compiler
Build Tool
Build Server
Metals
Editor
sbt-bridge
BSP
LSP
export interface Diagnostic {
/** The range at which the message applies.*/
range: Range;
/**
* The diagnostic's severity. Can be omitted. If omitted it is up to the
* client to interpret diagnostics as error, warning, info or hint.
*/
severity?: DiagnosticSeverity;
/** The diagnostic's code, which might appear in the user interface. */
code?: integer | string;
/** An optional property to describe the error code. */
codeDescription?: CodeDescription;
/**
* A human-readable string describing the source of this
* diagnostic, e.g. 'typescript' or 'super lint'.
*/
source?: string;
/** The diagnostic's message. */
message: string;
/** Additional metadata about the diagnostic. */
tags?: DiagnosticTag[];
/**
* An array of related diagnostic information, e.g. when symbol-names within
* a scope collide all definitions can be marked via this property.
*/
relatedInformation?: DiagnosticRelatedInformation[];
/**
* A data entry field that is preserved between a
* `textDocument/publishDiagnostics` notification and
* `textDocument/codeAction` request.
*/
data?: unknown;
}
protected override def publishDiagnostic(problem: Problem): Unit = {
for {
id <- problem.position.sourcePath.toOption
diagnostic <- toDiagnostic(problem)
filePath <- toSafePath(VirtualFileRef.of(id))
} {
problemsByFile(filePath) = problemsByFile.getOrElse(filePath, Vector.empty) :+ diagnostic
val params = PublishDiagnosticsParams(
TextDocumentIdentifier(filePath.toUri),
buildTarget,
originId = None,
Vector(diagnostic),
reset = false
)
exchange.notifyEvent("build/publishDiagnostics", params)
}
private def publishDiagnostics(
path: AbsolutePath,
queue: ju.Queue[Diagnostic],
): Unit = {
if (!path.isFile) return didDelete(path)
val current = path.toInputFromBuffers(buffers)
val snapshot = snapshots.getOrElse(path, current)
val edit = TokenEditDistance(
snapshot,
current,
trees,
doNothingWhenUnchanged = false,
)
val uri = path.toURI.toString
val all = new ju.ArrayList[Diagnostic](queue.size() + 1)
for {
diagnostic <- queue.asScala
freshDiagnostic <- toFreshDiagnostic(edit, diagnostic, snapshot)
} {
all.add(freshDiagnostic)
}
// Removed some deduplication stuff here to save room
languageClient.publishDiagnostics(new PublishDiagnosticsParams(uri, all))
}
Message
Severity
Position
trait Foo:
def hello(): Unit
def goodBye(): Unit
class Greeting extends Foo
trait Foo[A]:
def foo: Int
def foo =
second[String]
inline def first[A]: Int =
compiletime.summonInline[Foo[A]].foo
inline def second[A]: Int =
first[A] + 1
Message
Severity
Position
object Main:
val greeting = "hello"
greeting = "hi"
fn main() {
let greeting = "hello";
greeting = "hi";
}
Message
Severity
Position
struct Diagnostic {
/// The primary error message.
message: String,
code: Option<DiagnosticCode>,
/// "error: internal compiler error", "error", "warning", "note", "help".
level: &'static str,
spans: Vec<DiagnosticSpan>,
/// Associated diagnostic messages.
children: Vec<Diagnostic>,
/// The message as rustc would render it.
rendered: Option<String>,
}
struct DiagnosticCode {
/// The code itself.
code: String,
/// An explanation for the code.
explanation: Option<&'static str>,
}
struct DiagnosticSpan {
file_name: String,
byte_start: u32,
byte_end: u32,
/// 1-based.
line_start: usize,
line_end: usize,
/// 1-based, character offset.
column_start: usize,
column_end: usize,
/// Is this a "primary" span -- meaning the point, or one of the points,
/// where the error occurred?
is_primary: bool,
/// Source text from the start of line_start to the end of line_end.
text: Vec<DiagnosticSpanLine>,
/// Label that should be placed at this location (if any)
label: Option<String>,
/// If we are suggesting a replacement, this will contain text
/// that should be sliced in atop this span.
suggested_replacement: Option<String>,
/// If the suggestion is approximate
suggestion_applicability: Option<Applicability>,
/// Macro invocations that created the code at this span, if any.
expansion: Option<Box<DiagnosticSpanMacroExpansion>>,
}
"Fix-it" hints provide advice for fixing small, localized problems in source code. When Clang produces a diagnostic about a particular problem that it can work around (e.g., non-standard or redundant syntax, missing keywords, common mistakes, etc.), it may also provide specific guidance in the form of a code transformation to correct the problem.
object TypeMismatch {
private val regexStart = """type mismatch;""".r
private val regexMiddle = """(F|f)ound\s*: (.*)""".r
private val regexEnd = """(R|r)equired: (.*)""".r
def unapply(d: l.Diagnostic): Option[(String, l.Diagnostic)] = {
d.getMessage().split("\n").map(_.trim()) match {
/* Scala 3:
* Found: ("" : String)
* Required: Int
*/
case Array(regexMiddle(_, toType), regexEnd(_, _)) =>
Some((toType.trim(), d))
/* Scala 2:
* type mismatch;
* found : Int(122)
* required: String
*/
case Array(regexStart(), regexMiddle(_, toType), regexEnd(_, _)) =>
Some((toType.trim(), d))
case _ =>
None
}
}
}
New Problem.java in sbt-interfaces
public interface Problem {
String category();
Severity severity();
String message();
Position position();
default Optional<String> rendered() {
return Optional.empty();
}
default Optional<DiagnosticCode> diagnosticCode() {
return Optional.empty();
}
default List<DiagnosticRelatedInformation> diagnosticRelatedInforamation() {
return Collections.emptyList();
}
}
"diagnostics": [
{
"range": {
"start": {
"line": 9,
"character": 15
},
"end": {
"line": 9,
"character": 19
}
},
"severity": 1,
"code": "7",
"source": "sbt",
"message": "Found: (\u001b[32m\"hi\"\u001b[0m : String)\nRequired: Int\n\nThe following import might make progress towards fixing the problem:\n\n import sourcecode.Text.generate\n\n"
}
],
lwronski just merged in a great example of this in https://github.com/scalameta/metals/pull/4297
"diagnostics": [
{
"range": {
"start": {
"line": 0,
"character": 15
},
"end": {
"line": 0,
"character": 40
}
},
"severity": 4,
"source": "scala-cli",
"message": "com.lihaoyi::os-lib:0.7.8 is outdated, update to 0.8.1\n com.lihaoyi::os-lib:0.7.8 -\u003e com.lihaoyi::os-lib:0.8.1",
"data": {
"range": {
"start": {
"line": 0,
"character": 15
},
"end": {
"line": 0,
"character": 40
}
},
"newText": "com.lihaoyi::os-lib:0.8.1"
}
}
],
"diagnostics": [
{
"range": {
"start": {
"line": 11,
"character": 6
},
"end": {
"line": 11,
"character": 14
}
},
"severity": 1,
"source": "bloop",
"message": "class Greeting needs to be abstract, since:\nit has 2 unimplemented members.\n/** As seen from class Greeting, the missing signatures are as follows.\n * For convenience, these are usable as stub implementations.\n */\n def goodBye(): Unit \u003d ???\n def hello(): Unit \u003d ???\n"
}
]
"diagnostics": [
{
"range": {
"start": { ... },
"end": { ... }
},
"severity": 1,
"code": "034",
"source": "dotty",
"message": "class Greeting needs to be abstract, since:\nit has 2 unimplemented members.\n/** As seen from class Greeting, the missing signatures are as follows.\n * For convenience, these are usable as stub implementations.\n */\n",
"data": {
"range": {
"start": { ... },
"end": { ... }
},
"newText": "def goodBye(): Unit \u003d ???\n def hello(): Unit \u003d ???\n"
}
}
]