1use crate::types::CodeAction;
33use perl_parser_core::ast::{Node, NodeKind};
34use std::collections::HashSet;
35
36mod error_checking;
37mod extract_subroutine;
38mod extract_variable;
39mod helpers;
40mod import_management;
41mod loop_conversion;
42mod postfix;
43
44use helpers::Helpers;
45
46pub struct EnhancedCodeActionsProvider {
48 source: String,
49 lines: Vec<String>,
50}
51
52impl EnhancedCodeActionsProvider {
53 pub fn new(source: String) -> Self {
55 let lines = source.lines().map(|s| s.to_string()).collect();
56 Self { source, lines }
57 }
58
59 pub fn get_enhanced_refactoring_actions(
61 &self,
62 ast: &Node,
63 range: (usize, usize),
64 ) -> Vec<CodeAction> {
65 let mut actions = Vec::new();
66 let mut extract_var_seen: HashSet<(usize, String)> = HashSet::new();
69
70 self.collect_actions_for_range(ast, range, false, &mut actions, &mut extract_var_seen);
72
73 actions.extend(self.get_global_refactorings(ast));
75
76 actions
77 }
78
79 fn collect_actions_for_range(
86 &self,
87 node: &Node,
88 range: (usize, usize),
89 is_control_body: bool,
90 actions: &mut Vec<CodeAction>,
91 extract_var_seen: &mut HashSet<(usize, String)>,
92 ) {
93 if node.location.start <= range.1 && node.location.end >= range.0 {
95 let helpers = Helpers::new(&self.source, &self.lines);
96
97 let node_reaches_selection_end = node.location.end >= range.1;
104 if node_reaches_selection_end && self.is_extractable_expression(node) {
105 let action =
106 extract_variable::create_extract_variable_action(node, &self.source, &helpers);
107 if let Some(decl) = action.edit.changes.first() {
108 let key = (decl.location.start, decl.new_text.clone());
109 if extract_var_seen.insert(key) {
110 actions.push(action);
111 }
112 } else {
113 actions.push(action);
114 }
115 }
116
117 if let Some(action) = loop_conversion::convert_loop_style(node, &self.source) {
119 actions.push(action);
120 }
121
122 if let Some(action) = error_checking::add_error_checking(node, &self.source) {
124 actions.push(action);
125 }
126
127 if let Some(action) = postfix::convert_to_postfix(node, &self.source) {
129 actions.push(action);
130 }
131
132 if !is_control_body && self.is_extractable_block(node) {
134 actions.push(extract_subroutine::create_extract_subroutine_action(
135 node,
136 &self.source,
137 &helpers,
138 ));
139 }
140 }
141
142 match &node.kind {
144 NodeKind::Program { statements } => {
145 for stmt in statements {
146 self.collect_actions_for_range(stmt, range, false, actions, extract_var_seen);
147 }
148 }
149 NodeKind::Block { statements } => {
150 for stmt in statements {
151 self.collect_actions_for_range(stmt, range, false, actions, extract_var_seen);
152 }
153 }
154 NodeKind::ExpressionStatement { expression } => {
155 self.collect_actions_for_range(expression, range, false, actions, extract_var_seen);
156 }
157 NodeKind::If { condition, then_branch, elsif_branches, else_branch } => {
158 self.collect_actions_for_range(condition, range, false, actions, extract_var_seen);
159 self.collect_actions_for_range(
160 then_branch,
161 range,
162 true, actions,
164 extract_var_seen,
165 );
166 for (cond, branch) in elsif_branches {
167 self.collect_actions_for_range(cond, range, false, actions, extract_var_seen);
168 self.collect_actions_for_range(branch, range, true, actions, extract_var_seen);
169 }
170 if let Some(branch) = else_branch {
171 self.collect_actions_for_range(branch, range, true, actions, extract_var_seen);
172 }
173 }
174 NodeKind::FunctionCall { args, .. } => {
175 for arg in args {
176 self.collect_actions_for_range(arg, range, false, actions, extract_var_seen);
177 }
178 }
179 NodeKind::Binary { left, right, .. } => {
180 self.collect_actions_for_range(left, range, false, actions, extract_var_seen);
181 self.collect_actions_for_range(right, range, false, actions, extract_var_seen);
182 }
183 NodeKind::Assignment { lhs, rhs, .. } => {
184 self.collect_actions_for_range(lhs, range, false, actions, extract_var_seen);
185 self.collect_actions_for_range(rhs, range, false, actions, extract_var_seen);
186 }
187 NodeKind::VariableDeclaration { variable, initializer, .. } => {
188 self.collect_actions_for_range(variable, range, false, actions, extract_var_seen);
189 if let Some(init) = initializer {
190 self.collect_actions_for_range(init, range, false, actions, extract_var_seen);
191 }
192 }
193 NodeKind::For { init, condition, update, body, .. } => {
194 if let Some(init) = init {
195 self.collect_actions_for_range(init, range, false, actions, extract_var_seen);
196 }
197 if let Some(condition) = condition {
198 self.collect_actions_for_range(
199 condition,
200 range,
201 false,
202 actions,
203 extract_var_seen,
204 );
205 }
206 if let Some(update) = update {
207 self.collect_actions_for_range(update, range, false, actions, extract_var_seen);
208 }
209 self.collect_actions_for_range(
210 body,
211 range,
212 true, actions,
214 extract_var_seen,
215 );
216 }
217 NodeKind::Foreach { variable, list, body, continue_block } => {
218 self.collect_actions_for_range(variable, range, false, actions, extract_var_seen);
219 self.collect_actions_for_range(list, range, false, actions, extract_var_seen);
220 self.collect_actions_for_range(body, range, true, actions, extract_var_seen);
221 if let Some(cb) = continue_block {
222 self.collect_actions_for_range(cb, range, false, actions, extract_var_seen);
223 }
224 }
225 NodeKind::While { condition, body, .. } => {
226 self.collect_actions_for_range(condition, range, false, actions, extract_var_seen);
227 self.collect_actions_for_range(
228 body,
229 range,
230 true, actions,
232 extract_var_seen,
233 );
234 }
235 NodeKind::MethodCall { object, args, .. } => {
236 self.collect_actions_for_range(object, range, false, actions, extract_var_seen);
237 for arg in args {
238 self.collect_actions_for_range(arg, range, false, actions, extract_var_seen);
239 }
240 }
241 NodeKind::Subroutine { body, prototype, signature, .. } => {
242 self.collect_actions_for_range(
243 body,
244 range,
245 true, actions,
247 extract_var_seen,
248 );
249 if let Some(proto) = prototype {
250 self.collect_actions_for_range(proto, range, false, actions, extract_var_seen);
251 }
252 if let Some(sig) = signature {
253 self.collect_actions_for_range(sig, range, false, actions, extract_var_seen);
254 }
255 }
256 _ => {}
257 }
258 }
259
260 fn is_extractable_expression(&self, node: &Node) -> bool {
262 matches!(
263 &node.kind,
264 NodeKind::FunctionCall { .. }
265 | NodeKind::Binary { .. }
266 | NodeKind::Unary { .. }
267 | NodeKind::MethodCall { .. }
268 | NodeKind::Ternary { .. }
269 )
270 }
271
272 fn is_extractable_block(&self, node: &Node) -> bool {
274 matches!(&node.kind, NodeKind::Block { .. })
275 }
276
277 fn get_global_refactorings(&self, ast: &Node) -> Vec<CodeAction> {
279 let mut actions = Vec::new();
280 let helpers = Helpers::new(&self.source, &self.lines);
281
282 if let Some(action) = import_management::add_missing_imports(ast, &self.source, &helpers) {
284 actions.push(action);
285 }
286
287 if let Some(action) = import_management::organize_imports(ast, &self.source, &helpers) {
289 actions.push(action);
290 }
291
292 actions.extend(self.add_recommended_pragmas(&helpers));
294
295 actions
296 }
297
298 fn add_recommended_pragmas(&self, helpers: &Helpers<'_>) -> Vec<CodeAction> {
300 use crate::types::{CodeAction, CodeActionEdit, CodeActionKind};
301 use perl_lsp_rename::TextEdit;
302 use perl_parser_core::ast::SourceLocation;
303
304 let mut actions = Vec::new();
305
306 let has_strict = self.source.contains("use strict");
308 let has_warnings = self.source.contains("use warnings");
309
310 if !has_strict || !has_warnings {
311 let mut pragmas = Vec::new();
312 if !has_strict {
313 pragmas.push("use strict;");
314 }
315 if !has_warnings {
316 pragmas.push("use warnings;");
317 }
318
319 let insert_pos = helpers.find_pragma_insert_position();
320
321 actions.push(CodeAction {
322 title: format!("Add missing pragmas ({})", pragmas.join(", ")),
323 kind: CodeActionKind::QuickFix,
324 diagnostics: Vec::new(),
325 edit: CodeActionEdit {
326 changes: vec![TextEdit {
327 location: SourceLocation { start: insert_pos, end: insert_pos },
328 new_text: format!("{}\n", pragmas.join("\n")),
329 }],
330 },
331 is_preferred: true,
332 });
333 }
334
335 if !self.source.contains("use utf8") && helpers.has_non_ascii_content() {
337 let insert_pos = helpers.find_pragma_insert_position();
338
339 actions.push(CodeAction {
340 title: "Add UTF-8 support".to_string(),
341 kind: CodeActionKind::QuickFix,
342 diagnostics: Vec::new(),
343 edit: CodeActionEdit {
344 changes: vec![TextEdit {
345 location: SourceLocation { start: insert_pos, end: insert_pos },
346 new_text: "use utf8;\nuse open qw(:std :utf8);\n".to_string(),
347 }],
348 },
349 is_preferred: false,
350 });
351 }
352
353 actions
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360 use perl_parser_core::Parser;
361 use perl_tdd_support::must;
362
363 #[test]
364 fn test_extract_variable() {
365 let source = "my $x = length($string) + 10;";
366 let mut parser = Parser::new(source);
367 let ast = must(parser.parse());
368
369 let provider = EnhancedCodeActionsProvider::new(source.to_string());
370 let actions = provider.get_enhanced_refactoring_actions(&ast, (8, 23)); for action in &actions {
374 eprintln!("Action: {}", action.title);
375 }
376
377 assert!(!actions.is_empty(), "Expected at least one action");
378 assert!(
379 actions.iter().any(|a| a.title.contains("Extract")),
380 "Expected an Extract action, got: {:?}",
381 actions.iter().map(|a| &a.title).collect::<Vec<_>>()
382 );
383 }
384
385 #[test]
386 fn test_add_error_checking() {
387 let source = "open my $fh, '<', 'file.txt';";
388 let mut parser = Parser::new(source);
389 let ast = must(parser.parse());
390
391 let provider = EnhancedCodeActionsProvider::new(source.to_string());
392 let actions = provider.get_enhanced_refactoring_actions(&ast, (0, 30));
393
394 assert!(actions.iter().any(|a| a.title.contains("error checking")));
395 }
396
397 #[test]
398 fn test_convert_to_postfix() {
399 let source = "if ($debug) { print \"Debug\\n\"; }";
400 let mut parser = Parser::new(source);
401 let ast = must(parser.parse());
402
403 let provider = EnhancedCodeActionsProvider::new(source.to_string());
404 let actions = provider.get_enhanced_refactoring_actions(&ast, (0, source.len()));
405
406 assert!(actions.iter().any(|a| a.title.contains("postfix")));
407 }
408}
409
410#[cfg(test)]
411mod extract_variable_tests {
412 use super::*;
413 use perl_parser_core::Parser;
414 use perl_tdd_support::must;
415
416 #[test]
417 fn test_extract_hash_access_to_variable() {
418 let source = "my $x = $hash{$key};";
420 let mut parser = Parser::new(source);
421 let ast = must(parser.parse());
422
423 let provider = EnhancedCodeActionsProvider::new(source.to_string());
424 let actions = provider.get_enhanced_refactoring_actions(&ast, (8, 19));
426
427 let extract_actions: Vec<_> =
428 actions.iter().filter(|a| a.title.contains("Extract")).collect();
429
430 assert!(
431 !extract_actions.is_empty(),
432 "Expected an Extract action for hash access, got: {:?}",
433 actions.iter().map(|a| &a.title).collect::<Vec<_>>()
434 );
435
436 let action = &extract_actions[0];
438 let decl_edit = &action.edit.changes[0];
439 assert!(
440 decl_edit.new_text.contains("my $val"),
441 "Expected variable name '$val' for hash access, got: {}",
442 decl_edit.new_text
443 );
444 }
445
446 #[test]
447 fn test_extract_method_call_to_variable() {
448 let source = "print $obj->method();";
449 let mut parser = Parser::new(source);
450 let ast = must(parser.parse());
451
452 let provider = EnhancedCodeActionsProvider::new(source.to_string());
453 let actions = provider.get_enhanced_refactoring_actions(&ast, (6, 20));
455
456 let extract_actions: Vec<_> =
457 actions.iter().filter(|a| a.title.contains("Extract")).collect();
458
459 assert!(
460 !extract_actions.is_empty(),
461 "Expected an Extract action for method call, got: {:?}",
462 actions.iter().map(|a| &a.title).collect::<Vec<_>>()
463 );
464
465 let action = &extract_actions[0];
467 let decl_edit = &action.edit.changes[0];
468 assert!(
469 decl_edit.new_text.contains("my $result"),
470 "Expected variable name '$result' for method call, got: {}",
471 decl_edit.new_text
472 );
473
474 let replace_edit = &action.edit.changes[1];
476 assert!(
477 replace_edit.new_text.contains("$result"),
478 "Expected replacement with '$result', got: {}",
479 replace_edit.new_text
480 );
481 }
482
483 #[test]
484 fn test_extract_method_call_new_suggests_instance() {
485 let source = "my $x = Foo->new();";
486 let mut parser = Parser::new(source);
487 let ast = must(parser.parse());
488
489 let provider = EnhancedCodeActionsProvider::new(source.to_string());
490 let actions = provider.get_enhanced_refactoring_actions(&ast, (8, 18));
491
492 let extract_actions: Vec<_> =
493 actions.iter().filter(|a| a.title.contains("Extract")).collect();
494
495 assert!(
496 !extract_actions.is_empty(),
497 "Expected an Extract action for constructor call, got: {:?}",
498 actions.iter().map(|a| &a.title).collect::<Vec<_>>()
499 );
500
501 let action = &extract_actions[0];
503 let decl_edit = &action.edit.changes[0];
504 assert!(
505 decl_edit.new_text.contains("my $instance"),
506 "Expected variable name '$instance' for ->new(), got: {}",
507 decl_edit.new_text
508 );
509 }
510
511 #[test]
512 fn test_extract_variable_edit_structure() {
513 let source = "my $x = $obj->get();";
514 let mut parser = Parser::new(source);
515 let ast = must(parser.parse());
516
517 let provider = EnhancedCodeActionsProvider::new(source.to_string());
518 let actions = provider.get_enhanced_refactoring_actions(&ast, (8, 19));
519
520 let extract_actions: Vec<_> =
521 actions.iter().filter(|a| a.title.contains("Extract")).collect();
522
523 assert!(!extract_actions.is_empty(), "Expected at least one extract action");
524
525 let action = &extract_actions[0];
526 assert_eq!(action.edit.changes.len(), 2, "Expected exactly 2 edits (insert + replace)");
527
528 let insert_edit = &action.edit.changes[0];
530 assert!(
531 insert_edit.new_text.starts_with("my $"),
532 "First edit should be a variable declaration"
533 );
534 assert!(insert_edit.new_text.ends_with(";\n"), "Declaration should end with semicolon");
535
536 let replace_edit = &action.edit.changes[1];
538 assert!(
539 replace_edit.new_text.starts_with('$'),
540 "Second edit should be a variable reference"
541 );
542 }
543}