bms_rs/
diagnostics.rs

1//! Fancy diagnostics support using `ariadne`.
2//!
3//! This module provides convenient methods to convert errors carrying `SourcePosMixin`
4//! (such as `LexWarningWithRange`, `ParseWarningWithRange`, `AstBuildWarningWithRange`,
5//! `AstParseWarningWithRange`, and the aggregated `BmsWarning`) to `ariadne::Report`
6//! without modifying existing error type definitions.
7//!
8//! Since `SourcePosMixin` contains index span information (start/end byte offsets), this module
9//! lets ariadne automatically handle row/column calculations for display purposes.
10//!
11//! # Usage Example
12//!
13//! ```rust
14//! use bms_rs::{
15//!     bms::{BmsWarning, default_config, command::channel::mapper::KeyLayoutBeat, parse_bms},
16//!     diagnostics::emit_bms_warnings,
17//! };
18//!
19//! // Parse BMS file
20//! let bms_source = "#TITLE Test\n#ARTIST Composer\n#INVALID command\n";
21//! let output = parse_bms(bms_source, default_config()).expect("must be parsed");
22//!
23//! // Output all warnings
24//! emit_bms_warnings("test.bms", bms_source, &output.warnings);
25//! ```
26
27use ariadne::{Color, Label, Report, ReportKind, Source};
28
29/// Simple source container that holds the filename and source text.
30/// Ariadne will automatically handle row/column calculations from byte offsets.
31///
32/// # Usage Example
33///
34/// ```rust
35/// use bms_rs::diagnostics::SimpleSource;
36///
37/// // Create source container
38/// let source_text = "#TITLE test\n#ARTIST composer\n";
39/// let source = SimpleSource::new("test.bms", source_text);
40///
41/// // Get source text
42/// assert_eq!(source.text(), source_text);
43/// ```
44pub struct SimpleSource<'a> {
45    /// Name of the source file.
46    name: &'a str,
47    /// Source text content.
48    text: &'a str,
49}
50
51impl<'a> SimpleSource<'a> {
52    /// Create a new source container instance.
53    ///
54    /// # Parameters
55    /// * `name` - Name of the source file
56    /// * `text` - Complete text content of the source file
57    #[must_use]
58    pub const fn new(name: &'a str, text: &'a str) -> Self {
59        Self { name, text }
60    }
61
62    /// Get source text content.
63    ///
64    /// # Returns
65    /// Returns the complete text content of the source file
66    #[must_use]
67    pub const fn text(&self) -> &'a str {
68        self.text
69    }
70
71    /// Get source file name.
72    ///
73    /// # Returns
74    /// Returns the name of the source file
75    #[must_use]
76    pub const fn name(&self) -> &'a str {
77        self.name
78    }
79}
80
81/// Trait for converting positioned errors to `ariadne::Report`.
82///
83/// # Usage Example
84///
85/// ```rust
86/// use bms_rs::{diagnostics::{SimpleSource, ToAriadne, emit_bms_warnings}, bms::BmsWarning};
87/// use ariadne::Source;
88///
89/// // Assume there are warnings generated during BMS parsing
90/// let warnings: Vec<BmsWarning> = vec![/* warnings obtained from parsing */];
91/// let source_text = "#TITLE test\n#ARTIST composer\n";
92///
93/// // Simpler way: use convenience function
94/// emit_bms_warnings("test.bms", source_text, &warnings);
95///
96/// // Or handle each warning manually:
97/// let source = SimpleSource::new("test.bms", source_text);
98/// let ariadne_source = Source::from(source_text);
99///
100/// for warning in &warnings {
101///     let report = warning.to_report(&source);
102///     // Use ariadne to render the report - ariadne will automatically handle row/column calculation
103///     let _ = report.print(("test.bms".to_string(), ariadne_source.clone()));
104/// }
105/// ```
106pub trait ToAriadne {
107    /// Convert error to ariadne Report.
108    ///
109    /// # Parameters
110    /// * `src` - Source file container (used for filename, ariadne handles row/column calculation)
111    ///
112    /// # Returns
113    /// Returns the constructed ariadne Report
114    fn to_report<'a>(&self, src: &SimpleSource<'a>)
115    -> Report<'a, (String, std::ops::Range<usize>)>;
116}
117
118/// Helper to build a styled ariadne `Report` consistently.
119///
120/// This reduces duplication across multiple `ToAriadne` implementations.
121#[must_use]
122pub fn build_report<'a>(
123    src: &SimpleSource<'a>,
124    kind: ReportKind<'a>,
125    range: std::ops::Range<usize>,
126    title: &str,
127    label_message: impl ToString,
128    color: Color,
129) -> Report<'a, (String, std::ops::Range<usize>)> {
130    let filename = src.name().to_string();
131    Report::build(kind, (filename.clone(), range.clone()))
132        .with_message(title)
133        .with_label(
134            Label::new((filename, range))
135                .with_message(label_message.to_string())
136                .with_color(color),
137        )
138        .finish()
139}
140
141/// Convenience method: batch render `BmsWarning` list.
142///
143/// This function automatically creates `SimpleSource` and generates beautiful diagnostic output for each warning.
144/// Ariadne will automatically handle row/column calculations from the provided byte ranges.
145///
146/// # Usage Example
147///
148/// ```rust
149/// use bms_rs::{diagnostics::emit_bms_warnings, bms::BmsWarning};
150///
151/// // BMS source text
152/// let bms_source = "#TITLE My Song\n#ARTIST Composer\n#BPM 120\n";
153///
154/// // Assume warning list obtained from parsing
155/// let warnings: Vec<BmsWarning> = vec![/* parsing warnings */];
156///
157/// // Batch output all warnings - ariadne will automatically calculate row/column positions
158/// emit_bms_warnings("my_song.bms", bms_source, &warnings);
159/// ```
160///
161/// # Parameters
162/// * `name` - Name of the source file, used for display in diagnostic information
163/// * `source` - Complete BMS source text
164/// * `warnings` - List of warnings to display
165pub fn emit_bms_warnings<'a>(
166    name: &'a str,
167    source: &'a str,
168    warnings: impl IntoIterator<Item = &'a crate::bms::BmsWarning>,
169) {
170    let simple = SimpleSource::new(name, source);
171    let ariadne_source = Source::from(source);
172    for w in warnings {
173        let report = w.to_report(&simple);
174        let _ = report.print((name.to_string(), ariadne_source.clone()));
175    }
176}