lintel_check/
diagnostics.rs1use miette::{Diagnostic, LabeledSpan, NamedSource, SourceSpan};
2use thiserror::Error;
3
4#[derive(Debug, Error, Diagnostic)]
6#[error("{message}")]
7pub struct ParseDiagnostic {
8 #[source_code]
9 pub src: NamedSource<String>,
10
11 #[label("here")]
12 pub span: SourceSpan,
13
14 pub message: String,
15}
16
17#[derive(Debug, Error)]
19#[error("{message}")]
20pub struct ValidationDiagnostic {
21 pub src: NamedSource<String>,
22
23 pub span: SourceSpan,
24
25 pub path: String,
26
27 pub instance_path: String,
28
29 pub message: String,
30}
31
32impl Diagnostic for ValidationDiagnostic {
33 fn source_code(&self) -> Option<&dyn miette::SourceCode> {
34 Some(&self.src)
35 }
36
37 fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
38 let label = if self.instance_path.is_empty() {
39 "here".to_string()
40 } else {
41 self.instance_path.clone()
42 };
43 Some(Box::new(std::iter::once(LabeledSpan::new(
44 Some(label),
45 self.span.offset(),
46 self.span.len(),
47 ))))
48 }
49}
50
51#[derive(Debug, Error, Diagnostic)]
53#[error("{path}: {message}")]
54pub struct FileDiagnostic {
55 pub path: String,
56 pub message: String,
57}
58
59pub fn offset_to_line_col(content: &str, offset: usize) -> (usize, usize) {
63 let offset = offset.min(content.len());
64 let mut line = 1;
65 let mut col = 1;
66 for (i, ch) in content.char_indices() {
67 if i >= offset {
68 break;
69 }
70 if ch == '\n' {
71 line += 1;
72 col = 1;
73 } else {
74 col += 1;
75 }
76 }
77 (line, col)
78}
79
80pub fn find_instance_path_offset(content: &str, instance_path: &str) -> usize {
85 if instance_path.is_empty() || instance_path == "/" {
86 return 0;
87 }
88
89 let segment = instance_path.rsplit('/').next().unwrap_or("");
91 if segment.is_empty() {
92 return 0;
93 }
94
95 let json_key = format!("\"{segment}\"");
97 if let Some(pos) = content.find(&json_key) {
98 return pos;
99 }
100
101 let yaml_key = format!("{segment}:");
103 let quoted_yaml_key = format!("\"{segment}\":");
104 let mut offset = 0;
105 for line in content.lines() {
106 let trimmed = line.trim_start();
107 if trimmed.starts_with(&yaml_key) || trimmed.starts_with("ed_yaml_key) {
108 let key_start = line.len() - trimmed.len();
109 return offset + key_start;
110 }
111 offset += line.len() + 1; }
113
114 0
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn offset_zero_returns_line_one_col_one() {
123 assert_eq!(offset_to_line_col("hello", 0), (1, 1));
124 }
125
126 #[test]
127 fn offset_within_first_line() {
128 assert_eq!(offset_to_line_col("hello world", 5), (1, 6));
129 }
130
131 #[test]
132 fn offset_at_second_line() {
133 assert_eq!(offset_to_line_col("ab\ncd\nef", 3), (2, 1));
134 }
135
136 #[test]
137 fn offset_middle_of_second_line() {
138 assert_eq!(offset_to_line_col("ab\ncd\nef", 4), (2, 2));
139 }
140
141 #[test]
142 fn offset_at_third_line() {
143 assert_eq!(offset_to_line_col("ab\ncd\nef", 6), (3, 1));
144 }
145
146 #[test]
147 fn offset_past_end_clamps() {
148 assert_eq!(offset_to_line_col("ab\ncd", 100), (2, 3));
149 }
150
151 #[test]
152 fn empty_content() {
153 assert_eq!(offset_to_line_col("", 0), (1, 1));
154 }
155}