1use crate::modernize;
69use crate::quick_fixes;
70use crate::refactors;
71use crate::types::QuickFixDiagnostic;
72
73pub use crate::types::{CodeAction, CodeActionKind};
74
75use perl_diagnostics_codes::DiagnosticCode;
76use perl_lsp_diagnostics::Diagnostic;
77use perl_parser_core::Node;
78
79fn to_quick_fix_diagnostic(diag: &Diagnostic) -> QuickFixDiagnostic {
83 QuickFixDiagnostic { range: diag.range, message: diag.message.clone(), code: diag.code.clone() }
84}
85
86pub struct CodeActionsProvider {
91 source: String,
92}
93
94impl CodeActionsProvider {
95 pub fn new(source: String) -> Self {
97 Self { source }
98 }
99
100 pub fn get_code_actions(
102 &self,
103 ast: &Node,
104 range: (usize, usize),
105 diagnostics: &[Diagnostic],
106 ) -> Vec<CodeAction> {
107 let mut actions = Vec::new();
108
109 for diagnostic in diagnostics {
111 let qf_diag = to_quick_fix_diagnostic(diagnostic);
112 if let Some(code) = &diagnostic.code {
113 match code.as_str() {
114 c if c == DiagnosticCode::UndefinedVariable.as_str() => {
116 actions.extend(quick_fixes::fix_undefined_variable(&self.source, &qf_diag));
117 }
118 c if c == DiagnosticCode::UnusedVariable.as_str() => {
120 actions.extend(quick_fixes::fix_unused_variable(&self.source, &qf_diag));
121 }
122 c if c == DiagnosticCode::AssignmentInCondition.as_str() => {
124 actions.extend(quick_fixes::fix_assignment_in_condition(
125 &self.source,
126 &qf_diag,
127 ));
128 }
129 c if c == DiagnosticCode::MissingStrict.as_str() => {
131 actions.extend(quick_fixes::add_use_strict());
132 }
133 c if c == DiagnosticCode::MissingWarnings.as_str() => {
135 actions.extend(quick_fixes::add_use_warnings());
136 }
137 c if c == DiagnosticCode::DeprecatedDefined.as_str() => {
139 actions.extend(quick_fixes::fix_deprecated_defined(&self.source, &qf_diag));
140 }
141 c if c == DiagnosticCode::NumericComparisonWithUndef.as_str() => {
143 actions.extend(quick_fixes::fix_numeric_undef(&self.source, &qf_diag));
144 }
145 c if c == DiagnosticCode::UnquotedBareword.as_str() => {
147 actions.extend(quick_fixes::fix_bareword(&self.source, &qf_diag));
148 }
149 c if c == DiagnosticCode::ParseError.as_str()
152 || c == DiagnosticCode::SyntaxError.as_str() =>
153 {
154 actions.extend(quick_fixes::fix_parse_error(&self.source, &qf_diag, c));
155 }
156 code if code.starts_with("parse-error-") => {
158 actions.extend(quick_fixes::fix_parse_error(&self.source, &qf_diag, code));
159 }
160 c if c == DiagnosticCode::UnusedParameter.as_str() => {
162 actions.extend(quick_fixes::fix_unused_parameter(&qf_diag));
163 }
164 c if c == DiagnosticCode::VariableShadowing.as_str() => {
166 actions.extend(quick_fixes::fix_variable_shadowing(&qf_diag));
167 }
168 c if c == DiagnosticCode::BarewordFilehandle.as_str() => {
170 actions.extend(quick_fixes::fix_bareword_filehandle(&qf_diag));
171 }
172 c if c == DiagnosticCode::TwoArgOpen.as_str() => {
174 actions.extend(quick_fixes::fix_two_arg_open(&qf_diag));
175 }
176 _ => {}
177 }
178 }
179 }
180
181 if range.0 == 0 || self.source[..range.0].lines().count() <= 1 {
184 actions.extend(quick_fixes::fix_hardcoded_shebang(&self.source));
185 }
186
187 actions.extend(refactors::get_refactoring_actions(&self.source, ast, range));
189
190 actions.extend(modernize::get_modernize_actions(&self.source));
192
193 actions
194 }
195}
196
197#[cfg(test)]
198mod tests {
199 use super::*;
200 use perl_lsp_diagnostics::DiagnosticSeverity;
201 use perl_parser_core::Parser;
202 use perl_tdd_support::must;
203
204 fn make_diagnostic(start: usize, end: usize, code: &str, msg: &str) -> Diagnostic {
206 Diagnostic {
207 range: (start, end),
208 severity: DiagnosticSeverity::Error,
209 code: Some(code.to_string()),
210 message: msg.to_string(),
211 related_information: Vec::new(),
212 tags: Vec::new(),
213 suggestion: None,
214 }
215 }
216
217 #[test]
218 fn test_undefined_variable_fix() {
219 let source = "use strict;\nprint $undefined;";
220 let mut parser = Parser::new(source);
221 let ast = must(parser.parse());
222
223 let diagnostics = vec![make_diagnostic(
226 18, 28, "PL103",
229 "Undefined variable '$undefined'",
230 )];
231
232 let provider = CodeActionsProvider::new(source.to_string());
233 let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
234
235 assert!(
236 actions.iter().any(|a| a.title.contains("Declare") || a.title.contains("my")),
237 "Expected action to declare variable, got: {:?}",
238 actions
239 );
240 }
241
242 #[test]
243 fn test_assignment_in_condition_fix() {
244 let source = "if ($x = 5) { }";
245 let mut parser = Parser::new(source);
246 let ast = must(parser.parse());
247
248 let diagnostics = vec![make_diagnostic(
251 4, 10, "PL403",
254 "Assignment in condition",
255 )];
256
257 let provider = CodeActionsProvider::new(source.to_string());
258 let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
259
260 assert!(
261 actions.iter().any(|a| a.title.contains("==")),
262 "Expected action to change to comparison, got: {:?}",
263 actions
264 );
265 }
266
267 #[test]
268 fn test_hardcoded_shebang_suggests_portable() {
269 let source = "#!/usr/bin/perl\nuse strict;\n";
270 let mut parser = Parser::new(source);
271 let ast = must(parser.parse());
272 let diagnostics = vec![];
273
274 let provider = CodeActionsProvider::new(source.to_string());
275 let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
276
277 let shebang_actions: Vec<_> =
278 actions.iter().filter(|a| a.title.contains("portable shebang")).collect();
279
280 assert_eq!(shebang_actions.len(), 1, "Expected one shebang action");
281 assert_eq!(shebang_actions[0].edit.changes[0].new_text, "#!/usr/bin/env perl");
282 }
283
284 #[test]
285 fn test_hardcoded_shebang_preserves_flags() {
286 let source = "#!/usr/bin/perl -w\nuse strict;\n";
287 let mut parser = Parser::new(source);
288 let ast = must(parser.parse());
289 let diagnostics = vec![];
290
291 let provider = CodeActionsProvider::new(source.to_string());
292 let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
293
294 let shebang_actions: Vec<_> =
295 actions.iter().filter(|a| a.title.contains("portable shebang")).collect();
296
297 assert_eq!(shebang_actions.len(), 1);
298 assert_eq!(shebang_actions[0].edit.changes[0].new_text, "#!/usr/bin/env perl -w");
299 }
300
301 #[test]
302 fn test_env_perl_shebang_not_flagged() {
303 let source = "#!/usr/bin/env perl\nuse strict;\n";
304 let mut parser = Parser::new(source);
305 let ast = must(parser.parse());
306 let diagnostics = vec![];
307
308 let provider = CodeActionsProvider::new(source.to_string());
309 let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
310
311 let shebang_actions: Vec<_> =
312 actions.iter().filter(|a| a.title.contains("portable shebang")).collect();
313
314 assert!(shebang_actions.is_empty(), "env perl should not be flagged");
315 }
316
317 #[test]
318 fn test_no_shebang_not_flagged() {
319 let source = "use strict;\nuse warnings;\n";
320 let mut parser = Parser::new(source);
321 let ast = must(parser.parse());
322 let diagnostics = vec![];
323
324 let provider = CodeActionsProvider::new(source.to_string());
325 let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
326
327 let shebang_actions: Vec<_> =
328 actions.iter().filter(|a| a.title.contains("portable shebang")).collect();
329
330 assert!(shebang_actions.is_empty(), "No shebang should not be flagged");
331 }
332
333 #[test]
334 fn test_local_bin_perl_shebang() {
335 let source = "#!/usr/local/bin/perl\nuse strict;\n";
336 let mut parser = Parser::new(source);
337 let ast = must(parser.parse());
338 let diagnostics = vec![];
339
340 let provider = CodeActionsProvider::new(source.to_string());
341 let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
342
343 let shebang_actions: Vec<_> =
344 actions.iter().filter(|a| a.title.contains("portable shebang")).collect();
345
346 assert_eq!(shebang_actions.len(), 1, "Local bin perl should be flagged");
347 assert_eq!(shebang_actions[0].edit.changes[0].new_text, "#!/usr/bin/env perl");
348 }
349
350 #[test]
351 fn test_shebang_with_taint_flag() {
352 let source = "#!/usr/bin/perl -T\nuse strict;\n";
353 let mut parser = Parser::new(source);
354 let ast = must(parser.parse());
355 let diagnostics = vec![];
356
357 let provider = CodeActionsProvider::new(source.to_string());
358 let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
359
360 let shebang_actions: Vec<_> =
361 actions.iter().filter(|a| a.title.contains("portable shebang")).collect();
362
363 assert_eq!(shebang_actions.len(), 1);
364 assert_eq!(shebang_actions[0].edit.changes[0].new_text, "#!/usr/bin/env perl -T");
365 }
366
367 #[test]
368 fn test_bash_shebang_not_flagged() {
369 let source = "#!/bin/bash\necho hello\n";
370 let mut parser = Parser::new(source);
371 let ast = must(parser.parse());
372 let diagnostics = vec![];
373
374 let provider = CodeActionsProvider::new(source.to_string());
375 let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
376
377 let shebang_actions: Vec<_> =
378 actions.iter().filter(|a| a.title.contains("portable shebang")).collect();
379
380 assert!(shebang_actions.is_empty(), "Non-perl shebang should not be flagged");
381 }
382}