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 pub fn positions_of_offsets(self, offsets: &[u32]) -> Vec<Position> {
229 let mut out = vec![
230 Position {
231 line: 0,
232 character: 0,
233 };
234 offsets.len()
235 ];
236 if offsets.is_empty() {
237 return out;
238 }
239
240 let mut indexed: Vec<(usize, u32)> = offsets.iter().copied().enumerate().collect();
241 indexed.sort_by_key(|&(_, offset)| offset);
242
243 let mut i = 0usize;
244 while i < indexed.len() {
245 let offset = indexed[i].1.min(self.source.len() as u32);
246 let line = self.line_of(offset);
247 let line_idx = line as usize;
248 let line_start = self.line_starts.get(line_idx).copied().unwrap_or(0) as usize;
249 let line_end = self
250 .line_starts
251 .get(line_idx + 1)
252 .map(|&s| (s as usize).saturating_sub(1))
253 .unwrap_or(self.source.len())
254 .min(self.source.len());
255 let raw = &self.source[line_start..line_end];
256 let line_text = raw.strip_suffix('\r').unwrap_or(raw);
257
258 let mut group_end = i + 1;
259 while group_end < indexed.len() && self.line_of(indexed[group_end].1) == line {
260 group_end += 1;
261 }
262
263 let mut char_iter = line_text.char_indices().peekable();
264 let mut utf16_col = 0u32;
265 for &(original_idx, target_offset) in &indexed[i..group_end] {
266 let target_in_line = (target_offset as usize)
267 .saturating_sub(line_start)
268 .min(line_text.len());
269 while let Some(&(byte_idx, ch)) = char_iter.peek() {
270 if byte_idx >= target_in_line {
271 break;
272 }
273 utf16_col += ch.len_utf16() as u32;
274 char_iter.next();
275 }
276 out[original_idx] = Position {
277 line,
278 character: utf16_col,
279 };
280 }
281
282 i = group_end;
283 }
284
285 out
286 }
287
288 #[inline]
289 pub fn line_starts(self) -> &'a [u32] {
290 self.line_starts
291 }
292
293 #[inline]
296 pub fn line_of(self, offset: u32) -> u32 {
297 match self.line_starts.partition_point(|&s| s <= offset) {
298 0 => 0,
299 i => (i - 1) as u32,
300 }
301 }
302
303 pub fn byte_of_position(self, pos: Position) -> u32 {
308 let line_idx = pos.line as usize;
309 let Some(&line_start) = self.line_starts.get(line_idx) else {
310 return self.source.len() as u32;
311 };
312 let line_start = line_start as usize;
313 let line_end = self
314 .line_starts
315 .get(line_idx + 1)
316 .map(|&s| (s as usize).saturating_sub(1))
317 .unwrap_or(self.source.len());
318 let raw = &self.source[line_start..line_end.min(self.source.len())];
319 let line = raw.strip_suffix('\r').unwrap_or(raw);
320 let mut col_utf16: u32 = 0;
321 let mut byte_in_line: usize = 0;
322 for ch in line.chars() {
323 if col_utf16 >= pos.character {
324 break;
325 }
326 col_utf16 += ch.len_utf16() as u32;
327 byte_in_line += ch.len_utf8();
328 }
329 (line_start + byte_in_line) as u32
330 }
331
332 pub fn range_of(self, span: Span) -> Range {
333 Range {
334 start: self.position_of(span.start),
335 end: self.position_of(span.end),
336 }
337 }
338
339 pub fn name_range(self, name: &str) -> Range {
340 let start = str_offset(self.source, name).unwrap_or(0);
341 Range {
342 start: self.position_of(start),
343 end: self.position_of(start + name.len() as u32),
344 }
345 }
346
347 pub fn name_range_in_span(self, name: &str, span: php_ast::Span) -> Range {
358 let s = span.start as usize;
359 let e = (span.end as usize).min(self.source.len());
360 let found = self
361 .source
362 .get(s..e)
363 .and_then(|slice| slice.find(name))
364 .map(|off| span.start + off as u32)
365 .or_else(|| str_offset(self.source, name));
366 match found {
367 Some(start) => Range {
368 start: self.position_of(start),
369 end: self.position_of(start + name.len() as u32),
370 },
371 None => {
372 let pos = self.position_of(span.start);
373 Range {
374 start: pos,
375 end: pos,
376 }
377 }
378 }
379 }
380
381 pub fn name_range_after_attrs(
390 self,
391 name: &str,
392 attributes: &[php_ast::Attribute<'_, '_>],
393 span: php_ast::Span,
394 ) -> Range {
395 let start = attributes.last().map(|a| a.span.end).unwrap_or(span.start);
396 self.name_range_in_span(name, php_ast::Span::new(start, span.end))
397 }
398}
399
400pub fn offset_to_position(source: &str, line_starts: &[u32], offset: u32) -> Position {
407 let offset_usize = (offset as usize).min(source.len());
408 let line = match line_starts.partition_point(|&s| s <= offset) {
410 0 => 0u32,
411 i => (i - 1) as u32,
412 };
413 let line_start = line_starts.get(line as usize).copied().unwrap_or(0) as usize;
414 let segment = &source[line_start..offset_usize];
415 let segment = segment.strip_suffix('\r').unwrap_or(segment);
417 let character = segment.chars().map(|c| c.len_utf16() as u32).sum::<u32>();
418 Position { line, character }
419}
420
421pub fn str_offset(source: &str, substr: &str) -> Option<u32> {
432 let src_ptr = source.as_ptr() as usize;
433 let sub_ptr = substr.as_ptr() as usize;
434 if sub_ptr >= src_ptr && sub_ptr + substr.len() <= src_ptr + source.len() {
435 return Some((sub_ptr - src_ptr) as u32);
436 }
437 let mut search_pos = 0;
441 while let Some(offset) = source[search_pos..].find(substr) {
442 let abs_offset = search_pos + offset;
443 let is_start_boundary = abs_offset == 0
444 || !source[..abs_offset]
445 .chars()
446 .last()
447 .map(|c| c.is_alphanumeric() || c == '_')
448 .unwrap_or(false);
449 let end_pos = abs_offset + substr.len();
450 let is_end_boundary = end_pos >= source.len()
451 || !source[end_pos..]
452 .chars()
453 .next()
454 .map(|c| c.is_alphanumeric() || c == '_')
455 .unwrap_or(false);
456
457 if is_start_boundary && is_end_boundary {
458 return Some(abs_offset as u32);
459 }
460
461 search_pos = abs_offset + 1;
462 }
463 None
464}
465
466pub fn name_range(source: &str, line_starts: &[u32], name: &str) -> Option<Range> {
468 let start = str_offset(source, name)?;
469 Some(Range {
470 start: offset_to_position(source, line_starts, start),
471 end: offset_to_position(source, line_starts, start + name.len() as u32),
472 })
473}
474
475pub fn str_offset_in_range(source: &str, span: Span, name: &str) -> Option<u32> {
478 let span_start = span.start as usize;
479 let span_end = span.end as usize;
480 if span_end > source.len() {
481 return None;
482 }
483 let span_text = &source[span_start..span_end];
484 let offset = str_offset(span_text, name)?;
485 Some(span_start as u32 + offset)
486}
487
488pub fn use_item_alias_and_fqn(item: &php_ast::UseItem<'_, '_>) -> (String, String) {
495 let fqn = item.name.to_string_repr().into_owned();
496 let alias = item
497 .alias
498 .map(|a| a.to_string())
499 .unwrap_or_else(|| crate::text::fqn_short_name(&fqn).to_string());
500 (alias, fqn)
501}
502
503pub fn format_type_hint(hint: &TypeHint<'_, '_>) -> String {
507 fmt_kind(&hint.kind)
508}
509
510fn fmt_kind(kind: &TypeHintKind<'_, '_>) -> String {
511 match kind {
512 TypeHintKind::Named(name) => name.to_string_repr().to_string(),
513 TypeHintKind::Keyword(builtin, _) => builtin.as_str().to_string(),
514 TypeHintKind::Nullable(inner) => format!("?{}", format_type_hint(inner)),
515 TypeHintKind::Union(types) => types
516 .iter()
517 .map(format_type_hint)
518 .collect::<Vec<_>>()
519 .join("|"),
520 TypeHintKind::Intersection(types) => types
521 .iter()
522 .map(format_type_hint)
523 .collect::<Vec<_>>()
524 .join("&"),
525 }
526}
527
528#[cfg(test)]
529mod tests {
530 use super::*;
531
532 #[test]
533 fn byte_of_position_past_last_line_clamps_to_end() {
534 let src = "<?php\nfoo";
535 let doc = ParsedDoc::parse(src.to_string());
536 let pos = Position {
537 line: 99,
538 character: 0,
539 };
540 assert_eq!(doc.view().byte_of_position(pos), src.len() as u32);
541 }
542
543 #[test]
544 fn parses_empty_source() {
545 let doc = ParsedDoc::parse("<?php".to_string());
546 assert!(doc.errors.is_empty());
547 assert!(doc.program().stmts.is_empty());
548 }
549
550 #[test]
551 fn parses_function() {
552 let doc = ParsedDoc::parse("<?php\nfunction foo() {}".to_string());
553 assert_eq!(doc.program().stmts.len(), 1);
554 match &doc.program().stmts[0].kind {
555 php_ast::StmtKind::Function(f) => assert_eq!(f.name.as_str(), Some("foo")),
556 other => panic!("expected a function declaration, got {other:?}"),
557 }
558 }
559
560 #[test]
561 fn offset_to_position_first_line() {
562 let src = "<?php\nfoo";
563 let doc = ParsedDoc::parse(src.to_string());
564 assert_eq!(
565 offset_to_position(src, doc.line_starts(), 0),
566 Position {
567 line: 0,
568 character: 0
569 }
570 );
571 }
572
573 #[test]
574 fn offset_to_position_second_line() {
575 let src = "<?php\nfoo";
577 let doc = ParsedDoc::parse(src.to_string());
578 assert_eq!(
579 offset_to_position(src, doc.line_starts(), 6),
580 Position {
581 line: 1,
582 character: 0
583 }
584 );
585 }
586
587 #[test]
588 fn offset_to_position_multibyte_utf16() {
589 let src = "a\u{1F600}b";
594 let doc = ParsedDoc::parse(src.to_string());
595 assert_eq!(
596 offset_to_position(src, doc.line_starts(), 5), Position {
598 line: 0,
599 character: 3
600 } );
602 }
603
604 #[test]
605 fn offset_to_position_crlf_start_of_line() {
606 let src = "foo\r\nbar";
609 let doc = ParsedDoc::parse(src.to_string());
610 assert_eq!(
611 offset_to_position(src, doc.line_starts(), 5), Position {
613 line: 1,
614 character: 0
615 }
616 );
617 }
618
619 #[test]
620 fn offset_to_position_crlf_does_not_count_cr_in_column() {
621 let src = "foo\r\nbar";
624 let doc = ParsedDoc::parse(src.to_string());
625 assert_eq!(
626 offset_to_position(src, doc.line_starts(), 3), Position {
628 line: 0,
629 character: 3
630 }
631 );
632 }
633
634 #[test]
635 fn offset_to_position_crlf_multiline() {
636 let src = "a\r\nb\r\nc";
639 let doc = ParsedDoc::parse(src.to_string());
640 assert_eq!(
641 offset_to_position(src, doc.line_starts(), 6), Position {
643 line: 2,
644 character: 0
645 }
646 );
647 assert_eq!(
648 offset_to_position(src, doc.line_starts(), 3), Position {
650 line: 1,
651 character: 0
652 }
653 );
654 }
655
656 #[test]
657 fn positions_of_offsets_matches_single_lookup() {
658 let src = "<?php\r\n$a = \"café\";\r\n$b = \"😀\";\r\n";
659 let doc = ParsedDoc::parse(src.to_string());
660 let offsets = vec![0, 6, 10, 18, 23, 28, 32, src.len() as u32];
661 let batched = doc.view().positions_of_offsets(&offsets);
662 let expected: Vec<Position> = offsets
663 .iter()
664 .map(|&offset| offset_to_position(src, doc.line_starts(), offset))
665 .collect();
666 assert_eq!(batched, expected);
667 }
668
669 #[test]
670 fn positions_of_offsets_preserves_input_order() {
671 let src = "a\nbb\nccc";
672 let doc = ParsedDoc::parse(src.to_string());
673 let offsets = vec![5, 0, 3, 1];
674 assert_eq!(
675 doc.view().positions_of_offsets(&offsets),
676 vec![
677 Position {
678 line: 2,
679 character: 0,
680 },
681 Position {
682 line: 0,
683 character: 0,
684 },
685 Position {
686 line: 1,
687 character: 1,
688 },
689 Position {
690 line: 0,
691 character: 1,
692 },
693 ]
694 );
695 }
696
697 #[test]
698 fn str_offset_finds_substr() {
699 let src = "<?php\nfunction foo() {}";
700 let name = &src[15..18]; assert_eq!(str_offset(src, name), Some(15));
702 }
703
704 #[test]
705 fn str_offset_content_fallback_for_different_allocation() {
706 let owned = "foo".to_string();
709 assert_eq!(str_offset("<?php foo", &owned), Some(6));
710 }
711
712 #[test]
713 fn str_offset_unrelated_content_returns_none() {
714 let owned = "bar".to_string();
715 assert_eq!(str_offset("<?php foo", &owned), None);
716 }
717
718 #[test]
723 fn file_imports_is_memoized_across_calls() {
724 let doc = ParsedDoc::parse("<?php\nuse App\\Foo;\nuse App\\Bar as B;\n".to_string());
725
726 let first = doc.file_imports();
727 assert_eq!(first.get("Foo").map(String::as_str), Some("App\\Foo"));
728 assert_eq!(first.get("B").map(String::as_str), Some("App\\Bar"));
729
730 let second = doc.file_imports();
731 assert!(
732 Arc::ptr_eq(&first, &second),
733 "repeat calls must share the memoized map, not recompute it"
734 );
735 }
736}