1use std::cell::{Cell, OnceCell, RefCell};
2
3use crate::unicode::line_separator_at;
4use crate::{Problem, Severity};
5
6#[derive(Debug)]
12pub struct Report<'a> {
13 text: &'a str,
14 problems: Vec<Problem>,
15 line_starts: OnceCell<Vec<usize>>,
16 json5: bool,
18 ascii: OnceCell<bool>,
20 furthest: Cell<(usize, usize, usize)>,
23 checkpoints: RefCell<Vec<(usize, usize)>>,
26 #[cfg(test)]
28 counted: Cell<usize>,
29}
30
31#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
35#[non_exhaustive]
36pub struct Location {
37 pub line: usize,
39 pub column: usize,
41}
42
43impl Location {
44 pub const START: Location = Location { line: 1, column: 1 };
46}
47
48impl<'a> Report<'a> {
49 pub(crate) fn new(text: &'a str, mut problems: Vec<Problem>, json5: bool) -> Self {
51 problems.sort_by_key(Problem::offset);
53 Report {
54 text,
55 problems,
56 line_starts: OnceCell::new(),
57 json5,
58 ascii: OnceCell::new(),
59 furthest: Cell::new((0, 1, 1)),
60 checkpoints: RefCell::new(vec![(0, 0)]),
61 #[cfg(test)]
62 counted: Cell::new(0),
63 }
64 }
65
66 pub fn problems(&self) -> &[Problem] {
68 &self.problems
69 }
70
71 pub fn passed(&self) -> bool {
73 self.problems.iter().all(|p| p.severity() < Severity::Error)
74 }
75
76 pub fn location(&self, offset: usize) -> Location {
83 let offset = offset.min(self.text.len());
84 let starts = self
85 .line_starts
86 .get_or_init(|| line_starts(self.text, self.json5));
87 let line = starts.partition_point(|&s| s <= offset);
88 let start = starts[line - 1];
89 if *self.ascii.get_or_init(|| self.text.is_ascii()) {
90 return Location {
91 line,
92 column: 1 + offset - start,
93 };
94 }
95 let (furthest, furthest_line, furthest_column) = self.furthest.get();
96 let column = if furthest_line == line && furthest <= offset {
97 furthest_column + self.count(furthest, offset)
98 } else if offset - start <= CHUNK {
99 1 + self.count(start, offset)
100 } else {
101 1 + self
103 .chars_before(offset)
104 .saturating_sub(self.chars_before(start))
105 };
106 if offset >= furthest {
107 self.furthest.set((offset, line, column));
108 }
109 Location { line, column }
110 }
111
112 fn count(&self, from: usize, to: usize) -> usize {
115 #[cfg(test)]
116 self.counted.set(self.counted.get() + to.abs_diff(from));
117 self.text.get(from..to).map_or(0, |s| s.chars().count())
118 }
119
120 fn chars_before(&self, offset: usize) -> usize {
122 let mut checkpoints = self.checkpoints.borrow_mut();
123 while let Some(&(at, before)) = checkpoints.last().filter(|(at, _)| at + CHUNK <= offset) {
124 let mut next = (at + CHUNK).min(self.text.len());
125 while !self.text.is_char_boundary(next) {
126 next += 1;
127 }
128 checkpoints.push((next, before + self.count(at, next)));
129 }
130 let (at, before) = checkpoints[checkpoints.partition_point(|&(at, _)| at <= offset) - 1];
131 before + self.count(at, offset)
132 }
133}
134
135const CHUNK: usize = 4096;
137
138fn line_starts(text: &str, json5: bool) -> Vec<usize> {
141 let bytes = text.as_bytes();
142 let mut starts = vec![0];
143 let mut i = 0;
144 let third = if json5 { 0xE2 } else { b'\n' };
147 while let Some(found) = memchr::memchr3(b'\n', b'\r', third, &bytes[i..]) {
148 let at = i + found;
149 match bytes[at] {
150 b'\r' if bytes.get(at + 1) == Some(&b'\n') => {
151 starts.push(at + 2);
152 i = at + 2;
153 continue;
154 }
155 b'\n' | b'\r' => starts.push(at + 1),
156 _ if line_separator_at(bytes, at) => starts.push(at + 3),
157 _ => {}
158 }
159 i = at + 1;
160 }
161 starts
162}
163
164#[cfg(test)]
165mod tests {
166 use super::{Location, Report, line_starts};
167 use crate::{Config, Dialect, check};
168
169 #[test]
170 fn every_json5_line_terminator_starts_a_line() {
171 let text = "a\nb\r\nc\rd\u{2028}e\u{2029}f";
172 assert_eq!(line_starts(text, true), vec![0, 2, 5, 7, 11, 15]);
173 assert_eq!(line_starts(text, false), vec![0, 2, 5, 7]);
175 assert_eq!(line_starts("no breaks, but é and ★", true), vec![0]);
176 }
177
178 #[test]
179 fn locations_agree_in_any_order() {
180 let report = Report::new("aé★b\ncdé", Vec::new(), true);
181 let at = |offset| report.location(offset);
182 let expected = [
183 (6, (1, 4)),
184 (1, (1, 2)),
185 (9, (2, 2)),
186 (7, (1, 5)),
187 (12, (2, 4)),
188 ];
189 for (offset, (line, column)) in expected {
190 assert_eq!(at(offset), Location { line, column }, "offset {offset}");
191 }
192 }
193
194 #[test]
195 fn checkpoints_land_on_character_boundaries() {
196 let text = format!("a{}", "\u{e9}".repeat(5000));
198 let report = Report::new(&text, Vec::new(), true);
199 for k in (0..5000).rev().step_by(97) {
200 let at = report.location(1 + 2 * k);
201 assert_eq!(
202 at,
203 Location {
204 line: 1,
205 column: 2 + k
206 },
207 "character {k}"
208 );
209 }
210 }
211
212 #[test]
213 fn ascii_columns_need_no_counting() {
214 let keys: Vec<String> = (0..1000).map(|i| format!("\"k{i}\":1")).collect();
217 let text = format!("{{{},{}}}", keys.join(","), keys.join(","));
218 let report = check(&text, &Config::new(Dialect::Json));
219 assert_eq!(report.problems().len(), 1000);
220 for problem in report.problems() {
221 let at = report.location(problem.offset());
222 assert_eq!(at.column, problem.offset() + 1);
223 report.location(problem.related().map(|r| r.offset).unwrap_or_default());
224 }
225 assert_eq!(report.counted.get(), 0);
226 }
227
228 #[test]
229 fn non_ascii_columns_cost_at_most_a_chunk_each() {
230 let keys: Vec<String> = (0..10_000).map(|i| format!("\"k{i}\":1")).collect();
233 let text = format!("{{\"\u{e9}\":0,{},{}}}", keys.join(","), keys.join(","));
234 let report = check(&text, &Config::new(Dialect::Json));
235 assert_eq!(report.problems().len(), 10_000);
236 for problem in report.problems() {
237 let at = report.location(problem.offset());
238 assert_eq!(at.column, problem.offset());
240 let first = problem.related().map(|r| r.offset).unwrap_or_default();
241 assert_eq!(report.location(first).column, first);
242 }
243 let bound = 2 * text.len() + 2 * super::CHUNK * report.problems().len();
244 assert!(
245 report.counted.get() <= bound,
246 "{} > {bound}",
247 report.counted.get()
248 );
249 }
250}