From a9e9dd452e23fa2e816df926a56c1f743eb32488 Mon Sep 17 00:00:00 2001
From: Ben Bridle <ben@derelict.engineering>
Date: Tue, 11 Mar 2025 16:18:01 +1300
Subject: Implement source chains

A SourceSpan can now contain a child SourceSpan, ad infinitum, in order
to represent a chain of locations. The report_source_issue function
has been changed to print the entire chain, instead of just one
SourceSpan.

The report_source_issue function has also been changed to correctly
print SourceSpans that extend across multiple source lines.
---
 src/reports/mod.rs | 144 ++++++++++++++++++++++++++++++++++++-----------------
 1 file changed, 97 insertions(+), 47 deletions(-)

(limited to 'src/reports/mod.rs')

diff --git a/src/reports/mod.rs b/src/reports/mod.rs
index 01635b4..bbddba9 100644
--- a/src/reports/mod.rs
+++ b/src/reports/mod.rs
@@ -15,57 +15,107 @@ pub use ansi::*;
 
 
 pub fn report_source_issue(level: LogLevel, context: &Context, message: &str) {
-    // Prepare variables.
-    let in_merged = &context.source.in_merged;
-    let line_num = in_merged.start.line + 1;
-    let digits = line_num.to_string().len();
-    let w = digits + 3;
-    let arrow = "-->";
-    let mut string = message.to_string();
-
-    macro_rules! push {
-        ($($tokens:tt)*) => { string.push_str(&format!($($tokens)*)) };
+    let mut reporter = SourceIssueReporter::new(context, message, level);
+    reporter.format_source_span(context.source);
+    let string = reporter.output;
+
+    // Print the completed message.
+    match level {
+        LogLevel::Info  => log::info!( "{string}"),
+        LogLevel::Warn  => log::warn!( "{string}"),
+        LogLevel::Error => log::error!("{string}"),
+        LogLevel::Fatal => log::fatal!("{string}"),
     }
+}
+
 
-    // Format message and locations.
-    push!("{NORMAL}\n");
-    let location = context.source.location();
-    push!("{BLUE}{arrow:>w$}{NORMAL} {location}\n", w=w);
-
-    // Format source context.
-    let left = in_merged.start.column;
-    let right = in_merged.end.column + 1;
-    let source_line = context.source_code.split('\n').nth(in_merged.start.line)
-        .unwrap_or("<error reading line from source>");
-    let space = " ";
-    let colour = match level {
-        LogLevel::Info => BLUE,
-        LogLevel::Warn => YELLOW,
-        LogLevel::Error => RED,
-        LogLevel::Fatal => RED,
-    };
-
-    // Print source code line.
-    push!("{BLUE} {line_num} | {NORMAL}");
-    for (i, c) in source_line.chars().enumerate() {
-        if i == left { push!("{colour}") }
-        if i == right { push!("{NORMAL}") }
-        push!("{c}");
+struct SourceIssueReporter<'a> {
+    context: &'a Context<'a>,
+    output: String,
+    level: LogLevel,
+    digits: usize,
+}
+
+impl<'a> SourceIssueReporter<'a> {
+    pub fn new(context: &'a Context<'a>, message: &str, level: LogLevel) -> Self {
+        let output = format!("{message}\n");
+        let digits = get_end_line_number(context.source).to_string().len();
+        Self { context, level, output, digits }
     }
-    push!("{NORMAL}\n");
 
-    // Print source code underline.
-    push!("{BLUE} {space:>w$} | {NORMAL}", w=digits);
-    for _ in 0..left { push!(" "); }
-    push!("{colour}");
-    for _ in left..right { push!("^"); }
-    push!("{NORMAL}");
+    pub fn format_source_span(&mut self, source: &SourceSpan) {
+        let colour = match self.level {
+            LogLevel::Info => BLUE,
+            LogLevel::Warn => YELLOW,
+            LogLevel::Error => RED,
+            LogLevel::Fatal => RED,
+        };
 
-    // Print the completed message.
-    match level {
-        LogLevel::Info  => log::info!( "{}", string),
-        LogLevel::Warn  => log::warn!( "{}", string),
-        LogLevel::Error => log::error!("{}", string),
-        LogLevel::Fatal => log::fatal!("{}", string),
+        // Print location line.
+        self.output.push_str(NORMAL);
+        let arrow = "-->";
+        let location = source.location();
+        self.push(&format!("{BLUE}{arrow:>w$}{NORMAL} {location}\n", w=self.digits+3));
+
+        let line_count = location.end.line - location.start.line + 1;
+        for line_i in 0..line_count {
+            let merged_i = source.in_merged.start.line + line_i;
+            let location_i = location.start.line + line_i;
+            // Format source context.
+            let source_line = self.context.source_code.split('\n').nth(merged_i)
+                .unwrap_or("<error reading line from source>");
+            let left = match line_i == 0 {
+                true => location.start.column,
+                false => 0,
+            };
+            let right = match line_i + 1 == line_count {
+                true => location.end.column + 1,
+                false => source_line.chars().count(),
+            };
+
+            // Print source code line.
+            self.push(&format!("{BLUE} {line_num:>w$} | {NORMAL}", line_num=location_i+1, w=self.digits));
+            for (i, c) in source_line.chars().enumerate() {
+                if i == left  { self.output.push_str(colour) }
+                if i == right { self.output.push_str(NORMAL) }
+                self.output.push(c);
+            }
+            self.output.push_str(NORMAL);
+            self.output.push('\n');
+
+            // Print an underline on the final line.
+            if line_i + 1 == line_count {
+                // Print source code underline.
+                let space = " ";
+                self.push(&format!("{BLUE} {space:>w$} | {NORMAL}", w=self.digits));
+                for _ in 0..left { self.output.push_str(" "); }
+                self.output.push_str(colour);
+                for _ in left..right { self.output.push_str("^"); }
+                self.output.push_str(NORMAL);
+                self.output.push('\n');
+            }
+        }
+
+        // Recurse.
+        if let Some(child) = &source.child {
+            self.format_source_span(&child);
+        }
+
+        // Remove the trailing new-line.
+        self.output.pop();
+    }
+
+    fn push(&mut self, string: &str) {
+        self.output.push_str(&string);
+    }
+}
+
+
+// Return the highest 1-based end line number of any location in the chain.
+fn get_end_line_number(source: &SourceSpan) -> usize {
+    let end_line_number = source.location().end.line + 1;
+    match &source.child {
+        Some(child) => std::cmp::max(end_line_number, get_end_line_number(&child)),
+        None => end_line_number,
     }
 }
-- 
cgit v1.2.3-70-g09d2