Skip to main content

php_rs_parser/
diagnostics.rs

1use crate::PhpVersion;
2use php_ast::Span;
3use std::borrow::Cow;
4use thiserror::Error;
5
6/// Diagnostic severity. Mirrors `php -l`'s split between fatal errors and
7/// warnings (e.g. `final private method` is a PHP warning, not a fatal).
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
9#[non_exhaustive]
10pub enum Severity {
11    /// Fatal in PHP.
12    #[default]
13    Error,
14    /// Non-fatal in PHP.
15    Warning,
16}
17
18/// A parse error or diagnostic emitted during parsing.
19///
20/// The parser recovers from all errors and always produces a complete AST,
21/// so errors are informational rather than fatal. Each variant carries a
22/// [`Span`] identifying the source location.
23#[derive(Debug, Clone, Error)]
24#[non_exhaustive]
25pub enum ParseError {
26    /// A specific token was expected but a different one was found.
27    #[error("expected {expected}, found {found}")]
28    Expected {
29        /// Description of the expected token.
30        expected: Cow<'static, str>,
31        /// Description of the token actually found, e.g. `"';'"` or
32        /// `"identifier"`. A description rather than the lexer's token kind,
33        /// so that new tokens are not a breaking change for this type.
34        found: Cow<'static, str>,
35        /// Source range of the error.
36        span: Span,
37    },
38
39    /// An expression was expected but not found (e.g. empty parentheses).
40    #[error("expected expression")]
41    ExpectedExpression {
42        /// Source range of the error.
43        span: Span,
44    },
45
46    /// A statement was expected but not found.
47    #[error("expected statement")]
48    ExpectedStatement {
49        /// Source range of the error.
50        span: Span,
51    },
52
53    /// PHP source must start with `<?php` or `<?`.
54    #[error("expected opening PHP tag")]
55    ExpectedOpenTag {
56        /// Source range of the error.
57        span: Span,
58    },
59
60    /// A string literal was opened but never closed.
61    #[error("unterminated string literal")]
62    UnterminatedString {
63        /// Source range of the error.
64        span: Span,
65    },
66
67    /// A required token was missing after another construct.
68    #[error("expected {expected} after {after}")]
69    ExpectedAfter {
70        /// Description of the expected token.
71        expected: Cow<'static, str>,
72        /// Construct the token should follow.
73        after: Cow<'static, str>,
74        /// Source range of the error.
75        span: Span,
76    },
77
78    /// A delimiter (parenthesis, bracket, brace) was opened but never closed.
79    #[error("expected {delimiter} to close the delimiter opened at byte {}", opened_at.start)]
80    UnclosedDelimiter {
81        /// The expected closing delimiter, e.g. `"')'"`.
82        delimiter: Cow<'static, str>,
83        /// Where the delimiter was opened.
84        opened_at: Span,
85        /// Source range of the error.
86        span: Span,
87    },
88
89    /// A construct that is syntactically valid but semantically forbidden
90    /// (e.g. `(unset)` cast, deprecated syntax). Equivalent to a PHP fatal.
91    #[error("{message}")]
92    Forbidden {
93        /// Error message.
94        message: Cow<'static, str>,
95        /// Source range of the error.
96        span: Span,
97    },
98
99    /// A construct PHP only warns about (e.g. `final private` method). Treated
100    /// as a non-fatal diagnostic; `severity()` returns [`Severity::Warning`].
101    #[error("{message}")]
102    ForbiddenWarning {
103        /// Error message.
104        message: Cow<'static, str>,
105        /// Source range of the error.
106        span: Span,
107    },
108
109    /// Statements, expressions or string interpolations are nested deeper than
110    /// the parser's recursion budget. The innermost construct is replaced by an
111    /// error node so that parsing stays within a bounded stack.
112    #[error("maximum nesting depth exceeded")]
113    NestingTooDeep {
114        /// Source range of the error.
115        span: Span,
116    },
117
118    /// The source is larger than 4 GiB, so byte offsets no longer fit in a
119    /// [`Span`]. Nothing is parsed and the program is empty.
120    #[error("source is {len} bytes; the maximum is 4 GiB")]
121    SourceTooLarge {
122        /// Length of the rejected source in bytes.
123        len: usize,
124        /// Always empty (`0..0`).
125        span: Span,
126    },
127
128    /// Syntax that requires a newer PHP version than the targeted one.
129    /// Emitted by [`crate::parse_with`] when the source uses features
130    /// unavailable in the specified [`crate::PhpVersion`].
131    #[error("'{feature}' requires PHP {required} or higher")]
132    VersionTooLow {
133        /// Name of the feature used.
134        feature: Cow<'static, str>,
135        /// Minimum PHP version required.
136        required: PhpVersion,
137        /// PHP version targeted.
138        used: PhpVersion,
139        /// Source range of the error.
140        span: Span,
141    },
142}
143
144impl ParseError {
145    /// Source range of the error.
146    pub fn span(&self) -> Span {
147        match self {
148            ParseError::Expected { span, .. }
149            | ParseError::ExpectedExpression { span }
150            | ParseError::ExpectedStatement { span }
151            | ParseError::ExpectedOpenTag { span }
152            | ParseError::UnterminatedString { span }
153            | ParseError::ExpectedAfter { span, .. }
154            | ParseError::UnclosedDelimiter { span, .. }
155            | ParseError::Forbidden { span, .. }
156            | ParseError::ForbiddenWarning { span, .. }
157            | ParseError::NestingTooDeep { span }
158            | ParseError::SourceTooLarge { span, .. }
159            | ParseError::VersionTooLow { span, .. } => *span,
160        }
161    }
162
163    /// Returns the diagnostic severity. Currently only [`ParseError::ForbiddenWarning`]
164    /// is at warning level; every other variant is an error.
165    pub fn severity(&self) -> Severity {
166        match self {
167            ParseError::ForbiddenWarning { .. } => Severity::Warning,
168            _ => Severity::Error,
169        }
170    }
171}