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/// Error returned when parsing fails.
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct ParseError {
16    /// Human-readable description of the error.
17    pub message: String,
18    /// Optional byte offset in the input where the error occurred.
19    pub offset: Option<usize>,
20    /// Optional line number (1-based).
21    pub line: Option<u32>,
22    /// Optional column (1-based).
23    pub column: Option<usize>,
24    /// Optional length of the error span in bytes (for LSP range end).
25    pub length: Option<usize>,
26    /// Severity (defaults to Error when not set).
27    pub severity: Option<DiagnosticSeverity>,
28    /// Optional code for quick fixes or documentation (e.g. "expected_keyword").
29    pub code: Option<String>,
30    /// What was expected at this position (e.g. "';' or '}'", "'package' or 'namespace'").
31    pub expected: Option<String>,
32    /// Snippet of what was found at the error position (for display).
33    pub found: Option<String>,
34    /// Short hint on how to fix the error.
35    pub suggestion: Option<String>,
36}
37
38impl ParseError {
39    pub fn new(message: impl Into<String>) -> Self {
40        Self {
41            message: message.into(),
42            offset: None,
43            line: None,
44            column: None,
45            length: None,
46            severity: None,
47            code: None,
48            expected: None,
49            found: None,
50            suggestion: None,
51        }
52    }
53
54    pub fn with_offset(mut self, offset: usize) -> Self {
55        self.offset = Some(offset);
56        self
57    }
58
59    /// Set offset, line, and column for error location.
60    pub fn with_location(mut self, offset: usize, line: u32, column: usize) -> Self {
61        self.offset = Some(offset);
62        self.line = Some(line);
63        self.column = Some(column);
64        self
65    }
66
67    pub fn with_length(mut self, length: usize) -> Self {
68        self.length = Some(length);
69        self
70    }
71
72    pub fn with_severity(mut self, severity: DiagnosticSeverity) -> Self {
73        self.severity = Some(severity);
74        self
75    }
76
77    pub fn with_code(mut self, code: impl Into<String>) -> Self {
78        self.code = Some(code.into());
79        self
80    }
81
82    pub fn with_expected(mut self, expected: impl Into<String>) -> Self {
83        self.expected = Some(expected.into());
84        self
85    }
86
87    pub fn with_found(mut self, found: impl Into<String>) -> Self {
88        self.found = Some(found.into());
89        self
90    }
91
92    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
93        self.suggestion = Some(suggestion.into());
94        self
95    }
96
97    /// LSP uses 0-based line and 0-based character. Returns (start_line, start_character, end_line, end_character).
98    /// Returns `None` if position is unknown.
99    pub fn to_lsp_range(&self) -> Option<(u32, u32, u32, u32)> {
100        let (line, column) = (self.line?, self.column?);
101        let len = self.length.unwrap_or(1);
102        let start_line = line.saturating_sub(1);
103        let start_char = column.saturating_sub(1);
104        let end_line = start_line;
105        let end_char = start_char.saturating_add(len);
106        Some((start_line, start_char as u32, end_line, end_char as u32))
107    }
108}
109
110impl std::fmt::Display for ParseError {
111    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
112        let base = self
113            .expected
114            .as_deref()
115            .map(|e| format!("expected {e}"))
116            .unwrap_or_else(|| self.message.clone());
117        let mut msg = base;
118        if let Some(ref found) = self.found {
119            if !msg.contains("(found ") {
120                msg.push_str(&format!(" (found '{found}')"));
121            }
122        }
123        if let Some(ref suggestion) = self.suggestion {
124            msg.push_str(&format!(" {suggestion}"));
125        }
126        match (self.offset, self.line, self.column) {
127            (Some(_), Some(line), Some(col)) => write!(f, "{msg} at line {line}, column {col}"),
128            (Some(off), _, _) => write!(f, "{msg} at offset {off}"),
129            _ => write!(f, "{msg}"),
130        }
131    }
132}
133
134impl std::error::Error for ParseError {}