mirror of
https://github.com/enso-org/enso.git
synced 2024-12-22 20:31:45 +03:00
Remove fansi dependency from runtime-compiler (#8847)
Moves `fansi` dependency from `runtime-compiler` into `runtime`. # Important Notes I have not refactored [DiagnosticFormatter.scala](https://github.com/enso-org/enso/pull/8847/files#diff-8e73cf562742d6b0510acfe30af940fb9252e32be27a023f9705908a464e08ed) into Java just yet - I don't know what should be the replacement for now. I have just moved that source from `runtime-compiler` to `runtime`.
This commit is contained in:
parent
343a644051
commit
a70cbacecf
@ -1619,6 +1619,7 @@ lazy val runtime = (project in file("engine/runtime"))
|
||||
libraryDependencies ++= jmh ++ jaxb ++ GraalVM.langsPkgs ++ Seq(
|
||||
"org.apache.commons" % "commons-lang3" % commonsLangVersion,
|
||||
"org.apache.tika" % "tika-core" % tikaVersion,
|
||||
"com.lihaoyi" %% "fansi" % fansiVersion,
|
||||
"org.graalvm.polyglot" % "polyglot" % graalMavenPackagesVersion % "provided",
|
||||
"org.graalvm.sdk" % "polyglot-tck" % graalMavenPackagesVersion % "provided",
|
||||
"org.graalvm.truffle" % "truffle-api" % graalMavenPackagesVersion % "provided",
|
||||
@ -1871,8 +1872,7 @@ lazy val `runtime-compiler` =
|
||||
"junit" % "junit" % junitVersion % Test,
|
||||
"com.github.sbt" % "junit-interface" % junitIfVersion % Test,
|
||||
"org.scalatest" %% "scalatest" % scalatestVersion % Test,
|
||||
"org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion % "provided",
|
||||
"com.lihaoyi" %% "fansi" % fansiVersion
|
||||
"org.netbeans.api" % "org-openide-util-lookup" % netbeansApiVersion % "provided"
|
||||
)
|
||||
)
|
||||
.dependsOn(`runtime-parser`)
|
||||
|
@ -12,6 +12,7 @@ import org.enso.compiler.Compiler;
|
||||
import org.enso.compiler.PackageRepository;
|
||||
import org.enso.compiler.Passes;
|
||||
import org.enso.compiler.core.CompilerStub;
|
||||
import org.enso.compiler.core.ir.Diagnostic;
|
||||
import org.enso.compiler.data.BindingsMap;
|
||||
import org.enso.compiler.data.CompilerConfig;
|
||||
import org.enso.editions.LibraryName;
|
||||
@ -51,6 +52,16 @@ public interface CompilerContext extends CompilerStub {
|
||||
|
||||
Module findTopScopeModule(String name);
|
||||
|
||||
/**
|
||||
* Format the given diagnostic into a string. The returned string might have ANSI colors.
|
||||
*
|
||||
* @param module May be null if inline diagnostics is required.
|
||||
* @param diagnostic
|
||||
* @param isOutputRedirected True if the output is not system's out. If true, no ANSI color escape
|
||||
* characters will be inside the returned string.
|
||||
*/
|
||||
String formatDiagnostic(Module module, Diagnostic diagnostic, boolean isOutputRedirected);
|
||||
|
||||
// threads
|
||||
boolean isCreateThreadAllowed();
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
package org.enso.compiler
|
||||
|
||||
import com.oracle.truffle.api.source.{Source, SourceSection}
|
||||
import com.oracle.truffle.api.source.{Source}
|
||||
import org.enso.compiler.context.{
|
||||
CompilerContext,
|
||||
FreshNameSupply,
|
||||
@ -22,7 +22,7 @@ import org.enso.compiler.core.ir.MetadataStorage.MetadataPair
|
||||
import org.enso.compiler.core.ir.expression.Error
|
||||
import org.enso.compiler.core.ir.module.scope.Export
|
||||
import org.enso.compiler.core.ir.module.scope.Import
|
||||
import org.enso.compiler.core.ir.module.scope.imports;
|
||||
import org.enso.compiler.core.ir.module.scope.imports
|
||||
import org.enso.compiler.core.EnsoParser
|
||||
import org.enso.compiler.data.{BindingsMap, CompilerConfig}
|
||||
import org.enso.compiler.exception.CompilationAbortedException
|
||||
@ -60,7 +60,7 @@ import scala.jdk.OptionConverters._
|
||||
class Compiler(
|
||||
val context: CompilerContext,
|
||||
val packageRepository: PackageRepository,
|
||||
config: CompilerConfig
|
||||
private val config: CompilerConfig
|
||||
) {
|
||||
private val freshNameSupply: FreshNameSupply = new FreshNameSupply
|
||||
private val passes: Passes = new Passes(config)
|
||||
@ -75,6 +75,9 @@ class Compiler(
|
||||
else context.getOut
|
||||
private lazy val ensoCompiler: EnsoParser = new EnsoParser()
|
||||
|
||||
/** Java accessor */
|
||||
def getConfig(): CompilerConfig = config
|
||||
|
||||
/** The thread pool that handles parsing of modules. */
|
||||
private val pool: ExecutorService = if (config.parallelParsing) {
|
||||
new ThreadPoolExecutor(
|
||||
@ -678,7 +681,7 @@ class Compiler(
|
||||
|
||||
ensoCompiler.generateIRInline(tree).map { ir =>
|
||||
val compilerOutput = runCompilerPhasesInline(ir, newContext)
|
||||
runErrorHandlingInline(compilerOutput, source, newContext)
|
||||
runErrorHandlingInline(compilerOutput, newContext)
|
||||
(newContext, compilerOutput, source)
|
||||
}
|
||||
}
|
||||
@ -843,12 +846,10 @@ class Compiler(
|
||||
* context) for the inline compiler flow.
|
||||
*
|
||||
* @param ir the IR after compilation passes.
|
||||
* @param source the original source code.
|
||||
* @param inlineContext the inline compilation context.
|
||||
*/
|
||||
private def runErrorHandlingInline(
|
||||
ir: Expression,
|
||||
source: Source,
|
||||
inlineContext: InlineContext
|
||||
): Unit = {
|
||||
val errors = GatherDiagnostics
|
||||
@ -858,7 +859,7 @@ class Compiler(
|
||||
"No diagnostics metadata right after the gathering pass."
|
||||
)
|
||||
.diagnostics
|
||||
val hasErrors = reportDiagnostics(errors, source)
|
||||
val hasErrors = reportDiagnostics(errors, null)
|
||||
if (hasErrors && inlineContext.compilerConfig.isStrictErrors) {
|
||||
throw new CompilationAbortedException
|
||||
}
|
||||
@ -979,7 +980,7 @@ class Compiler(
|
||||
diagnostics
|
||||
.foldLeft(false) { case (result, (mod, diags)) =>
|
||||
if (diags.nonEmpty) {
|
||||
reportDiagnostics(diags, mod.getSource) || result
|
||||
reportDiagnostics(diags, mod) || result
|
||||
} else {
|
||||
result
|
||||
}
|
||||
@ -990,210 +991,22 @@ class Compiler(
|
||||
* exception breaking the execution flow if there are errors.
|
||||
*
|
||||
* @param diagnostics all the diagnostics found in the program IR.
|
||||
* @param source the original source code.
|
||||
* @param compilerModule The module in which the diagnostics should be reported. Or null if run inline.
|
||||
* @return whether any errors were encountered.
|
||||
*/
|
||||
private def reportDiagnostics(
|
||||
diagnostics: List[Diagnostic],
|
||||
source: Source
|
||||
compilerModule: CompilerContext.Module
|
||||
): Boolean = {
|
||||
diagnostics.foreach(diag =>
|
||||
printDiagnostic(new DiagnosticFormatter(diag, source).format())
|
||||
)
|
||||
val isOutputRedirected = config.outputRedirect.isDefined
|
||||
diagnostics.foreach { diag =>
|
||||
val formattedDiag =
|
||||
context.formatDiagnostic(compilerModule, diag, isOutputRedirected)
|
||||
printDiagnostic(formattedDiag)
|
||||
}
|
||||
diagnostics.exists(_.isInstanceOf[Error])
|
||||
}
|
||||
|
||||
/** Formatter of IR diagnostics. Heavily inspired by GCC. Can format one-line as well as multiline
|
||||
* diagnostics. The output is colorized if the output stream supports ANSI colors.
|
||||
* Also prints the offending lines from the source along with line number - the same way as
|
||||
* GCC does.
|
||||
* @param diagnostic the diagnostic to pretty print
|
||||
* @param source the original source code
|
||||
*/
|
||||
private class DiagnosticFormatter(
|
||||
private val diagnostic: Diagnostic,
|
||||
private val source: Source
|
||||
) {
|
||||
private val maxLineNum = 99999
|
||||
private val blankLinePrefix = " | "
|
||||
private val maxSourceLinesToPrint = 3
|
||||
private val linePrefixSize = blankLinePrefix.length
|
||||
private val outSupportsAnsiColors: Boolean = outSupportsColors
|
||||
private val (textAttrs: fansi.Attrs, subject: String) = diagnostic match {
|
||||
case _: Error => (fansi.Color.Red ++ fansi.Bold.On, "error: ")
|
||||
case _: Warning => (fansi.Color.Yellow ++ fansi.Bold.On, "warning: ")
|
||||
case _ => throw new IllegalStateException("Unexpected diagnostic type")
|
||||
}
|
||||
|
||||
def fileLocationFromSection(loc: IdentifiedLocation) = {
|
||||
val section =
|
||||
source.createSection(loc.location().start(), loc.location().length());
|
||||
val locStr = "" + section.getStartLine() + ":" + section
|
||||
.getStartColumn() + "-" + section.getEndLine() + ":" + section
|
||||
.getEndColumn()
|
||||
source.getName() + "[" + locStr + "]";
|
||||
}
|
||||
|
||||
private val sourceSection: Option[SourceSection] =
|
||||
diagnostic.location match {
|
||||
case Some(location) =>
|
||||
Some(source.createSection(location.start, location.length))
|
||||
case None => None
|
||||
}
|
||||
private val shouldPrintLineNumber = sourceSection match {
|
||||
case Some(section) =>
|
||||
section.getStartLine <= maxLineNum && section.getEndLine <= maxLineNum
|
||||
case None => false
|
||||
}
|
||||
|
||||
def format(): String = {
|
||||
sourceSection match {
|
||||
case Some(section) =>
|
||||
val isOneLine = section.getStartLine == section.getEndLine
|
||||
val srcPath: String =
|
||||
if (source.getPath == null && source.getName == null) {
|
||||
"<Unknown source>"
|
||||
} else if (source.getPath != null) {
|
||||
source.getPath
|
||||
} else {
|
||||
source.getName
|
||||
}
|
||||
if (isOneLine) {
|
||||
val lineNumber = section.getStartLine
|
||||
val startColumn = section.getStartColumn
|
||||
val endColumn = section.getEndColumn
|
||||
var str = fansi.Str()
|
||||
str ++= fansi
|
||||
.Str(srcPath + ":" + lineNumber + ":" + startColumn + ": ")
|
||||
.overlay(fansi.Bold.On)
|
||||
str ++= fansi.Str(subject).overlay(textAttrs)
|
||||
str ++= diagnostic.formattedMessage(fileLocationFromSection)
|
||||
str ++= "\n"
|
||||
str ++= oneLineFromSourceColored(lineNumber, startColumn, endColumn)
|
||||
str ++= "\n"
|
||||
str ++= underline(startColumn, endColumn)
|
||||
if (outSupportsAnsiColors) {
|
||||
str.render.stripLineEnd
|
||||
} else {
|
||||
str.plainText.stripLineEnd
|
||||
}
|
||||
} else {
|
||||
var str = fansi.Str()
|
||||
str ++= fansi
|
||||
.Str(
|
||||
srcPath + ":[" + section.getStartLine + ":" + section.getStartColumn + "-" + section.getEndLine + ":" + section.getEndColumn + "]: "
|
||||
)
|
||||
.overlay(fansi.Bold.On)
|
||||
str ++= fansi.Str(subject).overlay(textAttrs)
|
||||
str ++= diagnostic.formattedMessage(fileLocationFromSection)
|
||||
str ++= "\n"
|
||||
val printAllSourceLines =
|
||||
section.getEndLine - section.getStartLine <= maxSourceLinesToPrint
|
||||
val endLine =
|
||||
if (printAllSourceLines) section.getEndLine
|
||||
else section.getStartLine + maxSourceLinesToPrint
|
||||
for (lineNum <- section.getStartLine to endLine) {
|
||||
str ++= oneLineFromSource(lineNum)
|
||||
str ++= "\n"
|
||||
}
|
||||
if (!printAllSourceLines) {
|
||||
val restLineCount =
|
||||
section.getEndLine - section.getStartLine - maxSourceLinesToPrint
|
||||
str ++= blankLinePrefix + "... and " + restLineCount + " more lines ..."
|
||||
str ++= "\n"
|
||||
}
|
||||
if (outSupportsAnsiColors) {
|
||||
str.render.stripLineEnd
|
||||
} else {
|
||||
str.plainText.stripLineEnd
|
||||
}
|
||||
}
|
||||
case None =>
|
||||
// There is no source section associated with the diagnostics
|
||||
var str = fansi.Str()
|
||||
val fileLocation = diagnostic.location match {
|
||||
case Some(_) =>
|
||||
fileLocationFromSectionOption(diagnostic.location, source)
|
||||
case None =>
|
||||
Option(source.getPath).getOrElse("<Unknown source>")
|
||||
}
|
||||
|
||||
str ++= fansi
|
||||
.Str(fileLocation)
|
||||
.overlay(fansi.Bold.On)
|
||||
str ++= ": "
|
||||
str ++= fansi.Str(subject).overlay(textAttrs)
|
||||
str ++= diagnostic.formattedMessage(fileLocationFromSection)
|
||||
if (outSupportsAnsiColors) {
|
||||
str.render.stripLineEnd
|
||||
} else {
|
||||
str.plainText.stripLineEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @see https://github.com/termstandard/colors/
|
||||
* @see https://no-color.org/
|
||||
* @return
|
||||
*/
|
||||
private def outSupportsColors: Boolean = {
|
||||
if (System.console() == null) {
|
||||
// Non-interactive output is always without color support
|
||||
return false
|
||||
}
|
||||
if (System.getenv("NO_COLOR") != null) {
|
||||
return false
|
||||
}
|
||||
if (config.outputRedirect.isDefined) {
|
||||
return false
|
||||
}
|
||||
if (System.getenv("COLORTERM") != null) {
|
||||
return true
|
||||
}
|
||||
if (System.getenv("TERM") != null) {
|
||||
val termEnv = System.getenv("TERM").toLowerCase
|
||||
return termEnv.split("-").contains("color") || termEnv
|
||||
.split("-")
|
||||
.contains("256color")
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
private def oneLineFromSource(lineNum: Int): String = {
|
||||
val line = source.createSection(lineNum).getCharacters.toString
|
||||
linePrefix(lineNum) + line
|
||||
}
|
||||
|
||||
private def oneLineFromSourceColored(
|
||||
lineNum: Int,
|
||||
startCol: Int,
|
||||
endCol: Int
|
||||
): String = {
|
||||
val line = source.createSection(lineNum).getCharacters.toString
|
||||
linePrefix(lineNum) + fansi
|
||||
.Str(line)
|
||||
.overlay(textAttrs, startCol - 1, endCol)
|
||||
}
|
||||
|
||||
private def linePrefix(lineNum: Int): String = {
|
||||
if (shouldPrintLineNumber) {
|
||||
val pipeSymbol = " | "
|
||||
val prefixWhitespaces =
|
||||
linePrefixSize - lineNum.toString.length - pipeSymbol.length
|
||||
" " * prefixWhitespaces + lineNum + pipeSymbol
|
||||
} else {
|
||||
blankLinePrefix
|
||||
}
|
||||
}
|
||||
|
||||
private def underline(startColumn: Int, endColumn: Int): String = {
|
||||
val sectionLen = endColumn - startColumn
|
||||
blankLinePrefix +
|
||||
" " * (startColumn - 1) +
|
||||
fansi.Str("^" + ("~" * sectionLen)).overlay(textAttrs)
|
||||
}
|
||||
}
|
||||
|
||||
private def fileLocationFromSectionOption(
|
||||
loc: Option[IdentifiedLocation],
|
||||
source: Source
|
||||
|
@ -274,8 +274,6 @@ public final class EnsoLanguage extends TruffleLanguage<EnsoContext> {
|
||||
} catch (UnhandledEntity e) {
|
||||
throw new InlineParsingException("Unhandled entity: " + e.entity(), e);
|
||||
} catch (CompilationAbortedException e) {
|
||||
assert outputRedirect.toString().lines().count() > 1
|
||||
: "Expected a header line from the compiler";
|
||||
String compilerErrOutput = outputRedirect.toString();
|
||||
throw new InlineParsingException(compilerErrOutput, e);
|
||||
} finally {
|
||||
|
@ -85,8 +85,7 @@ public abstract class EvalNode extends BaseNode {
|
||||
var mod = newInlineContext.module$access$0().module$access$0();
|
||||
|
||||
var m = org.enso.interpreter.runtime.Module.fromCompilerModule(mod);
|
||||
var toTruffle =
|
||||
new IrToTruffle(context, src, m.getScope(), compiler.org$enso$compiler$Compiler$$config);
|
||||
var toTruffle = new IrToTruffle(context, src, m.getScope(), compiler.getConfig());
|
||||
var expr = toTruffle.runInline(ir, sco, "<inline_source>");
|
||||
|
||||
if (shouldCaptureResultScope) {
|
||||
|
@ -484,6 +484,26 @@ public final class EnsoContext {
|
||||
return environment.asGuestValue(obj);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the output is a terminal that supports ANSI colors. {@see
|
||||
* https://github.com/termstandard/colors/} {@see https://no-color.org/}
|
||||
*/
|
||||
public boolean isColorTerminalOutput() {
|
||||
var envVars = environment.getEnvironment();
|
||||
if (envVars.get("NO_COLOR") != null) {
|
||||
return false;
|
||||
}
|
||||
if (envVars.get("COLORTERM") != null) {
|
||||
return true;
|
||||
}
|
||||
if (envVars.get("TERM") != null) {
|
||||
var termEnv = envVars.get("TERM").toLowerCase();
|
||||
return Arrays.stream(termEnv.split("-"))
|
||||
.anyMatch(str -> str.equals("color") || str.equals("256color"));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to lookup a Java class (host symbol in Truffle terminology) by its fully qualified name.
|
||||
* This method also tries to lookup inner classes. More specifically, if the provided name
|
||||
|
@ -16,6 +16,8 @@ import org.enso.compiler.PackageRepository;
|
||||
import org.enso.compiler.Passes;
|
||||
import org.enso.compiler.context.CompilerContext;
|
||||
import org.enso.compiler.context.FreshNameSupply;
|
||||
import org.enso.compiler.core.ir.Diagnostic;
|
||||
import org.enso.compiler.core.ir.IdentifiedLocation;
|
||||
import org.enso.compiler.data.BindingsMap;
|
||||
import org.enso.compiler.data.CompilerConfig;
|
||||
import org.enso.compiler.pass.analyse.BindingAnalysis$;
|
||||
@ -23,6 +25,7 @@ import org.enso.editions.LibraryName;
|
||||
import org.enso.interpreter.caches.Cache;
|
||||
import org.enso.interpreter.caches.ModuleCache;
|
||||
import org.enso.interpreter.runtime.type.Types;
|
||||
import org.enso.interpreter.runtime.util.DiagnosticFormatter;
|
||||
import org.enso.pkg.Package;
|
||||
import org.enso.pkg.QualifiedName;
|
||||
import org.enso.polyglot.CompilationStage;
|
||||
@ -232,6 +235,45 @@ final class TruffleCompilerContext implements CompilerContext {
|
||||
return option.isEmpty() ? null : option.get().asCompilerModule();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return true if the given location is inside module. More specifically, if the location's bounds
|
||||
* point inside the character bounds of the module.
|
||||
*
|
||||
* <p>Note that it is possible that a {@link Diagnostic}'s location has a bigger size than the
|
||||
* size of the module.
|
||||
*/
|
||||
private static boolean isLocationInsideModule(
|
||||
CompilerContext.Module module, IdentifiedLocation location) {
|
||||
try {
|
||||
return location.end() <= module.getSource().getLength();
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError("Unreachable", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String formatDiagnostic(
|
||||
CompilerContext.Module module, Diagnostic diagnostic, boolean isOutputRedirected) {
|
||||
DiagnosticFormatter diagnosticFormatter;
|
||||
if (module != null && diagnostic.location().isDefined()) {
|
||||
var location = diagnostic.location().get();
|
||||
if (isLocationInsideModule(module, location)) {
|
||||
Source source;
|
||||
try {
|
||||
source = module.getSource();
|
||||
} catch (IOException e) {
|
||||
throw new AssertionError(e);
|
||||
}
|
||||
assert source != null;
|
||||
diagnosticFormatter = new DiagnosticFormatter(diagnostic, source, isOutputRedirected);
|
||||
return diagnosticFormatter.format();
|
||||
}
|
||||
}
|
||||
var emptySource = Source.newBuilder(LanguageInfo.ID, "", null).build();
|
||||
diagnosticFormatter = new DiagnosticFormatter(diagnostic, emptySource, isOutputRedirected);
|
||||
return diagnosticFormatter.format();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public Future<Boolean> serializeLibrary(
|
||||
|
@ -0,0 +1,216 @@
|
||||
package org.enso.interpreter.runtime.util
|
||||
|
||||
import com.oracle.truffle.api.source.{Source, SourceSection}
|
||||
import org.enso.compiler.core.ir.expression.Error
|
||||
import org.enso.compiler.core.ir.{Diagnostic, IdentifiedLocation, Warning}
|
||||
import org.enso.interpreter.runtime.EnsoContext
|
||||
|
||||
/** Formatter of IR diagnostics. Heavily inspired by GCC. Can format one-line as well as multiline
|
||||
* diagnostics. The output is colorized if the output stream supports ANSI colors.
|
||||
* Also prints the offending lines from the source along with line number - the same way as
|
||||
* GCC does.
|
||||
*
|
||||
* @param diagnostic the diagnostic to pretty print
|
||||
* @param source the original source code
|
||||
*/
|
||||
class DiagnosticFormatter(
|
||||
private val diagnostic: Diagnostic,
|
||||
private val source: Source,
|
||||
private val isOutputRedirected: Boolean
|
||||
) {
|
||||
private val maxLineNum = 99999
|
||||
private val blankLinePrefix = " | "
|
||||
private val maxSourceLinesToPrint = 3
|
||||
private val linePrefixSize = blankLinePrefix.length
|
||||
private val outSupportsAnsiColors: Boolean = outSupportsColors
|
||||
private val (textAttrs: fansi.Attrs, subject: String) = diagnostic match {
|
||||
case _: Error => (fansi.Color.Red ++ fansi.Bold.On, "error: ")
|
||||
case _: Warning => (fansi.Color.Yellow ++ fansi.Bold.On, "warning: ")
|
||||
case _ => throw new IllegalStateException("Unexpected diagnostic type")
|
||||
}
|
||||
|
||||
def fileLocationFromSection(loc: IdentifiedLocation) = {
|
||||
val section =
|
||||
source.createSection(loc.location().start(), loc.location().length());
|
||||
val locStr = "" + section.getStartLine() + ":" + section
|
||||
.getStartColumn() + "-" + section.getEndLine() + ":" + section
|
||||
.getEndColumn()
|
||||
source.getName() + "[" + locStr + "]";
|
||||
}
|
||||
|
||||
private val sourceSection: Option[SourceSection] =
|
||||
diagnostic.location match {
|
||||
case Some(location) =>
|
||||
if (location.length > source.getLength) {
|
||||
None
|
||||
} else {
|
||||
Some(source.createSection(location.start, location.length))
|
||||
}
|
||||
case None => None
|
||||
}
|
||||
private val shouldPrintLineNumber = sourceSection match {
|
||||
case Some(section) =>
|
||||
section.getStartLine <= maxLineNum && section.getEndLine <= maxLineNum
|
||||
case None => false
|
||||
}
|
||||
|
||||
def format(): String = {
|
||||
sourceSection match {
|
||||
case Some(section) =>
|
||||
val isOneLine = section.getStartLine == section.getEndLine
|
||||
val srcPath: String =
|
||||
if (source.getPath == null && source.getName == null) {
|
||||
"<Unknown source>"
|
||||
} else if (source.getPath != null) {
|
||||
source.getPath
|
||||
} else {
|
||||
source.getName
|
||||
}
|
||||
var str = fansi.Str()
|
||||
if (isOneLine) {
|
||||
val lineNumber = section.getStartLine
|
||||
val startColumn = section.getStartColumn
|
||||
val endColumn = section.getEndColumn
|
||||
val isLocationEmpty = startColumn == endColumn
|
||||
str ++= fansi
|
||||
.Str(srcPath + ":" + lineNumber + ":" + startColumn + ": ")
|
||||
.overlay(fansi.Bold.On)
|
||||
str ++= fansi.Str(subject).overlay(textAttrs)
|
||||
str ++= diagnostic.formattedMessage(fileLocationFromSection)
|
||||
if (!isLocationEmpty) {
|
||||
str ++= "\n"
|
||||
str ++= oneLineFromSourceColored(lineNumber, startColumn, endColumn)
|
||||
str ++= "\n"
|
||||
str ++= underline(startColumn, endColumn)
|
||||
}
|
||||
} else {
|
||||
str ++= fansi
|
||||
.Str(
|
||||
srcPath + ":[" + section.getStartLine + ":" + section.getStartColumn + "-" + section.getEndLine + ":" + section.getEndColumn + "]: "
|
||||
)
|
||||
.overlay(fansi.Bold.On)
|
||||
str ++= fansi.Str(subject).overlay(textAttrs)
|
||||
str ++= diagnostic.formattedMessage(fileLocationFromSection)
|
||||
str ++= "\n"
|
||||
val printAllSourceLines =
|
||||
section.getEndLine - section.getStartLine <= maxSourceLinesToPrint
|
||||
val endLine =
|
||||
if (printAllSourceLines) section.getEndLine
|
||||
else section.getStartLine + maxSourceLinesToPrint
|
||||
for (lineNum <- section.getStartLine to endLine) {
|
||||
str ++= oneLineFromSource(lineNum)
|
||||
str ++= "\n"
|
||||
}
|
||||
if (!printAllSourceLines) {
|
||||
val restLineCount =
|
||||
section.getEndLine - section.getStartLine - maxSourceLinesToPrint
|
||||
str ++= blankLinePrefix + "... and " + restLineCount + " more lines ..."
|
||||
str ++= "\n"
|
||||
}
|
||||
}
|
||||
if (outSupportsAnsiColors) {
|
||||
str.render.stripLineEnd
|
||||
} else {
|
||||
str.plainText.stripLineEnd
|
||||
}
|
||||
case None =>
|
||||
// There is no source section associated with the diagnostics
|
||||
var str = fansi.Str()
|
||||
val fileLocation = diagnostic.location match {
|
||||
case Some(_) =>
|
||||
fileLocationFromSectionOption(diagnostic.location, source)
|
||||
case None =>
|
||||
Option(source.getPath).getOrElse("<Unknown source>")
|
||||
}
|
||||
|
||||
str ++= fansi
|
||||
.Str(fileLocation)
|
||||
.overlay(fansi.Bold.On)
|
||||
str ++= ": "
|
||||
str ++= fansi.Str(subject).overlay(textAttrs)
|
||||
str ++= diagnostic.formattedMessage(fileLocationFromSection)
|
||||
if (outSupportsAnsiColors) {
|
||||
str.render.stripLineEnd
|
||||
} else {
|
||||
str.plainText.stripLineEnd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @see https://github.com/termstandard/colors/
|
||||
* @see https://no-color.org/
|
||||
* @return
|
||||
*/
|
||||
private def outSupportsColors: Boolean = {
|
||||
if (System.console() == null) {
|
||||
// Non-interactive output is always without color support
|
||||
return false
|
||||
}
|
||||
if (isOutputRedirected) {
|
||||
return false
|
||||
}
|
||||
return EnsoContext.get(null).isColorTerminalOutput;
|
||||
}
|
||||
|
||||
private def oneLineFromSource(lineNum: Int): String = {
|
||||
val line = source.createSection(lineNum).getCharacters.toString
|
||||
linePrefix(lineNum) + line
|
||||
}
|
||||
|
||||
private def oneLineFromSourceColored(
|
||||
lineNum: Int,
|
||||
startCol: Int,
|
||||
endCol: Int
|
||||
): String = {
|
||||
val line = source.createSection(lineNum).getCharacters.toString
|
||||
linePrefix(lineNum) + fansi
|
||||
.Str(line)
|
||||
.overlay(textAttrs, startCol - 1, endCol)
|
||||
}
|
||||
|
||||
private def linePrefix(lineNum: Int): String = {
|
||||
if (shouldPrintLineNumber) {
|
||||
val pipeSymbol = " | "
|
||||
val prefixWhitespaces =
|
||||
linePrefixSize - lineNum.toString.length - pipeSymbol.length
|
||||
" " * prefixWhitespaces + lineNum + pipeSymbol
|
||||
} else {
|
||||
blankLinePrefix
|
||||
}
|
||||
}
|
||||
|
||||
private def underline(startColumn: Int, endColumn: Int): String = {
|
||||
val sectionLen = endColumn - startColumn
|
||||
blankLinePrefix +
|
||||
" " * (startColumn - 1) +
|
||||
fansi.Str("^" + ("~" * sectionLen)).overlay(textAttrs)
|
||||
}
|
||||
|
||||
private def fileLocationFromSectionOption(
|
||||
loc: Option[IdentifiedLocation],
|
||||
source: Source
|
||||
): String = {
|
||||
val srcLocation = loc match {
|
||||
case Some(identifiedLoc)
|
||||
if isLocationInSourceBounds(identifiedLoc, source) =>
|
||||
val section =
|
||||
source.createSection(identifiedLoc.start, identifiedLoc.length)
|
||||
val locStr =
|
||||
"" + section.getStartLine + ":" +
|
||||
section.getStartColumn + "-" +
|
||||
section.getEndLine + ":" +
|
||||
section.getEndColumn
|
||||
"[" + locStr + "]"
|
||||
case _ => ""
|
||||
}
|
||||
|
||||
source.getPath + ":" + srcLocation
|
||||
}
|
||||
|
||||
private def isLocationInSourceBounds(
|
||||
loc: IdentifiedLocation,
|
||||
source: Source
|
||||
): Boolean = {
|
||||
loc.end() <= source.getLength
|
||||
}
|
||||
}
|
@ -404,10 +404,9 @@ public class DebuggingEnsoTest {
|
||||
+ " DebugException",
|
||||
DebugException.class,
|
||||
() -> event.getTopStackFrame().eval("non_existing_identifier"));
|
||||
assertTrue(
|
||||
exception
|
||||
.getMessage()
|
||||
.contains("The name `non_existing_identifier` could not be found"));
|
||||
assertThat(
|
||||
exception.getMessage(),
|
||||
containsString("The name `non_existing_identifier` could not be found"));
|
||||
|
||||
assertThrows(
|
||||
DebugException.class,
|
||||
|
@ -0,0 +1,102 @@
|
||||
package org.enso.interpreter.test;
|
||||
|
||||
import static org.hamcrest.MatcherAssert.assertThat;
|
||||
import static org.hamcrest.Matchers.*;
|
||||
|
||||
import com.oracle.truffle.api.source.Source;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.logging.Level;
|
||||
import org.enso.compiler.core.ir.Diagnostic;
|
||||
import org.enso.interpreter.runtime.EnsoContext;
|
||||
import org.enso.interpreter.runtime.util.DiagnosticFormatter;
|
||||
import org.enso.polyglot.LanguageInfo;
|
||||
import org.enso.polyglot.MethodNames.Module;
|
||||
import org.enso.polyglot.MethodNames.TopScope;
|
||||
import org.enso.polyglot.RuntimeOptions;
|
||||
import org.graalvm.polyglot.Context;
|
||||
import org.graalvm.polyglot.PolyglotException;
|
||||
import org.graalvm.polyglot.io.IOAccess;
|
||||
import org.junit.After;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
public class DiagnosticFormatterTest {
|
||||
private Context ctx;
|
||||
private OutputStream output;
|
||||
private EnsoContext ensoCtx;
|
||||
|
||||
@Before
|
||||
public void initCtx() {
|
||||
output = new ByteArrayOutputStream();
|
||||
ctx =
|
||||
Context.newBuilder()
|
||||
.allowExperimentalOptions(true)
|
||||
.allowIO(IOAccess.ALL)
|
||||
.allowAllAccess(true)
|
||||
.option(RuntimeOptions.LOG_LEVEL, Level.WARNING.getName())
|
||||
.logHandler(System.err)
|
||||
.option(RuntimeOptions.STRICT_ERRORS, "true")
|
||||
.option(
|
||||
RuntimeOptions.LANGUAGE_HOME_OVERRIDE,
|
||||
Paths.get("../../distribution/component").toFile().getAbsolutePath())
|
||||
.out(output)
|
||||
.err(output)
|
||||
.environment("NO_COLOR", "true")
|
||||
.build();
|
||||
ensoCtx = ctx.getBindings(LanguageInfo.ID).invokeMember(TopScope.LEAK_CONTEXT).asHostObject();
|
||||
}
|
||||
|
||||
@After
|
||||
public void closeCtx() throws IOException {
|
||||
ctx.close();
|
||||
output.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testOneLineDiagnostics() throws IOException {
|
||||
var code = "main = foo";
|
||||
var polyglotSrc =
|
||||
org.graalvm.polyglot.Source.newBuilder(LanguageInfo.ID, code, "tmp_test.enso").build();
|
||||
var expectedDiagnostics =
|
||||
"""
|
||||
tmp_test:1:8: error: The name `foo` could not be found.
|
||||
1 | main = foo
|
||||
| ^~~""";
|
||||
var module = ctx.eval(polyglotSrc);
|
||||
try {
|
||||
module.invokeMember(Module.EVAL_EXPRESSION, "main");
|
||||
} catch (PolyglotException e) {
|
||||
assertThat(output.toString(), containsString(expectedDiagnostics));
|
||||
}
|
||||
var moduleOpt = ensoCtx.getTopScope().getModule("tmp_test");
|
||||
assertThat(moduleOpt.isPresent(), is(true));
|
||||
var moduleIr = moduleOpt.get().getIr();
|
||||
var diags = gatherDiagnostics(moduleIr);
|
||||
assertThat("There should be just one Diagnostic in main method", diags.size(), is(1));
|
||||
|
||||
var src = Source.newBuilder(LanguageInfo.ID, code, "tmp_test").build();
|
||||
var diag = diags.get(0);
|
||||
var diagFormatter = new DiagnosticFormatter(diag, src, true);
|
||||
var formattedDiag = diagFormatter.format();
|
||||
assertThat(formattedDiag, containsString(expectedDiagnostics));
|
||||
}
|
||||
|
||||
private static List<Diagnostic> gatherDiagnostics(org.enso.compiler.core.ir.Module moduleIr) {
|
||||
List<Diagnostic> diags = new ArrayList<>();
|
||||
moduleIr
|
||||
.preorder()
|
||||
.foreach(
|
||||
ir -> {
|
||||
if (ir instanceof Diagnostic diag) {
|
||||
diags.add(diag);
|
||||
}
|
||||
return null;
|
||||
});
|
||||
return diags;
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user