1use ariadne::{Color, Label};
2use glob::{self, glob_with};
3use lazy_format::lazy_format;
4use std::{
5 collections::{HashMap, HashSet, VecDeque},
6 ffi::OsStr,
7 fmt::{self, Formatter},
8 fs::File,
9 io::{self, Read, Write},
10 iter::once,
11 path::{Path, PathBuf},
12};
13
14pub use crate::{trim::trim_trailing_whitespace, types::*};
15
16use crate::{IncludedGlob, SourceId, get_includes};
17
18#[derive(Clone)]
34pub struct BeancountSources {
35 root_path: Option<PathBuf>,
36 root_source_id: SourceId,
37 root_content: String,
38 root_content_char_indices: Vec<usize>,
39 included_globs: HashMap<PathBuf, IncludedGlob>,
40 included_content: HashMap<PathBuf, IncludedSource>,
41 source_id_strings: Vec<String>, }
43
44#[derive(Clone, Debug)]
45enum IncludedSource {
46 Content(SourceId, String, Vec<usize>), Error(String),
48 Duplicate,
49}
50
51impl BeancountSources {
52 fn try_read_with_includes(root_path: PathBuf) -> io::Result<Self> {
53 let root_content = read(&root_path)?;
54 Ok(Self::read_with_includes(Some(root_path), root_content))
55 }
56
57 fn read_with_includes(root_path: Option<PathBuf>, root_content: String) -> Self {
58 let root_source_id = SourceId::default();
59 let root_source_id_string = root_path
60 .as_ref()
61 .map(|p| p.to_string_lossy().into())
62 .unwrap_or("inline".to_string());
63 let mut source_id_strings = Vec::from([root_source_id_string]);
64
65 let mut pending_includes = get_includes(&root_content, root_source_id)
66 .into_iter()
67 .map(|included| resolve_included_path(root_path.as_ref(), included.as_ref()))
68 .collect::<VecDeque<_>>();
69
70 let mut included_globs = HashMap::new();
71 let mut included_content: HashMap<PathBuf, IncludedSource> = HashMap::new();
72
73 let mut canonical_paths =
75 if let Some(canonical_root) = root_path.as_ref().and_then(|p| p.canonicalize().ok()) {
76 HashSet::from([canonical_root])
77 } else {
78 HashSet::default()
79 };
80
81 while !pending_includes.is_empty() {
82 let included = pending_includes.pop_front().unwrap();
83 let included_str = included.to_string_lossy();
84 let included_str = included_str.as_ref();
85
86 match glob_with(
87 included_str,
88 glob::MatchOptions {
89 case_sensitive: true,
90 require_literal_separator: true,
91 require_literal_leading_dot: true,
92 },
93 ) {
94 Err(e) => {
95 included_globs.insert(included, IncludedGlob::Error(e.to_string()));
96 }
97 Ok(globbed_includes) => {
98 let mut glob_expansions = Vec::default();
99
100 for globbed_include in globbed_includes {
101 match globbed_include {
102 Err(e) => {
103 let path = e.path().to_path_buf();
104 glob_expansions.push(path.clone());
105 included_content.insert(path, IncludedSource::Error(e.to_string()));
106 }
107 Ok(globbed_include) => {
108 glob_expansions.push(globbed_include.clone());
109
110 if let Ok(canonical_path) = globbed_include.canonicalize() {
111 if canonical_paths.contains(&canonical_path) {
112 included_content
114 .entry(globbed_include)
115 .or_insert(IncludedSource::Duplicate);
116 } else {
117 canonical_paths.insert(canonical_path);
118
119 let source_id = SourceId::from(source_id_strings.len());
120 source_id_strings
121 .push(globbed_include.to_string_lossy().into());
122
123 let included_source = read(&globbed_include).map_or_else(
124 |e| {
125 IncludedSource::Error(format!(
126 "{}: {}",
127 globbed_include.to_string_lossy(),
128 e
129 ))
130 },
131 |c| {
132 let char_indices = c
136 .char_indices()
137 .map(|(i, _)| i)
138 .collect::<Vec<_>>();
139 IncludedSource::Content(source_id, c, char_indices)
140 },
141 );
142
143 included_content
146 .insert(globbed_include.clone(), included_source);
147 let included_source =
148 included_content.get(&globbed_include).unwrap();
149
150 if let IncludedSource::Content(_, content, _) =
151 included_source
152 {
153 let mut includes = get_includes(content, source_id)
154 .into_iter()
155 .map(|included_path| {
156 resolve_included_path(
157 Some(&globbed_include),
158 included_path.as_ref(),
159 )
160 })
161 .collect::<VecDeque<_>>();
162 pending_includes.append(&mut includes);
163 }
164 }
165 }
166 }
167 }
168 }
169
170 included_globs.insert(included, IncludedGlob::Expanded(glob_expansions));
171 }
172 }
173 }
174
175 let root_content_char_indices = root_content
176 .char_indices()
177 .map(|(i, _)| i)
178 .collect::<Vec<_>>();
179
180 Self {
181 root_path,
182 root_source_id,
183 root_content,
184 root_content_char_indices,
185 included_globs,
186 included_content,
187 source_id_strings,
188 }
189 }
190
191 #[deprecated(since = "0.12.0", note = "Use `write_errors_or_warnings` instead")]
192 pub fn write<W, E, K>(&self, w: &mut W, errors_or_warnings: Vec<E>) -> io::Result<()>
193 where
194 W: Write,
195 E: Into<AnnotatedErrorOrWarning<K>>,
196 K: ErrorOrWarningKind,
197 {
198 self.write_errors_or_warnings(w, errors_or_warnings)
199 }
200
201 pub fn write_errors_or_warnings<W, E, K>(
203 &self,
204 w: &mut W,
205 errors_or_warnings: Vec<E>,
206 ) -> io::Result<()>
207 where
208 W: Write,
209 E: Into<AnnotatedErrorOrWarning<K>>,
210 K: ErrorOrWarningKind,
211 {
212 for error_or_warning in errors_or_warnings.into_iter() {
213 let AnnotatedErrorOrWarning {
214 error_or_warning,
215 annotation,
216 } = error_or_warning.into();
217
218 self.write_report::<W, K, ErrorOrWarning<K>>(w, &error_or_warning)?;
219
220 if let Some(annotation) = annotation {
221 w.write_fmt(core::format_args!("{}\n", &annotation))?;
223 }
224 }
225 Ok(())
226 }
227
228 pub fn write_report<W, K, R>(&self, w: &mut W, report: &R) -> io::Result<()>
230 where
231 W: Write,
232 K: ErrorOrWarningKind,
233 R: Report,
234 {
235 write_report::<W, K, R, _>(
236 w,
237 report,
238 &|span| self.get_adjusted_source(span),
239 self.sources(),
240 )
241 }
242
243 pub fn resolve_span<'a>(&'a self, span: &Span) -> SpannedSource<'a> {
246 let (source_content, source_id_str, byte_span, rune_span) = self.get_adjusted_source(*span);
247
248 let file_name = if Into::<SourceId>::into(span.source) == self.root_source_id {
249 self.root_path.as_ref().and(Some(source_id_str))
250 } else {
251 Some(source_id_str)
252 };
253
254 let mut source_chars = source_content.chars();
255 let start_line = source_chars
256 .by_ref()
257 .take(rune_span.start)
258 .filter(|c| *c == '\n')
259 .count()
260 + 1;
261 let lines_spanned = source_chars
262 .by_ref()
263 .take(rune_span.end - rune_span.start)
264 .filter(|c| *c == '\n')
265 .count();
266 let end_line = start_line + lines_spanned;
267
268 SpannedSource {
269 file_name,
270 start_line,
271 end_line,
272 content: source_content
273 .get(byte_span.start..byte_span.end)
274 .unwrap_or(""),
275 }
276 }
277
278 fn byte_to_rune(&self, char_indices: &[usize], byte_span: Span) -> Span {
279 let mut rune_span = byte_span;
280 rune_span.start = char_indices.partition_point(|&i| i < byte_span.start);
281 rune_span.end = char_indices.partition_point(|&i| i < byte_span.end);
282 rune_span
283 }
284
285 pub fn error_source_text<'a, K>(&'a self, error_or_warning: &ErrorOrWarning<K>) -> &'a str
286 where
287 K: ErrorOrWarningKind,
288 {
289 let (source_content, _, byte_span, _rune_span) =
290 self.get_adjusted_source(error_or_warning.0.span);
291 &source_content[byte_span.start..byte_span.end]
292 }
293
294 fn get_adjusted_source(&self, span: Span) -> (&str, &str, Span, Span) {
295 let safe_span = if span.source >= self.source_id_strings.len() {
296 Span {
300 source: self.root_source_id.into(),
301 start: 0,
302 end: 0,
303 }
304 } else {
305 span
306 };
307 let source_id = safe_span.source.into();
308 let source_id_str = self.source_id_string(source_id);
309 let empty_char_indices = Vec::default();
310 let (source_content, source_content_char_indices) = if source_id == self.root_source_id {
311 (self.root_content.as_str(), &self.root_content_char_indices)
312 } else if let IncludedSource::Content(_, content, content_char_indices) =
313 self.included_content.get(Path::new(source_id_str)).unwrap()
314 {
315 (content.as_str(), content_char_indices)
316 } else {
317 ("", &empty_char_indices)
318 };
319
320 let byte_span = trimmed_span(source_content, safe_span);
321 let rune_span = byte_to_rune(source_content_char_indices, byte_span);
322
323 (source_content, source_id_str, byte_span, rune_span)
324 }
325
326 fn source_id_string(&self, source_id: SourceId) -> &str {
327 self.source_id_strings[Into::<usize>::into(source_id)].as_str()
328 }
329
330 fn sources(&self) -> Vec<(String, &str)> {
331 once((
332 self.source_id_string(self.root_source_id).to_string(),
333 self.root_content.as_str(),
334 ))
335 .chain(
336 self.included_content
337 .iter()
338 .filter_map(|(_, included_source)| {
339 if let IncludedSource::Content(source_id, content, _) = included_source {
340 Some((
341 self.source_id_string(*source_id).to_string(),
342 content.as_str(),
343 ))
344 } else {
345 None
346 }
347 }),
348 )
349 .collect()
350 }
351
352 pub(crate) fn content_iter(&self) -> impl Iterator<Item = (SourceId, Option<&Path>, &str)> {
353 once((
354 self.root_source_id,
355 self.root_path.as_deref(),
356 self.root_content.as_str(),
357 ))
358 .chain(
359 self.included_content
360 .iter()
361 .filter_map(|(pathbuf, included_source)| {
362 if let IncludedSource::Content(source_id, content, _) = included_source {
363 Some((*source_id, Some(pathbuf.as_path()), content.as_str()))
364 } else {
365 None
366 }
367 }),
368 )
369 }
370
371 pub(crate) fn num_sources(&self) -> usize {
373 self.source_id_strings.len()
374 }
375
376 pub(crate) fn root_path(&self) -> Option<&Path> {
377 self.root_path.as_deref()
378 }
379
380 pub(crate) fn included_globs(&self) -> &HashMap<PathBuf, IncludedGlob> {
381 &self.included_globs
382 }
383
384 pub(crate) fn error_paths(&self) -> HashMap<Option<&Path>, String> {
385 self.included_content
386 .iter()
387 .filter_map(|(pathbuf, included_source)| {
388 if let IncludedSource::Error(e) = included_source {
389 Some((Some(pathbuf.as_path()), e.clone()))
390 } else {
391 None
392 }
393 })
394 .collect::<HashMap<_, _>>()
395 }
396}
397
398impl TryFrom<PathBuf> for BeancountSources {
399 type Error = io::Error;
400
401 fn try_from(source_path: PathBuf) -> io::Result<Self> {
402 Self::try_read_with_includes(source_path)
403 }
404}
405
406impl TryFrom<&Path> for BeancountSources {
407 type Error = io::Error;
408
409 fn try_from(source_path: &Path) -> io::Result<Self> {
410 Self::try_read_with_includes(source_path.to_owned())
411 }
412}
413
414impl From<String> for BeancountSources {
415 fn from(source_string: String) -> Self {
416 Self::read_with_includes(None, source_string)
417 }
418}
419
420impl From<&str> for BeancountSources {
421 fn from(source_string: &str) -> Self {
422 Self::read_with_includes(None, source_string.to_owned())
423 }
424}
425
426impl std::fmt::Debug for BeancountSources {
427 fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
428 writeln!(f, "BeancountSources(",)?;
429
430 for (path, included_source) in &self.included_content {
431 match included_source {
432 IncludedSource::Content(source_id, content, _) => writeln!(
433 f,
434 " {} ok len {},",
435 self.source_id_string(*source_id),
436 content.len()
437 )?,
438 IncludedSource::Error(e) => writeln!(f, " {:?} err {},", path, e)?,
439 IncludedSource::Duplicate => writeln!(f, " {:?} duplicate include", path)?,
440 }
441 }
442
443 writeln!(f, ")",)
444 }
445}
446
447#[derive(Clone)]
448pub struct SyntheticSources<'a> {
449 sources: &'a BeancountSources,
450 base_id: usize,
451 content: HashMap<String, (SourceId, String, Vec<usize>)>, source_id_strings: Vec<String>, }
454
455impl<'a> SyntheticSources<'a> {
456 pub fn new(sources: &'a BeancountSources) -> Self {
457 SyntheticSources {
458 sources,
459 base_id: sources.num_sources(),
460 content: HashMap::default(),
461 source_id_strings: Vec::default(),
462 }
463 }
464}
465
466impl<'a> SyntheticSources<'a> {
467 fn sources(&self) -> Vec<(String, &str)> {
468 let mut sources = self.sources.sources();
469 sources.extend(
470 self.content.iter().map(|(source_id_str, (_, content, _))| {
471 (source_id_str.to_string(), content.as_str())
472 }),
473 );
474 sources
475 }
476
477 pub fn create_synthetic_span(&mut self, source_name: &str, content_fragment: &str) -> Span {
480 if let Some((source_id, content, char_indices)) = self.content.get_mut(source_name) {
481 let start = content.len();
482 let end = start + content_fragment.len();
483 let span = Span {
484 source: (*source_id).into(),
485 start,
486 end,
487 };
488
489 char_indices.extend(content_fragment.char_indices().map(|(i, _)| i));
490 content.push_str(content_fragment);
491
492 span
493 } else {
494 let source = self.source_id_strings.len() + self.base_id;
495 self.source_id_strings.push(source_name.to_string());
496
497 let span = Span {
498 source,
499 start: 0,
500 end: content_fragment.len(),
501 };
502
503 let content = content_fragment.to_string();
504 let char_indices = content.char_indices().map(|(i, _)| i).collect::<Vec<_>>();
505 self.content.insert(
506 source_name.to_string(),
507 (source.into(), content, char_indices),
508 );
509
510 span
511 }
512 }
513
514 pub fn write_errors_or_warnings<W, E, K>(
516 &self,
517 w: &mut W,
518 errors_or_warnings: Vec<E>,
519 ) -> io::Result<()>
520 where
521 W: Write,
522 E: Into<AnnotatedErrorOrWarning<K>>,
523 K: ErrorOrWarningKind,
524 {
525 for error_or_warning in errors_or_warnings.into_iter() {
526 let AnnotatedErrorOrWarning {
527 error_or_warning,
528 annotation,
529 } = error_or_warning.into();
530
531 self.write_report::<W, K, ErrorOrWarning<K>>(w, &error_or_warning)?;
532
533 if let Some(annotation) = annotation {
534 w.write_fmt(core::format_args!("{}\n", &annotation))?;
536 }
537 }
538 Ok(())
539 }
540
541 pub fn write_report<W, K, R>(&self, w: &mut W, report: &R) -> io::Result<()>
543 where
544 W: Write,
545 K: ErrorOrWarningKind,
546 R: Report,
547 {
548 write_report::<W, K, R, _>(
549 w,
550 report,
551 &|span| self.get_adjusted_source(span),
552 self.sources(),
553 )
554 }
555
556 pub fn resolve_span<'s>(&'s self, span: &Span) -> SpannedSource<'s> {
559 resolve_span(*span, &|span| self.get_adjusted_source(span), true)
560 }
561
562 pub fn error_source_text<'s, K>(&'s self, error_or_warning: &ErrorOrWarning<K>) -> &'s str
563 where
564 K: ErrorOrWarningKind,
565 {
566 let (source_content, _, byte_span, _rune_span) =
567 self.get_adjusted_source(error_or_warning.0.span);
568 &source_content[byte_span.start..byte_span.end]
569 }
570
571 fn get_adjusted_source(&self, span: Span) -> (&str, &str, Span, Span) {
572 if span.source >= self.base_id && span.source < self.base_id + self.source_id_strings.len()
573 {
574 let source_id_str = self.source_id_strings[span.source - self.base_id].as_str();
575
576 let (_, content, content_char_indices) = self.content.get(source_id_str).unwrap();
577 let content = content.as_str();
578
579 let byte_span = trimmed_span(content, span);
580 let rune_span = byte_to_rune(content_char_indices, byte_span);
581
582 (content, source_id_str, byte_span, rune_span)
583 } else {
584 self.sources.get_adjusted_source(span)
585 }
586 }
587}
588
589pub(crate) fn resolve_included_path(
591 including_path: Option<&PathBuf>,
592 included_path: &Path,
593) -> PathBuf {
594 match including_path.and_then(|p| path_dir(p.as_ref())) {
595 Some(p) => p.join(included_path),
596 None => included_path.to_path_buf(),
597 }
598}
599
600fn path_dir(p: &Path) -> Option<&Path> {
602 p.parent().and_then(|p| {
603 if !AsRef::<OsStr>::as_ref(&p).is_empty() {
604 Some(p)
605 } else {
606 None
607 }
608 })
609}
610
611fn read<P>(file_path: P) -> io::Result<String>
612where
613 P: AsRef<Path>,
614{
615 let mut f = File::open(&file_path)?;
616 let mut file_content = String::new();
617
618 f.read_to_string(&mut file_content)?;
620 Ok(file_content)
621}
622
623fn write_report<'a, W, K, R, F>(
624 w: &mut W,
625 report: &R,
626 get_adjusted_source: &F,
627 sources: Vec<(String, &str)>,
628) -> io::Result<()>
629where
630 W: Write,
631 K: ErrorOrWarningKind,
632 R: Report,
633 F: Fn(Span) -> (&'a str, &'a str, Span, Span),
634{
635 let (src_id, span) =
636 source_id_string_and_adjusted_rune_span(report.span(), get_adjusted_source);
637 let color = K::color();
638 let report_kind = K::report_kind();
639
640 ariadne::Report::build(report_kind, (src_id.clone(), (span.start..span.end)))
641 .with_message(report.message())
642 .with_labels(Some(
643 Label::new((src_id, (span.start..span.end)))
644 .with_message(report.reason())
645 .with_color(color),
646 ))
647 .with_labels(report.contexts().map(|(label, span)| {
648 let (src_id, span) = source_id_string_and_adjusted_rune_span(span, get_adjusted_source);
649 Label::new((src_id, (span.start..span.end)))
650 .with_message(lazy_format!("in this {}", label))
651 .with_color(Color::Yellow)
652 }))
653 .with_labels(report.related().map(|(label, span)| {
654 let (src_id, span) = source_id_string_and_adjusted_rune_span(span, get_adjusted_source);
655 Label::new((src_id, (span.start..span.end)))
656 .with_message(lazy_format!("{}", label))
657 .with_color(Color::Yellow)
658 }))
659 .finish()
660 .write(ariadne::sources(sources), w)
661}
662
663fn resolve_span<'a, F>(
666 span: Span,
667 get_adjusted_source: &F,
668 source_id_is_file_name: bool,
669) -> SpannedSource<'a>
670where
671 F: Fn(Span) -> (&'a str, &'a str, Span, Span),
672{
673 let (source_content, source_id_str, byte_span, rune_span) = get_adjusted_source(span);
674
675 let mut source_chars = source_content.chars();
676 let start_line = source_chars
677 .by_ref()
678 .take(rune_span.start)
679 .filter(|c| *c == '\n')
680 .count()
681 + 1;
682 let lines_spanned = source_chars
683 .by_ref()
684 .take(rune_span.end - rune_span.start)
685 .filter(|c| *c == '\n')
686 .count();
687 let end_line = start_line + lines_spanned;
688
689 SpannedSource {
690 file_name: source_id_is_file_name.then_some(source_id_str),
691 start_line,
692 end_line,
693 content: source_content
694 .get(byte_span.start..byte_span.end)
695 .unwrap_or(""),
696 }
697}
698
699fn source_id_string_and_adjusted_rune_span<'a, F>(
700 span: Span,
701 get_adjusted_source: &F,
702) -> (String, Span)
703where
704 F: Fn(Span) -> (&'a str, &'a str, Span, Span),
705{
706 let (_, source_id, _byte_span, rune_span) = get_adjusted_source(span);
707 (source_id.to_string(), rune_span)
708}
709
710fn trimmed_span(source: &str, span: Span) -> Span {
711 let mut trimmed = span;
712
713 if source.get(span.start..span.end).is_none() {
715 trimmed.start = 0;
716 trimmed.end = 0;
717 } else {
718 trimmed.end = trim_trailing_whitespace(source, span.start, span.end);
719 }
720 trimmed
721}
722
723fn byte_to_rune(char_indices: &[usize], byte_span: Span) -> Span {
724 let mut rune_span = byte_span;
725 rune_span.start = char_indices.partition_point(|&i| i < byte_span.start);
726 rune_span.end = char_indices.partition_point(|&i| i < byte_span.end);
727 rune_span
728}