rscad_openscad/eval/
diag.rs1use openscad_parser::prelude::{Ast, Span};
9
10#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd)]
12pub enum Severity {
13 Info,
14 Warning,
15 Error,
16}
17
18impl core::fmt::Display for Severity {
19 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
20 match self {
21 Severity::Info => f.write_str("info"),
22 Severity::Warning => f.write_str("warning"),
23 Severity::Error => f.write_str("error"),
24 }
25 }
26}
27
28#[derive(Clone, Debug, PartialEq)]
30pub struct ScadDiagnostic {
31 pub severity: Severity,
32 pub message: String,
33 pub location: Option<String>,
35}
36
37impl core::fmt::Display for ScadDiagnostic {
38 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
39 match &self.location {
40 Some(loc) => write!(f, "{}: {} ({})", self.severity, self.message, loc),
41 None => write!(f, "{}: {}", self.severity, self.message),
42 }
43 }
44}
45
46#[derive(Clone, Debug)]
48pub struct Fatal {
49 pub message: String,
50 pub span: Option<Span>,
51}
52
53impl Fatal {
54 pub fn new(message: impl Into<String>, span: Option<Span>) -> Self {
55 Fatal {
56 message: message.into(),
57 span,
58 }
59 }
60}
61
62pub type EvalResult<T> = core::result::Result<T, Fatal>;
64
65type WarnKey = (String, Option<(usize, usize, usize)>);
67
68#[derive(Debug, Default)]
70pub(crate) struct DiagSink {
71 raw: Vec<(Severity, String, Option<Span>)>,
72 seen: std::collections::HashSet<WarnKey>,
73}
74
75impl DiagSink {
76 pub fn info(&mut self, message: impl Into<String>, span: Option<Span>) {
77 let message = message.into();
78 tracing::info!(target: "scad", "{message}");
79 self.raw.push((Severity::Info, message, span));
80 }
81
82 pub fn warn(&mut self, message: impl Into<String>, span: Option<Span>) {
83 let message = message.into();
84 tracing::warn!(target: "scad", "{message}");
85 self.raw.push((Severity::Warning, message, span));
86 }
87
88 pub fn warn_once(&mut self, message: impl Into<String>, span: Option<Span>) {
91 let message = message.into();
92 let key = (message.clone(), span.map(|s| (s.start, s.end, s.context)));
93 if self.seen.insert(key) {
94 self.warn(message, span);
95 }
96 }
97
98 pub fn resolve(self, ast: &Ast) -> Vec<ScadDiagnostic> {
100 self.raw
101 .into_iter()
102 .map(|(severity, message, span)| ScadDiagnostic {
103 severity,
104 message,
105 location: span.and_then(|s| locate(ast, s)),
106 })
107 .collect()
108 }
109}
110
111pub(crate) fn locate(ast: &Ast, span: Span) -> Option<String> {
113 let path = ast.path_of(span.context)?;
114 let source = ast.unparsed.get(path)?.text();
115 let prefix = source.get(..span.start.min(source.len()))?;
116 let line = prefix.matches('\n').count() + 1;
117 let col = prefix
118 .rsplit_once('\n')
119 .map_or(prefix.chars().count(), |(_, tail)| tail.chars().count())
120 + 1;
121 Some(format!("{}:{line}:{col}", path.display()))
122}
123
124pub(crate) fn render_fatal(ast: &Ast, fatal: &Fatal) -> String {
126 match fatal.span.and_then(|s| locate(ast, s)) {
127 Some(loc) => format!("{} ({loc})", fatal.message),
128 None => fatal.message.clone(),
129 }
130}