1use std::collections::HashMap;
3use std::mem::ManuallyDrop;
4use std::sync::{LazyLock, Mutex};
5
6use php_ast::{Program, Span, TypeHint, TypeHintKind};
7use tower_lsp::lsp_types::{Position, Range};
8
9pub type MethodReturnsMap = HashMap<String, HashMap<String, String>>;
12
13const POOL_CAP: usize = 8;
16
17struct BumpPool {
18 #[allow(clippy::vec_box)]
22 pool: Mutex<Vec<Box<bumpalo::Bump>>>,
23}
24
25impl BumpPool {
26 fn take(&self) -> Box<bumpalo::Bump> {
27 self.pool
28 .lock()
29 .unwrap()
30 .pop()
31 .unwrap_or_else(|| Box::new(bumpalo::Bump::new()))
32 }
33
34 fn give(&self, mut arena: Box<bumpalo::Bump>) {
35 arena.reset();
36 let mut p = self.pool.lock().unwrap();
37 if p.len() < POOL_CAP {
38 p.push(arena);
39 }
40 }
41}
42
43static BUMP_POOL: LazyLock<BumpPool> = LazyLock::new(|| BumpPool {
44 pool: Mutex::new(Vec::new()),
45});
46
47struct ArenaGuard(Option<Box<bumpalo::Bump>>);
51
52impl Drop for ArenaGuard {
53 fn drop(&mut self) {
54 if let Some(arena) = self.0.take() {
55 BUMP_POOL.give(arena);
56 }
57 }
58}
59
60pub struct ParsedDoc {
75 program: ManuallyDrop<Box<Program<'static, 'static>>>,
76 pub errors: Vec<php_rs_parser::diagnostics::ParseError>,
77 #[allow(clippy::box_collection)]
78 _source: Box<String>,
79 line_starts: Vec<u32>,
80 _arena: ArenaGuard,
81}
82
83impl Drop for ParsedDoc {
84 fn drop(&mut self) {
85 unsafe { ManuallyDrop::drop(&mut self.program) };
89 }
90}
91
92unsafe impl Send for ParsedDoc {}
94unsafe impl Sync for ParsedDoc {}
95
96impl ParsedDoc {
97 pub fn parse(source: String) -> Self {
98 let source_box = Box::new(source);
99 let arena_box = BUMP_POOL.take();
101
102 let src_ref: &'static str =
106 unsafe { std::mem::transmute::<&str, &'static str>(source_box.as_str()) };
107 let arena_ref: &'static bumpalo::Bump = unsafe {
108 std::mem::transmute::<&bumpalo::Bump, &'static bumpalo::Bump>(arena_box.as_ref())
109 };
110
111 let result = php_rs_parser::parse(arena_ref, src_ref);
112
113 let line_starts = build_line_starts(src_ref);
114
115 ParsedDoc {
116 program: ManuallyDrop::new(Box::new(result.program)),
117 errors: result.errors,
118 _source: source_box,
119 line_starts,
120 _arena: ArenaGuard(Some(arena_box)),
121 }
122 }
123
124 #[inline]
129 pub fn program(&self) -> &Program<'_, '_> {
130 &self.program
131 }
132
133 #[inline]
135 pub fn source(&self) -> &str {
136 &self._source
137 }
138
139 pub fn line_starts(&self) -> &[u32] {
142 &self.line_starts
143 }
144
145 pub fn view(&self) -> SourceView<'_> {
147 SourceView {
148 source: self.source(),
149 line_starts: self.line_starts(),
150 }
151 }
152}
153
154impl Default for ParsedDoc {
155 fn default() -> Self {
156 ParsedDoc::parse(String::new())
157 }
158}
159
160fn build_line_starts(source: &str) -> Vec<u32> {
165 let mut starts = vec![0u32];
166 for (i, b) in source.bytes().enumerate() {
167 if b == b'\n' {
168 starts.push(i as u32 + 1);
169 }
170 }
171 starts
172}
173
174#[derive(Copy, Clone)]
177pub struct SourceView<'a> {
178 source: &'a str,
179 line_starts: &'a [u32],
180}
181
182impl<'a> SourceView<'a> {
183 #[inline]
184 pub fn source(self) -> &'a str {
185 self.source
186 }
187
188 pub fn position_of(self, offset: u32) -> Position {
189 offset_to_position(self.source, self.line_starts, offset)
190 }
191
192 #[inline]
193 pub fn line_starts(self) -> &'a [u32] {
194 self.line_starts
195 }
196
197 #[inline]
200 pub fn line_of(self, offset: u32) -> u32 {
201 match self.line_starts.partition_point(|&s| s <= offset) {
202 0 => 0,
203 i => (i - 1) as u32,
204 }
205 }
206
207 pub fn byte_of_position(self, pos: Position) -> u32 {
212 let line_idx = pos.line as usize;
213 let line_start = self.line_starts.get(line_idx).copied().unwrap_or(0) as usize;
214 let line_end = self
215 .line_starts
216 .get(line_idx + 1)
217 .map(|&s| (s as usize).saturating_sub(1))
218 .unwrap_or(self.source.len());
219 let raw = &self.source[line_start..line_end.min(self.source.len())];
220 let line = raw.strip_suffix('\r').unwrap_or(raw);
221 let mut col_utf16: u32 = 0;
222 let mut byte_in_line: usize = 0;
223 for ch in line.chars() {
224 if col_utf16 >= pos.character {
225 break;
226 }
227 col_utf16 += ch.len_utf16() as u32;
228 byte_in_line += ch.len_utf8();
229 }
230 (line_start + byte_in_line) as u32
231 }
232
233 pub fn range_of(self, span: Span) -> Range {
234 Range {
235 start: self.position_of(span.start),
236 end: self.position_of(span.end),
237 }
238 }
239
240 pub fn name_range(self, name: &str) -> Range {
241 let start = str_offset(self.source, name);
242 Range {
243 start: self.position_of(start),
244 end: self.position_of(start + name.len() as u32),
245 }
246 }
247}
248
249pub fn offset_to_position(source: &str, line_starts: &[u32], offset: u32) -> Position {
256 let offset_usize = (offset as usize).min(source.len());
257 let line = match line_starts.partition_point(|&s| s <= offset) {
259 0 => 0u32,
260 i => (i - 1) as u32,
261 };
262 let line_start = line_starts.get(line as usize).copied().unwrap_or(0) as usize;
263 let segment = &source[line_start..offset_usize];
264 let segment = segment.strip_suffix('\r').unwrap_or(segment);
266 let character = segment.chars().map(|c| c.len_utf16() as u32).sum::<u32>();
267 Position { line, character }
268}
269
270pub fn span_to_range(source: &str, line_starts: &[u32], span: Span) -> Range {
272 Range {
273 start: offset_to_position(source, line_starts, span.start),
274 end: offset_to_position(source, line_starts, span.end),
275 }
276}
277
278pub fn str_offset(source: &str, substr: &str) -> u32 {
285 let src_ptr = source.as_ptr() as usize;
286 let sub_ptr = substr.as_ptr() as usize;
287 if sub_ptr >= src_ptr && sub_ptr + substr.len() <= src_ptr + source.len() {
288 return (sub_ptr - src_ptr) as u32;
289 }
290 source.find(substr).unwrap_or(0) as u32
292}
293
294pub fn name_range(source: &str, line_starts: &[u32], name: &str) -> Range {
296 let start = str_offset(source, name);
297 Range {
298 start: offset_to_position(source, line_starts, start),
299 end: offset_to_position(source, line_starts, start + name.len() as u32),
300 }
301}
302
303pub fn format_type_hint(hint: &TypeHint<'_, '_>) -> String {
307 fmt_kind(&hint.kind)
308}
309
310fn fmt_kind(kind: &TypeHintKind<'_, '_>) -> String {
311 match kind {
312 TypeHintKind::Named(name) => name.to_string_repr().to_string(),
313 TypeHintKind::Keyword(builtin, _) => builtin.as_str().to_string(),
314 TypeHintKind::Nullable(inner) => format!("?{}", format_type_hint(inner)),
315 TypeHintKind::Union(types) => types
316 .iter()
317 .map(format_type_hint)
318 .collect::<Vec<_>>()
319 .join("|"),
320 TypeHintKind::Intersection(types) => types
321 .iter()
322 .map(format_type_hint)
323 .collect::<Vec<_>>()
324 .join("&"),
325 }
326}
327
328#[cfg(test)]
329mod tests {
330 use super::*;
331
332 #[test]
333 fn parses_empty_source() {
334 let doc = ParsedDoc::parse("<?php".to_string());
335 assert!(doc.errors.is_empty());
336 assert!(doc.program().stmts.is_empty());
337 }
338
339 #[test]
340 fn parses_function() {
341 let doc = ParsedDoc::parse("<?php\nfunction foo() {}".to_string());
342 assert_eq!(doc.program().stmts.len(), 1);
343 }
344
345 #[test]
346 fn offset_to_position_first_line() {
347 let src = "<?php\nfoo";
348 let doc = ParsedDoc::parse(src.to_string());
349 assert_eq!(
350 offset_to_position(src, doc.line_starts(), 0),
351 Position {
352 line: 0,
353 character: 0
354 }
355 );
356 }
357
358 #[test]
359 fn offset_to_position_second_line() {
360 let src = "<?php\nfoo";
362 let doc = ParsedDoc::parse(src.to_string());
363 assert_eq!(
364 offset_to_position(src, doc.line_starts(), 6),
365 Position {
366 line: 1,
367 character: 0
368 }
369 );
370 }
371
372 #[test]
373 fn offset_to_position_multibyte_utf16() {
374 let src = "a\u{1F600}b";
379 let doc = ParsedDoc::parse(src.to_string());
380 assert_eq!(
381 offset_to_position(src, doc.line_starts(), 5), Position {
383 line: 0,
384 character: 3
385 } );
387 }
388
389 #[test]
390 fn offset_to_position_crlf_start_of_line() {
391 let src = "foo\r\nbar";
394 let doc = ParsedDoc::parse(src.to_string());
395 assert_eq!(
396 offset_to_position(src, doc.line_starts(), 5), Position {
398 line: 1,
399 character: 0
400 }
401 );
402 }
403
404 #[test]
405 fn offset_to_position_crlf_does_not_count_cr_in_column() {
406 let src = "foo\r\nbar";
409 let doc = ParsedDoc::parse(src.to_string());
410 assert_eq!(
411 offset_to_position(src, doc.line_starts(), 3), Position {
413 line: 0,
414 character: 3
415 }
416 );
417 }
418
419 #[test]
420 fn offset_to_position_crlf_multiline() {
421 let src = "a\r\nb\r\nc";
424 let doc = ParsedDoc::parse(src.to_string());
425 assert_eq!(
426 offset_to_position(src, doc.line_starts(), 6), Position {
428 line: 2,
429 character: 0
430 }
431 );
432 assert_eq!(
433 offset_to_position(src, doc.line_starts(), 3), Position {
435 line: 1,
436 character: 0
437 }
438 );
439 }
440
441 #[test]
442 fn str_offset_finds_substr() {
443 let src = "<?php\nfunction foo() {}";
444 let name = &src[15..18]; assert_eq!(str_offset(src, name), 15);
446 }
447
448 #[test]
449 fn str_offset_content_fallback_for_different_allocation() {
450 let owned = "foo".to_string();
453 assert_eq!(str_offset("<?php foo", &owned), 6);
454 }
455
456 #[test]
457 fn str_offset_unrelated_content_returns_zero() {
458 let owned = "bar".to_string();
459 assert_eq!(str_offset("<?php foo", &owned), 0);
460 }
461}