From ae94a1dc8a253c34bccaf1f4feac30076b5320cf Mon Sep 17 00:00:00 2001 From: aNNiMON Date: Fri, 6 Oct 2023 20:08:07 +0300 Subject: [PATCH] Show location of the closest error from call stack --- .../java/com/annimon/ownlang/Console.java | 35 +++++++++---- .../annimon/ownlang/stages/StagesData.java | 7 +++ .../util/ErrorsLocationFormatterStage.java | 39 +++------------ .../com/annimon/ownlang/util/SimpleError.java | 11 ++++- .../util/SourceLocationFormatterStage.java | 49 +++++++++++++++++++ .../ownlang/util/input/SourceLoaderStage.java | 7 ++- 6 files changed, 104 insertions(+), 44 deletions(-) create mode 100644 ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLocationFormatterStage.java diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/Console.java b/ownlang-core/src/main/java/com/annimon/ownlang/Console.java index 76cafa9..3b7676e 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/Console.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/Console.java @@ -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) { diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/stages/StagesData.java b/ownlang-core/src/main/java/com/annimon/ownlang/stages/StagesData.java index 0f7a403..4633a08 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/stages/StagesData.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/stages/StagesData.java @@ -1,5 +1,7 @@ package com.annimon.ownlang.stages; +import java.util.function.Supplier; + public interface StagesData { T get(String tag); @@ -9,5 +11,10 @@ public interface StagesData { return value != null ? value : other; } + default T getOrDefault(String tag, Supplier otherSuppler) { + T value = get(tag); + return value != null ? value : otherSuppler.get(); + } + void put(String tag, Object input); } diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/ErrorsLocationFormatterStage.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/ErrorsLocationFormatterStage.java index 2b73da7..22d5c98 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/util/ErrorsLocationFormatterStage.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/ErrorsLocationFormatterStage.java @@ -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, String> { + public static final String TAG_POSITIONS = "formattedPositions"; @Override public String perform(StagesData stagesData, Iterable 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()); - } - } - } } \ No newline at end of file diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/SimpleError.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/SimpleError.java index 3b17494..16057c2 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/util/SimpleError.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/SimpleError.java @@ -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; + } } diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLocationFormatterStage.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLocationFormatterStage.java new file mode 100644 index 0000000..b798f9f --- /dev/null +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/SourceLocationFormatterStage.java @@ -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 { + + @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()); + } + } + } +} \ No newline at end of file diff --git a/ownlang-core/src/main/java/com/annimon/ownlang/util/input/SourceLoaderStage.java b/ownlang-core/src/main/java/com/annimon/ownlang/util/input/SourceLoaderStage.java index f25682d..4079054 100644 --- a/ownlang-core/src/main/java/com/annimon/ownlang/util/input/SourceLoaderStage.java +++ b/ownlang-core/src/main/java/com/annimon/ownlang/util/input/SourceLoaderStage.java @@ -10,13 +10,16 @@ import java.nio.charset.StandardCharsets; public class SourceLoaderStage implements Stage { - 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);