use crate::modernize;
use crate::quick_fixes;
use crate::refactors;
use crate::types::QuickFixDiagnostic;
pub use crate::types::{CodeAction, CodeActionKind};
use perl_diagnostics_codes::DiagnosticCode;
use perl_lsp_diagnostics::Diagnostic;
use perl_parser_core::Node;
fn to_quick_fix_diagnostic(diag: &Diagnostic) -> QuickFixDiagnostic {
QuickFixDiagnostic { range: diag.range, message: diag.message.clone(), code: diag.code.clone() }
}
pub struct CodeActionsProvider {
source: String,
}
impl CodeActionsProvider {
pub fn new(source: String) -> Self {
Self { source }
}
pub fn get_code_actions(
&self,
ast: &Node,
range: (usize, usize),
diagnostics: &[Diagnostic],
) -> Vec<CodeAction> {
let mut actions = Vec::new();
for diagnostic in diagnostics {
let qf_diag = to_quick_fix_diagnostic(diagnostic);
if let Some(code) = &diagnostic.code {
match code.as_str() {
c if c == DiagnosticCode::UndefinedVariable.as_str() => {
actions.extend(quick_fixes::fix_undefined_variable(&self.source, &qf_diag));
}
c if c == DiagnosticCode::UnusedVariable.as_str() => {
actions.extend(quick_fixes::fix_unused_variable(&self.source, &qf_diag));
}
c if c == DiagnosticCode::AssignmentInCondition.as_str() => {
actions.extend(quick_fixes::fix_assignment_in_condition(
&self.source,
&qf_diag,
));
}
c if c == DiagnosticCode::MissingStrict.as_str() => {
actions.extend(quick_fixes::add_use_strict());
}
c if c == DiagnosticCode::MissingWarnings.as_str() => {
actions.extend(quick_fixes::add_use_warnings());
}
c if c == DiagnosticCode::DeprecatedDefined.as_str() => {
actions.extend(quick_fixes::fix_deprecated_defined(&self.source, &qf_diag));
}
c if c == DiagnosticCode::NumericComparisonWithUndef.as_str() => {
actions.extend(quick_fixes::fix_numeric_undef(&self.source, &qf_diag));
}
c if c == DiagnosticCode::UnquotedBareword.as_str() => {
actions.extend(quick_fixes::fix_bareword(&self.source, &qf_diag));
}
c if c == DiagnosticCode::ParseError.as_str()
|| c == DiagnosticCode::SyntaxError.as_str() =>
{
actions.extend(quick_fixes::fix_parse_error(&self.source, &qf_diag, c));
}
code if code.starts_with("parse-error-") => {
actions.extend(quick_fixes::fix_parse_error(&self.source, &qf_diag, code));
}
c if c == DiagnosticCode::UnusedParameter.as_str() => {
actions.extend(quick_fixes::fix_unused_parameter(&qf_diag));
}
c if c == DiagnosticCode::VariableShadowing.as_str() => {
actions.extend(quick_fixes::fix_variable_shadowing(&qf_diag));
}
c if c == DiagnosticCode::BarewordFilehandle.as_str() => {
actions.extend(quick_fixes::fix_bareword_filehandle(&qf_diag));
}
c if c == DiagnosticCode::TwoArgOpen.as_str() => {
actions.extend(quick_fixes::fix_two_arg_open(&qf_diag));
}
_ => {}
}
}
}
if range.0 == 0 || self.source[..range.0].lines().count() <= 1 {
actions.extend(quick_fixes::fix_hardcoded_shebang(&self.source));
}
actions.extend(refactors::get_refactoring_actions(&self.source, ast, range));
actions.extend(modernize::get_modernize_actions(&self.source));
actions
}
}
#[cfg(test)]
mod tests {
use super::*;
use perl_lsp_diagnostics::DiagnosticSeverity;
use perl_parser_core::Parser;
use perl_tdd_support::must;
fn make_diagnostic(start: usize, end: usize, code: &str, msg: &str) -> Diagnostic {
Diagnostic {
range: (start, end),
severity: DiagnosticSeverity::Error,
code: Some(code.to_string()),
message: msg.to_string(),
related_information: Vec::new(),
tags: Vec::new(),
suggestion: None,
}
}
#[test]
fn test_undefined_variable_fix() {
let source = "use strict;\nprint $undefined;";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let diagnostics = vec![make_diagnostic(
18, 28, "PL103",
"Undefined variable '$undefined'",
)];
let provider = CodeActionsProvider::new(source.to_string());
let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
assert!(
actions.iter().any(|a| a.title.contains("Declare") || a.title.contains("my")),
"Expected action to declare variable, got: {:?}",
actions
);
}
#[test]
fn test_assignment_in_condition_fix() {
let source = "if ($x = 5) { }";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let diagnostics = vec![make_diagnostic(
4, 10, "PL403",
"Assignment in condition",
)];
let provider = CodeActionsProvider::new(source.to_string());
let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
assert!(
actions.iter().any(|a| a.title.contains("==")),
"Expected action to change to comparison, got: {:?}",
actions
);
}
#[test]
fn test_hardcoded_shebang_suggests_portable() {
let source = "#!/usr/bin/perl\nuse strict;\n";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let diagnostics = vec![];
let provider = CodeActionsProvider::new(source.to_string());
let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
let shebang_actions: Vec<_> =
actions.iter().filter(|a| a.title.contains("portable shebang")).collect();
assert_eq!(shebang_actions.len(), 1, "Expected one shebang action");
assert_eq!(shebang_actions[0].edit.changes[0].new_text, "#!/usr/bin/env perl");
}
#[test]
fn test_hardcoded_shebang_preserves_flags() {
let source = "#!/usr/bin/perl -w\nuse strict;\n";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let diagnostics = vec![];
let provider = CodeActionsProvider::new(source.to_string());
let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
let shebang_actions: Vec<_> =
actions.iter().filter(|a| a.title.contains("portable shebang")).collect();
assert_eq!(shebang_actions.len(), 1);
assert_eq!(shebang_actions[0].edit.changes[0].new_text, "#!/usr/bin/env perl -w");
}
#[test]
fn test_env_perl_shebang_not_flagged() {
let source = "#!/usr/bin/env perl\nuse strict;\n";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let diagnostics = vec![];
let provider = CodeActionsProvider::new(source.to_string());
let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
let shebang_actions: Vec<_> =
actions.iter().filter(|a| a.title.contains("portable shebang")).collect();
assert!(shebang_actions.is_empty(), "env perl should not be flagged");
}
#[test]
fn test_no_shebang_not_flagged() {
let source = "use strict;\nuse warnings;\n";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let diagnostics = vec![];
let provider = CodeActionsProvider::new(source.to_string());
let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
let shebang_actions: Vec<_> =
actions.iter().filter(|a| a.title.contains("portable shebang")).collect();
assert!(shebang_actions.is_empty(), "No shebang should not be flagged");
}
#[test]
fn test_local_bin_perl_shebang() {
let source = "#!/usr/local/bin/perl\nuse strict;\n";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let diagnostics = vec![];
let provider = CodeActionsProvider::new(source.to_string());
let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
let shebang_actions: Vec<_> =
actions.iter().filter(|a| a.title.contains("portable shebang")).collect();
assert_eq!(shebang_actions.len(), 1, "Local bin perl should be flagged");
assert_eq!(shebang_actions[0].edit.changes[0].new_text, "#!/usr/bin/env perl");
}
#[test]
fn test_shebang_with_taint_flag() {
let source = "#!/usr/bin/perl -T\nuse strict;\n";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let diagnostics = vec![];
let provider = CodeActionsProvider::new(source.to_string());
let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
let shebang_actions: Vec<_> =
actions.iter().filter(|a| a.title.contains("portable shebang")).collect();
assert_eq!(shebang_actions.len(), 1);
assert_eq!(shebang_actions[0].edit.changes[0].new_text, "#!/usr/bin/env perl -T");
}
#[test]
fn test_bash_shebang_not_flagged() {
let source = "#!/bin/bash\necho hello\n";
let mut parser = Parser::new(source);
let ast = must(parser.parse());
let diagnostics = vec![];
let provider = CodeActionsProvider::new(source.to_string());
let actions = provider.get_code_actions(&ast, (0, source.len()), &diagnostics);
let shebang_actions: Vec<_> =
actions.iter().filter(|a| a.title.contains("portable shebang")).collect();
assert!(shebang_actions.is_empty(), "Non-perl shebang should not be flagged");
}
}