1#[cfg(feature = "lsp-compat")]
7use perl_parser_core::ast::{Node, NodeKind};
8
9#[cfg(feature = "lsp-compat")]
10use lsp_types::LocationLink;
11#[cfg(feature = "lsp-compat")]
12use std::collections::HashMap;
13#[cfg(feature = "lsp-compat")]
14use std::str::FromStr;
15
16pub struct TypeDefinitionProvider;
21
22impl TypeDefinitionProvider {
23 pub fn new() -> Self {
25 Self
26 }
27
28 #[cfg(feature = "lsp-compat")]
30 pub fn find_type_definition(
31 &self,
32 ast: &Node,
33 line: u32,
34 character: u32,
35 uri: &str,
36 documents: &HashMap<String, String>,
37 ) -> Option<Vec<LocationLink>> {
38 let source_text = documents.get(uri)?;
40
41 let target_node = self.find_node_at_position(ast, line, character, source_text)?;
43
44 let type_name = self.extract_type_name(&target_node)?;
46
47 self.find_package_definition_in_docs(&type_name, uri, documents)
49 }
50
51 #[cfg(feature = "lsp-compat")]
57 fn find_package_definition_in_docs(
58 &self,
59 package_name: &str,
60 _origin_uri: &str,
61 documents: &HashMap<String, String>,
62 ) -> Option<Vec<LocationLink>> {
63 let mut locations = Vec::new();
64
65 for (doc_uri, source_text) in documents {
66 if let Ok(ast) = perl_parser_core::Parser::new(source_text).parse() {
67 self.find_package_in_node(&ast, package_name, doc_uri, source_text, &mut locations);
68 }
69 }
70
71 if !locations.is_empty() { Some(locations) } else { None }
72 }
73
74 #[cfg(feature = "lsp-compat")]
76 fn extract_type_name(&self, node: &Node) -> Option<String> {
77 match &node.kind {
78 NodeKind::VariableDeclaration { variable, attributes, .. } => {
80 for attr in attributes {
83 if attr.contains("::") || attr.chars().next().is_some_and(|c| c.is_uppercase())
85 {
86 return Some(attr.clone());
88 }
89 }
90 if let NodeKind::Variable { name, .. } = &variable.kind {
92 if name.contains("::") {
94 let parts: Vec<&str> = name.split("::").collect();
96 if parts.len() >= 2 {
97 return Some(parts[..parts.len() - 1].join("::"));
98 }
99 }
100 }
101 None
102 }
103 NodeKind::MethodCall { object, .. } => {
105 self.infer_object_type(object)
107 }
108 NodeKind::Variable { .. } => {
110 None
113 }
114 NodeKind::Identifier { name } => {
116 if name.contains("::") {
117 let parts: Vec<&str> = name.split("::").collect();
119 if parts.len() >= 2 {
120 return Some(parts[..parts.len() - 1].join("::"));
122 }
123 }
124 if name.chars().next().is_some_and(|c| c.is_uppercase()) {
126 return Some(name.clone());
128 }
129 None
130 }
131 NodeKind::Binary { op, left, right } if op == "->" => {
133 if let NodeKind::Identifier { name: pkg } = &left.kind {
135 if let NodeKind::Identifier { name: method } = &right.kind
136 && method == "new"
137 {
138 return Some(pkg.clone());
139 }
140 return Some(pkg.clone());
142 }
143 None
144 }
145 NodeKind::FunctionCall { name, args } if name == "bless" => {
147 if args.len() >= 2 {
148 match &args[1].kind {
150 NodeKind::String { value, .. } => Some(value.clone()),
151 NodeKind::Identifier { name } => Some(name.clone()),
152 NodeKind::Variable { name, .. } => {
153 Some(name.clone())
155 }
156 _ => None,
157 }
158 } else if args.len() == 1 {
159 None
161 } else {
162 None
163 }
164 }
165 NodeKind::Binary { op, right, .. } if op == "isa" => match &right.kind {
167 NodeKind::String { value, .. } => Some(value.clone()),
168 NodeKind::Identifier { name } => Some(name.clone()),
169 _ => None,
170 },
171 NodeKind::ExpressionStatement { expression } => self.extract_type_name(expression),
173 _ => None,
174 }
175 }
176
177 #[cfg(feature = "lsp-compat")]
179 fn infer_object_type(&self, object: &Node) -> Option<String> {
180 match &object.kind {
181 NodeKind::Variable { name, .. } => {
182 if name == "$self" || name == "$this" {
185 None
187 } else {
188 None
189 }
190 }
191 NodeKind::FunctionCall { name, .. } if name == "new" => {
193 None
195 }
196 _ => None,
197 }
198 }
199
200 #[cfg(feature = "lsp-compat")]
202 #[cfg_attr(not(test), allow(dead_code))]
203 fn find_package_definition(
204 &self,
205 ast: &Node,
206 package_name: &str,
207 uri: &str,
208 source_text: &str,
209 ) -> Option<Vec<LocationLink>> {
210 let mut locations = Vec::new();
211 self.find_package_in_node(ast, package_name, uri, source_text, &mut locations);
212
213 if !locations.is_empty() { Some(locations) } else { None }
214 }
215
216 #[cfg(feature = "lsp-compat")]
218 fn find_package_in_node(
219 &self,
220 node: &Node,
221 package_name: &str,
222 uri: &str,
223 source_text: &str,
224 locations: &mut Vec<LocationLink>,
225 ) {
226 match &node.kind {
227 NodeKind::Package { name, .. } if name == package_name => {
228 let (target_start_line, target_start_char) =
230 perl_parser_core::engine::position::offset_to_utf16_line_col(
231 source_text,
232 node.location.start,
233 );
234 let (target_end_line, target_end_char) =
235 perl_parser_core::engine::position::offset_to_utf16_line_col(
236 source_text,
237 node.location.end,
238 );
239
240 let target_range = lsp_types::Range {
241 start: lsp_types::Position {
242 line: target_start_line,
243 character: target_start_char,
244 },
245 end: lsp_types::Position { line: target_end_line, character: target_end_char },
246 };
247
248 if let Ok(target_uri) = lsp_types::Uri::from_str(uri) {
251 locations.push(LocationLink {
252 origin_selection_range: None, target_uri,
254 target_range,
255 target_selection_range: target_range,
256 });
257 }
258 }
259 _ => {}
260 }
261
262 self.visit_children(node, |child| {
264 self.find_package_in_node(child, package_name, uri, source_text, locations);
265 });
266 }
267
268 #[cfg(feature = "lsp-compat")]
270 fn visit_children<F>(&self, node: &Node, mut f: F)
271 where
272 F: FnMut(&Node),
273 {
274 match &node.kind {
275 NodeKind::Program { statements } | NodeKind::Block { statements } => {
276 for stmt in statements {
277 f(stmt);
278 }
279 }
280 NodeKind::Package { block: Some(b), .. } => {
281 f(b);
282 }
283 NodeKind::VariableDeclaration { variable, initializer, .. } => {
284 f(variable);
285 if let Some(init) = initializer {
286 f(init);
287 }
288 }
289 NodeKind::Assignment { lhs, rhs, .. } => {
290 f(lhs);
291 f(rhs);
292 }
293 NodeKind::Binary { left, right, .. } => {
294 f(left);
295 f(right);
296 }
297 NodeKind::MethodCall { object, args, .. } => {
298 f(object);
299 for arg in args {
300 f(arg);
301 }
302 }
303 NodeKind::FunctionCall { args, .. } => {
304 for arg in args {
305 f(arg);
306 }
307 }
308 NodeKind::Subroutine { body, .. } => {
309 f(body);
310 }
311 NodeKind::ExpressionStatement { expression } => {
312 f(expression);
313 }
314 NodeKind::If { condition, then_branch, else_branch, .. } => {
315 f(condition);
316 f(then_branch);
317 if let Some(else_b) = else_branch {
318 f(else_b);
319 }
320 }
321 NodeKind::While { condition, body, .. } => {
322 f(condition);
323 f(body);
324 }
325 NodeKind::For { init, condition, update, body, .. } => {
326 if let Some(i) = init {
327 f(i);
328 }
329 if let Some(c) = condition {
330 f(c);
331 }
332 if let Some(upd) = update {
333 f(upd);
334 }
335 f(body);
336 }
337 NodeKind::Foreach { variable, list, body, continue_block } => {
338 f(variable);
339 if let Some(cb) = continue_block {
340 f(cb);
341 }
342 f(list);
343 f(body);
344 if let Some(cb) = continue_block {
345 f(cb);
346 }
347 }
348 _ => {
349 }
351 }
352 }
353
354 #[cfg(feature = "lsp-compat")]
356 fn find_node_at_position(
357 &self,
358 node: &Node,
359 line: u32,
360 character: u32,
361 source_text: &str,
362 ) -> Option<Node> {
363 let offset = perl_parser_core::engine::position::utf16_line_col_to_offset(
365 source_text,
366 line,
367 character,
368 );
369
370 self.find_node_at_offset(node, offset)
372 }
373
374 #[cfg(feature = "lsp-compat")]
376 fn find_node_at_offset(&self, node: &Node, offset: usize) -> Option<Node> {
377 if offset < node.location.start || offset > node.location.end {
379 return None;
380 }
381
382 let mut best_match = None;
384 self.visit_children(node, |child| {
385 if let Some(found) = self.find_node_at_offset(child, offset) {
386 if best_match.is_none()
388 || found.location.end - found.location.start
389 < best_match
390 .as_ref()
391 .map_or(usize::MAX, |n: &Node| n.location.end - n.location.start)
392 {
393 best_match = Some(found);
394 }
395 }
396 });
397
398 best_match.or_else(|| Some(node.clone()))
400 }
401}
402
403impl Default for TypeDefinitionProvider {
404 fn default() -> Self {
405 Self::new()
406 }
407}
408
409#[cfg(all(test, feature = "lsp-compat"))]
410mod tests {
411 use super::*;
412 use perl_parser_core::Parser;
413 use perl_tdd_support::{must, must_some};
414
415 #[test]
416 fn test_find_package_definition() {
417 let code = r#"
418package MyClass;
419
420sub new {
421 my $class = shift;
422 bless {}, $class;
423}
424
425package main;
426
427my $obj = MyClass->new();
428$obj->method();
429"#;
430 let mut parser = Parser::new(code);
431 let ast = must(parser.parse());
432
433 let provider = TypeDefinitionProvider::new();
434 let uri = "file:///test.pl";
435
436 let locations = provider.find_package_definition(&ast, "MyClass", uri, code);
438 assert!(locations.is_some());
439 let locs = must_some(locations);
440 assert_eq!(locs.len(), 1);
441 }
442
443 #[test]
444 fn test_extract_type_from_constructor() {
445 let code = "my $obj = Package::Name->new();";
446 let mut parser = Parser::new(code);
447 let _ast = must(parser.parse());
448
449 let _provider = TypeDefinitionProvider::new();
450
451 }
454
455 #[test]
456 fn test_full_type_definition_flow() {
457 let code = r#"
458package MyClass;
459
460sub new {
461 my $class = shift;
462 bless {}, $class;
463}
464
465package main;
466
467my $obj = MyClass->new();
468$obj->method();
469"#;
470 let mut parser = Parser::new(code);
471 let ast = must(parser.parse());
472
473 let provider = TypeDefinitionProvider::new();
474 let uri = "file:///test.pl";
475
476 let mut documents = std::collections::HashMap::new();
477 documents.insert(uri.to_string(), code.to_string());
478
479 let line = 10;
482 let character = 10;
483
484 let locations = provider.find_type_definition(&ast, line, character, uri, &documents);
485
486 if let Some(ref locs) = locations {
488 eprintln!("Found {} locations", locs.len());
489 for loc in locs {
490 eprintln!("Location: {:?}", loc);
491 }
492 } else {
493 eprintln!("No locations found");
494
495 let offset =
498 perl_parser_core::engine::position::utf16_line_col_to_offset(code, line, character);
499 eprintln!("Offset: {}", offset);
500 if let Some(node) = provider.find_node_at_offset(&ast, offset) {
501 eprintln!("Node kind: {:?}", node.kind);
502 if let Some(type_name) = provider.extract_type_name(&node) {
503 eprintln!("Extracted type name: {}", type_name);
504 } else {
505 eprintln!("Could not extract type name from node");
506 }
507 } else {
508 eprintln!("Could not find node at offset");
509 }
510 }
511
512 assert!(locations.is_some(), "Should find type definition for MyClass->new()");
513 let locs = must_some(locations);
514 assert_eq!(locs.len(), 1, "Should find exactly one definition");
515 }
516}