A diagnostic reporting library for Java, ported from the Rust library miette.
yvette is published for Java 8 and above.
Gradle (build.gradle / build.gradle.kts):
implementation("com.opencastsoftware:yvette:0.2.0")
Maven (pom.xml):
<dependency>
<groupId>com.opencastsoftware</groupId>
<artifactId>yvette</artifactId>
<version>0.2.0</version>
</dependency>
To use the GraphicalReportHandler
and other features of the package com.opencastsoftware.yvette.handlers.graphical
, the jansi library is also needed:
Gradle (build.gradle / build.gradle.kts):
implementation("org.fusesource.jansi:jansi:2.4.0")
Maven (pom.xml):
<dependency>
<groupId>org.fusesource.jansi</groupId>
<artifactId>jansi</artifactId>
<version>2.4.0</version>
</dependency>
In order to display diagnostics with yvette, your application's error messages must implement the abstract class Diagnostic. All of the methods of this class may return null
, but bear in mind that your diagnostics may not be very helpful without a message
.
A BasicDiagnostic implementation is provided, which can be used to wrap exceptions:
Diagnostic err = new BasicDiagnostic(e.getMessage(), e.getCause());
You can then implement a ReportHandler to display your diagnostics using the Appendable instance you wish to use for output, for example System.out, System.err or a StringBuilder.
yvette provides a GraphicalReportHandler which produces output like the screenshot above. You can create one of these using the GraphicalReportHandler.builder()
static method:
ReportHandler reportHandler = GraphicalReportHandler.builder()
.withRgbColours(RgbColours.PREFERRED)
.buildFor(System.err);
There are some terminal feature detection features in yvette. If you wish to bypass these, use the withColours
, withTerminalWidth
and withUnicode
methods of the builder to enable or disable those features explicitly. However, please bear in mind that using withColours
to force enable colour output will override the NO_COLOR detection implemented by this library.
Once you have a ReportHandler, it can be used to output diagnostics:
Diagnostic diagnostic = ???; // A diagnostic from your application
reportHandler.display(diagnostic, System.err);
yvette provides an implementation of Thread.UncaughtExceptionHandler which can be used to replace the default handler for all threads.
import com.opencastsoftware.yvette.UncaughtExceptionHandler;
ReportHandler handler = ???; // Your report handler, obtained as described above
try {
UncaughtExceptionHandler.install(handler, System.err); // Installs the new handler
} finally {
UncaughtExceptionHandler.uninstall(); // Restores the default handler
}
The setup above will print diagnostics to STDERR:
new Thread(() -> {
Throwable exc = new FileNotFoundException("Couldn't find the file BadFile.java");
exc.initCause(new AccessDeniedException("Access denied to file BadFile.java"));
throw new RuntimeException("Whoops!", exc);
}).start();
/*
Uncaught exception in thread Thread-5:
x Whoops!
|-> Couldn't find the file BadFile.java
`-> Access denied to file BadFile.java
java.lang.RuntimeException: Whoops!
at com.opencastsoftware.yvette.UncaughtExceptionHandlerTest.lambda$replacesThreadPoolHandler$2(UncaughtExceptionHandlerTest.java:87)
at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
at java.lang.Thread.run(Thread.java:750)
Caused by: java.io.FileNotFoundException: Couldn't find the file BadFile.java
at com.opencastsoftware.yvette.UncaughtExceptionHandlerTest.lambda$replacesThreadPoolHandler$2(UncaughtExceptionHandlerTest.java:85)
... 3 more
Caused by: java.nio.file.AccessDeniedException: Access denied to file BadFile.java
at com.opencastsoftware.yvette.UncaughtExceptionHandlerTest.lambda$replacesThreadPoolHandler$2(UncaughtExceptionHandlerTest.java:86)
... 3 more
*/
It can also be set as the uncaught exception handler for new threads in a thread pool by using a ThreadFactory:
Thread.UncaughtExceptionHandler excHandler = UncaughtExceptionHandler.create(handler, System.err);
ThreadFactory threadFactory = runnable -> {
Thread thread = new Thread(runnable);
thread.setUncaughtExceptionHandler(excHandler);
thread.setDaemon(true);
return newThread;
};
This is not an exact port of miette - there are some differences and unported features:
- Where miette and the Language Server Protocol's definitions deviate, we have erred on the side of alignment with LSP. This is for ease of integrating yvette with language server applications. For example, miette uses
SourceSpan
to keep track of the byte offset and length of a span within a source file. However, yvette's equivalent ofSourceSpan
is calledRange
, and specifies a start and endPosition
, each of which refers to a zero-indexed line and character position within a document. The upside of this is alignment with LSP, but the downside is that we can no longer efficiently read arbitrary offsets of a source file in order to get the span contents. - Only the
GraphicalReportHandler
andToStringReportHandler
are currently implemented, there is noNarratableReportHandler
orJSONReportHandler
as yet. - There is no special handling of tab characters or unicode character width in yvette yet, which may mean that your highlights are misaligned with the source code they are supposed to underline.
- Related diagnostics are not currently implemented in yvette, since the definition of
DiagnosticRelatedInformation
is sufficiently different in the Language Server Protocol that we haven't decided how to implement it yet.
This project wouldn't exist without the work of zkat and the other miette contributors.
All code in this repository is licensed under the Apache License, Version 2.0. See LICENSE.