microcad_lang/diag/
diagnostic.rs

1// Copyright © 2024-2025 The µcad authors <info@ucad.xyz>
2// SPDX-License-Identifier: AGPL-3.0-or-later
3
4use crate::{diag::*, resolve::*, src_ref::*};
5
6/// Diagnostic message with source code reference attached.
7pub enum Diagnostic {
8    /// Trace message.
9    Trace(Refer<String>),
10    /// Informative message.
11    Info(Refer<String>),
12    /// Warning.
13    Warning(Refer<Box<dyn std::error::Error>>),
14    /// Error.
15    Error(Refer<Box<dyn std::error::Error>>),
16}
17
18impl Diagnostic {
19    /// Get diagnostic level.
20    pub fn level(&self) -> Level {
21        match self {
22            Diagnostic::Trace(_) => Level::Trace,
23            Diagnostic::Info(_) => Level::Info,
24            Diagnostic::Warning(_) => Level::Warning,
25            Diagnostic::Error(_) => Level::Error,
26        }
27    }
28
29    /// Get message (errors will be serialized).
30    pub fn message(&self) -> String {
31        match self {
32            Diagnostic::Trace(msg) | Diagnostic::Info(msg) => msg.to_string(),
33            Diagnostic::Warning(err) | Diagnostic::Error(err) => err.to_string(),
34        }
35    }
36
37    /// Return line of the error
38    pub fn line(&self) -> Option<usize> {
39        let src_ref = match self {
40            Diagnostic::Trace(r) => r.src_ref(),
41            Diagnostic::Info(r) => r.src_ref(),
42            Diagnostic::Warning(r) => r.src_ref(),
43            Diagnostic::Error(r) => r.src_ref(),
44        };
45        src_ref.as_ref().map(|r| r.at.line)
46    }
47
48    /// Pretty print the diagnostic.
49    ///
50    /// This will print the diagnostic to the given writer, including the source code reference.
51    ///
52    /// # Arguments
53    ///
54    /// * `w` - The writer to write to.
55    /// * `source_file_by_hash` - Hash provider to get the source file by hash.
56    ///
57    /// This will print:
58    ///
59    /// ```text
60    /// error: This is an error
61    ///   ---> filename:1:8
62    ///     |
63    ///  1  | part Circle(radius: length) {}
64    ///     |        ^^^^^^
65    /// ```
66    pub fn pretty_print(
67        &self,
68        f: &mut dyn std::fmt::Write,
69        source_by_hash: &impl GetSourceByHash,
70        line_offset: usize,
71    ) -> std::fmt::Result {
72        let src_ref = self.src_ref();
73
74        let source_file = source_by_hash.get_by_hash(src_ref.source_hash());
75
76        fn make_relative(path: &std::path::Path) -> String {
77            let current_dir = std::env::current_dir().expect("current dir");
78            if let Ok(path) = path.canonicalize() {
79                pathdiff::diff_paths(path, current_dir)
80                    .expect("related paths:\n  {path:?}\n  {current_dir:?}")
81            } else {
82                path.to_path_buf()
83            }
84            .to_string_lossy()
85            .to_string()
86        }
87        match &src_ref {
88            SrcRef(None) => writeln!(f, "{}: {}", self.level(), self.message())?,
89            SrcRef(Some(src_ref)) => {
90                writeln!(f, "{}: {}", self.level(), self.message())?;
91                writeln!(
92                    f,
93                    "  ---> {}:{}",
94                    source_file
95                        .as_ref()
96                        .map(|sf| make_relative(&sf.filename()))
97                        .unwrap_or(crate::invalid_no_ansi!(FILE).to_string()),
98                    src_ref.with_line_offset(line_offset).at
99                )?;
100                writeln!(f, "     |",)?;
101
102                let line = source_file
103                    .as_ref()
104                    .map(|sf| {
105                        sf.get_line(src_ref.at.line - 1)
106                            .unwrap_or(crate::invalid!(LINE))
107                    })
108                    .unwrap_or(crate::invalid_no_ansi!(FILE));
109
110                writeln!(
111                    f,
112                    "{: >4} | {}",
113                    src_ref.with_line_offset(line_offset).at.line,
114                    line
115                )?;
116                writeln!(
117                    f,
118                    "{: >4} | {}",
119                    "",
120                    " ".repeat(src_ref.at.col - 1)
121                        + &"^".repeat(src_ref.range.len().min(line.len())),
122                )?;
123                writeln!(f, "     |",)?;
124            }
125        }
126
127        Ok(())
128    }
129}
130
131impl SrcReferrer for Diagnostic {
132    fn src_ref(&self) -> SrcRef {
133        match self {
134            Diagnostic::Trace(message) => message.src_ref(),
135            Diagnostic::Info(message) => message.src_ref(),
136            Diagnostic::Warning(error) => error.src_ref(),
137            Diagnostic::Error(error) => error.src_ref(),
138        }
139    }
140}
141
142impl std::fmt::Display for Diagnostic {
143    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
144        match self {
145            Diagnostic::Trace(message) => write!(f, "trace: {}: {message}", self.src_ref()),
146            Diagnostic::Info(message) => write!(f, "info: {}: {message}", self.src_ref()),
147            Diagnostic::Warning(error) => write!(f, "warning: {}: {error}", self.src_ref()),
148            Diagnostic::Error(error) => write!(f, "error: {}: {error}", self.src_ref()),
149        }
150    }
151}
152
153impl std::fmt::Debug for Diagnostic {
154    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
155        match self {
156            Diagnostic::Trace(message) => write!(f, "trace: {}: {message}", self.src_ref()),
157            Diagnostic::Info(message) => write!(f, "info: {}: {message}", self.src_ref()),
158            Diagnostic::Warning(error) => write!(f, "warning: {}: {error:?}", self.src_ref()),
159            Diagnostic::Error(error) => write!(f, "error: {}: {error:?}", self.src_ref()),
160        }
161    }
162}