Skip to content

Commit d8fa170

Browse files
committed
feat: add line/column information to error reports.
1 parent 7bf921b commit d8fa170

File tree

4 files changed

+118
-5
lines changed

4 files changed

+118
-5
lines changed

go/compiler.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,10 @@ type Label struct {
156156
Level string `json:"level"`
157157
// Origin of the code where the error occurred.
158158
CodeOrigin string `json:"code_origin"`
159+
// Line number
160+
Line int64 `json:"line"`
161+
// Column number
162+
Column int64 `json:"column"`
159163
// The code span highlighted by this label.
160164
Span Span `json:"span"`
161165
// Text associated to the label.

go/compiler_test.go

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,9 +144,11 @@ func TestErrors(t *testing.T) {
144144
Title: "invariant boolean expression",
145145
Labels: []Label{
146146
{
147-
Level: "warning",
148-
Span: Span{Start: 25, End: 29},
149-
Text: "this expression is always true",
147+
Level: "warning",
148+
Line: 1,
149+
Column: 26,
150+
Span: Span{Start: 25, End: 29},
151+
Text: "this expression is always true",
150152
},
151153
},
152154
Footers: []Footer{
@@ -174,6 +176,8 @@ func TestErrors(t *testing.T) {
174176
{
175177
Level: "error",
176178
CodeOrigin: "test.yar",
179+
Line: 1,
180+
Column: 26,
177181
Span: Span{Start: 25, End: 28},
178182
Text: "this identifier has not been declared",
179183
},
@@ -272,6 +276,8 @@ func TestWarnings(t *testing.T) {
272276
{
273277
Level: "warning",
274278
CodeOrigin: "",
279+
Line: 1,
280+
Column: 31,
275281
Span: Span{Start: 30, End: 40},
276282
Text: "these consecutive jumps will be treated as [0-2]",
277283
},
@@ -291,6 +297,8 @@ func TestWarnings(t *testing.T) {
291297
{
292298
Level: "warning",
293299
CodeOrigin: "",
300+
Line: 1,
301+
Column: 22,
294302
Span: Span{Start: 21, End: 43},
295303
Text: "this pattern may slow down the scan",
296304
},

lib/src/compiler/report.rs

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ impl From<Span> for CodeLoc {
8383
/// - `title`: The title of the report (e.g., "unexpected negative number").
8484
/// - `labels`: A collection of labels included in the report. Each label
8585
/// contains a level, a span, and associated text.
86+
/// - `footers`: A collection notes that appear after the end of the report.
8687
#[derive(Clone)]
8788
pub(crate) struct Report {
8889
code_cache: Arc<CodeCache>,
@@ -110,12 +111,24 @@ impl Report {
110111
code_loc.source_id.unwrap_or(self.default_source_id);
111112

112113
let code_cache = self.code_cache.read();
113-
let code_origin =
114-
code_cache.get(&source_id).unwrap().origin.clone();
114+
let cache_entry = code_cache.get(&source_id).unwrap();
115+
let code_origin = cache_entry.origin.clone();
116+
117+
// This could be faster if we maintain an ordered vector with the
118+
// byte offset where each line begins. By doing a binary search
119+
// on that vector, we can locate the line number in O(log(N))
120+
// instead of O(N).
121+
let (line, column) = byte_offset_to_line_col(
122+
&cache_entry.code,
123+
code_loc.span.start(),
124+
)
125+
.unwrap();
115126

116127
Label {
117128
level: level_as_text(*level),
118129
code_origin,
130+
line,
131+
column,
119132
span: code_loc.span.clone(),
120133
text,
121134
}
@@ -232,6 +245,8 @@ impl Display for Report {
232245
pub struct Label<'a> {
233246
level: &'a str,
234247
code_origin: Option<String>,
248+
line: usize,
249+
column: usize,
235250
span: Span,
236251
text: &'a str,
237252
}
@@ -436,3 +451,87 @@ fn level_as_text(level: Level) -> &'static str {
436451
Level::Help => "help",
437452
}
438453
}
454+
455+
/// Given a text slice and a position indicated as a byte offset, returns
456+
/// the same position as a (line, column) pair.
457+
fn byte_offset_to_line_col(
458+
text: &str,
459+
byte_offset: usize,
460+
) -> Option<(usize, usize)> {
461+
// Check if the byte_offset is valid
462+
if byte_offset > text.len() {
463+
return None; // Out of bounds
464+
}
465+
466+
let mut line = 1;
467+
let mut col = 1;
468+
469+
// Iterate through the characters (not bytes) in the string
470+
for (i, c) in text.char_indices() {
471+
if i == byte_offset {
472+
return Some((line, col));
473+
}
474+
if c == '\n' {
475+
line += 1;
476+
col = 1; // Reset column to 1 after a newline
477+
} else {
478+
col += 1;
479+
}
480+
}
481+
482+
// If the byte_offset points to the last byte of the string, return the final position
483+
if byte_offset == text.len() {
484+
return Some((line, col));
485+
}
486+
487+
None
488+
}
489+
490+
#[cfg(test)]
491+
mod tests {
492+
use crate::compiler::report::byte_offset_to_line_col;
493+
494+
#[test]
495+
fn byte_offset_to_line_col_single_line() {
496+
let text = "Hello, World!";
497+
assert_eq!(byte_offset_to_line_col(text, 0), Some((1, 1))); // Start of the string
498+
assert_eq!(byte_offset_to_line_col(text, 7), Some((1, 8))); // Byte offset of 'W'
499+
assert_eq!(byte_offset_to_line_col(text, 12), Some((1, 13))); // Byte offset of '!'
500+
}
501+
502+
#[test]
503+
fn byte_offset_to_line_col_multiline() {
504+
let text = "Hello\nRust\nWorld!";
505+
assert_eq!(byte_offset_to_line_col(text, 0), Some((1, 1))); // First character
506+
assert_eq!(byte_offset_to_line_col(text, 5), Some((1, 6))); // End of first line (newline)
507+
assert_eq!(byte_offset_to_line_col(text, 6), Some((2, 1))); // Start of second line ('R')
508+
assert_eq!(byte_offset_to_line_col(text, 9), Some((2, 4))); // Byte offset of 't' in "Rust"
509+
assert_eq!(byte_offset_to_line_col(text, 11), Some((3, 1))); // Start of third line ('W')
510+
}
511+
512+
#[test]
513+
fn byte_offset_to_line_col_empty_string() {
514+
let text = "";
515+
assert_eq!(byte_offset_to_line_col(text, 0), Some((1, 1)));
516+
}
517+
518+
#[test]
519+
fn byte_offset_to_line_col_out_of_bounds() {
520+
let text = "Hello, World!";
521+
assert_eq!(byte_offset_to_line_col(text, text.len() + 1), None);
522+
}
523+
524+
#[test]
525+
fn byte_offset_to_line_col_end_of_string() {
526+
let text = "Hello, World!";
527+
assert_eq!(byte_offset_to_line_col(text, text.len()), Some((1, 14))); // Last position after '!'
528+
}
529+
530+
#[test]
531+
fn byte_offset_to_line_col_multibyte_characters() {
532+
let text = "Hello, 你好!";
533+
assert_eq!(byte_offset_to_line_col(text, 7), Some((1, 8))); // Position of '你'
534+
assert_eq!(byte_offset_to_line_col(text, 10), Some((1, 9))); // Position of '好'
535+
assert_eq!(byte_offset_to_line_col(text, 13), Some((1, 10))); // Position of '!'
536+
}
537+
}

lib/src/compiler/tests/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,8 @@ fn errors_serialization() {
808808
{
809809
"level": "error",
810810
"code_origin": "test.yar",
811+
"line": 1,
812+
"column": 23,
811813
"span": { "start": 22, "end": 25 },
812814
"text": "this identifier has not been declared"
813815
}

0 commit comments

Comments
 (0)