Skip to main content

mir_analyzer/parser/
mod.rs

1pub mod docblock;
2pub mod type_from_hint;
3
4use std::sync::Arc;
5
6use php_ast::Span;
7use php_rs_parser::parse;
8use thiserror::Error;
9
10pub use docblock::{DocblockParser, ParsedDocblock};
11pub use type_from_hint::type_from_hint;
12
13// ---------------------------------------------------------------------------
14// ParseError
15// ---------------------------------------------------------------------------
16
17#[derive(Debug, Error)]
18pub enum ParseError {
19    #[error("PHP parse error in {file}: {message}")]
20    SyntaxError { file: Arc<str>, message: String },
21}
22
23// ---------------------------------------------------------------------------
24// ParsedFile — result of parsing a single PHP file
25// ---------------------------------------------------------------------------
26
27pub struct ParsedFile<'arena, 'src> {
28    pub program: php_ast::ast::Program<'arena, 'src>,
29    pub errors: Vec<ParseError>,
30    pub file: Arc<str>,
31}
32
33// ---------------------------------------------------------------------------
34// FileParser
35// ---------------------------------------------------------------------------
36
37pub struct FileParser {
38    pub arena: bumpalo::Bump,
39}
40
41impl FileParser {
42    pub fn new() -> Self {
43        Self {
44            arena: bumpalo::Bump::new(),
45        }
46    }
47
48    /// Parse a PHP source string.
49    /// The returned `ParsedFile` borrows from both `self.arena` and `src`.
50    /// The arena must outlive the parsed file.
51    pub fn parse<'arena, 'src>(
52        &'arena self,
53        src: &'src str,
54        file: Arc<str>,
55    ) -> ParsedFile<'arena, 'src> {
56        let result = parse(&self.arena, src);
57        let errors = result
58            .errors
59            .iter()
60            .map(|e| ParseError::SyntaxError {
61                file: file.clone(),
62                message: e.to_string(),
63            })
64            .collect();
65
66        ParsedFile {
67            program: result.program,
68            errors,
69            file,
70        }
71    }
72}
73
74impl Default for FileParser {
75    fn default() -> Self {
76        Self::new()
77    }
78}
79
80// ---------------------------------------------------------------------------
81// Source location helpers
82// ---------------------------------------------------------------------------
83
84/// Extract the exact source text covered by a span.
85pub fn span_text(src: &str, span: Span) -> Option<String> {
86    if span.start >= span.end {
87        return None;
88    }
89    let s = span.start as usize;
90    let e = (span.end as usize).min(src.len());
91    src.get(s..e)
92        .map(|t| t.trim().to_string())
93        .filter(|t| !t.is_empty())
94}
95
96/// Extract the source line containing a span.
97pub fn span_snippet(src: &str, span: Span) -> String {
98    let offset = span.start as usize;
99    let line_start = src[..offset].rfind('\n').map(|p| p + 1).unwrap_or(0);
100    let line_end = src[offset..]
101        .find('\n')
102        .map(|p| offset + p)
103        .unwrap_or(src.len());
104    src[line_start..line_end].to_string()
105}
106
107// ---------------------------------------------------------------------------
108// Docblock extraction from source text
109// ---------------------------------------------------------------------------
110
111/// Scan backwards from `offset` and return the `/** ... */` docblock comment
112/// that immediately precedes the token at that position, if any.
113/// The docblock must be separated from the declaration only by whitespace.
114pub fn find_preceding_docblock(source: &str, offset: u32) -> Option<String> {
115    let offset = (offset as usize).min(source.len());
116    if offset == 0 {
117        return None;
118    }
119    let before = &source[..offset];
120    let trimmed = before.trim_end();
121    if !trimmed.ends_with("*/") {
122        return None;
123    }
124    let end = trimmed.rfind("*/")?;
125    let start = trimmed[..end].rfind("/**")?;
126    Some(trimmed[start..end + 2].to_string())
127}
128
129// ---------------------------------------------------------------------------
130// Name resolution helper — join Name parts to a string
131// ---------------------------------------------------------------------------
132
133pub fn name_to_string(name: &php_ast::ast::Name<'_, '_>) -> String {
134    name.to_string_repr().into_owned()
135}