use php_ast::{Program, Span, TypeHint, TypeHintKind};
use tower_lsp::lsp_types::{Position, Range};
pub struct ParsedDoc {
program: Box<Program<'static, 'static>>,
pub errors: Vec<php_rs_parser::diagnostics::ParseError>,
_arena: Box<bumpalo::Bump>,
#[allow(clippy::box_collection)]
_source: Box<String>,
}
unsafe impl Send for ParsedDoc {}
unsafe impl Sync for ParsedDoc {}
impl ParsedDoc {
pub fn parse(source: String) -> Self {
let source_box = Box::new(source);
let arena_box = Box::new(bumpalo::Bump::new());
let src_ref: &'static str =
unsafe { std::mem::transmute::<&str, &'static str>(source_box.as_str()) };
let arena_ref: &'static bumpalo::Bump = unsafe {
std::mem::transmute::<&bumpalo::Bump, &'static bumpalo::Bump>(arena_box.as_ref())
};
let result = php_rs_parser::parse(arena_ref, src_ref);
ParsedDoc {
program: Box::new(result.program),
errors: result.errors,
_arena: arena_box,
_source: source_box,
}
}
#[inline]
pub fn program(&self) -> &Program<'_, '_> {
&self.program
}
#[inline]
pub fn source(&self) -> &str {
&self._source
}
}
impl Default for ParsedDoc {
fn default() -> Self {
ParsedDoc::parse(String::new())
}
}
pub fn offset_to_position(source: &str, offset: u32) -> Position {
let offset = (offset as usize).min(source.len());
let prefix = &source[..offset];
let line = prefix.bytes().filter(|&b| b == b'\n').count() as u32;
let last_nl = prefix.rfind('\n').map(|i| i + 1).unwrap_or(0);
let line_segment = prefix[last_nl..]
.strip_suffix('\r')
.unwrap_or(&prefix[last_nl..]);
let character = line_segment
.chars()
.map(|c| c.len_utf16() as u32)
.sum::<u32>();
Position { line, character }
}
pub fn span_to_range(source: &str, span: Span) -> Range {
Range {
start: offset_to_position(source, span.start),
end: offset_to_position(source, span.end),
}
}
pub fn str_offset(source: &str, substr: &str) -> u32 {
let src_ptr = source.as_ptr() as usize;
let sub_ptr = substr.as_ptr() as usize;
if sub_ptr >= src_ptr && sub_ptr + substr.len() <= src_ptr + source.len() {
return (sub_ptr - src_ptr) as u32;
}
source.find(substr).unwrap_or(0) as u32
}
pub fn name_range(source: &str, name: &str) -> Range {
let start = str_offset(source, name);
Range {
start: offset_to_position(source, start),
end: offset_to_position(source, start + name.len() as u32),
}
}
pub fn format_type_hint(hint: &TypeHint<'_, '_>) -> String {
fmt_kind(&hint.kind)
}
fn fmt_kind(kind: &TypeHintKind<'_, '_>) -> String {
match kind {
TypeHintKind::Named(name) => name.to_string_repr().to_string(),
TypeHintKind::Keyword(builtin, _) => builtin.as_str().to_string(),
TypeHintKind::Nullable(inner) => format!("?{}", format_type_hint(inner)),
TypeHintKind::Union(types) => types
.iter()
.map(format_type_hint)
.collect::<Vec<_>>()
.join("|"),
TypeHintKind::Intersection(types) => types
.iter()
.map(format_type_hint)
.collect::<Vec<_>>()
.join("&"),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_empty_source() {
let doc = ParsedDoc::parse("<?php".to_string());
assert!(doc.errors.is_empty());
assert!(doc.program().stmts.is_empty());
}
#[test]
fn parses_function() {
let doc = ParsedDoc::parse("<?php\nfunction foo() {}".to_string());
assert_eq!(doc.program().stmts.len(), 1);
}
#[test]
fn offset_to_position_first_line() {
assert_eq!(
offset_to_position("<?php\nfoo", 0),
Position {
line: 0,
character: 0
}
);
}
#[test]
fn offset_to_position_second_line() {
assert_eq!(
offset_to_position("<?php\nfoo", 6),
Position {
line: 1,
character: 0
}
);
}
#[test]
fn offset_to_position_multibyte_utf16() {
let src = "a\u{1F600}b";
assert_eq!(
offset_to_position(src, 5), Position {
line: 0,
character: 3
} );
}
#[test]
fn offset_to_position_crlf_start_of_line() {
let src = "foo\r\nbar";
assert_eq!(
offset_to_position(src, 5), Position {
line: 1,
character: 0
}
);
}
#[test]
fn offset_to_position_crlf_does_not_count_cr_in_column() {
let src = "foo\r\nbar";
assert_eq!(
offset_to_position(src, 3), Position {
line: 0,
character: 3
}
);
}
#[test]
fn offset_to_position_crlf_multiline() {
let src = "a\r\nb\r\nc";
assert_eq!(
offset_to_position(src, 6), Position {
line: 2,
character: 0
}
);
assert_eq!(
offset_to_position(src, 3), Position {
line: 1,
character: 0
}
);
}
#[test]
fn str_offset_finds_substr() {
let src = "<?php\nfunction foo() {}";
let name = &src[15..18]; assert_eq!(str_offset(src, name), 15);
}
#[test]
fn str_offset_content_fallback_for_different_allocation() {
let owned = "foo".to_string();
assert_eq!(str_offset("<?php foo", &owned), 6);
}
#[test]
fn str_offset_unrelated_content_returns_zero() {
let owned = "bar".to_string();
assert_eq!(str_offset("<?php foo", &owned), 0);
}
}