1use php_ast::{Program, Span, TypeHint, TypeHintKind};
3use tower_lsp::lsp_types::{Position, Range};
4
5pub struct ParsedDoc {
18 program: Box<Program<'static, 'static>>,
20 pub errors: Vec<php_rs_parser::diagnostics::ParseError>,
21 _arena: Box<bumpalo::Bump>,
22 #[allow(clippy::box_collection)]
23 _source: Box<String>,
24}
25
26unsafe impl Send for ParsedDoc {}
28unsafe impl Sync for ParsedDoc {}
29
30impl ParsedDoc {
31 pub fn parse(source: String) -> Self {
32 let source_box = Box::new(source);
33 let arena_box = Box::new(bumpalo::Bump::new());
34
35 let src_ref: &'static str =
39 unsafe { std::mem::transmute::<&str, &'static str>(source_box.as_str()) };
40 let arena_ref: &'static bumpalo::Bump = unsafe {
41 std::mem::transmute::<&bumpalo::Bump, &'static bumpalo::Bump>(arena_box.as_ref())
42 };
43
44 let result = php_rs_parser::parse(arena_ref, src_ref);
45
46 ParsedDoc {
47 program: Box::new(result.program),
48 errors: result.errors,
49 _arena: arena_box,
50 _source: source_box,
51 }
52 }
53
54 #[inline]
59 pub fn program(&self) -> &Program<'_, '_> {
60 &self.program
61 }
62
63 #[inline]
65 pub fn source(&self) -> &str {
66 &self._source
67 }
68}
69
70impl Default for ParsedDoc {
71 fn default() -> Self {
72 ParsedDoc::parse(String::new())
73 }
74}
75
76pub fn offset_to_position(source: &str, offset: u32) -> Position {
84 let offset = (offset as usize).min(source.len());
85 let prefix = &source[..offset];
86 let line = prefix.bytes().filter(|&b| b == b'\n').count() as u32;
87 let last_nl = prefix.rfind('\n').map(|i| i + 1).unwrap_or(0);
88 let line_segment = prefix[last_nl..]
90 .strip_suffix('\r')
91 .unwrap_or(&prefix[last_nl..]);
92 let character = line_segment
93 .chars()
94 .map(|c| c.len_utf16() as u32)
95 .sum::<u32>();
96 Position { line, character }
97}
98
99pub fn span_to_range(source: &str, span: Span) -> Range {
101 Range {
102 start: offset_to_position(source, span.start),
103 end: offset_to_position(source, span.end),
104 }
105}
106
107pub fn str_offset(source: &str, substr: &str) -> u32 {
114 let src_ptr = source.as_ptr() as usize;
115 let sub_ptr = substr.as_ptr() as usize;
116 if sub_ptr >= src_ptr && sub_ptr + substr.len() <= src_ptr + source.len() {
117 return (sub_ptr - src_ptr) as u32;
118 }
119 source.find(substr).unwrap_or(0) as u32
121}
122
123pub fn name_range(source: &str, name: &str) -> Range {
125 let start = str_offset(source, name);
126 Range {
127 start: offset_to_position(source, start),
128 end: offset_to_position(source, start + name.len() as u32),
129 }
130}
131
132pub fn format_type_hint(hint: &TypeHint<'_, '_>) -> String {
136 fmt_kind(&hint.kind)
137}
138
139fn fmt_kind(kind: &TypeHintKind<'_, '_>) -> String {
140 match kind {
141 TypeHintKind::Named(name) => name.to_string_repr().to_string(),
142 TypeHintKind::Keyword(builtin, _) => builtin.as_str().to_string(),
143 TypeHintKind::Nullable(inner) => format!("?{}", format_type_hint(inner)),
144 TypeHintKind::Union(types) => types
145 .iter()
146 .map(format_type_hint)
147 .collect::<Vec<_>>()
148 .join("|"),
149 TypeHintKind::Intersection(types) => types
150 .iter()
151 .map(format_type_hint)
152 .collect::<Vec<_>>()
153 .join("&"),
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn parses_empty_source() {
163 let doc = ParsedDoc::parse("<?php".to_string());
164 assert!(doc.errors.is_empty());
165 assert!(doc.program().stmts.is_empty());
166 }
167
168 #[test]
169 fn parses_function() {
170 let doc = ParsedDoc::parse("<?php\nfunction foo() {}".to_string());
171 assert_eq!(doc.program().stmts.len(), 1);
172 }
173
174 #[test]
175 fn offset_to_position_first_line() {
176 assert_eq!(
177 offset_to_position("<?php\nfoo", 0),
178 Position {
179 line: 0,
180 character: 0
181 }
182 );
183 }
184
185 #[test]
186 fn offset_to_position_second_line() {
187 assert_eq!(
189 offset_to_position("<?php\nfoo", 6),
190 Position {
191 line: 1,
192 character: 0
193 }
194 );
195 }
196
197 #[test]
198 fn offset_to_position_multibyte_utf16() {
199 let src = "a\u{1F600}b";
204 assert_eq!(
205 offset_to_position(src, 5), Position {
207 line: 0,
208 character: 3
209 } );
211 }
212
213 #[test]
214 fn offset_to_position_crlf_start_of_line() {
215 let src = "foo\r\nbar";
218 assert_eq!(
219 offset_to_position(src, 5), Position {
221 line: 1,
222 character: 0
223 }
224 );
225 }
226
227 #[test]
228 fn offset_to_position_crlf_does_not_count_cr_in_column() {
229 let src = "foo\r\nbar";
232 assert_eq!(
233 offset_to_position(src, 3), Position {
235 line: 0,
236 character: 3
237 }
238 );
239 }
240
241 #[test]
242 fn offset_to_position_crlf_multiline() {
243 let src = "a\r\nb\r\nc";
246 assert_eq!(
247 offset_to_position(src, 6), Position {
249 line: 2,
250 character: 0
251 }
252 );
253 assert_eq!(
254 offset_to_position(src, 3), Position {
256 line: 1,
257 character: 0
258 }
259 );
260 }
261
262 #[test]
263 fn str_offset_finds_substr() {
264 let src = "<?php\nfunction foo() {}";
265 let name = &src[15..18]; assert_eq!(str_offset(src, name), 15);
267 }
268
269 #[test]
270 fn str_offset_content_fallback_for_different_allocation() {
271 let owned = "foo".to_string();
274 assert_eq!(str_offset("<?php foo", &owned), 6);
275 }
276
277 #[test]
278 fn str_offset_unrelated_content_returns_zero() {
279 let owned = "bar".to_string();
280 assert_eq!(str_offset("<?php foo", &owned), 0);
281 }
282}