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:
Pavel Marek 2024-02-02 12:45:19 +01:00 committed by GitHub
parent 343a644051
commit a70cbacecf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 414 additions and 214 deletions

View File

@ -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`)

View File

@ -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();

View File

@ -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

View File

@ -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 {

View File

@ -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) {

View File

@ -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

View File

@ -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(

View File

@ -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
}
}

View File

@ -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,

View File

@ -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;
}
}