Skip to content

Commit 4dcdca5

Browse files
Mark code in messages with backticks, and name the lines they refer to
Headlines, hints, origin notes, the declared-here gloss, alias where lines and counterexamples write code as `code`. Plain-text clients get the backticks, as rustc gives them; the terminal shows code in blue without them, unless NO_COLOR is set or the output is not a terminal. A note or hint that names a line also carries it as a SourceReference (the line, the words naming the place, file, line and column), which LJDiagnostic.getReferences() gives clients to link to. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 590268e commit 4dcdca5

24 files changed

Lines changed: 373 additions & 202 deletions
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package liquidjava.diagnostics;
2+
3+
import java.util.regex.Matcher;
4+
import java.util.regex.Pattern;
5+
6+
/**
7+
* Code inside a message, marked with backticks as in {@code found `x > 0`}. Clients that show plain text get the
8+
* backticks; the terminal shows the code in colour instead, or keeps the backticks when colour is off
9+
*/
10+
public final class Code {
11+
12+
// a run of backticks, the code, and a run of the same length
13+
private static final Pattern SPAN = Pattern.compile("(`+)(.+?)(?<!`)\\1(?!`)");
14+
15+
private Code() {
16+
}
17+
18+
/**
19+
* {@code code} marked as code, fenced by more backticks than any run it holds, padded if it starts or ends with one
20+
*/
21+
public static String of(String code) {
22+
String fence = "`";
23+
while (code.contains(fence))
24+
fence += "`";
25+
String pad = code.startsWith("`") || code.endsWith("`") ? " " : "";
26+
return fence + pad + code + pad + fence;
27+
}
28+
29+
/** {@code message} with each code span in colour and without its backticks, or as it is when colour is off */
30+
public static String render(String message) {
31+
return render(message, Colors.enabled());
32+
}
33+
34+
static String render(String message, boolean colour) {
35+
if (message == null || !colour)
36+
return message;
37+
Matcher span = SPAN.matcher(message);
38+
StringBuilder rendered = new StringBuilder();
39+
while (span.find()) {
40+
String code = span.group(2);
41+
if (code.length() > 2 && code.startsWith(" ") && code.endsWith(" "))
42+
code = code.substring(1, code.length() - 1);
43+
span.appendReplacement(rendered, Matcher.quoteReplacement(Colors.BLUE + code + Colors.RESET));
44+
}
45+
span.appendTail(rendered);
46+
return rendered.toString();
47+
}
48+
}

