Skip to main content

php_rs_parser/
diagnostics.rs

1use php_ast::Span;
2use php_lexer::TokenKind;
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)]
9pub enum Severity {
10    /// Fatal in PHP.
11    #[default]
12    Error,
13    /// Non-fatal in PHP.
14    Warning,
15}
16
17/// A parse error or diagnostic emitted during parsing.
18///
19/// The parser recovers from all errors and always produces a complete AST,
20/// so errors are informational rather than fatal. Each variant carries a
21/// [`Span`] identifying the source location.
22#[derive(Debug, Clone, Error)]
23pub enum ParseError {
24    /// A specific token was expected but a different one was found.
25    #[error("expected {expected}, found {found}")]
26    Expected {
27        /// Description of the expected token.
28        expected: Cow<'static, str>,
29        /// Token actually found.
30        found: TokenKind,
31        /// Source range of the error.
32        span: Span,
33    },
34
35    /// An expression was expected but not found (e.g. empty parentheses).
36    #[error("expected expression")]
37    ExpectedExpression {
38        /// Source range of the error.
39        span: Span,
40    },
41
42    /// A statement was expected but not found.
43    #[error("expected statement")]
44    ExpectedStatement {
45        /// Source range of the error.
46        span: Span,
47    },
48
49    /// PHP source must start with `<?php` or `<?`.
50    #[error("expected opening PHP tag")]
51    ExpectedOpenTag {
52        /// Source range of the error.
53        span: Span,
54    },
55
56    /// A string literal was opened but never closed.
57    #[error("unterminated string literal")]
58    UnterminatedString {
59        /// Source range of the error.
60        span: Span,
61    },
62
63    /// A required token was missing after another construct.
64    #[error("expected {expected} after {after}")]
65    ExpectedAfter {
66        /// Description of the expected token.
67        expected: Cow<'static, str>,
68        /// Construct the token should follow.
69        after: Cow<'static, str>,
70        /// Source range of the error.
71        span: Span,
72    },
73
74    /// A delimiter (parenthesis, bracket, brace) was opened but never closed.
75    #[error("unclosed {delimiter} opened at {opened_at:?}")]
76    UnclosedDelimiter {
77        /// The unclosed delimiter.
78        delimiter: Cow<'static, str>,
79        /// Where the delimiter was opened.
80        opened_at: Span,
81        /// Source range of the error.
82        span: Span,
83    },
84
85    /// A construct that is syntactically valid but semantically forbidden
86    /// (e.g. `(unset)` cast, deprecated syntax). Equivalent to a PHP fatal.
87    #[error("{message}")]
88    Forbidden {
89        /// Error message.
90        message: Cow<'static, str>,
91        /// Source range of the error.
92        span: Span,
93    },
94
95    /// A construct PHP only warns about (e.g. `final private` method). Treated
96    /// as a non-fatal diagnostic; `severity()` returns [`Severity::Warning`].
97    #[error("{message}")]
98    ForbiddenWarning {
99        /// Error message.
100        message: Cow<'static, str>,
101        /// Source range of the error.
102        span: Span,
103    },
104
105    /// Syntax that requires a newer PHP version than the targeted one.
106    /// Emitted by [`crate::parse_versioned`] when the source uses features
107    /// unavailable in the specified [`crate::PhpVersion`].
108    #[error("'{feature}' requires PHP {required} or higher")]
109    VersionTooLow {
110        /// Name of the feature used.
111        feature: Cow<'static, str>,
112        /// Minimum PHP version required.
113        required: Cow<'static, str>,
114        /// PHP version targeted.
115        used: Cow<'static, str>,
116        /// Source range of the error.
117        span: Span,
118    },
119}
120
121impl ParseError {
122    /// Source range of the error.
123    pub fn span(&self) -> Span {
124        match self {
125            ParseError::Expected { span, .. }
126            | ParseError::ExpectedExpression { span }
127            | ParseError::ExpectedStatement { span }
128            | ParseError::ExpectedOpenTag { span }
129            | ParseError::UnterminatedString { span }
130            | ParseError::ExpectedAfter { span, .. }
131            | ParseError::UnclosedDelimiter { span, .. }
132            | ParseError::Forbidden { span, .. }
133            | ParseError::ForbiddenWarning { span, .. }
134            | ParseError::VersionTooLow { span, .. } => *span,
135        }
136    }
137
138    /// Returns the diagnostic severity. Currently only [`ParseError::ForbiddenWarning`]
139    /// is at warning level; every other variant is an error.
140    pub fn severity(&self) -> Severity {
141        match self {
142            ParseError::ForbiddenWarning { .. } => Severity::Warning,
143            _ => Severity::Error,
144        }
145    }
146}