1use std::mem::ManuallyDrop;
7use std::sync::{Arc, LazyLock, Mutex};
8
9use php_ast::{Program, Span, TypeHint, TypeHintKind};
10use tower_lsp::lsp_types::{Position, Range};
11
12const POOL_CAP: usize = 8;
15
16struct BumpPool {
17 #[allow(clippy::vec_box)]
21 pool: Mutex<Vec<Box<bumpalo::Bump>>>,
22}
23
24impl BumpPool {
25 fn take(&self) -> Box<bumpalo::Bump> {
26 self.pool
27 .lock()
28 .unwrap()
29 .pop()
30 .unwrap_or_else(|| Box::new(bumpalo::Bump::new()))
31 }
32
33 fn give(&self, mut arena: Box<bumpalo::Bump>) {
34 arena.reset();
35 let mut p = self.pool.lock().unwrap();
36 if p.len() < POOL_CAP {
37 p.push(arena);
38 }
39 }
40}
41
42static BUMP_POOL: LazyLock<BumpPool> = LazyLock::new(|| BumpPool {
43 pool: Mutex::new(Vec::new()),
44});
45
46struct ArenaGuard(Option<Box<bumpalo::Bump>>);
50
51impl Drop for ArenaGuard {
52 fn drop(&mut self) {
53 if let Some(arena) = self.0.take() {
54 BUMP_POOL.give(arena);
55 }
56 }
57}
58
59pub struct ParsedDoc {
76 program: ManuallyDrop<Box<Program<'static, 'static>>>,
77 pub errors: Vec<php_rs_parser::diagnostics::ParseError>,
78 _source: Arc<str>,
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: impl Into<Arc<str>>) -> Self {
101 let source: Arc<str> = source.into();
102 let arena_box = BUMP_POOL.take();
104
105 let src_ref: &'static str = unsafe { std::mem::transmute::<&str, &'static str>(&*source) };
111 let arena_ref: &'static bumpalo::Bump = unsafe {
112 std::mem::transmute::<&bumpalo::Bump, &'static bumpalo::Bump>(arena_box.as_ref())
113 };
114
115 let result = php_rs_parser::parse_arena(arena_ref, src_ref);
116
117 let line_starts = build_line_starts(src_ref);
118
119 ParsedDoc {
120 program: ManuallyDrop::new(Box::new(result.program)),
121 errors: result.errors,
122 _source: source,
123 line_starts,
124 _arena: ArenaGuard(Some(arena_box)),
125 }
126 }
127
128 #[inline]
133 pub fn program(&self) -> &Program<'_, '_> {
134 &self.program
135 }
136
137 #[inline]
139 pub fn source(&self) -> &str {
140 &self._source
141 }
142
143 #[inline]
149 pub fn source_arc(&self) -> Arc<str> {
150 self._source.clone()
151 }
152
153 pub fn line_starts(&self) -> &[u32] {
156 &self.line_starts
157 }
158
159 pub fn view(&self) -> SourceView<'_> {
161 SourceView {
162 source: self.source(),
163 line_starts: self.line_starts(),
164 }
165 }
166}
167
168impl Default for ParsedDoc {
169 fn default() -> Self {
170 ParsedDoc::parse("")
171 }
172}
173
174fn build_line_starts(source: &str) -> Vec<u32> {
179 let mut starts = vec![0u32];
180 for (i, b) in source.bytes().enumerate() {
181 if b == b'\n' {
182 starts.push(i as u32 + 1);
183 }
184 }
185 starts
186}
187
188#[derive(Copy, Clone)]
191pub struct SourceView<'a> {
192 source: &'a str,
193 line_starts: &'a [u32],
194}
195
196impl<'a> SourceView<'a> {
197 #[inline]
198 pub fn source(self) -> &'a str {
199 self.source
200 }
201
202 pub fn position_of(self, offset: u32) -> Position {
203 offset_to_position(self.source, self.line_starts, offset)
204 }
205
206 #[inline]
207 pub fn line_starts(self) -> &'a [u32] {
208 self.line_starts
209 }
210
211 #[inline]
214 pub fn line_of(self, offset: u32) -> u32 {
215 match self.line_starts.partition_point(|&s| s <= offset) {
216 0 => 0,
217 i => (i - 1) as u32,
218 }
219 }
220
221 pub fn byte_of_position(self, pos: Position) -> u32 {
226 let line_idx = pos.line as usize;
227 let line_start = self.line_starts.get(line_idx).copied().unwrap_or(0) as usize;
228 let line_end = self
229 .line_starts
230 .get(line_idx + 1)
231 .map(|&s| (s as usize).saturating_sub(1))
232 .unwrap_or(self.source.len());
233 let raw = &self.source[line_start..line_end.min(self.source.len())];
234 let line = raw.strip_suffix('\r').unwrap_or(raw);
235 let mut col_utf16: u32 = 0;
236 let mut byte_in_line: usize = 0;
237 for ch in line.chars() {
238 if col_utf16 >= pos.character {
239 break;
240 }
241 col_utf16 += ch.len_utf16() as u32;
242 byte_in_line += ch.len_utf8();
243 }
244 (line_start + byte_in_line) as u32
245 }
246
247 pub fn range_of(self, span: Span) -> Range {
248 Range {
249 start: self.position_of(span.start),
250 end: self.position_of(span.end),
251 }
252 }
253
254 pub fn name_range(self, name: &str) -> Range {
255 let start = str_offset(self.source, name).unwrap_or(0);
256 Range {
257 start: self.position_of(start),
258 end: self.position_of(start + name.len() as u32),
259 }
260 }
261
262 pub fn name_range_in_span(self, name: &str, span: php_ast::Span) -> Range {
273 let s = span.start as usize;
274 let e = (span.end as usize).min(self.source.len());
275 let found = self
276 .source
277 .get(s..e)
278 .and_then(|slice| slice.find(name))
279 .map(|off| span.start + off as u32)
280 .or_else(|| str_offset(self.source, name));
281 match found {
282 Some(start) => Range {
283 start: self.position_of(start),
284 end: self.position_of(start + name.len() as u32),
285 },
286 None => {
287 let pos = self.position_of(span.start);
288 Range {
289 start: pos,
290 end: pos,
291 }
292 }
293 }
294 }
295
296 pub fn name_range_after_attrs(
305 self,
306 name: &str,
307 attributes: &[php_ast::Attribute<'_, '_>],
308 span: php_ast::Span,
309 ) -> Range {
310 let start = attributes.last().map(|a| a.span.end).unwrap_or(span.start);
311 self.name_range_in_span(name, php_ast::Span::new(start, span.end))
312 }
313}
314
315pub fn offset_to_position(source: &str, line_starts: &[u32], offset: u32) -> Position {
322 let offset_usize = (offset as usize).min(source.len());
323 let line = match line_starts.partition_point(|&s| s <= offset) {
325 0 => 0u32,
326 i => (i - 1) as u32,
327 };
328 let line_start = line_starts.get(line as usize).copied().unwrap_or(0) as usize;
329 let segment = &source[line_start..offset_usize];
330 let segment = segment.strip_suffix('\r').unwrap_or(segment);
332 let character = segment.chars().map(|c| c.len_utf16() as u32).sum::<u32>();
333 Position { line, character }
334}
335
336pub fn span_to_range(source: &str, line_starts: &[u32], span: Span) -> Range {
338 Range {
339 start: offset_to_position(source, line_starts, span.start),
340 end: offset_to_position(source, line_starts, span.end),
341 }
342}
343
344pub fn str_offset(source: &str, substr: &str) -> Option<u32> {
355 let src_ptr = source.as_ptr() as usize;
356 let sub_ptr = substr.as_ptr() as usize;
357 if sub_ptr >= src_ptr && sub_ptr + substr.len() <= src_ptr + source.len() {
358 return Some((sub_ptr - src_ptr) as u32);
359 }
360 let mut search_pos = 0;
364 while let Some(offset) = source[search_pos..].find(substr) {
365 let abs_offset = search_pos + offset;
366 let is_start_boundary = abs_offset == 0
367 || !source[..abs_offset]
368 .chars()
369 .last()
370 .map(|c| c.is_alphanumeric() || c == '_')
371 .unwrap_or(false);
372 let end_pos = abs_offset + substr.len();
373 let is_end_boundary = end_pos >= source.len()
374 || !source[end_pos..]
375 .chars()
376 .next()
377 .map(|c| c.is_alphanumeric() || c == '_')
378 .unwrap_or(false);
379
380 if is_start_boundary && is_end_boundary {
381 return Some(abs_offset as u32);
382 }
383
384 search_pos = abs_offset + 1;
385 }
386 None
387}
388
389pub fn name_range(source: &str, line_starts: &[u32], name: &str) -> Option<Range> {
391 let start = str_offset(source, name)?;
392 Some(Range {
393 start: offset_to_position(source, line_starts, start),
394 end: offset_to_position(source, line_starts, start + name.len() as u32),
395 })
396}
397
398pub fn str_offset_in_range(source: &str, span: Span, name: &str) -> Option<u32> {
401 let span_start = span.start as usize;
402 let span_end = span.end as usize;
403 if span_end > source.len() {
404 return None;
405 }
406 let span_text = &source[span_start..span_end];
407 let offset = str_offset(span_text, name)?;
408 Some(span_start as u32 + offset)
409}
410
411pub fn format_type_hint(hint: &TypeHint<'_, '_>) -> String {
415 fmt_kind(&hint.kind)
416}
417
418fn fmt_kind(kind: &TypeHintKind<'_, '_>) -> String {
419 match kind {
420 TypeHintKind::Named(name) => name.to_string_repr().to_string(),
421 TypeHintKind::Keyword(builtin, _) => builtin.as_str().to_string(),
422 TypeHintKind::Nullable(inner) => format!("?{}", format_type_hint(inner)),
423 TypeHintKind::Union(types) => types
424 .iter()
425 .map(format_type_hint)
426 .collect::<Vec<_>>()
427 .join("|"),
428 TypeHintKind::Intersection(types) => types
429 .iter()
430 .map(format_type_hint)
431 .collect::<Vec<_>>()
432 .join("&"),
433 }
434}
435
436#[cfg(test)]
437mod tests {
438 use super::*;
439
440 #[test]
441 fn parses_empty_source() {
442 let doc = ParsedDoc::parse("<?php".to_string());
443 assert!(doc.errors.is_empty());
444 assert!(doc.program().stmts.is_empty());
445 }
446
447 #[test]
448 fn parses_function() {
449 let doc = ParsedDoc::parse("<?php\nfunction foo() {}".to_string());
450 assert_eq!(doc.program().stmts.len(), 1);
451 }
452
453 #[test]
454 fn offset_to_position_first_line() {
455 let src = "<?php\nfoo";
456 let doc = ParsedDoc::parse(src.to_string());
457 assert_eq!(
458 offset_to_position(src, doc.line_starts(), 0),
459 Position {
460 line: 0,
461 character: 0
462 }
463 );
464 }
465
466 #[test]
467 fn offset_to_position_second_line() {
468 let src = "<?php\nfoo";
470 let doc = ParsedDoc::parse(src.to_string());
471 assert_eq!(
472 offset_to_position(src, doc.line_starts(), 6),
473 Position {
474 line: 1,
475 character: 0
476 }
477 );
478 }
479
480 #[test]
481 fn offset_to_position_multibyte_utf16() {
482 let src = "a\u{1F600}b";
487 let doc = ParsedDoc::parse(src.to_string());
488 assert_eq!(
489 offset_to_position(src, doc.line_starts(), 5), Position {
491 line: 0,
492 character: 3
493 } );
495 }
496
497 #[test]
498 fn offset_to_position_crlf_start_of_line() {
499 let src = "foo\r\nbar";
502 let doc = ParsedDoc::parse(src.to_string());
503 assert_eq!(
504 offset_to_position(src, doc.line_starts(), 5), Position {
506 line: 1,
507 character: 0
508 }
509 );
510 }
511
512 #[test]
513 fn offset_to_position_crlf_does_not_count_cr_in_column() {
514 let src = "foo\r\nbar";
517 let doc = ParsedDoc::parse(src.to_string());
518 assert_eq!(
519 offset_to_position(src, doc.line_starts(), 3), Position {
521 line: 0,
522 character: 3
523 }
524 );
525 }
526
527 #[test]
528 fn offset_to_position_crlf_multiline() {
529 let src = "a\r\nb\r\nc";
532 let doc = ParsedDoc::parse(src.to_string());
533 assert_eq!(
534 offset_to_position(src, doc.line_starts(), 6), Position {
536 line: 2,
537 character: 0
538 }
539 );
540 assert_eq!(
541 offset_to_position(src, doc.line_starts(), 3), Position {
543 line: 1,
544 character: 0
545 }
546 );
547 }
548
549 #[test]
550 fn str_offset_finds_substr() {
551 let src = "<?php\nfunction foo() {}";
552 let name = &src[15..18]; assert_eq!(str_offset(src, name), Some(15));
554 }
555
556 #[test]
557 fn str_offset_content_fallback_for_different_allocation() {
558 let owned = "foo".to_string();
561 assert_eq!(str_offset("<?php foo", &owned), Some(6));
562 }
563
564 #[test]
565 fn str_offset_unrelated_content_returns_none() {
566 let owned = "bar".to_string();
567 assert_eq!(str_offset("<?php foo", &owned), None);
568 }
569}