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
@@ -50,7 +50,8 @@ public class ListSort {
null, null,
null, null,
IoUtils.getCurrentWorkingDir(), IoUtils.getCurrentWorkingDir(),
StackFrameTransformers.defaultTransformer); StackFrameTransformers.defaultTransformer,
false);
private static final List<Object> list = new ArrayList<>(100000); private static final List<Object> list = new ArrayList<>(100000);
static { static {
@@ -19,6 +19,19 @@ Patterns are matched against the beginning of resource URIs.
At least one pattern needs to match for a resource to be readable. At least one pattern needs to match for a resource to be readable.
==== ====
[[color]]
.--color
[%collapsible]
====
Default: `auto` +
When to format messages with ANSI color codes.
Possible values:
- `"never"`: Never format
- `"auto"`: Format if `stdin`, `stdout`, or `stderr` are connected to a console.
- `"always"`: Always format
====
[[cache-dir]] [[cache-dir]]
.--cache-dir .--cache-dir
[%collapsible] [%collapsible]
@@ -67,6 +67,13 @@ The cache directory for storing packages.
If `null`, defaults to `~/.pkl/cache`. If `null`, defaults to `~/.pkl/cache`.
==== ====
.color: Property<Boolean>
[%collapsible]
====
Default: `false` +
Format messages using ANSI color.
====
.noCache: Property<Boolean> .noCache: Property<Boolean>
[%collapsible] [%collapsible]
==== ====
+2 -1
View File
@@ -96,7 +96,8 @@ class DocSnippetTestsEngine : HierarchicalTestEngine<DocSnippetTestsEngine.Execu
null, null,
null, null,
IoUtils.getCurrentWorkingDir(), IoUtils.getCurrentWorkingDir(),
StackFrameTransformers.defaultTransformer StackFrameTransformers.defaultTransformer,
false,
) )
return ExecutionContext(replServer) return ExecutionContext(replServer)
} }
@@ -120,7 +120,7 @@ constructor(
try { try {
moduleResolver.resolve(uri) moduleResolver.resolve(uri)
} catch (e: VmException) { } catch (e: VmException) {
throw e.toPklException(stackFrameTransformer) throw e.toPklException(stackFrameTransformer, options.base.color?.hasColor() ?: false)
} }
val substituted = val substituted =
pathStr pathStr
@@ -81,6 +81,7 @@ class CliProjectPackager(
cliOptions.normalizedWorkingDir, cliOptions.normalizedWorkingDir,
outputPath, outputPath,
stackFrameTransformer, stackFrameTransformer,
cliOptions.color?.hasColor() ?: false,
securityManager, securityManager,
httpClient, httpClient,
skipPublishCheck, skipPublishCheck,
@@ -66,7 +66,8 @@ internal class CliRepl(private val options: CliEvaluatorOptions) : CliCommand(op
project?.dependencies, project?.dependencies,
options.outputFormat, options.outputFormat,
options.base.normalizedWorkingDir, options.base.normalizedWorkingDir,
stackFrameTransformer stackFrameTransformer,
options.base.color?.hasColor() ?: false,
) )
Repl(options.base.normalizedWorkingDir, server).run() Repl(options.base.normalizedWorkingDir, server).run()
} }
@@ -41,7 +41,8 @@ class ReplMessagesTest {
null, null,
null, null,
"/".toPath(), "/".toPath(),
StackFrameTransformers.defaultTransformer StackFrameTransformers.defaultTransformer,
false,
) )
@Test @Test
@@ -20,6 +20,7 @@ import java.nio.file.Files
import java.nio.file.Path import java.nio.file.Path
import java.time.Duration import java.time.Duration
import java.util.regex.Pattern import java.util.regex.Pattern
import org.pkl.core.evaluatorSettings.Color
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader
import org.pkl.core.module.ProjectDependenciesManager import org.pkl.core.module.ProjectDependenciesManager
import org.pkl.core.util.IoUtils import org.pkl.core.util.IoUtils
@@ -101,6 +102,9 @@ data class CliBaseOptions(
/** The cache directory for storing packages. */ /** The cache directory for storing packages. */
private val moduleCacheDir: Path? = null, private val moduleCacheDir: Path? = null,
/** Whether to render errors in ANSI color. */
val color: Color? = null,
/** Whether to disable the module cache. */ /** Whether to disable the module cache. */
val noCache: Boolean = false, val noCache: Boolean = false,
@@ -284,6 +284,7 @@ abstract class CliCommand(protected val cliOptions: CliBaseOptions) {
.setEnvironmentVariables(environmentVariables) .setEnvironmentVariables(environmentVariables)
.addModuleKeyFactories(moduleKeyFactories(modulePathResolver)) .addModuleKeyFactories(moduleKeyFactories(modulePathResolver))
.addResourceReaders(resourceReaders(modulePathResolver)) .addResourceReaders(resourceReaders(modulePathResolver))
.setColor(cliOptions.color?.hasColor() ?: false)
.setLogger(Loggers.stdErr()) .setLogger(Loggers.stdErr())
.setTimeout(cliOptions.timeout) .setTimeout(cliOptions.timeout)
.setModuleCacheDir(moduleCacheDir) .setModuleCacheDir(moduleCacheDir)
@@ -17,6 +17,7 @@ package org.pkl.commons.cli.commands
import com.github.ajalt.clikt.parameters.groups.OptionGroup import com.github.ajalt.clikt.parameters.groups.OptionGroup
import com.github.ajalt.clikt.parameters.options.* import com.github.ajalt.clikt.parameters.options.*
import com.github.ajalt.clikt.parameters.types.enum
import com.github.ajalt.clikt.parameters.types.int import com.github.ajalt.clikt.parameters.types.int
import com.github.ajalt.clikt.parameters.types.long import com.github.ajalt.clikt.parameters.types.long
import com.github.ajalt.clikt.parameters.types.path import com.github.ajalt.clikt.parameters.types.path
@@ -29,6 +30,7 @@ import java.util.regex.Pattern
import org.pkl.commons.cli.CliBaseOptions import org.pkl.commons.cli.CliBaseOptions
import org.pkl.commons.cli.CliException import org.pkl.commons.cli.CliException
import org.pkl.commons.shlex import org.pkl.commons.shlex
import org.pkl.core.evaluatorSettings.Color
import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader import org.pkl.core.evaluatorSettings.PklEvaluatorSettings.ExternalReader
import org.pkl.core.runtime.VmUtils import org.pkl.core.runtime.VmUtils
import org.pkl.core.util.IoUtils import org.pkl.core.util.IoUtils
@@ -141,6 +143,17 @@ class BaseOptions : OptionGroup() {
) )
.associateProps() .associateProps()
val color: Color by
option(
names = arrayOf("--color"),
metavar = "<when>",
help =
"Whether to format messages in ANSI color. Possible values of <when> are 'never', 'auto', and 'always'."
)
.enum<Color> { it.name.lowercase() }
.single()
.default(Color.AUTO)
val noCache: Boolean by val noCache: Boolean by
option(names = arrayOf("--no-cache"), help = "Disable caching of packages") option(names = arrayOf("--no-cache"), help = "Disable caching of packages")
.single() .single()
@@ -265,6 +278,7 @@ class BaseOptions : OptionGroup() {
projectDir = projectOptions?.projectDir, projectDir = projectOptions?.projectDir,
timeout = timeout, timeout = timeout,
moduleCacheDir = cacheDir ?: defaults.normalizedModuleCacheDir, moduleCacheDir = cacheDir ?: defaults.normalizedModuleCacheDir,
color = color,
noCache = noCache, noCache = noCache,
testMode = testMode, testMode = testMode,
testPort = testPort, testPort = testPort,
@@ -41,6 +41,7 @@ import org.pkl.core.util.Nullable;
/** Utility library for static analysis of Pkl programs. */ /** Utility library for static analysis of Pkl programs. */
public class Analyzer { public class Analyzer {
private final StackFrameTransformer transformer; private final StackFrameTransformer transformer;
private final boolean color;
private final SecurityManager securityManager; private final SecurityManager securityManager;
private final @Nullable Path moduleCacheDir; private final @Nullable Path moduleCacheDir;
private final @Nullable DeclaredDependencies projectDependencies; private final @Nullable DeclaredDependencies projectDependencies;
@@ -49,12 +50,14 @@ public class Analyzer {
public Analyzer( public Analyzer(
StackFrameTransformer transformer, StackFrameTransformer transformer,
boolean color,
SecurityManager securityManager, SecurityManager securityManager,
Collection<ModuleKeyFactory> moduleKeyFactories, Collection<ModuleKeyFactory> moduleKeyFactories,
@Nullable Path moduleCacheDir, @Nullable Path moduleCacheDir,
@Nullable DeclaredDependencies projectDependencies, @Nullable DeclaredDependencies projectDependencies,
HttpClient httpClient) { HttpClient httpClient) {
this.transformer = transformer; this.transformer = transformer;
this.color = color;
this.securityManager = securityManager; this.securityManager = securityManager;
this.moduleCacheDir = moduleCacheDir; this.moduleCacheDir = moduleCacheDir;
this.projectDependencies = projectDependencies; this.projectDependencies = projectDependencies;
@@ -82,7 +85,7 @@ public class Analyzer {
} catch (PklException err) { } catch (PklException err) {
throw err; throw err;
} catch (VmException err) { } catch (VmException err) {
throw err.toPklException(transformer); throw err.toPklException(transformer, color);
} catch (Exception e) { } catch (Exception e) {
throw new PklBugException(e); throw new PklBugException(e);
} finally { } finally {
@@ -61,6 +61,8 @@ public final class EvaluatorBuilder {
private @Nullable String outputFormat; private @Nullable String outputFormat;
private boolean color = false;
private @Nullable StackFrameTransformer stackFrameTransformer; private @Nullable StackFrameTransformer stackFrameTransformer;
private @Nullable DeclaredDependencies dependencies; private @Nullable DeclaredDependencies dependencies;
@@ -144,6 +146,17 @@ public final class EvaluatorBuilder {
return new 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. */ /** Sets the given stack frame transformer, replacing any previously set transformer. */
public EvaluatorBuilder setStackFrameTransformer(StackFrameTransformer stackFrameTransformer) { public EvaluatorBuilder setStackFrameTransformer(StackFrameTransformer stackFrameTransformer) {
this.stackFrameTransformer = stackFrameTransformer; this.stackFrameTransformer = stackFrameTransformer;
@@ -475,6 +488,9 @@ public final class EvaluatorBuilder {
if (settings.rootDir() != null) { if (settings.rootDir() != null) {
setRootDir(settings.rootDir()); setRootDir(settings.rootDir());
} }
if (settings.color() != null) {
setColor(settings.color().hasColor());
}
if (Boolean.TRUE.equals(settings.noCache())) { if (Boolean.TRUE.equals(settings.noCache())) {
setModuleCacheDir(null); setModuleCacheDir(null);
} else if (settings.moduleCacheDir() != null) { } else if (settings.moduleCacheDir() != null) {
@@ -513,6 +529,7 @@ public final class EvaluatorBuilder {
return new EvaluatorImpl( return new EvaluatorImpl(
stackFrameTransformer, stackFrameTransformer,
color,
securityManager, securityManager,
httpClient, httpClient,
new LoggerImpl(logger, stackFrameTransformer), new LoggerImpl(logger, stackFrameTransformer),
@@ -57,6 +57,7 @@ import org.pkl.core.util.Nullable;
public class EvaluatorImpl implements Evaluator { public class EvaluatorImpl implements Evaluator {
protected final StackFrameTransformer frameTransformer; protected final StackFrameTransformer frameTransformer;
protected final boolean color;
protected final ModuleResolver moduleResolver; protected final ModuleResolver moduleResolver;
protected final Context polyglotContext; protected final Context polyglotContext;
protected final @Nullable Duration timeout; protected final @Nullable Duration timeout;
@@ -68,6 +69,7 @@ public class EvaluatorImpl implements Evaluator {
public EvaluatorImpl( public EvaluatorImpl(
StackFrameTransformer transformer, StackFrameTransformer transformer,
boolean color,
SecurityManager manager, SecurityManager manager,
HttpClient httpClient, HttpClient httpClient,
Logger logger, Logger logger,
@@ -82,6 +84,7 @@ public class EvaluatorImpl implements Evaluator {
securityManager = manager; securityManager = manager;
frameTransformer = transformer; frameTransformer = transformer;
this.color = color;
moduleResolver = new ModuleResolver(factories); moduleResolver = new ModuleResolver(factories);
this.logger = new BufferedLogger(logger); this.logger = new BufferedLogger(logger);
packageResolver = PackageResolver.getInstance(securityManager, httpClient, moduleCacheDir); packageResolver = PackageResolver.getInstance(securityManager, httpClient, moduleCacheDir);
@@ -304,20 +307,20 @@ public class EvaluatorImpl implements Evaluator {
.bug("Stack overflow") .bug("Stack overflow")
.withCause(e.getCause()) .withCause(e.getCause())
.build() .build()
.toPklException(frameTransformer); .toPklException(frameTransformer, color);
} }
handleTimeout(timeoutTask); handleTimeout(timeoutTask);
throw e.toPklException(frameTransformer); throw e.toPklException(frameTransformer, color);
} catch (VmException e) { } catch (VmException e) {
handleTimeout(timeoutTask); handleTimeout(timeoutTask);
throw e.toPklException(frameTransformer); throw e.toPklException(frameTransformer, color);
} catch (Exception e) { } catch (Exception e) {
throw new PklBugException(e); throw new PklBugException(e);
} catch (ExceptionInInitializerError e) { } catch (ExceptionInInitializerError e) {
if (!(e.getCause() instanceof VmException vmException)) { if (!(e.getCause() instanceof VmException vmException)) {
throw new PklBugException(e); throw new PklBugException(e);
} }
var pklException = vmException.toPklException(frameTransformer); var pklException = vmException.toPklException(frameTransformer, color);
var error = new ExceptionInInitializerError(pklException); var error = new ExceptionInInitializerError(pklException);
error.setStackTrace(e.getStackTrace()); error.setStackTrace(e.getStackTrace());
throw new PklBugException(error); 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 Map<String, String> env,
@Nullable List<Pattern> allowedModules, @Nullable List<Pattern> allowedModules,
@Nullable List<Pattern> allowedResources, @Nullable List<Pattern> allowedResources,
@Nullable Color color,
@Nullable Boolean noCache, @Nullable Boolean noCache,
@Nullable Path moduleCacheDir, @Nullable Path moduleCacheDir,
@Nullable List<Path> modulePath, @Nullable List<Path> modulePath,
@@ -102,11 +103,14 @@ public record PklEvaluatorSettings(
Collectors.toMap( Collectors.toMap(
Entry::getKey, entry -> ExternalReader.parse(entry.getValue()))); Entry::getKey, entry -> ExternalReader.parse(entry.getValue())));
var color = (String) pSettings.get("color");
return new PklEvaluatorSettings( return new PklEvaluatorSettings(
(Map<String, String>) pSettings.get("externalProperties"), (Map<String, String>) pSettings.get("externalProperties"),
(Map<String, String>) pSettings.get("env"), (Map<String, String>) pSettings.get("env"),
allowedModules, allowedModules,
allowedResources, allowedResources,
color == null ? null : Color.valueOf(color.toUpperCase()),
(Boolean) pSettings.get("noCache"), (Boolean) pSettings.get("noCache"),
moduleCacheDir, moduleCacheDir,
modulePath, modulePath,
@@ -198,6 +202,7 @@ public record PklEvaluatorSettings(
&& Objects.equals(env, that.env) && Objects.equals(env, that.env)
&& arePatternsEqual(allowedModules, that.allowedModules) && arePatternsEqual(allowedModules, that.allowedModules)
&& arePatternsEqual(allowedResources, that.allowedResources) && arePatternsEqual(allowedResources, that.allowedResources)
&& Objects.equals(color, that.color)
&& Objects.equals(noCache, that.noCache) && Objects.equals(noCache, that.noCache)
&& Objects.equals(moduleCacheDir, that.moduleCacheDir) && Objects.equals(moduleCacheDir, that.moduleCacheDir)
&& Objects.equals(timeout, that.timeout) && Objects.equals(timeout, that.timeout)
@@ -219,7 +224,8 @@ public record PklEvaluatorSettings(
@Override @Override
public int hashCode() { public int hashCode() {
var result = 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(allowedModules);
result = 31 * result + hashPatterns(allowedResources); result = 31 * result + hashPatterns(allowedResources);
return result; return result;
@@ -143,7 +143,7 @@ public final class Project {
.build(); .build();
} }
// stack frame transformer never used; this exception has no stack frames. // stack frame transformer never used; this exception has no stack frames.
throw vmException.toPklException(StackFrameTransformers.defaultTransformer); throw vmException.toPklException(StackFrameTransformers.defaultTransformer, false);
} }
throw e; throw e;
} catch (URISyntaxException e) { } catch (URISyntaxException e) {
@@ -192,6 +192,7 @@ public final class Project {
var analyzer = var analyzer =
new Analyzer( new Analyzer(
StackFrameTransformers.defaultTransformer, StackFrameTransformers.defaultTransformer,
builder.getColor(),
SecurityManagers.defaultManager, SecurityManagers.defaultManager,
builder.getModuleKeyFactories(), builder.getModuleKeyFactories(),
builder.getModuleCacheDir(), builder.getModuleCacheDir(),
@@ -517,6 +518,7 @@ public final class Project {
env, env,
allowedModules, allowedModules,
allowedResources, allowedResources,
null,
noCache, noCache,
moduleCacheDir, moduleCacheDir,
modulePath, modulePath,
@@ -98,6 +98,7 @@ public final class ProjectPackager {
private final Path workingDir; private final Path workingDir;
private final String outputPathPattern; private final String outputPathPattern;
private final StackFrameTransformer stackFrameTransformer; private final StackFrameTransformer stackFrameTransformer;
private final boolean color;
private final SecurityManager securityManager; private final SecurityManager securityManager;
private final PackageResolver packageResolver; private final PackageResolver packageResolver;
private final boolean skipPublishCheck; private final boolean skipPublishCheck;
@@ -108,6 +109,7 @@ public final class ProjectPackager {
Path workingDir, Path workingDir,
String outputPathPattern, String outputPathPattern,
StackFrameTransformer stackFrameTransformer, StackFrameTransformer stackFrameTransformer,
boolean color,
SecurityManager securityManager, SecurityManager securityManager,
HttpClient httpClient, HttpClient httpClient,
boolean skipPublishCheck, boolean skipPublishCheck,
@@ -116,6 +118,7 @@ public final class ProjectPackager {
this.workingDir = workingDir; this.workingDir = workingDir;
this.outputPathPattern = outputPathPattern; this.outputPathPattern = outputPathPattern;
this.stackFrameTransformer = stackFrameTransformer; this.stackFrameTransformer = stackFrameTransformer;
this.color = color;
this.securityManager = securityManager; this.securityManager = securityManager;
// intentionally use InMemoryPackageResolver // intentionally use InMemoryPackageResolver
this.packageResolver = PackageResolver.getInstance(securityManager, httpClient, null); this.packageResolver = PackageResolver.getInstance(securityManager, httpClient, null);
@@ -409,14 +412,14 @@ public final class ProjectPackager {
.evalError("invalidModuleUri", importStr) .evalError("invalidModuleUri", importStr)
.withSourceSection(sourceSection) .withSourceSection(sourceSection)
.build() .build()
.toPklException(stackFrameTransformer); .toPklException(stackFrameTransformer, color);
} }
if (importStr.startsWith("/") && !project.getProjectDir().toString().equals("/")) { if (importStr.startsWith("/") && !project.getProjectDir().toString().equals("/")) {
throw new VmExceptionBuilder() throw new VmExceptionBuilder()
.evalError("invalidRelativeProjectImport", importStr) .evalError("invalidRelativeProjectImport", importStr)
.withSourceSection(sourceSection) .withSourceSection(sourceSection)
.build() .build()
.toPklException(stackFrameTransformer); .toPklException(stackFrameTransformer, color);
} }
var currentPath = pklModulePath.getParent(); var currentPath = pklModulePath.getParent();
var importPath = Path.of(importUri.getPath()); var importPath = Path.of(importUri.getPath());
@@ -433,7 +436,7 @@ public final class ProjectPackager {
.evalError("invalidRelativeProjectImport", importStr) .evalError("invalidRelativeProjectImport", importStr)
.withSourceSection(sourceSection) .withSourceSection(sourceSection)
.build() .build()
.toPklException(stackFrameTransformer); .toPklException(stackFrameTransformer, color);
} }
} }
} }
@@ -78,12 +78,13 @@ public class ReplServer implements AutoCloseable {
@Nullable DeclaredDependencies projectDependencies, @Nullable DeclaredDependencies projectDependencies,
@Nullable String outputFormat, @Nullable String outputFormat,
Path workingDir, Path workingDir,
StackFrameTransformer frameTransformer) { StackFrameTransformer frameTransformer,
boolean color) {
this.workingDir = workingDir; this.workingDir = workingDir;
this.securityManager = securityManager; this.securityManager = securityManager;
this.moduleResolver = new ModuleResolver(moduleKeyFactories); 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())); replState = new ReplState(createEmptyReplModule(BaseModule.getModuleClass().getPrototype()));
var languageRef = new MutableReference<VmLanguage>(null); var languageRef = new MutableReference<VmLanguage>(null);
@@ -19,6 +19,7 @@ import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.function.Function; import java.util.function.Function;
import org.pkl.core.StackFrame; import org.pkl.core.StackFrame;
import org.pkl.core.runtime.TextFormatter.Element;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
public final class StackTraceRenderer { public final class StackTraceRenderer {
@@ -28,66 +29,68 @@ public final class StackTraceRenderer {
this.frameTransformer = frameTransformer; 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); var compressed = compressFrames(frames);
doRender(compressed, hint, builder, "", true); doRender(compressed, hint, out, "", true);
} }
// non-private for testing // non-private for testing
void doRender( void doRender(
List<Object /*StackFrame|StackFrameLoop*/> frames, List<Object /*StackFrame|StackFrameLoop*/> frames,
@Nullable String hint, @Nullable String hint,
StringBuilder builder, TextFormatter out,
String leftMargin, String leftMargin,
boolean isFirstElement) { boolean isFirstElement) {
for (var frame : frames) { for (var frame : frames) {
if (frame instanceof StackFrameLoop loop) { if (frame instanceof StackFrameLoop loop) {
// ensure a cycle of length 1 doesn't get rendered as a loop // ensure a cycle of length 1 doesn't get rendered as a loop
if (loop.count == 1) { if (loop.count == 1) {
doRender(loop.frames, null, builder, leftMargin, isFirstElement); doRender(loop.frames, null, out, leftMargin, isFirstElement);
} else { } else {
if (!isFirstElement) { 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 + ""; var newLeftMargin = leftMargin + "";
doRender(loop.frames, null, builder, newLeftMargin, isFirstElement); doRender(loop.frames, null, out, newLeftMargin, isFirstElement);
if (isFirstElement) { if (isFirstElement) {
renderHint(hint, builder, newLeftMargin); renderHint(hint, out, newLeftMargin);
isFirstElement = false; isFirstElement = false;
} }
builder.append(leftMargin).append("└─\n"); out.margin(leftMargin).margin("└─").newline();
} }
} else { } else {
if (!isFirstElement) { if (!isFirstElement) {
builder.append(leftMargin).append('\n'); out.margin(leftMargin).newline();
} }
renderFrame((StackFrame) frame, builder, leftMargin); renderFrame((StackFrame) frame, out, leftMargin);
} }
if (isFirstElement) { if (isFirstElement) {
renderHint(hint, builder, leftMargin); renderHint(hint, out, leftMargin);
isFirstElement = false; 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); var transformed = frameTransformer.apply(frame);
renderSourceLine(transformed, builder, leftMargin); renderSourceLine(transformed, out, leftMargin);
renderSourceLocation(transformed, builder, 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; if (hint == null || hint.isEmpty()) return;
builder.append('\n'); out.newline().margin(leftMargin).style(Element.HINT).append(hint).newline();
builder.append(leftMargin);
builder.append(hint);
builder.append('\n');
} }
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 originalSourceLine = frame.getSourceLines().get(0);
var leadingWhitespace = VmUtils.countLeadingWhitespace(originalSourceLine); var leadingWhitespace = VmUtils.countLeadingWhitespace(originalSourceLine);
var sourceLine = originalSourceLine.strip(); var sourceLine = originalSourceLine.strip();
@@ -98,27 +101,28 @@ public final class StackTraceRenderer {
: sourceLine.length(); : sourceLine.length();
var prefix = frame.getStartLine() + " | "; var prefix = frame.getStartLine() + " | ";
builder.append(leftMargin).append(prefix).append(sourceLine).append('\n'); out.margin(leftMargin)
builder.append(leftMargin); .style(Element.LINE_NUMBER)
//noinspection StringRepeatCanBeUsed .append(prefix)
for (int i = 1; i < prefix.length() + startColumn; i++) { .style(Element.TEXT)
builder.append(' '); .append(sourceLine)
} .newline()
//noinspection StringRepeatCanBeUsed .margin(leftMargin)
for (int i = startColumn; i <= endColumn; i++) { .repeat(prefix.length() + startColumn - 1, ' ')
builder.append('^'); .style(Element.ERROR)
} .repeat(endColumn - startColumn + 1, '^')
builder.append('\n'); .newline();
} }
private void renderSourceLocation(StackFrame frame, StringBuilder builder, String leftMargin) { private void renderSourceLocation(StackFrame frame, TextFormatter out, String leftMargin) {
builder.append(leftMargin).append("at "); out.margin(leftMargin)
if (frame.getMemberName() != null) { .style(Element.TEXT)
builder.append(frame.getMemberName()); .append("at ")
} else { .append(frame.getMemberName() != null ? frame.getMemberName() : "<unknown>")
builder.append("<unknown>"); .append(" (")
} .append(frame.getModuleUri())
builder.append(" (").append(frame.getModuleUri()).append(')').append('\n'); .append(")")
.newline();
} }
/** /**
@@ -59,7 +59,8 @@ public final class TestRunner {
try { try {
checkAmendsPklTest(testModule); checkAmendsPklTest(testModule);
} catch (VmException v) { } 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(); return resultsBuilder.setError(error).build();
} }
@@ -108,7 +109,7 @@ public final class TestRunner {
} catch (VmException err) { } catch (VmException err) {
var error = var error =
new TestResults.Error( new TestResults.Error(
err.getMessage(), err.toPklException(stackFrameTransformer)); err.getMessage(), err.toPklException(stackFrameTransformer, false));
resultBuilder.addError(error); resultBuilder.addError(error);
} }
return true; return true;
@@ -208,7 +209,7 @@ public final class TestRunner {
errored.set(true); errored.set(true);
testResultBuilder.addError( testResultBuilder.addError(
new TestResults.Error( new TestResults.Error(
err.getMessage(), err.toPklException(stackFrameTransformer))); err.getMessage(), err.toPklException(stackFrameTransformer, false)));
return true; return true;
} }
var expectedValue = VmUtils.readMember(expectedGroup, exampleIndex); var expectedValue = VmUtils.readMember(expectedGroup, exampleIndex);
@@ -305,7 +306,7 @@ public final class TestRunner {
} catch (VmException err) { } catch (VmException err) {
testResultBuilder.addError( testResultBuilder.addError(
new TestResults.Error( new TestResults.Error(
err.getMessage(), err.toPklException(stackFrameTransformer))); err.getMessage(), err.toPklException(stackFrameTransformer, false)));
allSucceeded.set(false); allSucceeded.set(false);
success.set(false); success.set(false);
return true; return true;
@@ -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 @Override
@TruffleBoundary @TruffleBoundary
public PklException toPklException(StackFrameTransformer transformer) { public PklException toPklException(StackFrameTransformer transformer, boolean color) {
var renderer = new VmExceptionRenderer(new StackTraceRenderer(transformer)); var renderer = new VmExceptionRenderer(new StackTraceRenderer(transformer), color);
var rendered = renderer.render(this); var rendered = renderer.render(this);
return new PklBugException(rendered, this); return new PklBugException(rendered, this);
} }
@@ -117,8 +117,8 @@ public abstract class VmException extends AbstractTruffleException {
} }
@TruffleBoundary @TruffleBoundary
public PklException toPklException(StackFrameTransformer transformer) { public PklException toPklException(StackFrameTransformer transformer, boolean color) {
var renderer = new VmExceptionRenderer(new StackTraceRenderer(transformer)); var renderer = new VmExceptionRenderer(new StackTraceRenderer(transformer), color);
var rendered = renderer.render(this); var rendered = renderer.render(this);
return new PklException(rendered); return new PklException(rendered);
} }
@@ -16,66 +16,67 @@
package org.pkl.core.runtime; package org.pkl.core.runtime;
import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary; import com.oracle.truffle.api.CompilerDirectives.TruffleBoundary;
import java.io.PrintWriter;
import java.net.URLEncoder; import java.net.URLEncoder;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.pkl.core.Release; import org.pkl.core.Release;
import org.pkl.core.runtime.TextFormatter.Element;
import org.pkl.core.util.ErrorMessages; import org.pkl.core.util.ErrorMessages;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
import org.pkl.core.util.StringBuilderWriter;
public final class VmExceptionRenderer { public final class VmExceptionRenderer {
private final @Nullable StackTraceRenderer stackTraceRenderer; private final @Nullable StackTraceRenderer stackTraceRenderer;
private final boolean color;
/** /**
* Constructs an error renderer with the given stack trace renderer. If stack trace renderer is * 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. * {@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.stackTraceRenderer = stackTraceRenderer;
this.color = color;
} }
@TruffleBoundary @TruffleBoundary
public String render(VmException exception) { public String render(VmException exception) {
var builder = new StringBuilder(); var formatter = TextFormatter.create(color);
render(exception, builder); render(exception, formatter);
return builder.toString(); return formatter.toString();
} }
private void render(VmException exception, StringBuilder builder) { private void render(VmException exception, TextFormatter out) {
if (exception instanceof VmBugException bugException) { if (exception instanceof VmBugException bugException) {
renderBugException(bugException, builder); renderBugException(bugException, out);
} else { } 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 // if a cause exists, it's more useful to report just that
var exceptionToReport = exception.getCause() != null ? exception.getCause() : exception; var exceptionToReport = exception.getCause() != null ? exception.getCause() : exception;
var exceptionUrl = URLEncoder.encode(exceptionToReport.toString(), StandardCharsets.UTF_8);
builder out.style(Element.TEXT)
.append("An unexpected error has occurred. Would you mind filing a bug report?\n") .append("An unexpected error has occurred. Would you mind filing a bug report?")
.append("Cmd+Double-click the link below to open an issue.\n") .newline()
.append( .append("Cmd+Double-click the link below to open an issue.")
"Please copy and paste the entire error output into the issue's description, provided you can share it.\n\n") .newline()
.append("https://github.com/apple/pkl/issues/new\n\n"); .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( renderException(exception, out, true);
URLEncoder.encode(exceptionToReport.toString(), StandardCharsets.UTF_8)
.replaceAll("\\+", "%20"));
builder.append("\n\n"); out.newline().style(Element.TEXT).append(Release.current().versionInfo()).newlines(2);
renderException(exception, builder, true);
builder.append('\n').append(Release.current().versionInfo()).append("\n\n");
exceptionToReport.printStackTrace(new PrintWriter(new StringBuilderWriter(builder))); exceptionToReport.printStackTrace(out.toPrintWriter());
} }
private void renderException(VmException exception, StringBuilder builder, boolean withHeader) { private void renderException(VmException exception, TextFormatter out, boolean withHeader) {
var header = "–– Pkl Error ––";
String message; String message;
var hint = exception.getHint(); var hint = exception.getHint();
if (exception.isExternalMessage()) { if (exception.isExternalMessage()) {
@@ -96,15 +97,9 @@ public final class VmExceptionRenderer {
} }
if (withHeader) { if (withHeader) {
builder.append(header).append('\n'); out.style(Element.ERROR_HEADER).append("–– Pkl Error ––").newline();
}
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).append(message).newline();
// include cause's message unless it's the same as this exception's message // include cause's message unless it's the same as this exception's message
if (exception.getCause() != null) { if (exception.getCause() != null) {
@@ -112,11 +107,11 @@ public final class VmExceptionRenderer {
var causeMessage = cause.getMessage(); var causeMessage = cause.getMessage();
// null for Truffle's LazyStackTrace // null for Truffle's LazyStackTrace
if (causeMessage != null && !causeMessage.equals(message)) { if (causeMessage != null && !causeMessage.equals(message)) {
builder out.style(Element.TEXT)
.append(cause.getClass().getSimpleName()) .append(cause.getClass().getSimpleName())
.append(": ") .append(": ")
.append(causeMessage) .append(causeMessage)
.append('\n'); .newline();
} }
} }
@@ -124,22 +119,28 @@ public final class VmExceptionRenderer {
exception.getProgramValues().stream().mapToInt(v -> v.name.length()).max().orElse(0); exception.getProgramValues().stream().mapToInt(v -> v.name.length()).max().orElse(0);
for (var value : exception.getProgramValues()) { for (var value : exception.getProgramValues()) {
builder.append(value.name); out.style(Element.TEXT)
builder.append(" ".repeat(Math.max(0, maxNameLength - value.name.length()))); .append(value.name)
builder.append(": "); .repeat(Math.max(0, maxNameLength - value.name.length()), ' ')
builder.append(value); .append(": ")
builder.append('\n'); .append(value)
.newline();
} }
if (stackTraceRenderer != null) { if (stackTraceRenderer != null) {
var frames = StackTraceGenerator.capture(exception); 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()) { if (!frames.isEmpty()) {
builder.append('\n'); stackTraceRenderer.render(frames, hint, out.newline());
stackTraceRenderer.render(frames, hint, builder);
} else if (hint != null) { } else if (hint != null) {
// render hint if there are no stack frames // render hint if there are no stack frames
builder.append('\n'); out.newline().style(Element.HINT).append(hint);
builder.append(hint);
} }
} }
} }
@@ -28,7 +28,7 @@ import org.pkl.core.stdlib.PklName;
public final class TestNodes { public final class TestNodes {
private static final VmExceptionRenderer noStackTraceExceptionRenderer = private static final VmExceptionRenderer noStackTraceExceptionRenderer =
new VmExceptionRenderer(null); new VmExceptionRenderer(null, false);
private TestNodes() {} private TestNodes() {}
@@ -31,6 +31,7 @@ class AnalyzerTest {
private val simpleAnalyzer = private val simpleAnalyzer =
Analyzer( Analyzer(
StackFrameTransformers.defaultTransformer, StackFrameTransformers.defaultTransformer,
false,
SecurityManagers.defaultManager, SecurityManagers.defaultManager,
listOf(ModuleKeyFactories.file, ModuleKeyFactories.standardLibrary, ModuleKeyFactories.pkg), listOf(ModuleKeyFactories.file, ModuleKeyFactories.standardLibrary, ModuleKeyFactories.pkg),
null, null,
@@ -108,6 +109,7 @@ class AnalyzerTest {
val analyzer = val analyzer =
Analyzer( Analyzer(
StackFrameTransformers.defaultTransformer, StackFrameTransformers.defaultTransformer,
false,
SecurityManagers.defaultManager, SecurityManagers.defaultManager,
listOf(ModuleKeyFactories.file, ModuleKeyFactories.standardLibrary, ModuleKeyFactories.pkg), listOf(ModuleKeyFactories.file, ModuleKeyFactories.standardLibrary, ModuleKeyFactories.pkg),
tempDir.resolve("packages"), tempDir.resolve("packages"),
@@ -177,6 +179,7 @@ class AnalyzerTest {
val analyzer = val analyzer =
Analyzer( Analyzer(
StackFrameTransformers.defaultTransformer, StackFrameTransformers.defaultTransformer,
false,
SecurityManagers.defaultManager, SecurityManagers.defaultManager,
listOf( listOf(
ModuleKeyFactories.file, ModuleKeyFactories.file,
@@ -288,6 +291,7 @@ class AnalyzerTest {
val analyzer = val analyzer =
Analyzer( Analyzer(
StackFrameTransformers.defaultTransformer, StackFrameTransformers.defaultTransformer,
false,
SecurityManagers.defaultManager, SecurityManagers.defaultManager,
listOf( listOf(
ModuleKeyFactories.file, ModuleKeyFactories.file,
@@ -43,7 +43,8 @@ class ReplServerTest {
null, null,
null, null,
"/".toPath(), "/".toPath(),
StackFrameTransformers.defaultTransformer StackFrameTransformers.defaultTransformer,
false,
) )
@Test @Test
@@ -78,7 +78,7 @@ class ImportsAndReadsParserTest {
assertThrows<VmException> { assertThrows<VmException> {
ImportsAndReadsParser.parse(moduleKey, moduleKey.resolve(SecurityManagers.defaultManager)) ImportsAndReadsParser.parse(moduleKey, moduleKey.resolve(SecurityManagers.defaultManager))
} }
assertThat(err.toPklException(StackFrameTransformers.defaultTransformer)) assertThat(err.toPklException(StackFrameTransformers.defaultTransformer, false))
.hasMessage( .hasMessage(
""" """
–– Pkl Error –– –– Pkl Error ––
@@ -65,6 +65,7 @@ class ProjectTest {
mapOf("one" to "1"), mapOf("one" to "1"),
listOf("foo:", "bar:").map(Pattern::compile), listOf("foo:", "bar:").map(Pattern::compile),
listOf("baz:", "biz:").map(Pattern::compile), listOf("baz:", "biz:").map(Pattern::compile),
null,
false, false,
path.resolve("cache/"), path.resolve("cache/"),
listOf(path.resolve("modulepath1/"), path.resolve("modulepath2/")), listOf(path.resolve("modulepath1/"), path.resolve("modulepath2/")),
@@ -190,7 +190,9 @@ class StackTraceRendererTest {
} }
val loop = StackTraceRenderer.StackFrameLoop(loopFrames, 1) val loop = StackTraceRenderer.StackFrameLoop(loopFrames, 1)
val frames = listOf(createFrame("bar", 1), createFrame("baz", 2), loop) 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) assertThat(renderedFrames)
.isEqualTo( .isEqualTo(
""" """
@@ -19,6 +19,7 @@ evaluatorSettings {
["two"] = "2" ["two"] = "2"
} }
moduleCacheDir = "my-cache-dir/" moduleCacheDir = "my-cache-dir/"
color = "always"
noCache = false noCache = false
rootDir = "my-root-dir/" rootDir = "my-root-dir/"
timeout = 5.min timeout = 5.min
@@ -445,6 +445,7 @@ public class PklPlugin implements Plugin<Project> {
task.getModulePath().from(spec.getModulePath()); task.getModulePath().from(spec.getModulePath());
task.getSettingsModule().set(spec.getSettingsModule()); task.getSettingsModule().set(spec.getSettingsModule());
task.getEvalRootDir().set(spec.getEvalRootDir()); task.getEvalRootDir().set(spec.getEvalRootDir());
task.getColor().set(spec.getColor());
task.getNoCache().set(spec.getNoCache()); task.getNoCache().set(spec.getNoCache());
task.getModuleCacheDir().set(spec.getModuleCacheDir()); task.getModuleCacheDir().set(spec.getModuleCacheDir());
task.getEvalTimeout().set(spec.getEvalTimeout()); task.getEvalTimeout().set(spec.getEvalTimeout());
@@ -45,6 +45,8 @@ public interface BasePklSpec {
DirectoryProperty getModuleCacheDir(); DirectoryProperty getModuleCacheDir();
Property<Boolean> getColor();
Property<Boolean> getNoCache(); Property<Boolean> getNoCache();
// use same type (Duration) as Gradle's `Task.timeout` // use same type (Duration) as Gradle's `Task.timeout`
@@ -48,6 +48,7 @@ import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskAction;
import org.pkl.commons.cli.CliBaseOptions; import org.pkl.commons.cli.CliBaseOptions;
import org.pkl.core.evaluatorSettings.Color;
import org.pkl.core.util.IoUtils; import org.pkl.core.util.IoUtils;
import org.pkl.core.util.LateInit; import org.pkl.core.util.LateInit;
import org.pkl.core.util.Nullable; import org.pkl.core.util.Nullable;
@@ -117,6 +118,10 @@ public abstract class BasePklTask extends DefaultTask {
@Internal @Internal
public abstract DirectoryProperty getModuleCacheDir(); public abstract DirectoryProperty getModuleCacheDir();
@Input
@Optional
public abstract Property<Boolean> getColor();
@Input @Input
@Optional @Optional
public abstract Property<Boolean> getNoCache(); public abstract Property<Boolean> getNoCache();
@@ -164,6 +169,7 @@ public abstract class BasePklTask extends DefaultTask {
null, null,
getEvalTimeout().getOrNull(), getEvalTimeout().getOrNull(),
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()), mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),
getColor().getOrElse(false) ? Color.ALWAYS : Color.NEVER,
getNoCache().getOrElse(false), getNoCache().getOrElse(false),
false, false,
false, false,
@@ -36,6 +36,7 @@ import org.gradle.api.tasks.Internal;
import org.gradle.api.tasks.Optional; import org.gradle.api.tasks.Optional;
import org.gradle.api.tasks.TaskAction; import org.gradle.api.tasks.TaskAction;
import org.pkl.commons.cli.CliBaseOptions; import org.pkl.commons.cli.CliBaseOptions;
import org.pkl.core.evaluatorSettings.Color;
import org.pkl.core.util.IoUtils; import org.pkl.core.util.IoUtils;
import org.pkl.core.util.Pair; import org.pkl.core.util.Pair;
@@ -175,6 +176,7 @@ public abstract class ModulesTask extends BasePklTask {
getProjectDir().isPresent() ? getProjectDir().get().getAsFile().toPath() : null, getProjectDir().isPresent() ? getProjectDir().get().getAsFile().toPath() : null,
getEvalTimeout().getOrNull(), getEvalTimeout().getOrNull(),
mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()), mapAndGetOrNull(getModuleCacheDir(), it1 -> it1.getAsFile().toPath()),
getColor().getOrElse(false) ? Color.ALWAYS : Color.NEVER,
getNoCache().getOrElse(false), getNoCache().getOrElse(false),
getOmitProjectSettings().getOrElse(false), getOmitProjectSettings().getOrElse(false),
getNoProject().getOrElse(false), getNoProject().getOrElse(false),
@@ -42,6 +42,7 @@ internal class BinaryEvaluator(
) : ) :
EvaluatorImpl( EvaluatorImpl(
transformer, transformer,
false,
manager, manager,
httpClient, httpClient,
logger, logger,
+10
View File
@@ -67,6 +67,16 @@ allowedModules: Listing<String(isRegex)>?
/// ``` /// ```
allowedResources: Listing<String(isRegex)>? allowedResources: Listing<String(isRegex)>?
/// When to format messages with ANSI color codes.
///
/// Possible values:
///
/// - `"never"`: Never format
/// - `"auto"`: Format if the process' stdin, stdout, or stderr are connected to a console.
/// - `"always"`: Always format
@Since { version = "0.27.0" }
color: ("never"|"auto"|"always")?
/// Disables the file system cache for `package:` modules. /// Disables the file system cache for `package:` modules.
/// ///
/// When caching is disabled, packages are loaded over the network and stored in memory. /// When caching is disabled, packages are loaded over the network and stored in memory.