1use std::borrow::Cow;
2use std::cell::Cell;
3use std::collections::HashMap;
4use std::fmt::{Debug, Display, Formatter};
5use std::sync::{Arc, RwLock, RwLockReadGuard, RwLockWriteGuard};
6
7use annotate_snippets::renderer::{AnsiColor, Color, DEFAULT_TERM_WIDTH};
8use annotate_snippets::{AnnotationKind, Group, Snippet, renderer};
9use serde::ser::SerializeStruct;
10use serde::{Serialize, Serializer};
11
12use yara_x_parser::Span;
13
14use crate::SourceCode;
15
16pub type Level = annotate_snippets::Level<'static>;
17
18#[derive(Hash, Eq, PartialEq, Clone, Copy, Debug, Default)]
22pub struct SourceId(u32);
23
24#[derive(PartialEq, Debug, Clone, Eq, Default)]
32pub struct CodeLoc {
33 source_id: Option<SourceId>,
34 span: Span,
35}
36
37impl CodeLoc {
38 pub(crate) fn new(source_id: Option<SourceId>, span: Span) -> Self {
39 Self { source_id, span }
40 }
41}
42
43pub struct Patch {
45 code_cache: Arc<CodeCache>,
46 code_loc: CodeLoc,
47 replacement: String,
48}
49
50impl Patch {
51 pub fn origin(&self) -> Option<String> {
53 self.code_cache
54 .read()
55 .get(&self.code_loc.source_id.unwrap())
56 .unwrap()
57 .origin
58 .clone()
59 }
60
61 pub fn span(&self) -> Span {
63 self.code_loc.span.clone()
64 }
65
66 pub fn replacement(&self) -> &str {
69 &self.replacement
70 }
71}
72
73#[derive(Clone)]
99pub(crate) struct Report {
100 code_cache: Arc<CodeCache>,
101 with_colors: bool,
102 max_width: usize,
103 level: Level,
104 code: &'static str,
105 title: String,
106 labels: Vec<(Level, CodeLoc, String)>,
107 footers: Vec<(Level, String)>,
108 sections: Vec<Section>,
109}
110
111#[derive(Clone)]
112pub(crate) struct Section {
113 level: Level,
114 title: String,
115 patches: Vec<(CodeLoc, String)>,
116}
117
118impl Report {
119 #[inline]
121 pub(crate) fn title(&self) -> &str {
122 self.title.as_str()
123 }
124
125 pub(crate) fn labels(&self) -> impl Iterator<Item = Label<'_>> {
127 self.labels.iter().map(|(level, code_loc, text)| {
128 let source_id =
129 code_loc.source_id.expect("CodeLoc without source ID");
130
131 let code_cache = self.code_cache.read();
132 let cache_entry = code_cache.get(&source_id).unwrap();
133 let span = code_loc.span.clone();
134
135 let (line, column) = match cache_entry
136 .byte_offset_to_line_col(span.start())
137 {
138 Some((line, column)) => (line, column),
139 None => panic!(
140 "can't find line and column for span {span} in code:\n{}",
141 &cache_entry.code
142 ),
143 };
144
145 Label {
146 level: level_as_text(level),
147 code_origin: cache_entry.origin.clone(),
148 line,
149 column,
150 span,
151 text,
152 }
153 })
154 }
155
156 #[inline]
158 pub(crate) fn footers(&self) -> impl Iterator<Item = Footer<'_>> {
159 self.footers
160 .iter()
161 .map(|(level, text)| Footer { level: level_as_text(level), text })
162 }
163
164 pub(crate) fn patches(&self) -> impl Iterator<Item = Patch> + use<'_> {
166 self.sections.iter().flat_map(|section| {
167 section.patches.iter().map(|(code_loc, replacement)| Patch {
168 code_cache: self.code_cache.clone(),
169 code_loc: code_loc.clone(),
170 replacement: replacement.clone(),
171 })
172 })
173 }
174
175 pub(crate) fn new_section<T: Into<String>>(
176 &mut self,
177 level: Level,
178 title: T,
179 ) -> &mut Self {
180 self.sections.push(Section {
181 level,
182 title: title.into(),
183 patches: vec![],
184 });
185 self
186 }
187
188 pub(crate) fn patch<R: Into<String>>(
189 &mut self,
190 code_loc: CodeLoc,
191 replacement: R,
192 ) -> &mut Self {
193 if self.sections.is_empty() {
194 self.new_section(Level::HELP, "consider the following change");
195 };
196 self.sections
197 .last_mut()
198 .unwrap()
199 .patches
200 .push((code_loc, replacement.into()));
201 self
202 }
203}
204
205impl Serialize for Report {
206 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
207 where
208 S: Serializer,
209 {
210 let labels = self.labels().collect::<Vec<_>>();
211 let footers = &self.footers().collect::<Vec<_>>();
212
213 let mut s = serializer.serialize_struct("report", 4)?;
214
215 s.serialize_field("code", &self.code)?;
216 s.serialize_field("title", &self.title)?;
217
218 if let Some(label) = labels
222 .iter()
223 .find(|label| label.level == level_as_text(&self.level))
224 {
225 s.serialize_field("line", &label.line)?;
226 s.serialize_field("column", &label.column)?;
227 }
228
229 s.serialize_field("labels", &labels)?;
230 s.serialize_field("footers", &footers)?;
231 s.serialize_field("text", &self.to_string())?;
232 s.end()
233 }
234}
235
236impl PartialEq for Report {
237 fn eq(&self, other: &Self) -> bool {
238 self.level.eq(&other.level)
239 && self.code.eq(other.code)
240 && self.title.eq(&other.title)
241 && self.labels.eq(&other.labels)
242 && self.footers.eq(&other.footers)
243 }
244}
245
246impl Eq for Report {}
247
248impl Debug for Report {
249 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
250 write!(f, "{self}")
251 }
252}
253
254impl Display for Report {
255 fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
256 let code_cache = self.code_cache.read();
257
258 let mut group = Group::with_title(
259 self.level.clone().primary_title(&self.title).id(self.code),
260 );
261
262 let mut source_ids = Vec::new();
263 for (_, label_ref, _) in &self.labels {
264 let sid = label_ref.source_id.unwrap();
265 if !source_ids.contains(&sid) {
266 source_ids.push(sid);
267 }
268 }
269
270 for source_id in source_ids {
271 let cache_entry = code_cache.get(&source_id).unwrap();
272
273 let min_offset = self
279 .labels
280 .iter()
281 .filter(|l| l.1.source_id.unwrap() == source_id)
282 .map(|l| l.1.span.start())
283 .min()
284 .unwrap();
285
286 let max_offset = self
287 .labels
288 .iter()
289 .filter(|l| l.1.source_id.unwrap() == source_id)
290 .map(|l| l.1.span.end())
291 .max()
292 .unwrap();
293
294 let (sliced_src, line_start, slice_start) =
295 get_source_slice(cache_entry, min_offset, max_offset);
296
297 let mut snippet = Snippet::source(sliced_src)
300 .line_start(line_start)
301 .path(cache_entry.origin.as_deref().unwrap_or("line"));
302
303 for (level, label_ref, label) in &self.labels {
304 if label_ref.source_id.unwrap() == source_id {
305 let annotation_kind = if matches!(level, &Level::ERROR) {
306 AnnotationKind::Primary
307 } else {
308 AnnotationKind::Context
309 };
310
311 let span_start =
314 label_ref.span.start().saturating_sub(slice_start);
315 let span_end =
316 label_ref.span.end().saturating_sub(slice_start);
317
318 snippet = snippet.annotation(
319 annotation_kind
320 .span(span_start..span_end)
321 .label(label),
322 );
323 }
324 }
325
326 group = group.element(snippet);
327 }
328
329 for (level, text) in &self.footers {
330 group = group.element(level.clone().message(text.as_str()));
331 }
332
333 let renderer = if self.with_colors {
334 annotate_snippets::Renderer::styled()
335 } else {
336 annotate_snippets::Renderer::plain()
337 };
338
339 let renderer = renderer.term_width(self.max_width);
340
341 let mut groups = vec![group];
342
343 for section in &self.sections {
344 if section.patches.is_empty() {
345 continue;
346 }
347 let sid = section.patches[0].0.source_id.unwrap();
348 let cache_entry = code_cache.get(&sid).unwrap();
349
350 let min_offset = section
353 .patches
354 .iter()
355 .map(|(loc, _)| loc.span.start())
356 .min()
357 .unwrap();
358 let max_offset = section
359 .patches
360 .iter()
361 .map(|(loc, _)| loc.span.end())
362 .max()
363 .unwrap();
364
365 let (sliced_src, line_start, slice_start) =
366 get_source_slice(cache_entry, min_offset, max_offset);
367
368 let mut snippet = Snippet::source(sliced_src)
369 .line_start(line_start)
370 .path(cache_entry.origin.as_deref().unwrap_or("line"));
371
372 for (code_loc, replacement) in §ion.patches {
373 let span_start =
375 code_loc.span.start().saturating_sub(slice_start);
376 let span_end = code_loc.span.end().saturating_sub(slice_start);
377
378 snippet = snippet.patch(annotate_snippets::Patch::new(
379 span_start..span_end,
380 replacement,
381 ))
382 }
383
384 groups.push(
385 section
386 .level
387 .clone()
388 .secondary_title(§ion.title)
389 .element(snippet),
390 );
391 }
392
393 let text = renderer.render(&groups);
394
395 write!(f, "{text}")
396 }
397}
398
399fn get_source_slice(
402 cache_entry: &CodeCacheEntry,
403 min_offset: usize,
404 max_offset: usize,
405) -> (&str, usize, usize) {
406 let line_starts = &cache_entry.line_starts;
407 let start_line_idx =
408 line_starts.partition_point(|&x| x <= min_offset).saturating_sub(1);
409 let end_line_idx =
410 line_starts.partition_point(|&x| x <= max_offset).saturating_sub(1);
411
412 let slice_start = line_starts[start_line_idx];
413 let slice_end = if end_line_idx + 1 < line_starts.len() {
414 line_starts[end_line_idx + 1]
415 } else {
416 cache_entry.code.len()
417 };
418
419 (
420 &cache_entry.code[slice_start..slice_end],
421 start_line_idx + 1,
422 slice_start,
423 )
424}
425
426#[derive(Serialize)]
428pub struct Label<'a> {
429 level: &'a str,
430 code_origin: Option<String>,
431 line: usize,
432 column: usize,
433 span: Span,
434 text: &'a str,
435}
436
437impl Label<'_> {
438 #[inline]
439 pub fn origin(&self) -> Option<&str> {
440 self.code_origin.as_deref()
441 }
442
443 #[inline]
444 pub fn span(&self) -> &Span {
445 &self.span
446 }
447
448 #[inline]
449 pub fn text(&self) -> &str {
450 self.text
451 }
452}
453
454#[derive(Serialize)]
456pub struct Footer<'a> {
457 level: &'a str,
458 text: &'a str,
459}
460
461pub struct ReportBuilder {
470 with_colors: bool,
471 max_width: usize,
472 current_source_id: Cell<Option<SourceId>>,
473 next_source_id: Cell<SourceId>,
474 code_cache: Arc<CodeCache>,
475}
476
477struct CodeCache {
479 data: RwLock<HashMap<SourceId, CodeCacheEntry>>,
480}
481
482impl CodeCache {
483 fn new() -> Self {
484 Self { data: RwLock::new(HashMap::new()) }
485 }
486
487 pub fn read(
488 &self,
489 ) -> RwLockReadGuard<'_, HashMap<SourceId, CodeCacheEntry>> {
490 self.data.read().unwrap()
491 }
492
493 pub fn write(
494 &self,
495 ) -> RwLockWriteGuard<'_, HashMap<SourceId, CodeCacheEntry>> {
496 self.data.write().unwrap()
497 }
498}
499
500struct CodeCacheEntry {
502 code: String,
503 line_starts: Vec<usize>,
504 origin: Option<String>,
505}
506
507impl CodeCacheEntry {
508 fn byte_offset_to_line_col(
511 &self,
512 byte_offset: usize,
513 ) -> Option<(usize, usize)> {
514 if byte_offset > self.code.len()
515 || !self.code.is_char_boundary(byte_offset)
516 {
517 return None;
518 }
519
520 let line = self.line_starts.partition_point(|&x| x <= byte_offset);
521 let line_start = self.line_starts[line - 1];
522 let col = self.code[line_start..byte_offset].chars().count() + 1;
523
524 Some((line, col))
525 }
526}
527
528impl Default for ReportBuilder {
529 fn default() -> Self {
530 Self::new()
531 }
532}
533
534impl ReportBuilder {
535 pub fn new() -> Self {
537 Self {
538 with_colors: false,
539 max_width: DEFAULT_TERM_WIDTH,
540 current_source_id: Cell::new(None),
541 next_source_id: Cell::new(SourceId(0)),
542 code_cache: Arc::new(CodeCache::new()),
543 }
544 }
545
546 pub fn with_colors(&mut self, yes: bool) -> &mut Self {
549 self.with_colors = yes;
550 self
551 }
552
553 pub fn max_width(&mut self, width: usize) -> &mut Self {
557 self.max_width = width;
558 self
559 }
560
561 pub fn get_current_source_id(&self) -> Option<SourceId> {
566 self.current_source_id.get()
567 }
568
569 pub fn set_current_source_id(&mut self, source_id: SourceId) {
571 self.current_source_id.set(Some(source_id));
572 }
573
574 pub fn span_to_code_loc(&self, span: Span) -> CodeLoc {
579 CodeLoc::new(self.get_current_source_id(), span)
580 }
581
582 pub fn green_style(&self) -> renderer::Style {
594 if self.with_colors {
595 renderer::Style::new()
596 .fg_color(Some(Color::Ansi(AnsiColor::BrightGreen)))
597 } else {
598 renderer::Style::new()
599 }
600 }
601
602 pub fn register_source(&self, src: &SourceCode) -> SourceId {
614 let source_id = self.next_source_id.get();
615 self.next_source_id.set(SourceId(source_id.0 + 1));
616 self.current_source_id.set(Some(source_id));
617
618 self.code_cache.write().entry(source_id).or_insert_with(|| {
619 let s = if let Some(s) = src.valid {
620 Cow::Borrowed(s)
621 } else {
622 String::from_utf8_lossy(src.raw.as_ref())
623 };
624 let code = s.replace('\t', " ");
625 let line_starts = compute_line_starts(&code);
626 CodeCacheEntry {
627 code,
632 line_starts,
633 origin: src.origin.clone(),
634 }
635 });
636
637 source_id
638 }
639
640 pub fn get_snippet(&self, span: Span) -> String {
642 let source_id = self.get_current_source_id().unwrap();
643 let code_cache = self.code_cache.read();
644 let cache_entry = code_cache.get(&source_id).unwrap();
645 let src = cache_entry.code.as_str();
646
647 src[span.range()].to_string()
648 }
649
650 pub fn create_report(
652 &self,
653 level: Level,
654 code: &'static str,
655 title: String,
656 labels: Vec<(Level, CodeLoc, String)>,
657 footers: Vec<(Level, Option<String>)>,
658 ) -> Report {
659 assert!(!labels.is_empty());
661
662 let footers = footers
664 .into_iter()
665 .filter_map(|(level, text)| text.map(|text| (level, text)))
666 .collect();
667
668 Report {
669 code_cache: self.code_cache.clone(),
670 with_colors: self.with_colors,
671 max_width: self.max_width,
672 level,
673 code,
674 title,
675 labels,
676 footers,
677 sections: Vec::new(),
678 }
679 }
680}
681
682fn level_as_text(level: &Level) -> &'static str {
683 match *level {
684 Level::ERROR => "error",
685 Level::WARNING => "warning",
686 Level::INFO => "info",
687 Level::NOTE => "note",
688 Level::HELP => "help",
689 _ => panic!("unsupported level {level:?}"),
690 }
691}
692
693fn compute_line_starts(text: &str) -> Vec<usize> {
694 let mut line_starts = vec![0];
695 for (i, c) in text.char_indices() {
696 if c == '\n' {
697 line_starts.push(i + 1);
698 }
699 }
700 line_starts
701}
702
703#[cfg(test)]
704mod tests {
705 use crate::compiler::report::{CodeCacheEntry, compute_line_starts};
706
707 fn helper(text: &str, offset: usize) -> Option<(usize, usize)> {
708 let line_starts = compute_line_starts(text);
709 let entry = CodeCacheEntry {
710 code: text.to_string(),
711 line_starts,
712 origin: None,
713 };
714 entry.byte_offset_to_line_col(offset)
715 }
716
717 #[test]
718 fn byte_offset_to_line_col_single_line() {
719 let text = "Hello, World!";
720 assert_eq!(helper(text, 0), Some((1, 1))); assert_eq!(helper(text, 7), Some((1, 8))); assert_eq!(helper(text, 12), Some((1, 13))); }
724
725 #[test]
726 fn byte_offset_to_line_col_multiline() {
727 let text = "Hello\nRust\nWorld!";
728 assert_eq!(helper(text, 0), Some((1, 1))); assert_eq!(helper(text, 5), Some((1, 6))); assert_eq!(helper(text, 6), Some((2, 1))); assert_eq!(helper(text, 9), Some((2, 4))); assert_eq!(helper(text, 11), Some((3, 1))); }
734
735 #[test]
736 fn byte_offset_to_line_col_empty_string() {
737 let text = "";
738 assert_eq!(helper(text, 0), Some((1, 1)));
739 }
740
741 #[test]
742 fn byte_offset_to_line_col_out_of_bounds() {
743 let text = "Hello, World!";
744 assert_eq!(helper(text, text.len() + 1), None);
745 }
746
747 #[test]
748 fn byte_offset_to_line_col_end_of_string() {
749 let text = "Hello, World!";
750 assert_eq!(helper(text, text.len()), Some((1, 14))); }
752
753 #[test]
754 fn byte_offset_to_line_col_multibyte_characters() {
755 let text = "Hello, 你好!";
756 assert_eq!(helper(text, 7), Some((1, 8))); assert_eq!(helper(text, 8), None); assert_eq!(helper(text, 10), Some((1, 9))); assert_eq!(helper(text, 13), Some((1, 10))); }
761}