mirror of
https://github.com/apple/pkl.git
synced 2026-04-10 10:53:40 +02:00
Add colours to Pkl errors in Cli output
To make error messages from Pkl eval easier to read, this change uses the Jansi library to colour the output, making it quicker and easier to scan error messages and understand what's happened. The Jansi library also detects if the CLI output is a terminal capable of handling colours, and will automatically strip out escape codes if the output won't support them (e.g. piping the output somewhere else).
This commit is contained in:
committed by
Philip K.F. Hölzenspies
parent
49aaf288cc
commit
0d7b95d3ff
@@ -15,15 +15,23 @@
|
||||
*/
|
||||
package org.pkl.core.runtime;
|
||||
|
||||
import static org.fusesource.jansi.Ansi.ansi;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
import org.fusesource.jansi.Ansi;
|
||||
import org.fusesource.jansi.Ansi.Color;
|
||||
import org.pkl.core.StackFrame;
|
||||
import org.pkl.core.util.Nullable;
|
||||
|
||||
public final class StackTraceRenderer {
|
||||
private final Function<StackFrame, StackFrame> frameTransformer;
|
||||
|
||||
private static final Ansi.Color frameColor = Color.YELLOW;
|
||||
private static final Ansi.Color lineNumColor = Color.BLUE;
|
||||
private static final Ansi.Color repetitionColor = Color.MAGENTA;
|
||||
|
||||
public StackTraceRenderer(Function<StackFrame, StackFrame> frameTransformer) {
|
||||
this.frameTransformer = frameTransformer;
|
||||
}
|
||||
@@ -40,6 +48,7 @@ public final class StackTraceRenderer {
|
||||
StringBuilder builder,
|
||||
String leftMargin,
|
||||
boolean isFirstElement) {
|
||||
var out = ansi(builder);
|
||||
for (var frame : frames) {
|
||||
if (frame instanceof StackFrameLoop loop) {
|
||||
// ensure a cycle of length 1 doesn't get rendered as a loop
|
||||
@@ -47,20 +56,28 @@ public final class StackTraceRenderer {
|
||||
doRender(loop.frames, null, builder, leftMargin, isFirstElement);
|
||||
} else {
|
||||
if (!isFirstElement) {
|
||||
builder.append(leftMargin).append("\n");
|
||||
out.fgBright(frameColor).a(leftMargin).reset().a("\n");
|
||||
}
|
||||
builder.append(leftMargin).append("┌─ ").append(loop.count).append(" repetitions of:\n");
|
||||
out.fgBright(frameColor)
|
||||
.a(leftMargin)
|
||||
.a("┌─ ")
|
||||
.reset()
|
||||
.bold()
|
||||
.fg(repetitionColor)
|
||||
.a(Integer.toString(loop.count))
|
||||
.reset()
|
||||
.a(" repetitions of:\n");
|
||||
var newLeftMargin = leftMargin + "│ ";
|
||||
doRender(loop.frames, null, builder, newLeftMargin, isFirstElement);
|
||||
if (isFirstElement) {
|
||||
renderHint(hint, builder, newLeftMargin);
|
||||
isFirstElement = false;
|
||||
}
|
||||
builder.append(leftMargin).append("└─\n");
|
||||
out.fgBright(frameColor).a(leftMargin).a("└─").reset().a("\n");
|
||||
}
|
||||
} else {
|
||||
if (!isFirstElement) {
|
||||
builder.append(leftMargin).append('\n');
|
||||
out.fgBright(frameColor).a(leftMargin).reset().a('\n');
|
||||
}
|
||||
renderFrame((StackFrame) frame, builder, leftMargin);
|
||||
}
|
||||
@@ -80,14 +97,16 @@ public final class StackTraceRenderer {
|
||||
|
||||
private void renderHint(@Nullable String hint, StringBuilder builder, String leftMargin) {
|
||||
if (hint == null || hint.isEmpty()) return;
|
||||
var out = ansi(builder);
|
||||
|
||||
builder.append('\n');
|
||||
builder.append(leftMargin);
|
||||
builder.append(hint);
|
||||
builder.append('\n');
|
||||
out.a('\n');
|
||||
out.fgBright(frameColor).a(leftMargin);
|
||||
out.fgBright(frameColor).bold().a(hint).reset();
|
||||
out.a('\n');
|
||||
}
|
||||
|
||||
private void renderSourceLine(StackFrame frame, StringBuilder builder, String leftMargin) {
|
||||
var out = ansi(builder);
|
||||
var originalSourceLine = frame.getSourceLines().get(0);
|
||||
var leadingWhitespace = VmUtils.countLeadingWhitespace(originalSourceLine);
|
||||
var sourceLine = originalSourceLine.strip();
|
||||
@@ -98,27 +117,36 @@ public final class StackTraceRenderer {
|
||||
: sourceLine.length();
|
||||
|
||||
var prefix = frame.getStartLine() + " | ";
|
||||
builder.append(leftMargin).append(prefix).append(sourceLine).append('\n');
|
||||
builder.append(leftMargin);
|
||||
out.fgBright(frameColor)
|
||||
.a(leftMargin)
|
||||
.fgBright(lineNumColor)
|
||||
.a(prefix)
|
||||
.reset()
|
||||
.a(sourceLine)
|
||||
.a('\n');
|
||||
out.fgBright(frameColor).a(leftMargin).reset();
|
||||
//noinspection StringRepeatCanBeUsed
|
||||
for (int i = 1; i < prefix.length() + startColumn; i++) {
|
||||
builder.append(' ');
|
||||
out.append(' ');
|
||||
}
|
||||
|
||||
out.fgRed();
|
||||
//noinspection StringRepeatCanBeUsed
|
||||
for (int i = startColumn; i <= endColumn; i++) {
|
||||
builder.append('^');
|
||||
out.a('^');
|
||||
}
|
||||
builder.append('\n');
|
||||
out.reset().a('\n');
|
||||
}
|
||||
|
||||
private void renderSourceLocation(StackFrame frame, StringBuilder builder, String leftMargin) {
|
||||
builder.append(leftMargin).append("at ");
|
||||
var out = ansi(builder);
|
||||
out.fgBright(frameColor).a(leftMargin).reset().a("at ");
|
||||
if (frame.getMemberName() != null) {
|
||||
builder.append(frame.getMemberName());
|
||||
out.a(frame.getMemberName());
|
||||
} else {
|
||||
builder.append("<unknown>");
|
||||
out.a("<unknown>");
|
||||
}
|
||||
builder.append(" (").append(frame.getModuleUri()).append(')').append('\n');
|
||||
out.a(" (").a(frame.getModuleUri()).a(')').a('\n');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -15,10 +15,14 @@
|
||||
*/
|
||||
package org.pkl.core.runtime;
|
||||
|
||||
import static org.fusesource.jansi.Ansi.ansi;
|
||||
|
||||
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
|
||||
import java.io.PrintWriter;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import org.fusesource.jansi.Ansi;
|
||||
import org.fusesource.jansi.Ansi.Color;
|
||||
import org.pkl.core.Release;
|
||||
import org.pkl.core.util.ErrorMessages;
|
||||
import org.pkl.core.util.Nullable;
|
||||
@@ -27,6 +31,8 @@ import org.pkl.core.util.StringBuilderWriter;
|
||||
public final class VmExceptionRenderer {
|
||||
private final @Nullable StackTraceRenderer stackTraceRenderer;
|
||||
|
||||
private static final Ansi.Color errorColor = Color.RED;
|
||||
|
||||
/**
|
||||
* Constructs an error renderer with the given stack trace renderer. If stack trace renderer is
|
||||
* {@code null}, stack traces will not be included in error output.
|
||||
@@ -73,7 +79,8 @@ public final class VmExceptionRenderer {
|
||||
}
|
||||
|
||||
private void renderException(VmException exception, StringBuilder builder) {
|
||||
var header = "–– Pkl Error ––";
|
||||
var out = ansi(builder);
|
||||
out.fg(errorColor).a("–– Pkl Error ––").reset();
|
||||
|
||||
String message;
|
||||
var hint = exception.getHint();
|
||||
@@ -94,7 +101,7 @@ public final class VmExceptionRenderer {
|
||||
message = exception.getMessage();
|
||||
}
|
||||
|
||||
builder.append(header).append('\n').append(message).append('\n');
|
||||
out.a('\n').fgBright(errorColor).a(message).reset().a('\n');
|
||||
|
||||
// include cause's message unless it's the same as this exception's message
|
||||
if (exception.getCause() != null) {
|
||||
|
||||
@@ -3,7 +3,6 @@ package org.pkl.core
|
||||
import org.pkl.commons.createTempFile
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.junit.jupiter.api.Assertions.assertFalse
|
||||
import org.junit.jupiter.api.Assertions.assertTrue
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.junit.jupiter.api.io.TempDir
|
||||
import org.pkl.commons.writeString
|
||||
@@ -11,7 +10,6 @@ import org.pkl.core.ModuleSource.*
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
import kotlin.io.path.createFile
|
||||
import kotlin.io.path.name
|
||||
|
||||
class EvaluateTestsTest {
|
||||
|
||||
|
||||
@@ -429,7 +429,7 @@ class EvaluatorTest {
|
||||
val evaluatorBuilder = EvaluatorBuilder.preconfigured().setModuleCacheDir(cacheDir)
|
||||
val project = Project.load(modulePath("/org/pkl/core/project/project6/PklProject"))
|
||||
evaluatorBuilder.setProjectDependencies(project.dependencies).build().use { evaluator ->
|
||||
assertThatCode {
|
||||
assertThatCode {
|
||||
evaluator.evaluateOutputText(modulePath("/org/pkl/core/project/project6/globWithinDependency.pkl"))
|
||||
}.hasMessageContaining("""
|
||||
Cannot resolve import in local dependency because scheme `modulepath` is not globbable.
|
||||
|
||||
@@ -149,8 +149,9 @@ class ProjectTest {
|
||||
.setModuleCacheDir(null)
|
||||
.setHttpClient(httpClient)
|
||||
.build()
|
||||
assertThatCode { evaluator.evaluate(ModuleSource.path(projectDir.resolve("bug.pkl"))) }
|
||||
.hasMessageStartingWith("""
|
||||
assertThatCode {
|
||||
evaluator.evaluate(ModuleSource.path(projectDir.resolve("bug.pkl")))
|
||||
}.hasMessageStartingWith("""
|
||||
–– Pkl Error ––
|
||||
Cannot download package `package://localhost:0/fruit@1.0.5` because the computed checksum for package metadata does not match the expected checksum.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user