1use std::fmt;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct Location {
12 pub file: String,
14 pub line: usize,
16 pub column: usize,
18}
19
20impl Location {
21 #[must_use]
26 pub fn from_offset(file: &str, text: &str, offset: usize) -> Self {
27 let before = &text[..text.floor_char_boundary(offset)];
28 let line = before.matches('\n').count() + 1;
29 let line_start = before.rfind('\n').map_or(0, |i| i + 1);
30 let column = before[line_start..].chars().count() + 1;
31 Self { file: file.to_owned(), line, column }
32 }
33}
34
35#[derive(Debug, Clone)]
38pub(crate) struct LineStarts(Vec<usize>);
39
40impl LineStarts {
41 pub(crate) fn new(text: &str) -> Self {
42 Self(std::iter::once(0).chain(text.match_indices('\n').map(|(index, _)| index + 1)).collect())
43 }
44
45 pub(crate) fn locate(&self, file: &str, text: &str, offset: usize) -> Location {
48 let offset = text.floor_char_boundary(offset);
49 let line = self.0.partition_point(|start| *start <= offset);
51 let line_start = self.0[line.saturating_sub(1)];
52 let column = text[line_start..offset].chars().count() + 1;
53 Location { file: file.to_owned(), line, column }
54 }
55}
56
57impl fmt::Display for Location {
58 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
59 write!(f, "{}:{}:{}", self.file, self.line, self.column)
60 }
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub enum Severity {
66 Error,
68 Warning,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
74pub struct Diagnostic {
75 pub severity: Severity,
77 pub location: Option<Location>,
79 pub message: String,
81}
82
83impl Diagnostic {
84 #[must_use]
86 pub fn error(location: Option<Location>, message: impl Into<String>) -> Self {
87 Self { severity: Severity::Error, location, message: message.into() }
88 }
89
90 #[must_use]
92 pub fn warning(location: Option<Location>, message: impl Into<String>) -> Self {
93 Self { severity: Severity::Warning, location, message: message.into() }
94 }
95}
96
97impl fmt::Display for Diagnostic {
98 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
99 let kind = match self.severity {
100 Severity::Error => "error",
101 Severity::Warning => "warning",
102 };
103 match &self.location {
104 Some(location) => write!(f, "{location}: {kind}: {}", self.message),
105 None => write!(f, "{kind}: {}", self.message),
106 }
107 }
108}
109
110#[cfg(test)]
111mod tests {
112 use super::*;
113
114 #[test]
115 fn offset_maps_to_line_and_column() {
116 let text = "a = 1\nbé = 2\n";
117 let at = Location::from_offset("x.toml", text, text.find('=').unwrap_or(0));
118 assert_eq!((at.line, at.column), (1, 3));
119 let second = text.rfind('=').unwrap_or(0);
120 let at = Location::from_offset("x.toml", text, second);
121 assert_eq!((at.line, at.column), (2, 4));
122 assert_eq!(at.to_string(), "x.toml:2:4");
123 }
124
125 #[test]
126 fn offset_inside_a_character_points_at_that_character() {
127 let at = Location::from_offset("x.toml", "a = 1\nbé = 2\n", 8);
128 assert_eq!((at.line, at.column), (2, 2));
129 }
130
131 #[test]
132 fn line_starts_locate_every_offset_like_from_offset() {
133 for text in
134 ["", "ab", "a = 1\nbé = 2\n", "\n\nx", "k = \"界\"\r\n\r\nv = 'é😀'\r\n# ü\n", "no newline at end é"]
135 {
136 let starts = LineStarts::new(text);
137 for offset in 0..=text.len() + 2 {
138 assert_eq!(
139 starts.locate("x.toml", text, offset),
140 Location::from_offset("x.toml", text, offset),
141 "{text:?} at {offset}"
142 );
143 }
144 }
145 }
146
147 #[test]
148 fn offset_past_end_is_clamped() {
149 let at = Location::from_offset("x.toml", "ab", 99);
150 assert_eq!((at.line, at.column), (1, 3));
151 }
152
153 #[test]
154 fn display_includes_severity_and_location() {
155 let d = Diagnostic::error(Some(Location::from_offset("t.toml", "x", 0)), "bad");
156 assert_eq!(d.to_string(), "t.toml:1:1: error: bad");
157 assert_eq!(Diagnostic::warning(None, "hm").to_string(), "warning: hm");
158 }
159}