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}
47
48impl ParseError {
49    pub fn new(message: impl Into<String>) -> Self {
50        Self {
51            message: message.into(),
52            offset: None,
53            line: None,
54            column: None,
55            length: None,
56            severity: None,
57            code: None,
58            expected: None,
59            found: None,
60            suggestion: None,
61            category: None,
62        }
63    }
64
65    pub fn with_offset(mut self, offset: usize) -> Self {
66        self.offset = Some(offset);
67        self
68    }
69
70    /// Set offset, line, and column for error location.
71    pub fn with_location(mut self, offset: usize, line: u32, column: usize) -> Self {
72        self.offset = Some(offset);
73        self.line = Some(line);
74        self.column = Some(column);
75        self
76    }
77
78    pub fn with_length(mut self, length: usize) -> Self {
79        self.length = Some(length);
80        self
81    }
82
83    pub fn with_severity(mut self, severity: DiagnosticSeverity) -> Self {
84        self.severity = Some(severity);
85        self
86    }
87
88    pub fn with_code(mut self, code: impl Into<String>) -> Self {
89        self.code = Some(code.into());
90        self
91    }
92
93    pub fn with_expected(mut self, expected: impl Into<String>) -> Self {
94        self.expected = Some(expected.into());
95        self
96    }
97
98    pub fn with_found(mut self, found: impl Into<String>) -> Self {
99        self.found = Some(found.into());
100        self
101    }
102
103    pub fn with_suggestion(mut self, suggestion: impl Into<String>) -> Self {
104        self.suggestion = Some(suggestion.into());
105        self
106    }
107
108    pub fn with_category(mut self, category: DiagnosticCategory) -> Self {
109        self.category = Some(category);
110        self
111    }
112
113    /// LSP uses 0-based line and 0-based character. Returns (start_line, start_character, end_line, end_character).
114    /// Returns `None` if position is unknown.
115    pub fn to_lsp_range(&self) -> Option<(u32, u32, u32, u32)> {
116        let (line, column) = (self.line?, self.column?);
117        let len = self.length.unwrap_or(1);
118        let start_line = line.saturating_sub(1);
119        let start_char = column.saturating_sub(1);
120        let end_line = start_line;
121        let end_char = start_char.saturating_add(len);
122        Some((start_line, start_char as u32, end_line, end_char as u32))
123    }
124}
125
126impl std::fmt::Display for ParseError {
127    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128        let base = self
129            .expected
130            .as_deref()
131            .map(|e| format!("expected {e}"))
132            .unwrap_or_else(|| self.message.clone());
133        let mut msg = base;
134        if let Some(ref found) = self.found {
135            if !msg.contains("(found ") {
136                msg.push_str(&format!(" (found '{found}')"));
137            }
138        }
139        if let Some(ref suggestion) = self.suggestion {
140            msg.push_str(&format!(" {suggestion}"));
141        }
142        match (self.offset, self.line, self.column) {
143            (Some(_), Some(line), Some(col)) => write!(f, "{msg} at line {line}, column {col}"),
144            (Some(off), _, _) => write!(f, "{msg} at offset {off}"),
145            _ => write!(f, "{msg}"),
146        }
147    }
148}
149
150impl std::error::Error for ParseError {}