Add color to error formatting (#746)

* Add color to error formatting

* Apply suggestions from code review

Co-authored-by: Daniel Chao <daniel.h.chao@gmail.com>

* Address reviewer comments

* Apply suggestions from code review

Co-authored-by: Daniel Chao <daniel.h.chao@gmail.com>

* Define style choices as operations on formatter (abandon semantic API)

* Adjust margin styling

* Review feedback

* Documentation nits

---------

Co-authored-by: Daniel Chao <daniel.h.chao@gmail.com>
This commit is contained in:
Philip K.F. Hölzenspies
2024-11-01 10:02:19 +00:00
committed by GitHub
parent e217cfcd6f
commit 03462fefae
38 changed files with 445 additions and 114 deletions

View File

@@ -41,6 +41,7 @@ import org.pkl.core.util.Nullable;
/** Utility library for static analysis of Pkl programs. */
public class Analyzer {
private final StackFrameTransformer transformer;
private final boolean color;
private final SecurityManager securityManager;
private final @Nullable Path moduleCacheDir;
private final @Nullable DeclaredDependencies projectDependencies;
@@ -49,12 +50,14 @@ public class Analyzer {
public Analyzer(
StackFrameTransformer transformer,
boolean color,
SecurityManager securityManager,
Collection<ModuleKeyFactory> moduleKeyFactories,
@Nullable Path moduleCacheDir,
@Nullable DeclaredDependencies projectDependencies,
HttpClient httpClient) {
this.transformer = transformer;
this.color = color;
this.securityManager = securityManager;
this.moduleCacheDir = moduleCacheDir;
this.projectDependencies = projectDependencies;
@@ -82,7 +85,7 @@ public class Analyzer {
} catch (PklException err) {
throw err;
} catch (VmException err) {
throw err.toPklException(transformer);
throw err.toPklException(transformer, color);
} catch (Exception e) {
throw new PklBugException(e);
} finally {

View File

@@ -61,6 +61,8 @@ public final class EvaluatorBuilder {
private @Nullable String outputFormat;
private boolean color = false;
private @Nullable StackFrameTransformer stackFrameTransformer;
private @Nullable DeclaredDependencies dependencies;
@@ -144,6 +146,17 @@ public final class EvaluatorBuilder {
return new EvaluatorBuilder();
}
/** Sets the option to render errors in ANSI color. */
public EvaluatorBuilder setColor(boolean color) {
this.color = color;
return this;
}
/** Returns the current setting of the option to render errors in ANSI color. */
public boolean getColor() {
return color;
}
/** Sets the given stack frame transformer, replacing any previously set transformer. */
public EvaluatorBuilder setStackFrameTransformer(StackFrameTransformer stackFrameTransformer) {
this.stackFrameTransformer = stackFrameTransformer;
@@ -475,6 +488,9 @@ public final class EvaluatorBuilder {
if (settings.rootDir() != null) {
setRootDir(settings.rootDir());
}
if (settings.color() != null) {
setColor(settings.color().hasColor());
}
if (Boolean.TRUE.equals(settings.noCache())) {
setModuleCacheDir(null);
} else if (settings.moduleCacheDir() != null) {
@@ -513,6 +529,7 @@ public final class EvaluatorBuilder {
return new EvaluatorImpl(
stackFrameTransformer,
color,
securityManager,
httpClient,
new LoggerImpl(logger, stackFrameTransformer),

View File

@@ -57,6 +57,7 @@ import org.pkl.core.util.Nullable;
public class EvaluatorImpl implements Evaluator {
protected final StackFrameTransformer frameTransformer;
protected final boolean color;
protected final ModuleResolver moduleResolver;
protected final Context polyglotContext;
protected final @Nullable Duration timeout;
@@ -68,6 +69,7 @@ public class EvaluatorImpl implements Evaluator {
public EvaluatorImpl(
StackFrameTransformer transformer,
boolean color,
SecurityManager manager,
HttpClient httpClient,
Logger logger,
@@ -82,6 +84,7 @@ public class EvaluatorImpl implements Evaluator {
securityManager = manager;
frameTransformer = transformer;
this.color = color;
moduleResolver = new ModuleResolver(factories);
this.logger = new BufferedLogger(logger);
packageResolver = PackageResolver.getInstance(securityManager, httpClient, moduleCacheDir);
@@ -304,20 +307,20 @@ public class EvaluatorImpl implements Evaluator {
.bug("Stack overflow")
.withCause(e.getCause())
.build()
.toPklException(frameTransformer);
.toPklException(frameTransformer, color);
}
handleTimeout(timeoutTask);
throw e.toPklException(frameTransformer);
throw e.toPklException(frameTransformer, color);
} catch (VmException e) {
handleTimeout(timeoutTask);
throw e.toPklException(frameTransformer);
throw e.toPklException(frameTransformer, color);
} catch (Exception e) {
throw new PklBugException(e);
} catch (ExceptionInInitializerError e) {
if (!(e.getCause() instanceof VmException vmException)) {
throw new PklBugException(e);
}
var pklException = vmException.toPklException(frameTransformer);
var pklException = vmException.toPklException(frameTransformer, color);
var error = new ExceptionInInitializerError(pklException);
error.setStackTrace(e.getStackTrace());
throw new PklBugException(error);

View File

@@ -0,0 +1,30 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.evaluatorSettings;
public enum Color {
NEVER,
AUTO,
ALWAYS;
public boolean hasColor() {
return switch (this) {
case AUTO -> System.console() != null;
case NEVER -> false;
case ALWAYS -> true;
};
}
}

View File

@@ -40,6 +40,7 @@ public record PklEvaluatorSettings(
@Nullable Map<String, String> env,
@Nullable List<Pattern> allowedModules,
@Nullable List<Pattern> allowedResources,
@Nullable Color color,
@Nullable Boolean noCache,
@Nullable Path moduleCacheDir,
@Nullable List<Path> modulePath,
@@ -102,11 +103,14 @@ public record PklEvaluatorSettings(
Collectors.toMap(
Entry::getKey, entry -> ExternalReader.parse(entry.getValue())));
var color = (String) pSettings.get("color");
return new PklEvaluatorSettings(
(Map<String, String>) pSettings.get("externalProperties"),
(Map<String, String>) pSettings.get("env"),
allowedModules,
allowedResources,
color == null ? null : Color.valueOf(color.toUpperCase()),
(Boolean) pSettings.get("noCache"),
moduleCacheDir,
modulePath,
@@ -198,6 +202,7 @@ public record PklEvaluatorSettings(
&& Objects.equals(env, that.env)
&& arePatternsEqual(allowedModules, that.allowedModules)
&& arePatternsEqual(allowedResources, that.allowedResources)
&& Objects.equals(color, that.color)
&& Objects.equals(noCache, that.noCache)
&& Objects.equals(moduleCacheDir, that.moduleCacheDir)
&& Objects.equals(timeout, that.timeout)
@@ -219,7 +224,8 @@ public record PklEvaluatorSettings(
@Override
public int hashCode() {
var result =
Objects.hash(externalProperties, env, noCache, moduleCacheDir, timeout, rootDir, http);
Objects.hash(
externalProperties, env, color, noCache, moduleCacheDir, timeout, rootDir, http);
result = 31 * result + hashPatterns(allowedModules);
result = 31 * result + hashPatterns(allowedResources);
return result;

View File

@@ -143,7 +143,7 @@ public final class Project {
.build();
}
// stack frame transformer never used; this exception has no stack frames.
throw vmException.toPklException(StackFrameTransformers.defaultTransformer);
throw vmException.toPklException(StackFrameTransformers.defaultTransformer, false);
}
throw e;
} catch (URISyntaxException e) {
@@ -192,6 +192,7 @@ public final class Project {
var analyzer =
new Analyzer(
StackFrameTransformers.defaultTransformer,
builder.getColor(),
SecurityManagers.defaultManager,
builder.getModuleKeyFactories(),
builder.getModuleCacheDir(),
@@ -517,6 +518,7 @@ public final class Project {
env,
allowedModules,
allowedResources,
null,
noCache,
moduleCacheDir,
modulePath,

View File

@@ -98,6 +98,7 @@ public final class ProjectPackager {
private final Path workingDir;
private final String outputPathPattern;
private final StackFrameTransformer stackFrameTransformer;
private final boolean color;
private final SecurityManager securityManager;
private final PackageResolver packageResolver;
private final boolean skipPublishCheck;
@@ -108,6 +109,7 @@ public final class ProjectPackager {
Path workingDir,
String outputPathPattern,
StackFrameTransformer stackFrameTransformer,
boolean color,
SecurityManager securityManager,
HttpClient httpClient,
boolean skipPublishCheck,
@@ -116,6 +118,7 @@ public final class ProjectPackager {
this.workingDir = workingDir;
this.outputPathPattern = outputPathPattern;
this.stackFrameTransformer = stackFrameTransformer;
this.color = color;
this.securityManager = securityManager;
// intentionally use InMemoryPackageResolver
this.packageResolver = PackageResolver.getInstance(securityManager, httpClient, null);
@@ -409,14 +412,14 @@ public final class ProjectPackager {
.evalError("invalidModuleUri", importStr)
.withSourceSection(sourceSection)
.build()
.toPklException(stackFrameTransformer);
.toPklException(stackFrameTransformer, color);
}
if (importStr.startsWith("/") && !project.getProjectDir().toString().equals("/")) {
throw new VmExceptionBuilder()
.evalError("invalidRelativeProjectImport", importStr)
.withSourceSection(sourceSection)
.build()
.toPklException(stackFrameTransformer);
.toPklException(stackFrameTransformer, color);
}
var currentPath = pklModulePath.getParent();
var importPath = Path.of(importUri.getPath());
@@ -433,7 +436,7 @@ public final class ProjectPackager {
.evalError("invalidRelativeProjectImport", importStr)
.withSourceSection(sourceSection)
.build()
.toPklException(stackFrameTransformer);
.toPklException(stackFrameTransformer, color);
}
}
}

View File

@@ -78,12 +78,13 @@ public class ReplServer implements AutoCloseable {
@Nullable DeclaredDependencies projectDependencies,
@Nullable String outputFormat,
Path workingDir,
StackFrameTransformer frameTransformer) {
StackFrameTransformer frameTransformer,
boolean color) {
this.workingDir = workingDir;
this.securityManager = securityManager;
this.moduleResolver = new ModuleResolver(moduleKeyFactories);
this.errorRenderer = new VmExceptionRenderer(new StackTraceRenderer(frameTransformer));
this.errorRenderer = new VmExceptionRenderer(new StackTraceRenderer(frameTransformer), color);
replState = new ReplState(createEmptyReplModule(BaseModule.getModuleClass().getPrototype()));
var languageRef = new MutableReference<VmLanguage>(null);

View File

@@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.List;
import java.util.function.Function;
import org.pkl.core.StackFrame;
import org.pkl.core.runtime.TextFormatter.Element;
import org.pkl.core.util.Nullable;
public final class StackTraceRenderer {
@@ -28,66 +29,68 @@ public final class StackTraceRenderer {
this.frameTransformer = frameTransformer;
}
public void render(List<StackFrame> frames, @Nullable String hint, StringBuilder builder) {
public void render(List<StackFrame> frames, @Nullable String hint, TextFormatter out) {
var compressed = compressFrames(frames);
doRender(compressed, hint, builder, "", true);
doRender(compressed, hint, out, "", true);
}
// non-private for testing
void doRender(
List<Object /*StackFrame|StackFrameLoop*/> frames,
@Nullable String hint,
StringBuilder builder,
TextFormatter out,
String leftMargin,
boolean isFirstElement) {
for (var frame : frames) {
if (frame instanceof StackFrameLoop loop) {
// ensure a cycle of length 1 doesn't get rendered as a loop
if (loop.count == 1) {
doRender(loop.frames, null, builder, leftMargin, isFirstElement);
doRender(loop.frames, null, out, leftMargin, isFirstElement);
} else {
if (!isFirstElement) {
builder.append(leftMargin).append("\n");
out.margin(leftMargin).newline();
}
builder.append(leftMargin).append("┌─ ").append(loop.count).append(" repetitions of:\n");
out.margin(leftMargin)
.margin("┌─ ")
.style(Element.STACK_OVERFLOW_LOOP_COUNT)
.append(loop.count)
.style(Element.TEXT)
.append(" repetitions of:\n");
var newLeftMargin = leftMargin + "";
doRender(loop.frames, null, builder, newLeftMargin, isFirstElement);
doRender(loop.frames, null, out, newLeftMargin, isFirstElement);
if (isFirstElement) {
renderHint(hint, builder, newLeftMargin);
renderHint(hint, out, newLeftMargin);
isFirstElement = false;
}
builder.append(leftMargin).append("└─\n");
out.margin(leftMargin).margin("└─").newline();
}
} else {
if (!isFirstElement) {
builder.append(leftMargin).append('\n');
out.margin(leftMargin).newline();
}
renderFrame((StackFrame) frame, builder, leftMargin);
renderFrame((StackFrame) frame, out, leftMargin);
}
if (isFirstElement) {
renderHint(hint, builder, leftMargin);
renderHint(hint, out, leftMargin);
isFirstElement = false;
}
}
}
private void renderFrame(StackFrame frame, StringBuilder builder, String leftMargin) {
private void renderFrame(StackFrame frame, TextFormatter out, String leftMargin) {
var transformed = frameTransformer.apply(frame);
renderSourceLine(transformed, builder, leftMargin);
renderSourceLocation(transformed, builder, leftMargin);
renderSourceLine(transformed, out, leftMargin);
renderSourceLocation(transformed, out, leftMargin);
}
private void renderHint(@Nullable String hint, StringBuilder builder, String leftMargin) {
private void renderHint(@Nullable String hint, TextFormatter out, String leftMargin) {
if (hint == null || hint.isEmpty()) return;
builder.append('\n');
builder.append(leftMargin);
builder.append(hint);
builder.append('\n');
out.newline().margin(leftMargin).style(Element.HINT).append(hint).newline();
}
private void renderSourceLine(StackFrame frame, StringBuilder builder, String leftMargin) {
private void renderSourceLine(StackFrame frame, TextFormatter out, String leftMargin) {
var originalSourceLine = frame.getSourceLines().get(0);
var leadingWhitespace = VmUtils.countLeadingWhitespace(originalSourceLine);
var sourceLine = originalSourceLine.strip();
@@ -98,27 +101,28 @@ public final class StackTraceRenderer {
: sourceLine.length();
var prefix = frame.getStartLine() + " | ";
builder.append(leftMargin).append(prefix).append(sourceLine).append('\n');
builder.append(leftMargin);
//noinspection StringRepeatCanBeUsed
for (int i = 1; i < prefix.length() + startColumn; i++) {
builder.append(' ');
}
//noinspection StringRepeatCanBeUsed
for (int i = startColumn; i <= endColumn; i++) {
builder.append('^');
}
builder.append('\n');
out.margin(leftMargin)
.style(Element.LINE_NUMBER)
.append(prefix)
.style(Element.TEXT)
.append(sourceLine)
.newline()
.margin(leftMargin)
.repeat(prefix.length() + startColumn - 1, ' ')
.style(Element.ERROR)
.repeat(endColumn - startColumn + 1, '^')
.newline();
}
private void renderSourceLocation(StackFrame frame, StringBuilder builder, String leftMargin) {
builder.append(leftMargin).append("at ");
if (frame.getMemberName() != null) {
builder.append(frame.getMemberName());
} else {
builder.append("<unknown>");
}
builder.append(" (").append(frame.getModuleUri()).append(')').append('\n');
private void renderSourceLocation(StackFrame frame, TextFormatter out, String leftMargin) {
out.margin(leftMargin)
.style(Element.TEXT)
.append("at ")
.append(frame.getMemberName() != null ? frame.getMemberName() : "<unknown>")
.append(" (")
.append(frame.getModuleUri())
.append(")")
.newline();
}
/**

View File

@@ -59,7 +59,8 @@ public final class TestRunner {
try {
checkAmendsPklTest(testModule);
} catch (VmException v) {
var error = new TestResults.Error(v.getMessage(), v.toPklException(stackFrameTransformer));
var error =
new TestResults.Error(v.getMessage(), v.toPklException(stackFrameTransformer, false));
return resultsBuilder.setError(error).build();
}
@@ -108,7 +109,7 @@ public final class TestRunner {
} catch (VmException err) {
var error =
new TestResults.Error(
err.getMessage(), err.toPklException(stackFrameTransformer));
err.getMessage(), err.toPklException(stackFrameTransformer, false));
resultBuilder.addError(error);
}
return true;
@@ -208,7 +209,7 @@ public final class TestRunner {
errored.set(true);
testResultBuilder.addError(
new TestResults.Error(
err.getMessage(), err.toPklException(stackFrameTransformer)));
err.getMessage(), err.toPklException(stackFrameTransformer, false)));
return true;
}
var expectedValue = VmUtils.readMember(expectedGroup, exampleIndex);
@@ -305,7 +306,7 @@ public final class TestRunner {
} catch (VmException err) {
testResultBuilder.addError(
new TestResults.Error(
err.getMessage(), err.toPklException(stackFrameTransformer)));
err.getMessage(), err.toPklException(stackFrameTransformer, false)));
allSucceeded.set(false);
success.set(false);
return true;

View File

@@ -0,0 +1,185 @@
/*
* Copyright © 2024 Apple Inc. and the Pkl project authors. All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.pkl.core.runtime;
import java.io.PrintWriter;
import java.util.HashMap;
import java.util.Map;
import org.pkl.core.util.Nullable;
import org.pkl.core.util.StringBuilderWriter;
/*
TODO:
* Make "margin matter" a facility of the formatter, managing margins in e.g. `newline()`.
- `pushMargin(String matter)` / `popMargin()`
* Replace implementation methods `repeat()` with more semantic equivalents.
- `underline(int startColumn, int endColumn)`
* Replace `newInstance()` with an alternative that doesn't require instance management,
i.e. better composition (currently only used for pre-rendering `hint`s).
* Assert assumed invariants (e.g. `append(String text)` checking there are no newlines).
* Replace `THEME_ANSI` with one read from `pkl:settings`.
*/
public final class TextFormatter {
public static final Map<Element, @Nullable Styling> THEME_PLAIN = new HashMap<>();
public static final Map<Element, @Nullable Styling> THEME_ANSI;
static {
THEME_ANSI =
Map.of(
Element.MARGIN, new Styling(Color.YELLOW, true, false),
Element.HINT, new Styling(Color.YELLOW, true, true),
Element.STACK_OVERFLOW_LOOP_COUNT, new Styling(Color.MAGENTA, false, false),
Element.LINE_NUMBER, new Styling(Color.BLUE, false, false),
Element.ERROR_HEADER, new Styling(Color.RED, false, false),
Element.ERROR, new Styling(Color.RED, false, true));
}
private final Map<Element, @Nullable Styling> theme;
private final StringBuilder builder = new StringBuilder();
private @Nullable Styling currentStyle;
private TextFormatter(Map<Element, Styling> theme) {
this.theme = theme;
this.currentStyle = theme.getOrDefault(Element.PLAIN, null);
}
public static TextFormatter create(boolean usingColor) {
return new TextFormatter(usingColor ? THEME_ANSI : THEME_PLAIN);
}
public PrintWriter toPrintWriter() {
return new PrintWriter(new StringBuilderWriter(builder));
}
public String toString() {
return builder.toString();
}
public TextFormatter newline() {
return newlines(1);
}
public TextFormatter newInstance() {
return new TextFormatter(theme);
}
public TextFormatter newlines(int count) {
return repeat(count, '\n');
}
public TextFormatter margin(String marginMatter) {
return style(Element.MARGIN).append(marginMatter);
}
public TextFormatter style(Element element) {
var style = theme.getOrDefault(element, null);
if (currentStyle == style) {
return this;
}
if (style == null) {
append("\033[0m");
currentStyle = style;
return this;
}
var colorCode =
style.bright() ? style.foreground().fgBrightCode() : style.foreground().fgCode();
append('\033');
append('[');
append(colorCode);
if (style.bold() && (currentStyle == null || !currentStyle.bold())) {
append(";1");
} else if (!style.bold() && currentStyle != null && currentStyle.bold()) {
append(";22");
}
append('m');
currentStyle = style;
return this;
}
public TextFormatter repeat(int width, char ch) {
for (var i = 0; i < width; i++) {
append(ch);
}
return this;
}
public TextFormatter append(String s) {
builder.append(s);
return this;
}
public TextFormatter append(char ch) {
builder.append(ch);
return this;
}
public TextFormatter append(int i) {
builder.append(i);
return this;
}
public TextFormatter append(Object obj) {
builder.append(obj);
return this;
}
public enum Element {
PLAIN,
MARGIN,
HINT,
STACK_OVERFLOW_LOOP_COUNT,
LINE_NUMBER,
TEXT,
ERROR_HEADER,
ERROR
}
public record Styling(Color foreground, boolean bold, boolean bright) {}
public enum Color {
BLACK(30),
RED(31),
GREEN(32),
YELLOW(33),
BLUE(34),
MAGENTA(35),
CYAN(36),
WHITE(37);
private final int code;
Color(int code) {
this.code = code;
}
public int fgCode() {
return code;
}
public int bgCode() {
return code + 10;
}
public int fgBrightCode() {
return code + 60;
}
public int bgBrightCode() {
return code + 70;
}
}
}

View File

@@ -58,8 +58,8 @@ public final class VmBugException extends VmException {
@Override
@TruffleBoundary
public PklException toPklException(StackFrameTransformer transformer) {
var renderer = new VmExceptionRenderer(new StackTraceRenderer(transformer));
public PklException toPklException(StackFrameTransformer transformer, boolean color) {
var renderer = new VmExceptionRenderer(new StackTraceRenderer(transformer), color);
var rendered = renderer.render(this);
return new PklBugException(rendered, this);
}

View File

@@ -117,8 +117,8 @@ public abstract class VmException extends AbstractTruffleException {
}
@TruffleBoundary
public PklException toPklException(StackFrameTransformer transformer) {
var renderer = new VmExceptionRenderer(new StackTraceRenderer(transformer));
public PklException toPklException(StackFrameTransformer transformer, boolean color) {
var renderer = new VmExceptionRenderer(new StackTraceRenderer(transformer), color);
var rendered = renderer.render(this);
return new PklException(rendered);
}

View File

@@ -16,66 +16,67 @@
package org.pkl.core.runtime;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors;
import org.pkl.core.Release;
import org.pkl.core.runtime.TextFormatter.Element;
import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.Nullable;
import org.pkl.core.util.StringBuilderWriter;
public final class VmExceptionRenderer {
private final @Nullable StackTraceRenderer stackTraceRenderer;
private final boolean color;
/**
* 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.
*/
public VmExceptionRenderer(@Nullable StackTraceRenderer stackTraceRenderer) {
public VmExceptionRenderer(@Nullable StackTraceRenderer stackTraceRenderer, boolean color) {
this.stackTraceRenderer = stackTraceRenderer;
this.color = color;
}
@TruffleBoundary
public String render(VmException exception) {
var builder = new StringBuilder();
render(exception, builder);
return builder.toString();
var formatter = TextFormatter.create(color);
render(exception, formatter);
return formatter.toString();
}
private void render(VmException exception, StringBuilder builder) {
private void render(VmException exception, TextFormatter out) {
if (exception instanceof VmBugException bugException) {
renderBugException(bugException, builder);
renderBugException(bugException, out);
} else {
renderException(exception, builder, true);
renderException(exception, out, true);
}
}
private void renderBugException(VmBugException exception, StringBuilder builder) {
private void renderBugException(VmBugException exception, TextFormatter out) {
// if a cause exists, it's more useful to report just that
var exceptionToReport = exception.getCause() != null ? exception.getCause() : exception;
var exceptionUrl = URLEncoder.encode(exceptionToReport.toString(), StandardCharsets.UTF_8);
builder
.append("An unexpected error has occurred. Would you mind filing a bug report?\n")
.append("Cmd+Double-click the link below to open an issue.\n")
.append(
"Please copy and paste the entire error output into the issue's description, provided you can share it.\n\n")
.append("https://github.com/apple/pkl/issues/new\n\n");
out.style(Element.TEXT)
.append("An unexpected error has occurred. Would you mind filing a bug report?")
.newline()
.append("Cmd+Double-click the link below to open an issue.")
.newline()
.append("Please copy and paste the entire error output into the issue's description.")
.newlines(2)
.append("https://github.com/apple/pkl/issues/new")
.newlines(2)
.append(exceptionUrl.replaceAll("\\+", "%20"))
.newlines(2);
builder.append(
URLEncoder.encode(exceptionToReport.toString(), StandardCharsets.UTF_8)
.replaceAll("\\+", "%20"));
renderException(exception, out, true);
builder.append("\n\n");
renderException(exception, builder, true);
builder.append('\n').append(Release.current().versionInfo()).append("\n\n");
out.newline().style(Element.TEXT).append(Release.current().versionInfo()).newlines(2);
exceptionToReport.printStackTrace(new PrintWriter(new StringBuilderWriter(builder)));
exceptionToReport.printStackTrace(out.toPrintWriter());
}
private void renderException(VmException exception, StringBuilder builder, boolean withHeader) {
var header = " Pkl Error ";
private void renderException(VmException exception, TextFormatter out, boolean withHeader) {
String message;
var hint = exception.getHint();
if (exception.isExternalMessage()) {
@@ -96,15 +97,9 @@ public final class VmExceptionRenderer {
}
if (withHeader) {
builder.append(header).append('\n');
}
builder.append(message).append('\n');
if (exception instanceof VmWrappedEvalException vmWrappedEvalException) {
var sb = new StringBuilder();
renderException(vmWrappedEvalException.getWrappedException(), sb, false);
hint = sb.toString().lines().map((it) -> ">\t" + it).collect(Collectors.joining("\n"));
out.style(Element.ERROR_HEADER).append(" Pkl Error ").newline();
}
out.style(Element.ERROR).append(message).newline();
// include cause's message unless it's the same as this exception's message
if (exception.getCause() != null) {
@@ -112,11 +107,11 @@ public final class VmExceptionRenderer {
var causeMessage = cause.getMessage();
// null for Truffle's LazyStackTrace
if (causeMessage != null && !causeMessage.equals(message)) {
builder
out.style(Element.TEXT)
.append(cause.getClass().getSimpleName())
.append(": ")
.append(causeMessage)
.append('\n');
.newline();
}
}
@@ -124,22 +119,28 @@ public final class VmExceptionRenderer {
exception.getProgramValues().stream().mapToInt(v -> v.name.length()).max().orElse(0);
for (var value : exception.getProgramValues()) {
builder.append(value.name);
builder.append(" ".repeat(Math.max(0, maxNameLength - value.name.length())));
builder.append(": ");
builder.append(value);
builder.append('\n');
out.style(Element.TEXT)
.append(value.name)
.repeat(Math.max(0, maxNameLength - value.name.length()), ' ')
.append(": ")
.append(value)
.newline();
}
if (stackTraceRenderer != null) {
var frames = StackTraceGenerator.capture(exception);
if (exception instanceof VmWrappedEvalException vmWrappedEvalException) {
var sb = out.newInstance();
renderException(vmWrappedEvalException.getWrappedException(), sb, false);
hint = sb.toString().lines().map((it) -> ">\t" + it).collect(Collectors.joining("\n"));
}
if (!frames.isEmpty()) {
builder.append('\n');
stackTraceRenderer.render(frames, hint, builder);
stackTraceRenderer.render(frames, hint, out.newline());
} else if (hint != null) {
// render hint if there are no stack frames
builder.append('\n');
builder.append(hint);
out.newline().style(Element.HINT).append(hint);
}
}
}

View File

@@ -28,7 +28,7 @@ import org.pkl.core.stdlib.PklName;
public final class TestNodes {
private static final VmExceptionRenderer noStackTraceExceptionRenderer =
new VmExceptionRenderer(null);
new VmExceptionRenderer(null, false);
private TestNodes() {}

View File

@@ -31,6 +31,7 @@ class AnalyzerTest {
private val simpleAnalyzer =
Analyzer(
StackFrameTransformers.defaultTransformer,
false,
SecurityManagers.defaultManager,
listOf(ModuleKeyFactories.file, ModuleKeyFactories.standardLibrary, ModuleKeyFactories.pkg),
null,
@@ -108,6 +109,7 @@ class AnalyzerTest {
val analyzer =
Analyzer(
StackFrameTransformers.defaultTransformer,
false,
SecurityManagers.defaultManager,
listOf(ModuleKeyFactories.file, ModuleKeyFactories.standardLibrary, ModuleKeyFactories.pkg),
tempDir.resolve("packages"),
@@ -177,6 +179,7 @@ class AnalyzerTest {
val analyzer =
Analyzer(
StackFrameTransformers.defaultTransformer,
false,
SecurityManagers.defaultManager,
listOf(
ModuleKeyFactories.file,
@@ -288,6 +291,7 @@ class AnalyzerTest {
val analyzer =
Analyzer(
StackFrameTransformers.defaultTransformer,
false,
SecurityManagers.defaultManager,
listOf(
ModuleKeyFactories.file,

View File

@@ -43,7 +43,8 @@ class ReplServerTest {
null,
null,
"/".toPath(),
StackFrameTransformers.defaultTransformer
StackFrameTransformers.defaultTransformer,
false,
)
@Test

View File

@@ -78,7 +78,7 @@ class ImportsAndReadsParserTest {
assertThrows<VmException> {
ImportsAndReadsParser.parse(moduleKey, moduleKey.resolve(SecurityManagers.defaultManager))
}
assertThat(err.toPklException(StackFrameTransformers.defaultTransformer))
assertThat(err.toPklException(StackFrameTransformers.defaultTransformer, false))
.hasMessage(
"""
Pkl Error

View File

@@ -65,6 +65,7 @@ class ProjectTest {
mapOf("one" to "1"),
listOf("foo:", "bar:").map(Pattern::compile),
listOf("baz:", "biz:").map(Pattern::compile),
null,
false,
path.resolve("cache/"),
listOf(path.resolve("modulepath1/"), path.resolve("modulepath2/")),

View File

@@ -190,7 +190,9 @@ class StackTraceRendererTest {
}
val loop = StackTraceRenderer.StackFrameLoop(loopFrames, 1)
val frames = listOf(createFrame("bar", 1), createFrame("baz", 2), loop)
val renderedFrames = buildString { renderer.doRender(frames, null, this, "", true) }
val formatter = TextFormatter.create(false)
renderer.doRender(frames, null, formatter, "", true)
val renderedFrames = formatter.toString()
assertThat(renderedFrames)
.isEqualTo(
"""

View File

@@ -19,6 +19,7 @@ evaluatorSettings {
["two"] = "2"
}
moduleCacheDir = "my-cache-dir/"
color = "always"
noCache = false
rootDir = "my-root-dir/"
timeout = 5.min