Skip to main content

rsbinder_aidl/
error.rs

1// Copyright 2022 Jeff Kim <hiking90@gmail.com>
2// SPDX-License-Identifier: Apache-2.0
3
4// Struct fields are used via trait implementation methods generated by miette's
5// #[derive(Diagnostic)], but rustc lint analysis cannot track proc-macro-generated
6// code, so warnings would be raised without this allow.
7#![allow(unused_assignments)]
8
9use miette::{Diagnostic, NamedSource, SourceSpan};
10use thiserror::Error;
11
12/// Top-level error type for the AIDL compiler
13#[derive(Error, Debug, Diagnostic)]
14pub enum AidlError {
15    #[error(transparent)]
16    #[diagnostic(transparent)]
17    Parse(Box<ParseError>),
18
19    #[error(transparent)]
20    #[diagnostic(transparent)]
21    Semantic(Box<SemanticError>),
22
23    #[error(transparent)]
24    #[diagnostic(transparent)]
25    Resolution(Box<ResolutionError>),
26
27    #[error(transparent)]
28    Io(#[from] std::io::Error),
29
30    /// Aggregates multiple errors when processing several AIDL files
31    #[error("{} error(s) occurred during AIDL compilation", errors.len())]
32    #[diagnostic(code(aidl::multiple_errors))]
33    Multiple {
34        #[related]
35        errors: Vec<AidlError>,
36    },
37}
38
39impl From<ParseError> for AidlError {
40    fn from(e: ParseError) -> Self {
41        AidlError::Parse(Box::new(e))
42    }
43}
44
45impl From<SemanticError> for AidlError {
46    fn from(e: SemanticError) -> Self {
47        AidlError::Semantic(Box::new(e))
48    }
49}
50
51impl From<ResolutionError> for AidlError {
52    fn from(e: ResolutionError) -> Self {
53        AidlError::Resolution(Box::new(e))
54    }
55}
56
57impl AidlError {
58    /// Collects errors, flattening any nested Multiple variants.
59    pub fn collect(errors: Vec<AidlError>) -> Option<AidlError> {
60        let flat: Vec<AidlError> = errors
61            .into_iter()
62            .flat_map(|e| match e {
63                AidlError::Multiple { errors } => errors,
64                other => vec![other],
65            })
66            .collect();
67        match flat.len() {
68            0 => None,
69            1 => flat.into_iter().next(),
70            _ => Some(AidlError::Multiple { errors: flat }),
71        }
72    }
73}
74
75/// Parse error (wraps a pest parser error)
76#[allow(unused)]
77#[derive(Error, Debug, Diagnostic)]
78#[error("AIDL syntax error")]
79#[diagnostic(code(aidl::parse_error))]
80pub struct ParseError {
81    #[source_code]
82    pub src: NamedSource<String>,
83    #[label("{message}")]
84    pub span: SourceSpan,
85    pub message: String,
86    #[help]
87    pub help: Option<String>,
88}
89
90/// Semantic errors (type validation, transaction codes, etc.)
91#[allow(unused)]
92#[derive(Error, Debug, Diagnostic)]
93pub enum SemanticError {
94    #[error("Interface '{interface}': transaction code {code} conflict between '{method1}' and '{method2}'")]
95    #[diagnostic(
96        code(aidl::duplicate_transaction_code),
97        help("use unique positive integer values for each method's transaction code")
98    )]
99    DuplicateTransactionCode {
100        interface: String,
101        method1: String,
102        method2: String,
103        code: i64,
104        #[source_code]
105        src: NamedSource<String>,
106        #[label("method '{method1}' uses code {code}")]
107        span: SourceSpan,
108        #[related]
109        related: Vec<DuplicateCodeRelated>,
110    },
111
112    #[error("Interface '{interface}': mixed explicit/implicit transaction IDs")]
113    #[diagnostic(
114        code(aidl::mixed_transaction_ids),
115        help("either all methods must have explicitly assigned transaction IDs or none of them should")
116    )]
117    MixedTransactionIds {
118        interface: String,
119        #[source_code]
120        src: NamedSource<String>,
121        #[label("defined in this interface")]
122        span: SourceSpan,
123    },
124
125    /// Code path unreachable with the current AIDL grammar.
126    /// The pest grammar's INTVALUE rule does not allow a minus sign, so this
127    /// variant is only covered by unit tests that construct MethodDecl directly.
128    #[error("Interface '{interface}': method '{method}' has negative transaction code {code}")]
129    #[diagnostic(
130        code(aidl::negative_transaction_code),
131        help("transaction codes must be non-negative integers within u32 range")
132    )]
133    NegativeTransactionCode {
134        interface: String,
135        method: String,
136        code: i64,
137        #[source_code]
138        src: NamedSource<String>,
139        #[label("negative code here")]
140        span: SourceSpan,
141    },
142
143    #[error("Interface '{interface}': method '{method}' has transaction code {code} exceeding u32 range")]
144    #[diagnostic(
145        code(aidl::transaction_code_overflow),
146        help("transaction codes must fit within u32 (0..=4294967295)")
147    )]
148    TransactionCodeOverflow {
149        interface: String,
150        method: String,
151        code: i64,
152        #[source_code]
153        src: NamedSource<String>,
154        #[label("overflowing code here")]
155        span: SourceSpan,
156    },
157
158    #[error("unsupported type: {type_name}")]
159    #[diagnostic(code(aidl::unsupported_type))]
160    UnsupportedType {
161        type_name: String,
162        #[help]
163        help: Option<String>,
164        #[source_code]
165        src: NamedSource<String>,
166        #[label("this type is not supported")]
167        span: SourceSpan,
168    },
169
170    #[error("invalid operation: {message}")]
171    #[diagnostic(code(aidl::invalid_operation))]
172    InvalidOperation {
173        message: String,
174        #[source_code]
175        src: NamedSource<String>,
176        #[label("here")]
177        span: SourceSpan,
178    },
179}
180
181/// Auxiliary diagnostic for marking the second conflicting location in
182/// DuplicateTransactionCode.
183///
184/// Note: because `{method}` is interpolated inside `#[label]`, the `method`
185/// field must be declared before `span` for the miette derive macro to work
186/// correctly (miette 7.x constraint).
187#[allow(unused)]
188#[derive(Error, Debug, Diagnostic)]
189#[error("conflicting method defined here")]
190pub struct DuplicateCodeRelated {
191    pub method: String,
192    #[source_code]
193    pub src: NamedSource<String>,
194    #[label("method '{method}' also uses this code")]
195    pub span: SourceSpan,
196}
197
198/// Name resolution errors (imports, namespaces)
199#[allow(unused)]
200#[derive(Error, Debug, Diagnostic)]
201pub enum ResolutionError {
202    #[error("import '{import}' not found")]
203    #[diagnostic(
204        code(aidl::import_not_found),
205        help("check that the imported type exists in the include paths")
206    )]
207    ImportNotFound {
208        import: String,
209        #[source_code]
210        src: NamedSource<String>,
211        #[label("imported here")]
212        span: SourceSpan,
213    },
214
215    #[error("unknown type '{name}'")]
216    #[diagnostic(
217        code(aidl::unknown_type),
218        help("verify that the type is defined and imported correctly")
219    )]
220    UnknownType {
221        name: String,
222        #[source_code]
223        src: NamedSource<String>,
224        #[label("referenced here")]
225        span: SourceSpan,
226    },
227}
228
229/// Lightweight error type used to propagate arithmetic errors from const_expr.rs.
230/// Carries only a message without source location; the caller (parser/generator)
231/// attaches source context and converts it into SemanticError::InvalidOperation.
232#[derive(Error, Debug)]
233#[error("{message}")]
234pub struct ConstExprError {
235    pub message: String,
236}
237
238impl ConstExprError {
239    pub fn new(message: impl Into<String>) -> Self {
240        Self {
241            message: message.into(),
242        }
243    }
244}
245
246/// Converts a pest error into a miette ParseError.
247///
248/// Generic over the Rule type so this utility does not depend on the
249/// concrete Rule type defined in parser.rs.
250pub fn pest_error_to_diagnostic<R: pest::RuleType>(
251    err: pest::error::Error<R>,
252    filename: &str,
253    source: &str,
254) -> ParseError {
255    let (offset, length) = match err.location {
256        pest::error::InputLocation::Pos(pos) => {
257            // Guard against exceeding the source range when the error is at EOF
258            let len = if pos >= source.len() { 0 } else { 1 };
259            (pos, len)
260        }
261        pest::error::InputLocation::Span((start, end)) => (start, end - start),
262    };
263
264    let message = match &err.variant {
265        pest::error::ErrorVariant::ParsingError {
266            positives,
267            negatives,
268        } => format_pest_expectations(positives, negatives),
269        pest::error::ErrorVariant::CustomError { message } => message.clone(),
270    };
271
272    ParseError {
273        src: NamedSource::new(filename, source.to_string()),
274        span: SourceSpan::new(offset.into(), length),
275        message,
276        help: None,
277    }
278}
279
280/// Formats expected/unexpected token information from a pest parse error into a human-readable message.
281fn format_pest_expectations<R: std::fmt::Debug>(positives: &[R], negatives: &[R]) -> String {
282    let mut parts = Vec::new();
283
284    if !positives.is_empty() {
285        let pos_str: Vec<String> = positives.iter().map(|r| format!("{r:?}")).collect();
286        parts.push(format!("expected {}", pos_str.join(", ")));
287    }
288
289    if !negatives.is_empty() {
290        let neg_str: Vec<String> = negatives.iter().map(|r| format!("{r:?}")).collect();
291        parts.push(format!("unexpected {}", neg_str.join(", ")));
292    }
293
294    if parts.is_empty() {
295        "syntax error".to_string()
296    } else {
297        parts.join("; ")
298    }
299}
300
301#[cfg(test)]
302mod tests {
303    use super::*;
304
305    fn span(offset: usize, len: usize) -> SourceSpan {
306        SourceSpan::new(offset.into(), len)
307    }
308
309    // 1.1a: ParseError Display trait
310    #[test]
311    fn test_parse_error_display() {
312        let err = ParseError {
313            src: NamedSource::new("test.aidl", "parcelable Foo {}".to_string()),
314            span: span(0, 1),
315            message: "unexpected token".to_string(),
316            help: None,
317        };
318        let display = format!("{err}");
319        assert!(display.contains("AIDL syntax error"), "Got: {display}");
320    }
321
322    // 1.1b: ParseError diagnostic code
323    #[test]
324    fn test_parse_error_diagnostic_code() {
325        use miette::Diagnostic;
326        let err = ParseError {
327            src: NamedSource::new("test.aidl", "parcelable Foo {}".to_string()),
328            span: span(0, 1),
329            message: "test error".to_string(),
330            help: None,
331        };
332        let code = err.code().expect("ParseError must have a diagnostic code");
333        assert_eq!(code.to_string(), "aidl::parse_error");
334    }
335
336    // 1.1c: ParseError source span
337    #[test]
338    fn test_parse_error_source_span() {
339        use miette::Diagnostic;
340        let err = ParseError {
341            src: NamedSource::new("test.aidl", "parcelable Foo {}".to_string()),
342            span: span(10, 3),
343            message: "test error".to_string(),
344            help: None,
345        };
346        let labels: Vec<_> = err.labels().expect("must have labels").collect();
347        assert_eq!(labels.len(), 1);
348        let label_span = labels[0].inner();
349        assert_eq!(label_span.offset(), 10);
350        assert_eq!(label_span.len(), 3);
351    }
352
353    // 1.1d: SemanticError variants Display
354    #[test]
355    fn test_semantic_error_variants_display() {
356        let err = SemanticError::MixedTransactionIds {
357            interface: "IFoo".to_string(),
358            src: NamedSource::new("test.aidl", "interface IFoo {}".to_string()),
359            span: span(0, 4),
360        };
361        let display = format!("{err}");
362        assert!(
363            display.contains("IFoo"),
364            "Display should contain interface name, got: {display}"
365        );
366        assert!(
367            display.contains("mixed"),
368            "Display should contain 'mixed', got: {display}"
369        );
370    }
371
372    // 1.1e: ResolutionError::ImportNotFound Display
373    #[test]
374    fn test_resolution_error_display() {
375        let err = ResolutionError::ImportNotFound {
376            import: "foo.bar.Baz".to_string(),
377            src: NamedSource::new("test.aidl", "import foo.bar.Baz;".to_string()),
378            span: span(0, 18),
379        };
380        let display = format!("{err}");
381        assert!(display.contains("foo.bar.Baz"), "Got: {display}");
382        assert!(display.contains("not found"), "Got: {display}");
383    }
384
385    // 1.1f: AidlError From<ParseError> conversion
386    #[test]
387    fn test_aidl_error_from_parse_error() {
388        let parse_err = ParseError {
389            src: NamedSource::new("test.aidl", "bad".to_string()),
390            span: span(0, 3),
391            message: "syntax error".to_string(),
392            help: None,
393        };
394        let aidl_err: AidlError = parse_err.into();
395        assert!(matches!(aidl_err, AidlError::Parse(_)));
396    }
397
398    // 1.1g: AidlError → Box<dyn Error> conversion (API compatibility)
399    #[test]
400    fn test_aidl_error_into_box_dyn_error() {
401        use std::error::Error;
402        let parse_err = ParseError {
403            src: NamedSource::new("test.aidl", "bad".to_string()),
404            span: span(0, 3),
405            message: "syntax error".to_string(),
406            help: None,
407        };
408        let aidl_err: AidlError = parse_err.into();
409        let _box_err: Box<dyn Error> = aidl_err.into();
410    }
411
412    // 1.1h: pest Pos location → SourceSpan conversion
413    #[test]
414    fn test_pest_error_to_diagnostic_pos() {
415        use miette::Diagnostic;
416        // Pos(42): source is long enough, so length == 1
417        let source = "a".repeat(100);
418        let err = ParseError {
419            src: NamedSource::new("test.aidl", source.clone()),
420            span: span(42, 1),
421            message: "test".to_string(),
422            help: None,
423        };
424        let labels: Vec<_> = err.labels().expect("must have labels").collect();
425        assert_eq!(labels[0].inner().offset(), 42);
426        assert_eq!(labels[0].inner().len(), 1);
427    }
428
429    // 1.1i: pest Span location → SourceSpan conversion
430    #[test]
431    fn test_pest_error_to_diagnostic_span() {
432        use miette::Diagnostic;
433        let err = ParseError {
434            src: NamedSource::new("test.aidl", "0123456789abcdefghij".to_string()),
435            span: span(10, 10),
436            message: "test".to_string(),
437            help: None,
438        };
439        let labels: Vec<_> = err.labels().expect("must have labels").collect();
440        assert_eq!(labels[0].inner().offset(), 10);
441        assert_eq!(labels[0].inner().len(), 10);
442    }
443
444    // 1.1j: pest EOF location → SourceSpan conversion (guards against out-of-range)
445    #[test]
446    fn test_pest_error_to_diagnostic_eof() {
447        let source = "abc";
448        // EOF: pos >= source.len() → length = 0
449        let (offset, length) = {
450            let pos = source.len();
451            let len = if pos >= source.len() { 0 } else { 1 };
452            (pos, len)
453        };
454        let err = ParseError {
455            src: NamedSource::new("test.aidl", source.to_string()),
456            span: span(offset, length),
457            message: "unexpected EOF".to_string(),
458            help: None,
459        };
460        use miette::Diagnostic;
461        let labels: Vec<_> = err.labels().expect("must have labels").collect();
462        assert_eq!(labels[0].inner().offset(), 3);
463        assert_eq!(labels[0].inner().len(), 0);
464    }
465
466    // 1.1k: AidlError::collect() — flattens nested Multiple variants
467    #[test]
468    fn test_aidl_error_collect_flatten() {
469        let make_parse_err = |msg: &str| {
470            AidlError::Parse(Box::new(ParseError {
471                src: NamedSource::new("test.aidl", msg.to_string()),
472                span: span(0, 1),
473                message: msg.to_string(),
474                help: None,
475            }))
476        };
477
478        let a = make_parse_err("error A");
479        let b = make_parse_err("error B");
480        let c = make_parse_err("error C");
481
482        // Multiple{[A, B]} + C → Multiple{[A, B, C]}
483        let nested = AidlError::Multiple { errors: vec![a, b] };
484        let result = AidlError::collect(vec![nested, c]);
485
486        match result {
487            Some(AidlError::Multiple { errors }) => {
488                assert_eq!(errors.len(), 3, "Expected 3 flattened errors");
489            }
490            other => panic!("Expected Multiple, got: {other:?}"),
491        }
492    }
493
494    // 1.1l: AidlError::collect() — single error is not wrapped in Multiple
495    #[test]
496    fn test_aidl_error_collect_single() {
497        let err = AidlError::Parse(Box::new(ParseError {
498            src: NamedSource::new("test.aidl", "bad".to_string()),
499            span: span(0, 3),
500            message: "syntax error".to_string(),
501            help: None,
502        }));
503        let result = AidlError::collect(vec![err]);
504        assert!(
505            matches!(result, Some(AidlError::Parse(_))),
506            "Single error should not be wrapped in Multiple"
507        );
508    }
509
510    // 1.1m: AidlError::collect() — empty collection returns None
511    #[test]
512    fn test_aidl_error_collect_empty() {
513        let result = AidlError::collect(vec![]);
514        assert!(result.is_none(), "Empty collection should return None");
515    }
516}