‎liquidjava-verifier/src/main/java/liquidjava/diagnostics/Colors.java‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
// ANSI color codes
44
public class Colors {
5+
6+
/**
7+
* Whether colour may mark code: not when NO_COLOR is set (https://no-color.org), nor when the output is not a
8+
* terminal
9+
*/
10+
public static boolean enabled() {
11+
String noColor = System.getenv("NO_COLOR");
12+
return (noColor == null || noColor.isEmpty()) && System.console() != null;
13+
}
14+
515
public static final String RESET = "\u001B[0m";
616
public static final String GREY = "\u001B[90m";
717
public static final String RED = "\u001B[31m";

‎liquidjava-verifier/src/main/java/liquidjava/diagnostics/LJDiagnostic.java‎

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ public class LJDiagnostic extends RuntimeException {
2323
private SourcePosition position;
2424
private final List<Hint> hints = new ArrayList<>();
2525
private final List<String> messageNotes = new ArrayList<>();
26+
private final List<SourceReference> references = new ArrayList<>();
2627
private String counterexample;
2728
private ErrorCategory category;
2829
private static final String PIPE = " | ";
@@ -87,6 +88,19 @@ public void addMessageNote(String note) {
8788
messageNotes.add(note);
8889
}
8990

91+
/** Adds a line printed under the message that names a place in the source, as {@code reference} tells */
92+
public void addMessageNote(SourceReference reference) {
93+
messageNotes.add(reference.message());
94+
references.add(reference);
95+
}
96+
97+
/** The places in the source that the message notes and hints name, each with the line that names it */
98+
public List<SourceReference> getReferences() {
99+
List<SourceReference> all = new ArrayList<>(references);
100+
hints.stream().map(Hint::reference).filter(Objects::nonNull).forEach(all::add);
101+
return all;
102+
}
103+
90104
/** Lines printed under the excerpt of where the refinement is declared that explain it */
91105
public List<String> getDeclarationNotes() {
92106
return List.of();
@@ -128,10 +142,10 @@ public String toString() {
128142
StringBuilder sb = new StringBuilder();
129143

130144
// title
131-
sb.append("\n").append(accentColor).append(title).append(": ").append(Colors.RESET).append(message)
145+
sb.append("\n").append(accentColor).append(title).append(": ").append(Colors.RESET).append(Code.render(message))
132146
.append("\n");
133147
for (String note : getMessageNotes())
134-
sb.append(" ").append(note).append("\n");
148+
sb.append(" ").append(Code.render(note)).append("\n");
135149

136150
// snippet
137151
String snippet = getSnippet();
@@ -142,14 +156,14 @@ public String toString() {
142156
// counterexample
143157
String counterexample = getCounterexampleStr();
144158
if (counterexample != null && !counterexample.isBlank()) {
145-
sb.append(" --> ").append(String.join("\n ", counterexample.split("\n"))).append("\n");
159+
sb.append(" --> ").append(Code.render(String.join("\n ", counterexample.split("\n")))).append("\n");
146160
}
147161

148162
// hints
149163
if (!hints.isEmpty()) {
150164
sb.append("\nHints:\n");
151165
for (Hint hint : hints)
152-
sb.append(" - ").append(String.join("\n ", hint.message().split("\n"))).append("\n");
166+
sb.append(" - ").append(Code.render(String.join("\n ", hint.message().split("\n")))).append("\n");
153167
}
154168

155169
// location
@@ -165,7 +179,7 @@ public String toString() {
165179
if (declarationSnippet != null) {
166180
sb.append(declarationSnippet);
167181
for (String note : getDeclarationNotes())
168-
sb.append(" ").append(note).append("\n");
182+
sb.append(" ").append(Code.render(note)).append("\n");
169183
}
170184

171185
sb.append("\n").append(declPos.getFile().getPath()).append(":").append(declPos.getLine())
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package liquidjava.diagnostics;
2+
3+
import spoon.reflect.cu.SourcePosition;
4+
5+
/**
6+
* A place in the source that a line of a diagnostic names, such as {@code line 134} in an origin note, for clients that
7+
* link to it
8+
*
9+
* @param message
10+
* the whole line, a message note or a hint, as the diagnostic gives it
11+
* @param text
12+
* the words of {@code message} that name the place, which occur in it once
13+
* @param file
14+
* the path of the file
15+
* @param line
16+
* the line, from 1
17+
* @param column
18+
* the column, from 1
19+
*/
20+
public record SourceReference(String message, String text, String file, int line, int column) {
21+
22+
public static SourceReference of(String message, String text, SourcePosition position) {
23+
return new SourceReference(message, text, position.getFile().getPath(), position.getLine(),
24+
position.getColumn());
25+
}
26+
}

‎liquidjava-verifier/src/main/java/liquidjava/diagnostics/errors/ErrorMessage.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import java.util.function.UnaryOperator;
99

1010
import liquidjava.diagnostics.TranslationTable;
11+
import liquidjava.diagnostics.Code;
1112
import liquidjava.processor.context.PlacementInCode;
1213
import liquidjava.rj_language.ast.Expression;
1314
import liquidjava.rj_language.ast.formatter.DisplayNames;
@@ -36,9 +37,9 @@ static ErrorMessage of(Expression expected, VCSimplificationResult found, Transl
3637
PlacementInCode placement = translationTable == null ? null : translationTable.get(name);
3738
return placement == null ? null : placement.getLine();
3839
});
39-
String expectedText = ExpressionFormatter.format(display.apply(expected), names);
40+
String expectedText = Code.of(ExpressionFormatter.format(display.apply(expected), names));
4041
String text = showsFound.test(shown)
41-
? String.format(frame, ExpressionFormatter.format(display.apply(shown), names), expectedText)
42+
? String.format(frame, Code.of(ExpressionFormatter.format(display.apply(shown), names)), expectedText)
4243
: String.format("expected %s but could not prove it", expectedText);
4344
return new ErrorMessage(text, names);
4445
}

‎liquidjava-verifier/src/main/java/liquidjava/diagnostics/errors/RefinementError.java‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import java.util.stream.Collectors;
1212

1313
import liquidjava.diagnostics.AliasExpansion;
14+
import liquidjava.diagnostics.Code;
1415
import liquidjava.diagnostics.TranslationTable;
1516
import liquidjava.diagnostics.categories.ErrorCategory;
1617
import liquidjava.processor.context.AliasWrapper;
@@ -68,7 +69,7 @@ public RefinementError(SourcePosition position, SourcePosition declarationPositi
6869
String counterexampleString = this.counterexample.assignments().stream()
6970
.map(a -> getDisplayNames().get(a.first()) + " == " + a.second())
7071
.collect(Collectors.joining(" && "));
71-
setCounterexampleStr("Counterexample: " + counterexampleString);
72+
setCounterexampleStr("Counterexample: " + Code.of(counterexampleString));
7273
}
7374
setCategory(categoryOf(expected, found));
7475
}
@@ -120,7 +121,7 @@ public List<AliasExpansion> getExpectedAliases() {
120121
@Override
121122
public List<String> getMessageNotes() {
122123
List<String> notes = new ArrayList<>(
123-
aliasDeclarations.stream().map(declaration -> "where " + declaration).toList());
124+
aliasDeclarations.stream().map(declaration -> "where " + Code.of(declaration)).toList());
124125
notes.addAll(super.getMessageNotes());
125126
return notes;
126127
}

‎liquidjava-verifier/src/main/java/liquidjava/diagnostics/hints/Hint.java‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,22 @@
11
package liquidjava.diagnostics.hints;
22

3+
import liquidjava.diagnostics.SourceReference;
4+
35
/**
46
* A suggestion on how to fix a diagnostic
57
*
68
* @param kind
79
* what the suggestion is about, so clients can show or filter hints separately
810
* @param message
9-
* the suggestion as shown to the user
11+
* the suggestion as shown to the user, with code marked as {@link liquidjava.diagnostics.Code} marks it
12+
* @param reference
13+
* the place in the source the suggestion names, or null
1014
*/
11-
public record Hint(Kind kind, String message) {
15+
public record Hint(Kind kind, String message, SourceReference reference) {
16+
17+
public Hint(Kind kind, String message) {
18+
this(kind, message, null);
19+
}
1220

1321
public enum Kind {
1422
/** A suggestion that does not come from a specific analysis */

0 commit comments

Comments
 (0)