pomsky_bin/result/
mod.rs

1use std::{fmt, path::Path};
2
3use helptext::text;
4use pomsky::diagnose::{DiagnosticCode, DiagnosticKind};
5use serde::{Deserialize, Serialize};
6
7use crate::format::Logger;
8
9mod serde_code;
10
11#[derive(Debug, PartialEq, Serialize, Deserialize)]
12pub struct CompilationResult {
13    /// Schema version
14    pub version: Version,
15    /// Whether compilation succeeded
16    ///
17    /// Equivalent to `result.output.is_some()`
18    pub success: bool,
19    /// File that was compiled
20    pub path: Option<String>,
21    /// Compilation result
22    #[serde(skip_serializing_if = "Option::is_none")]
23    pub output: Option<String>,
24    /// Array of errors and warnings
25    pub diagnostics: Vec<Diagnostic>,
26    /// Compilation time
27    pub timings: Timings,
28}
29
30#[derive(Debug, PartialEq, Serialize, Deserialize)]
31pub enum Version {
32    #[serde(rename = "1")]
33    V1,
34}
35
36impl CompilationResult {
37    #[allow(clippy::too_many_arguments)]
38    pub(crate) fn success(
39        path: Option<&Path>,
40        output: String,
41        time_all_micros: u128,
42        time_test_micros: u128,
43        diagnostics: impl IntoIterator<Item = pomsky::diagnose::Diagnostic>,
44        source_code: &str,
45        warnings: &crate::args::DiagnosticSet,
46        json: bool,
47    ) -> Self {
48        Self {
49            path: path
50                .map(|p| p.canonicalize().as_deref().unwrap_or(p).to_string_lossy().to_string()),
51            version: Version::V1,
52            success: true,
53            output: Some(output),
54            diagnostics: Self::convert_diagnostics(diagnostics, source_code, warnings, json),
55            timings: Timings::from_micros(time_all_micros, time_test_micros),
56        }
57    }
58
59    pub(crate) fn error(
60        path: Option<&Path>,
61        time_all_micros: u128,
62        time_test_micros: u128,
63        diagnostics: impl IntoIterator<Item = pomsky::diagnose::Diagnostic>,
64        source_code: &str,
65        warnings: &crate::args::DiagnosticSet,
66        json: bool,
67    ) -> Self {
68        Self {
69            path: path
70                .map(|p| p.canonicalize().as_deref().unwrap_or(p).to_string_lossy().to_string()),
71            version: Version::V1,
72            success: false,
73            output: None,
74            diagnostics: Self::convert_diagnostics(diagnostics, source_code, warnings, json),
75            timings: Timings::from_micros(time_all_micros, time_test_micros),
76        }
77    }
78
79    fn convert_diagnostics(
80        diagnostics: impl IntoIterator<Item = pomsky::diagnose::Diagnostic>,
81        source_code: &str,
82        warnings: &crate::args::DiagnosticSet,
83        json: bool,
84    ) -> Vec<Diagnostic> {
85        let source_code = Some(source_code);
86        diagnostics
87            .into_iter()
88            .filter_map(|d| match d.severity {
89                pomsky::diagnose::Severity::Warning if !warnings.is_enabled(d.kind) => None,
90                _ => Some(Diagnostic::from(d, source_code, json)),
91            })
92            .collect()
93    }
94
95    pub(crate) fn output(
96        self,
97        logger: &Logger,
98        json: bool,
99        new_line: bool,
100        in_test_suite: bool,
101        source_code: &str,
102    ) {
103        let success = self.success;
104        if json {
105            match serde_json::to_string(&self) {
106                Ok(string) => println!("{string}"),
107                Err(e) => eprintln!("{e}"),
108            }
109        } else {
110            if in_test_suite {
111                logger.basic().fmtln(if success { text![G!"ok"] } else { text![R!"failed"] });
112            }
113            self.output_human_readable(logger, new_line, in_test_suite, Some(source_code));
114        }
115        if !success && !in_test_suite {
116            std::process::exit(1);
117        }
118    }
119
120    fn output_human_readable(
121        mut self,
122        logger: &Logger,
123        new_line: bool,
124        in_test_suite: bool,
125        source_code: Option<&str>,
126    ) {
127        if self.output.is_none() {
128            self.diagnostics.retain(|d| d.severity == Severity::Error);
129        }
130        let initial_len = self.diagnostics.len();
131
132        if self.diagnostics.len() > 8 {
133            self.diagnostics.drain(8..);
134        }
135
136        for diag in &self.diagnostics {
137            diag.print_human_readable(logger, source_code);
138        }
139
140        if !self.diagnostics.is_empty() && initial_len > self.diagnostics.len() {
141            let omitted = initial_len - self.diagnostics.len();
142            logger.note().println(format_args!("{omitted} diagnostic(s) were omitted"));
143        }
144
145        if let Some(compiled) = &self.output {
146            if in_test_suite {
147                // do nothing
148            } else if new_line {
149                println!("{compiled}");
150            } else {
151                use std::io::Write;
152
153                print!("{compiled}");
154                std::io::stdout().flush().unwrap();
155            }
156        }
157    }
158}
159
160#[derive(Debug, PartialEq, Serialize, Deserialize)]
161pub struct Diagnostic {
162    /// "error" | "warning"
163    pub severity: Severity,
164    /// See [`DiagnosticKind`](pomsky::diagnose::DiagnosticKind)
165    ///
166    /// Currently "syntax" | "resolve" | "compat" | "unsupported" | "deprecated"
167    /// | "limits" | "other"
168    pub kind: Kind,
169    /// See [`DiagnosticCode`](pomsky::diagnose::DiagnosticCode)
170    #[serde(with = "serde_code", skip_serializing_if = "Option::is_none")]
171    pub code: Option<DiagnosticCode>,
172    /// List of locations that should be underlined
173    ///
174    /// Currently guaranteed to contain exactly 1 span
175    pub spans: Vec<Span>,
176    /// Error/warning message
177    pub description: String,
178    /// Help text
179    ///
180    /// Currently guaranteed to contain at most 1 string
181    pub help: Vec<String>,
182    /// Automatically applicable fixes
183    ///
184    /// Currently unused and guaranteed to be empty
185    pub fixes: Vec<QuickFix>,
186    /// Visual representation of the diagnostic as displayed in the CLI
187    pub visual: String,
188}
189
190#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Copy)]
191#[serde(rename_all = "lowercase")]
192pub enum Severity {
193    Error,
194    Warning,
195}
196
197impl From<pomsky::diagnose::Severity> for Severity {
198    fn from(value: pomsky::diagnose::Severity) -> Self {
199        match value {
200            pomsky::diagnose::Severity::Error => Severity::Error,
201            pomsky::diagnose::Severity::Warning => Severity::Warning,
202        }
203    }
204}
205
206#[derive(Debug, PartialEq, Serialize, Deserialize)]
207#[serde(rename_all = "lowercase")]
208pub enum Kind {
209    Syntax,
210    Resolve,
211    Compat,
212    Unsupported,
213    Deprecated,
214    Limits,
215    Invalid,
216    Test,
217    Other,
218}
219
220impl Kind {
221    pub fn as_str(&self) -> &'static str {
222        match self {
223            Kind::Syntax => "syntax",
224            Kind::Resolve => "resolve",
225            Kind::Compat => "compat",
226            Kind::Unsupported => "unsupported",
227            Kind::Deprecated => "deprecated",
228            Kind::Limits => "limits",
229            Kind::Invalid => "invalid",
230            Kind::Test => "test",
231            Kind::Other => "other",
232        }
233    }
234}
235
236impl From<DiagnosticKind> for Kind {
237    fn from(value: DiagnosticKind) -> Self {
238        match value {
239            DiagnosticKind::Syntax => Kind::Syntax,
240            DiagnosticKind::Resolve => Kind::Resolve,
241            DiagnosticKind::Compat => Kind::Compat,
242            DiagnosticKind::Unsupported => Kind::Unsupported,
243            DiagnosticKind::Deprecated => Kind::Deprecated,
244            DiagnosticKind::Limits => Kind::Limits,
245            DiagnosticKind::Invalid => Kind::Invalid,
246            DiagnosticKind::Test => Kind::Test,
247            DiagnosticKind::Other => Kind::Other,
248            _ => panic!("unknown diagnostic kind"),
249        }
250    }
251}
252
253#[derive(Debug, PartialEq, Serialize, Deserialize)]
254pub struct Timings {
255    /// time of all performed compilation steps in microseconds
256    pub all: u128,
257    pub tests: u128,
258}
259
260impl Timings {
261    pub fn from_micros(all: u128, tests: u128) -> Self {
262        Timings { all, tests }
263    }
264}
265
266#[derive(Debug, PartialEq, Serialize, Deserialize)]
267pub struct Span {
268    /// Start byte offset, counting from zero, assuming UTF-8 encoding.
269    ///
270    /// Guaranteed to be non-negative.
271    pub start: usize,
272    /// End byte offset, non-inclusive, counting from zero, assuming UTF-8
273    /// encoding.
274    ///
275    /// Guaranteed to be at least `start`.
276    pub end: usize,
277    /// Additional details only relevant to this specific span
278    ///
279    /// Currently unused, guaranteed to be absent
280    #[serde(skip_serializing_if = "Option::is_none")]
281    pub label: Option<String>,
282}
283
284impl From<std::ops::Range<usize>> for Span {
285    fn from(value: std::ops::Range<usize>) -> Self {
286        Span { start: value.start, end: value.end, label: None }
287    }
288}
289
290#[derive(Debug, PartialEq, Serialize, Deserialize)]
291pub struct QuickFix {
292    /// Short description what this quick fix does
293    pub description: String,
294    /// List of changes to fix this diagnostic.
295    ///
296    /// Guaranteed to be in source order and non-overlapping (e.g. `1-4`,
297    /// `7-12`, `14-15`, `16-16`)
298    pub replacements: Vec<Replacement>,
299}
300
301#[derive(Debug, PartialEq, Serialize, Deserialize)]
302pub struct Replacement {
303    /// Start byte offset, counting from zero, assuming UTF-8 encoding.
304    ///
305    /// Guaranteed to be non-negative.
306    pub start: usize,
307    /// End byte offset, non-inclusive, counting from zero, assuming UTF-8
308    /// encoding
309    ///
310    /// Guaranteed to be at least `start`.
311    pub end: usize,
312    /// Text to replace this part of code with
313    pub insert: String,
314}
315
316impl Diagnostic {
317    pub(crate) fn from(
318        value: pomsky::diagnose::Diagnostic,
319        source_code: Option<&str>,
320        json: bool,
321    ) -> Self {
322        let kind = value.kind.to_string();
323        let severity: &str = value.severity.into();
324
325        let visual = if json {
326            let display = value.display_ascii(source_code);
327            let visual = match value.code {
328                Some(code) => format!("{severity} {code}{kind}:{display}"),
329                None => format!("{severity}{kind}:{display}"),
330            };
331            drop(display);
332            visual
333        } else {
334            // unused
335            String::new()
336        };
337
338        Diagnostic {
339            severity: value.severity.into(),
340            kind: value.kind.into(),
341            code: value.code,
342            spans: value.span.range().into_iter().map(From::from).collect(),
343            description: value.msg,
344            help: value.help.into_iter().collect(),
345            fixes: vec![],
346            visual,
347        }
348    }
349
350    fn print_human_readable(&self, logger: &Logger, source_code: Option<&str>) {
351        let kind = self.kind.as_str();
352        let display = self.miette_display(source_code);
353
354        match self.code {
355            Some(code) => {
356                logger.diagnostic_with_code(self.severity, &code.to_string(), kind).print(display);
357            }
358            None => logger.diagnostic(self.severity, kind).print(display),
359        }
360    }
361
362    fn miette_display<'a>(&'a self, source_code: Option<&'a str>) -> impl std::fmt::Display + 'a {
363        use miette::ReportHandler;
364        use std::fmt;
365
366        #[derive(Debug)]
367        struct MietteDiagnostic<'a> {
368            diagnostic: &'a Diagnostic,
369            source_code: Option<&'a str>,
370        }
371
372        impl fmt::Display for MietteDiagnostic<'_> {
373            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
374                self.diagnostic.description.fmt(f)
375            }
376        }
377
378        impl std::error::Error for MietteDiagnostic<'_> {}
379
380        impl miette::Diagnostic for MietteDiagnostic<'_> {
381            fn help<'a>(&'a self) -> Option<Box<dyn fmt::Display + 'a>> {
382                if self.diagnostic.help.is_empty() {
383                    None
384                } else {
385                    let help = self.diagnostic.help.join("\n");
386                    Some(Box::new(help))
387                }
388            }
389
390            fn source_code(&self) -> Option<&dyn miette::SourceCode> {
391                self.source_code.as_ref().map(|s| s as &dyn miette::SourceCode)
392            }
393
394            fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
395                if let Some(Span { start, end, label }) = self.diagnostic.spans.first() {
396                    let label = label.as_deref().unwrap_or(match self.diagnostic.severity {
397                        Severity::Error => "error occurred here",
398                        Severity::Warning => "warning originated here",
399                    });
400                    Some(Box::new(std::iter::once(miette::LabeledSpan::new(
401                        Some(label.into()),
402                        *start,
403                        end - start,
404                    ))))
405                } else {
406                    None
407                }
408            }
409
410            fn severity(&self) -> Option<miette::Severity> {
411                Some(match self.diagnostic.severity {
412                    Severity::Error => miette::Severity::Error,
413                    Severity::Warning => miette::Severity::Warning,
414                })
415            }
416        }
417
418        struct Handler<'a>(MietteDiagnostic<'a>);
419
420        impl fmt::Display for Handler<'_> {
421            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
422                miette::MietteHandler::default().debug(&self.0, f)
423            }
424        }
425
426        Handler(MietteDiagnostic { diagnostic: self, source_code })
427    }
428}
429
430impl fmt::Display for CompilationResult {
431    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432        write!(f, "{self:?}")
433    }
434}