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}