1use std::collections::HashMap;
7use std::mem::ManuallyDrop;
8use std::sync::{Arc, LazyLock, Mutex, OnceLock};
9
10use php_ast::{Program, Span, TypeHint, TypeHintKind};
11use tower_lsp_server::ls_types::{Position, Range};
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 {
77 program: ManuallyDrop<Box<Program<'static, 'static>>>,
78 pub errors: Vec<php_rs_parser::diagnostics::ParseError>,
79 _source: Arc<str>,
80 line_starts: Vec<u32>,
81 _arena: ArenaGuard,
82 imports_cache: OnceLock<Arc<HashMap<String, String>>>,
87}
88
89impl Drop for ParsedDoc {
90 fn drop(&mut self) {
91 unsafe { ManuallyDrop::drop(&mut self.program) };
95 }
96}
97
98unsafe impl Send for ParsedDoc {}
100unsafe impl Sync for ParsedDoc {}
101
102impl ParsedDoc {
103 pub fn parse(source: impl Into<Arc<str>>) -> Self {
107 let source: Arc<str> = source.into();
108 let arena_box = BUMP_POOL.take();
110
111 let src_ref: &'static str = unsafe { std::mem::transmute::<&str, &'static str>(&*source) };
117 let arena_ref: &'static bumpalo::Bump = unsafe {
118 std::mem::transmute::<&bumpalo::Bump, &'static bumpalo::Bump>(arena_box.as_ref())
119 };
120
121 let result = php_rs_parser::parse_arena(arena_ref, src_ref);
122
123 let line_starts = build_line_starts(src_ref);
124
125 ParsedDoc {
126 program: ManuallyDrop::new(Box::new(result.program)),
127 errors: result.errors,
128 _source: source,
129 line_starts,
130 _arena: ArenaGuard(Some(arena_box)),
131 imports_cache: OnceLock::new(),
132 }
133 }
134
135 #[inline]
140 pub fn program(&self) -> &Program<'_, '_> {
141 &self.program
142 }
143
144 #[inline]
146 pub fn source(&self) -> &str {
147 &self._source
148 }
149
150 #[inline]
156 pub fn source_arc(&self) -> Arc<str> {
157 self._source.clone()
158 }
159
160 pub fn line_starts(&self) -> &[u32] {
163 &self.line_starts
164 }
165
166 pub fn view(&self) -> SourceView<'_> {
168 SourceView {
169 source: self.source(),
170 line_starts: self.line_starts(),
171 }
172 }
173
174 pub fn file_imports(&self) -> Arc<HashMap<String, String>> {
178 self.imports_cache
179 .get_or_init(|| Arc::new(crate::navigation::references::collect_file_imports(self)))
180 .clone()
181 }
182}
183
184impl Default for ParsedDoc {
185 fn default() -> Self {
186 ParsedDoc::parse("")
187 }
188}
189
190fn build_line_starts(source: &str) -> Vec<u32> {
195 let mut starts = vec![0u32];
196 for (i, b) in source.bytes().enumerate() {
197 if b == b'\n' {
198 starts.push(i as u32 + 1);
199 }
200 }
201 starts
202}
203
204#[derive(Copy, Clone)]
207pub struct SourceView<'a> {
208 source: &'a str,
209 line_starts: &'a [u32],
210}
211
212impl<'a> SourceView<'a> {
213 #[inline]
214 pub fn source(self) -> &'a str {
215 self.source
216 }
217
218 pub fn position_of(self, offset: u32) -> Position {
219 offset_to_position(self.source, self.line_starts, offset)
220 }
221
222 #[inline]
223 pub fn line_starts(self) -> &'a [u32] {
224 self.line_starts
225 }
226
227 #[inline]
230 pub fn line_of(self, offset: u32) -> u32 {
231 match self.line_starts.partition_point(|&s| s <= offset) {
232 0 => 0,
233 i => (i - 1) as u32,
234 }
235 }
236
237 pub fn byte_of_position(self, pos: Position) -> u32 {
242 let line_idx = pos.line as usize;
243 let line_start = self.line_starts.get(line_idx).copied().unwrap_or(0) as usize;
244 let line_end = self
245 .line_starts
246 .get(line_idx + 1)
247 .map(|&s| (s as usize).saturating_sub(1))
248 .unwrap_or(self.source.len());
249 let raw = &self.source[line_start..line_end.min(self.source.len())];
250 let line = raw.strip_suffix('\r').unwrap_or(raw);
251 let mut col_utf16: u32 = 0;
252 let mut byte_in_line: usize = 0;
253 for ch in line.chars() {
254 if col_utf16 >= pos.character {
255 break;
256 }
257 col_utf16 += ch.len_utf16() as u32;
258 byte_in_line += ch.len_utf8();
259 }
260 (line_start + byte_in_line) as u32
261 }
262
263 pub fn range_of(self, span: Span) -> Range {
264 Range {
265 start: self.position_of(span.start),
266 end: self.position_of(span.end),
267 }
268 }
269
270 pub fn name_range(self, name: &str) -> Range {
271 let start = str_offset(self.source, name).unwrap_or(0);
272 Range {
273 start: self.position_of(start),
274 end: self.position_of(start + name.len() as u32),
275 }
276 }
277
278 pub fn name_range_in_span(self, name: &str, span: php_ast::Span) -> Range {
289 let s = span.start as usize;
290 let e = (span.end as usize).min(self.source.len());
291 let found = self
292 .source
293 .get(s..e)
294 .and_then(|slice| slice.find(name))
295 .map(|off| span.start + off as u32)
296 .or_else(|| str_offset(self.source, name));
297 match found {
298 Some(start) => Range {
299 start: self.position_of(start),
300 end: self.position_of(start + name.len() as u32),
301 },
302 None => {
303 let pos = self.position_of(span.start);
304 Range {
305 start: pos,
306 end: pos,
307 }
308 }
309 }
310 }
311
312 pub fn name_range_after_attrs(
321 self,
322 name: &str,
323 attributes: &[php_ast::Attribute<'_, '_>],
324 span: php_ast::Span,
325 ) -> Range {
326 let start = attributes.last().map(|a| a.span.end).unwrap_or(span.start);
327 self.name_range_in_span(name, php_ast::Span::new(start, span.end))
328 }
329}
330
331pub fn offset_to_position(source: &str, line_starts: &[u32], offset: u32) -> Position {
338 let offset_usize = (offset as usize).min(source.len());
339 let line = match line_starts.partition_point(|&s| s <= offset) {
341 0 => 0u32,
342 i => (i - 1) as u32,
343 };
344 let line_start = line_starts.get(line as usize).copied().unwrap_or(0) as usize;
345 let segment = &source[line_start..offset_usize];
346 let segment = segment.strip_suffix('\r').unwrap_or(segment);
348 let character = segment.chars().map(|c| c.len_utf16() as u32).sum::<u32>();
349 Position { line, character }
350}
351
352pub fn span_to_range(source: &str, line_starts: &[u32], span: Span) -> Range {
354 Range {
355 start: offset_to_position(source, line_starts, span.start),
356 end: offset_to_position(source, line_starts, span.end),
357 }
358}
359
360pub fn str_offset(source: &str, substr: &str) -> Option<u32> {
371 let src_ptr = source.as_ptr() as usize;
372 let sub_ptr = substr.as_ptr() as usize;
373 if sub_ptr >= src_ptr && sub_ptr + substr.len() <= src_ptr + source.len() {
374 return Some((sub_ptr - src_ptr) as u32);
375 }
376 let mut search_pos = 0;
380 while let Some(offset) = source[search_pos..].find(substr) {
381 let abs_offset = search_pos + offset;
382 let is_start_boundary = abs_offset == 0
383 || !source[..abs_offset]
384 .chars()
385 .last()
386 .map(|c| c.is_alphanumeric() || c == '_')
387 .unwrap_or(false);
388 let end_pos = abs_offset + substr.len();
389 let is_end_boundary = end_pos >= source.len()
390 || !source[end_pos..]
391 .chars()
392 .next()
393 .map(|c| c.is_alphanumeric() || c == '_')
394 .unwrap_or(false);
395
396 if is_start_boundary && is_end_boundary {
397 return Some(abs_offset as u32);
398 }
399
400 search_pos = abs_offset + 1;
401 }
402 None
403}
404
405pub fn name_range(source: &str, line_starts: &[u32], name: &str) -> Option<Range> {
407 let start = str_offset(source, name)?;
408 Some(Range {
409 start: offset_to_position(source, line_starts, start),
410 end: offset_to_position(source, line_starts, start + name.len() as u32),
411 })
412}
413
414pub fn str_offset_in_range(source: &str, span: Span, name: &str) -> Option<u32> {
417 let span_start = span.start as usize;
418 let span_end = span.end as usize;
419 if span_end > source.len() {
420 return None;
421 }
422 let span_text = &source[span_start..span_end];
423 let offset = str_offset(span_text, name)?;
424 Some(span_start as u32 + offset)
425}
426
427pub fn use_item_alias_and_fqn(item: &php_ast::UseItem<'_, '_>) -> (String, String) {
434 let fqn = item.name.to_string_repr().into_owned();
435 let alias = item
436 .alias
437 .map(|a| a.to_string())
438 .unwrap_or_else(|| crate::text::fqn_short_name(&fqn).to_string());
439 (alias, fqn)
440}
441
442pub fn format_type_hint(hint: &TypeHint<'_, '_>) -> String {
446 fmt_kind(&hint.kind)
447}
448
449fn fmt_kind(kind: &TypeHintKind<'_, '_>) -> String {
450 match kind {
451 TypeHintKind::Named(name) => name.to_string_repr().to_string(),
452 TypeHintKind::Keyword(builtin, _) => builtin.as_str().to_string(),
453 TypeHintKind::Nullable(inner) => format!("?{}", format_type_hint(inner)),
454 TypeHintKind::Union(types) => types
455 .iter()
456 .map(format_type_hint)
457 .collect::<Vec<_>>()
458 .join("|"),
459 TypeHintKind::Intersection(types) => types
460 .iter()
461 .map(format_type_hint)
462 .collect::<Vec<_>>()
463 .join("&"),
464 }
465}
466
467#[cfg(test)]
468mod tests {
469 use super::*;
470
471 #[test]
472 fn parses_empty_source() {
473 let doc = ParsedDoc::parse("<?php".to_string());
474 assert!(doc.errors.is_empty());
475 assert!(doc.program().stmts.is_empty());
476 }
477
478 #[test]
479 fn parses_function() {
480 let doc = ParsedDoc::parse("<?php\nfunction foo() {}".to_string());
481 assert_eq!(doc.program().stmts.len(), 1);
482 }
483
484 #[test]
485 fn offset_to_position_first_line() {
486 let src = "<?php\nfoo";
487 let doc = ParsedDoc::parse(src.to_string());
488 assert_eq!(
489 offset_to_position(src, doc.line_starts(), 0),
490 Position {
491 line: 0,
492 character: 0
493 }
494 );
495 }
496
497 #[test]
498 fn offset_to_position_second_line() {
499 let src = "<?php\nfoo";
501 let doc = ParsedDoc::parse(src.to_string());
502 assert_eq!(
503 offset_to_position(src, doc.line_starts(), 6),
504 Position {
505 line: 1,
506 character: 0
507 }
508 );
509 }
510
511 #[test]
512 fn offset_to_position_multibyte_utf16() {
513 let src = "a\u{1F600}b";
518 let doc = ParsedDoc::parse(src.to_string());
519 assert_eq!(
520 offset_to_position(src, doc.line_starts(), 5), Position {
522 line: 0,
523 character: 3
524 } );
526 }
527
528 #[test]
529 fn offset_to_position_crlf_start_of_line() {
530 let src = "foo\r\nbar";
533 let doc = ParsedDoc::parse(src.to_string());
534 assert_eq!(
535 offset_to_position(src, doc.line_starts(), 5), Position {
537 line: 1,
538 character: 0
539 }
540 );
541 }
542
543 #[test]
544 fn offset_to_position_crlf_does_not_count_cr_in_column() {
545 let src = "foo\r\nbar";
548 let doc = ParsedDoc::parse(src.to_string());
549 assert_eq!(
550 offset_to_position(src, doc.line_starts(), 3), Position {
552 line: 0,
553 character: 3
554 }
555 );
556 }
557
558 #[test]
559 fn offset_to_position_crlf_multiline() {
560 let src = "a\r\nb\r\nc";
563 let doc = ParsedDoc::parse(src.to_string());
564 assert_eq!(
565 offset_to_position(src, doc.line_starts(), 6), Position {
567 line: 2,
568 character: 0
569 }
570 );
571 assert_eq!(
572 offset_to_position(src, doc.line_starts(), 3), Position {
574 line: 1,
575 character: 0
576 }
577 );
578 }
579
580 #[test]
581 fn str_offset_finds_substr() {
582 let src = "<?php\nfunction foo() {}";
583 let name = &src[15..18]; assert_eq!(str_offset(src, name), Some(15));
585 }
586
587 #[test]
588 fn str_offset_content_fallback_for_different_allocation() {
589 let owned = "foo".to_string();
592 assert_eq!(str_offset("<?php foo", &owned), Some(6));
593 }
594
595 #[test]
596 fn str_offset_unrelated_content_returns_none() {
597 let owned = "bar".to_string();
598 assert_eq!(str_offset("<?php foo", &owned), None);
599 }
600
601 #[test]
606 fn file_imports_is_memoized_across_calls() {
607 let doc = ParsedDoc::parse("<?php\nuse App\\Foo;\nuse App\\Bar as B;\n".to_string());
608
609 let first = doc.file_imports();
610 assert_eq!(first.get("Foo").map(String::as_str), Some("App\\Foo"));
611 assert_eq!(first.get("B").map(String::as_str), Some("App\\Bar"));
612
613 let second = doc.file_imports();
614 assert!(
615 Arc::ptr_eq(&first, &second),
616 "repeat calls must share the memoized map, not recompute it"
617 );
618 }
619}