Skip to main content

nginx_lint_parser/
error.rs

1//! Error types for the nginx configuration parser.
2//!
3//! Errors are split into two stages:
4//!
5//! - [`LexerError`] — failures during tokenization (unterminated strings, unexpected characters).
6//! - [`ParseError`] — failures during parsing (unexpected tokens, unclosed blocks, missing semicolons).
7//!
8//! Both carry a [`Position`] so that error messages can
9//! point to the exact line and column in the source.
10
11use crate::ast::Position;
12use std::fmt;
13use thiserror::Error;
14
15/// An error that occurs during tokenization (lexing).
16#[derive(Debug, Clone, Error)]
17pub enum LexerError {
18    /// A quoted string was opened but never closed before end-of-file.
19    #[error("Unterminated string starting at line {}, column {}", .position.line, .position.column)]
20    UnterminatedString { position: Position },
21
22    /// A backslash escape sequence was not recognized.
23    #[error("Invalid escape sequence '\\{ch}' at line {}, column {}", .position.line, .position.column)]
24    InvalidEscapeSequence { ch: char, position: Position },
25
26    /// A character was encountered that is not valid in any token position.
27    #[error("Unexpected character '{ch}' at line {}, column {}", .position.line, .position.column)]
28    UnexpectedChar { ch: char, position: Position },
29}
30
31impl LexerError {
32    /// Returns the source position where this error occurred.
33    pub fn position(&self) -> Position {
34        match self {
35            LexerError::UnterminatedString { position } => *position,
36            LexerError::InvalidEscapeSequence { position, .. } => *position,
37            LexerError::UnexpectedChar { position, .. } => *position,
38        }
39    }
40}
41
42/// An error that occurs during parsing.
43///
44/// Includes both parse-level errors and forwarded [`LexerError`]s.
45#[derive(Debug, Clone, Error)]
46pub enum ParseError {
47    /// A tokenization error propagated from the lexer.
48    #[error("{0}")]
49    Lexer(#[from] LexerError),
50
51    /// The parser found a different token than expected.
52    #[error("Expected '{expected}' but found '{found}' at line {}, column {}", .position.line, .position.column)]
53    UnexpectedToken {
54        expected: String,
55        found: String,
56        position: Position,
57    },
58
59    /// The input ended while the parser still expected more tokens.
60    #[error("Unexpected end of file at line {}, column {}", .position.line, .position.column)]
61    UnexpectedEof { position: Position },
62
63    /// An identifier was expected at the start of a directive but not found.
64    #[error("Expected directive name at line {}, column {}", .position.line, .position.column)]
65    ExpectedDirectiveName { position: Position },
66
67    /// A directive was not terminated with `;`.
68    #[error("Missing semicolon at line {}, column {}", .position.line, .position.column)]
69    MissingSemicolon { position: Position },
70
71    /// A `}` was found without a matching `{`.
72    #[error("Unmatched closing brace at line {}, column {}", .position.line, .position.column)]
73    UnmatchedCloseBrace { position: Position },
74
75    /// A `{` was opened but never closed before end-of-file.
76    #[error("Unclosed block starting at line {}, column {}", .position.line, .position.column)]
77    UnclosedBlock { position: Position },
78
79    /// A file could not be read from disk.
80    #[error("Failed to read file: {0}")]
81    IoError(String),
82}
83
84impl ParseError {
85    /// Returns the source position where this error occurred, if available.
86    ///
87    /// Returns `None` only for [`IoError`](ParseError::IoError) which has no
88    /// source position.
89    pub fn position(&self) -> Option<Position> {
90        match self {
91            ParseError::Lexer(e) => Some(e.position()),
92            ParseError::UnexpectedToken { position, .. } => Some(*position),
93            ParseError::UnexpectedEof { position } => Some(*position),
94            ParseError::ExpectedDirectiveName { position } => Some(*position),
95            ParseError::MissingSemicolon { position } => Some(*position),
96            ParseError::UnmatchedCloseBrace { position } => Some(*position),
97            ParseError::UnclosedBlock { position } => Some(*position),
98            ParseError::IoError(_) => None,
99        }
100    }
101}
102
103/// Result type alias for parser operations
104pub type ParseResult<T> = Result<T, ParseError>;
105
106/// Display implementation for user-friendly error messages
107impl fmt::Display for Position {
108    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
109        write!(f, "{}:{}", self.line, self.column)
110    }
111}