Skip to main content

sysml_v2_parser/
error.rs

1//! Parse error types for SysML v2 parser.
2//!
3//! All line and column values are **1-based**. Use [`ParseError::to_lsp_range`] for
4//! 0-based (line, character) ranges as used by the Language Server Protocol.
5
6/// Severity of a parse diagnostic (for language server integration).
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum DiagnosticSeverity {
9    Error,
10    Warning,
11}
12
13/// High-level diagnostic taxonomy for parser/evaluator reporting.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum DiagnosticCategory {
16    ParseError,
17    UnsupportedGrammarForm,
18    UnresolvedSymbol,
19}
20
21/// Error returned when parsing fails.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct ParseError {
24    /// Human-readable description of the error.
25    pub message: String,
26    /// Optional byte offset in the input where the error occurred.
27    pub offset: Option<usize>,
28    /// Optional line number (1-based).
29    pub line: Option<u32>,
30    /// Optional column (1-based).
31    pub column: Option<usize>,
32    /// Optional length of the error span in bytes (for LSP range end).
33    pub length: Option<usize>,
34    /// Severity (defaults to Error when not set).
35    pub severity: Option<DiagnosticSeverity>,
36    /// Optional code for quick fixes or documentation (e.g. "expected_keyword").
37    pub code: Option<String>,
38    /// What was expected at this position (e.g. "';' or '}'", "'package' or 'namespace'").
39    pub expected: Option<String>,
40    /// Snippet of what was found at the error position (for display).
41    pub found: Option<String>,
42    /// Short hint on how to fix the error.
43    pub suggestion: Option<String>,
44    /// High-level diagnostic category used by clients to classify failures.
45    pub category: Option<DiagnosticCategory>,
46    /// When true, this diagnostic is likely a consequence of an earlier error in the same body.
47    pub is_cascade: Option<bool>,
48}
49
50impl ParseError {
51    pub fn new(message: impl Into<String>) -> Self {
52        Self {
53            message: message.into(),
54            offset: None,
55            line: None,
56            column: None,
57            length: None,
58            severity: None,
59            code: None,
60            expected: None,
61            found: None,
62            suggestion: None,
63            category: None,
64            is_cascade: None,
65        }
66    }
67
68    pub fn with_offset(mut self, offset: usize) -> Self {
69        self.offset = Some(offset);
70        self
71    }
72
73    /// Set offset, line, and column for error location.
74    pub fn with_location(mut self, offset: usize, line: u32, column: usize) -> Self {
75        self.offset = Some(offset);
76        self.line = Some(line);
77        self.column = Some(column);
78        self
79    }
80
81    pub fn with_length(mut self, length: usize) -> Self {
82        self.length = Some(length);
83        self
84    }
85
86    pub fn with_severity(mut self, severity: DiagnosticSeverity) -> Self {
87        self.severity = Some(severity);
88        self
89    }
90
91    pub fn with_code(mut self, code: impl Into<String>) -> Self {
92        self.code = Some(code.into());
93        self
94    }
95
96    pub fn with_expected(mut self, expected: impl Into<String>) -> Self {
97        self.expected = Some(expected.into());
98        self
99    }
100
101    pub fn with_found(mut self, found: impl Into<String>) -> Self {
102        self.found = Some(found.into());
103        self
104    }
105
106    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
107        self.suggestion = Some(suggestion.into());
108        self
109    }
110
111    pub fn with_category(mut self, category: DiagnosticCategory) -> Self {
112        self.category = Some(category);
113        self
114    }
115
116    pub fn with_is_cascade(mut self, is_cascade: bool) -> Self {
117        self.is_cascade = Some(is_cascade);
118        self
119    }
120
121    /// LSP uses 0-based line and 0-based character. Returns (start_line, start_character, end_line, end_character).
122    /// Returns `None` if position is unknown.
123    pub fn to_lsp_range(&self) -> Option<(u32, u32, u32, u32)> {
124        let (line, column) = (self.line?, self.column?);
125        let len = self.length.unwrap_or(1);
126        let start_line = line.saturating_sub(1);
127        let start_char = column.saturating_sub(1);
128        let end_line = start_line;
129        let end_char = start_char.saturating_add(len);
130        Some((start_line, start_char as u32, end_line, end_char as u32))
131    }
132}
133
134impl std::fmt::Display for ParseError {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        let base = self
137            .expected
138            .as_deref()
139            .map(|e| format!("expected {e}"))
140            .unwrap_or_else(|| self.message.clone());
141        let mut msg = base;
142        if let Some(ref found) = self.found {
143            if !msg.contains("(found ") {
144                msg.push_str(&format!(" (found '{found}')"));
145            }
146        }
147        if let Some(ref suggestion) = self.suggestion {
148            msg.push_str(&format!(" {suggestion}"));
149        }
150        match (self.offset, self.line, self.column) {
151            (Some(_), Some(line), Some(col)) => write!(f, "{msg} at line {line}, column {col}"),
152            (Some(off), _, _) => write!(f, "{msg} at offset {off}"),
153            _ => write!(f, "{msg}"),
154        }
155    }
156}
157
158impl std::error::Error for ParseError {}