Show location of the closest error from call stack

This commit is contained in:
aNNiMON 2023-10-06 20:08:07 +03:00 committed by Victor Melnik
parent e304aafedd
commit ae94a1dc8a
6 changed files with 104 additions and 44 deletions

View File

@ -4,14 +4,16 @@ import com.annimon.ownlang.lib.CallStack;
import com.annimon.ownlang.outputsettings.ConsoleOutputSettings;
import com.annimon.ownlang.outputsettings.OutputSettings;
import com.annimon.ownlang.stages.StagesData;
import com.annimon.ownlang.util.ErrorsLocationFormatterStage;
import com.annimon.ownlang.util.ExceptionConverterStage;
import com.annimon.ownlang.util.ExceptionStackTraceToStringStage;
import com.annimon.ownlang.util.*;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.PrintStream;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.StringJoiner;
import static com.annimon.ownlang.util.ErrorsLocationFormatterStage.*;
public class Console {
@ -64,14 +66,29 @@ public class Console {
}
public static void handleException(StagesData stagesData, Thread thread, Exception exception) {
String mainError = new ExceptionConverterStage()
final var joiner = new StringJoiner("\n");
joiner.add(new ExceptionConverterStage()
.then((data, error) -> List.of(error))
.then(new ErrorsLocationFormatterStage())
.perform(stagesData, exception);
String callStack = CallStack.getFormattedCalls();
String stackTrace = new ExceptionStackTraceToStringStage()
.perform(stagesData, exception);
error(String.join("\n", mainError, "Thread: " + thread.getName(), callStack, stackTrace));
.perform(stagesData, exception));
final var processedPositions = stagesData.getOrDefault(TAG_POSITIONS, HashSet::new);
if (processedPositions.isEmpty()) {
// In case no source located errors were printed
// Find closest SourceLocated call stack frame
CallStack.getCalls().stream()
.limit(4)
.map(CallStack.CallInfo::range)
.filter(Objects::nonNull)
.findFirst()
.map(range -> new SourceLocationFormatterStage()
.perform(stagesData, range))
.ifPresent(joiner::add);
}
joiner.add("Thread: " + thread.getName());
joiner.add(CallStack.getFormattedCalls());
joiner.add(new ExceptionStackTraceToStringStage()
.perform(stagesData, exception));
error(joiner.toString());
}
public static void handleException(Thread thread, Throwable throwable) {

View File

@ -1,5 +1,7 @@
package com.annimon.ownlang.stages;
import java.util.function.Supplier;
public interface StagesData {
<T> T get(String tag);
@ -9,5 +11,10 @@ public interface StagesData {
return value != null ? value : other;
}
default <T> T getOrDefault(String tag, Supplier<? extends T> otherSuppler) {
T value = get(tag);
return value != null ? value : otherSuppler.get();
}
void put(String tag, Object input);
}

View File

@ -4,53 +4,28 @@ import com.annimon.ownlang.Console;
import com.annimon.ownlang.stages.Stage;
import com.annimon.ownlang.stages.StagesData;
import com.annimon.ownlang.util.input.SourceLoaderStage;
import java.util.HashSet;
import static com.annimon.ownlang.util.SourceLocationFormatterStage.printPosition;
public class ErrorsLocationFormatterStage implements Stage<Iterable<? extends SourceLocatedError>, String> {
public static final String TAG_POSITIONS = "formattedPositions";
@Override
public String perform(StagesData stagesData, Iterable<? extends SourceLocatedError> input) {
final var sb = new StringBuilder();
final String source = stagesData.getOrDefault(SourceLoaderStage.TAG_SOURCE, "");
final var lines = source.split("\r?\n");
final var lines = stagesData.getOrDefault(SourceLoaderStage.TAG_SOURCE_LINES, new String[0]);
for (SourceLocatedError error : input) {
sb.append(Console.newline());
sb.append(error);
sb.append(Console.newline());
final Range range = error.getRange();
if (range != null && lines.length > 0) {
var positions = stagesData.getOrDefault(TAG_POSITIONS, HashSet::new);
positions.add(range);
stagesData.put(TAG_POSITIONS, positions);
printPosition(sb, range.normalize(), lines);
}
}
return sb.toString();
}
private static void printPosition(StringBuilder sb, Range range, String[] lines) {
final Pos start = range.start();
final int linesCount = lines.length;;
if (range.isOnSameLine()) {
if (start.row() < linesCount) {
sb.append(lines[start.row()]);
sb.append(Console.newline());
sb.append(" ".repeat(start.col()));
sb.append("^".repeat(range.end().col() - start.col() + 1));
sb.append(Console.newline());
}
} else {
if (start.row() < linesCount) {
String line = lines[start.row()];
sb.append(line);
sb.append(Console.newline());
sb.append(" ".repeat(start.col()));
sb.append("^".repeat(Math.max(1, line.length() - start.col())));
sb.append(Console.newline());
}
final Pos end = range.end();
if (end.row() < linesCount) {
sb.append(lines[end.row()]);
sb.append(Console.newline());
sb.append("^".repeat(end.col()));
sb.append(Console.newline());
}
}
}
}

View File

@ -1,6 +1,10 @@
package com.annimon.ownlang.util;
public record SimpleError(String message) implements SourceLocatedError {
public record SimpleError(String message, Range range) implements SourceLocatedError {
public SimpleError(String message) {
this(message, null);
}
@Override
public String getMessage() {
return message;
@ -10,4 +14,9 @@ public record SimpleError(String message) implements SourceLocatedError {
public String toString() {
return message;
}
@Override
public Range getRange() {
return range;
}
}

View File

@ -0,0 +1,49 @@
package com.annimon.ownlang.util;
import com.annimon.ownlang.Console;
import com.annimon.ownlang.stages.Stage;
import com.annimon.ownlang.stages.StagesData;
import com.annimon.ownlang.util.input.SourceLoaderStage;
public class SourceLocationFormatterStage implements Stage<Range, String> {
@Override
public String perform(StagesData stagesData, Range input) {
final var lines = stagesData.getOrDefault(SourceLoaderStage.TAG_SOURCE_LINES, new String[0]);
final var sb = new StringBuilder();
if (input != null && lines.length > 0) {
printPosition(sb, input.normalize(), lines);
}
return sb.toString();
}
static void printPosition(StringBuilder sb, Range range, String[] lines) {
final Pos start = range.start();
final int linesCount = lines.length;;
if (range.isOnSameLine()) {
if (start.row() < linesCount) {
sb.append(lines[start.row()]);
sb.append(Console.newline());
sb.append(" ".repeat(start.col()));
sb.append("^".repeat(range.end().col() - start.col() + 1));
sb.append(Console.newline());
}
} else {
if (start.row() < linesCount) {
String line = lines[start.row()];
sb.append(line);
sb.append(Console.newline());
sb.append(" ".repeat(start.col()));
sb.append("^".repeat(Math.max(1, line.length() - start.col())));
sb.append(Console.newline());
}
final Pos end = range.end();
if (end.row() < linesCount) {
sb.append(lines[end.row()]);
sb.append(Console.newline());
sb.append("^".repeat(end.col()));
sb.append(Console.newline());
}
}
}
}

View File

@ -10,13 +10,16 @@ import java.nio.charset.StandardCharsets;
public class SourceLoaderStage implements Stage<InputSource, String> {
public static final String TAG_SOURCE = "source";
public static final String TAG_SOURCE_LINES = "sourceLines";
@Override
public String perform(StagesData stagesData, InputSource inputSource) {
try {
String result = inputSource.load();
stagesData.put(TAG_SOURCE, result);
final var lines = (result == null || result.isEmpty())
? new String[0]
: result.split("\r?\n");
stagesData.put(TAG_SOURCE_LINES, lines);
return result;
} catch (IOException e) {
throw new OwnLangRuntimeException("Unable to read input " + inputSource, e);