mirror of
https://github.com/apple/pkl.git
synced 2026-04-09 18:33:40 +02:00
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:
committed by
GitHub
parent
e217cfcd6f
commit
03462fefae
@@ -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 {
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
185
pkl-core/src/main/java/org/pkl/core/runtime/TextFormatter.java
Normal file
185
pkl-core/src/main/java/org/pkl/core/runtime/TextFormatter.java
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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() {}
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -43,7 +43,8 @@ class ReplServerTest {
|
||||
null,
|
||||
null,
|
||||
"/".toPath(),
|
||||
StackFrameTransformers.defaultTransformer
|
||||
StackFrameTransformers.defaultTransformer,
|
||||
false,
|
||||
)
|
||||
|
||||
@Test
|
||||
|
||||
@@ -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 ––
|
||||
|
||||
@@ -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/")),
|
||||
|
||||
@@ -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(
|
||||
"""
|
||||
|
||||
@@ -19,6 +19,7 @@ evaluatorSettings {
|
||||
["two"] = "2"
|
||||
}
|
||||
moduleCacheDir = "my-cache-dir/"
|
||||
color = "always"
|
||||
noCache = false
|
||||
rootDir = "my-root-dir/"
|
||||
timeout = 5.min
|
||||
|
||||
Reference in New Issue
Block a user