Skip to main content

perl_lsp_tooling/perl_critic/
analyzer.rs

1#[cfg(not(feature = "lsp-compat"))]
2use crate::perl_critic::ViolationSummary;
3#[cfg(feature = "lsp-compat")]
4use crate::perl_critic::perlcritic_quick_fix;
5use crate::perl_critic::{CriticConfig, QuickFix, Severity, Violation};
6use perl_lsp_critic_parser::parse_perlcritic_output;
7use perl_parser_core::position::{Position, Range};
8use perl_subprocess_runtime::SubprocessRuntime;
9use std::collections::HashMap;
10use std::path::Path;
11use std::sync::Arc;
12
13#[cfg(feature = "lsp-compat")]
14use lsp_types;
15
16/// Perl::Critic analyzer
17pub struct CriticAnalyzer {
18    /// Configuration settings for the analyzer
19    config: CriticConfig,
20    /// Cache of violations keyed by file path
21    cache: HashMap<String, Vec<Violation>>,
22    /// Subprocess runtime for executing perlcritic
23    runtime: Arc<dyn SubprocessRuntime>,
24}
25
26impl CriticAnalyzer {
27    /// Creates a new analyzer with the given configuration and runtime.
28    pub fn new(config: CriticConfig, runtime: Arc<dyn SubprocessRuntime>) -> Self {
29        Self { config, cache: HashMap::new(), runtime }
30    }
31
32    /// Creates a new analyzer with the OS subprocess runtime (non-WASM only).
33    #[cfg(not(target_arch = "wasm32"))]
34    pub fn with_os_runtime(config: CriticConfig) -> Self {
35        use perl_subprocess_runtime::OsSubprocessRuntime;
36        let timeout = config.timeout_secs;
37        Self::new(config, Arc::new(OsSubprocessRuntime::with_timeout(timeout)))
38    }
39
40    /// Run Perl::Critic on a file
41    pub fn analyze_file(&mut self, file_path: &Path) -> Result<Vec<Violation>, String> {
42        let path_str = file_path.to_string_lossy().to_string();
43        if let Some(cached) = self.cache.get(&path_str) {
44            return Ok(cached.clone());
45        }
46
47        let args = build_perlcritic_args(&self.config, &path_str);
48        let args_refs: Vec<&str> = args.iter().map(String::as_str).collect();
49        let output =
50            self.runtime.run_command("perlcritic", &args_refs, None).map_err(|e| e.message)?;
51        let violations = self.parse_output(&output.stdout, &path_str)?;
52        self.cache.insert(path_str, violations.clone());
53        Ok(violations)
54    }
55
56    /// Parse perlcritic output
57    fn parse_output(&self, output: &[u8], file_path: &str) -> Result<Vec<Violation>, String> {
58        let output_str = String::from_utf8_lossy(output);
59        Ok(parse_perlcritic_output(&output_str)
60            .into_iter()
61            .map(|parsed| Violation {
62                policy: parsed.policy.clone(),
63                description: parsed.message,
64                explanation: self.get_policy_explanation(&parsed.policy),
65                severity: Severity::from_number(parsed.severity),
66                range: Range {
67                    start: Position { byte: 0, line: parsed.line - 1, column: parsed.column - 1 },
68                    end: Position { byte: 0, line: parsed.line - 1, column: parsed.column },
69                },
70                file: file_path.to_string(),
71            })
72            .collect())
73    }
74
75    /// Get explanation for a policy
76    fn get_policy_explanation(&self, policy: &str) -> String {
77        format!("See perldoc Perl::Critic::Policy::{policy}")
78    }
79
80    /// Clear cache for a file
81    pub fn invalidate_cache(&mut self, file_path: &str) {
82        self.cache.remove(file_path);
83    }
84
85    /// Convert violations to diagnostics
86    #[cfg(feature = "lsp-compat")]
87    pub fn to_diagnostics(&self, violations: &[Violation]) -> Vec<lsp_types::Diagnostic> {
88        violations
89            .iter()
90            .map(|v| {
91                let lsp_range = lsp_types::Range::new(
92                    lsp_types::Position::new(v.range.start.line, v.range.start.column),
93                    lsp_types::Position::new(v.range.end.line, v.range.end.column),
94                );
95                lsp_types::Diagnostic {
96                    range: lsp_range,
97                    severity: Some(v.severity.to_diagnostic_severity()),
98                    code: Some(lsp_types::NumberOrString::String(v.policy.clone())),
99                    source: Some("perlcritic".to_string()),
100                    message: v.description.clone(),
101                    related_information: None,
102                    tags: None,
103                    code_description: None,
104                    data: None,
105                }
106            })
107            .collect()
108    }
109
110    /// Convert violations to violation summaries (for non-LSP contexts)
111    #[cfg(not(feature = "lsp-compat"))]
112    pub fn to_violation_summaries(&self, violations: &[Violation]) -> Vec<ViolationSummary> {
113        violations
114            .iter()
115            .map(|v| ViolationSummary {
116                policy: v.policy.clone(),
117                description: v.description.clone(),
118                severity: v.severity.to_severity_level(),
119                line: v.range.start.line as usize,
120            })
121            .collect()
122    }
123
124    /// Get quick fix for a violation
125    #[cfg(feature = "lsp-compat")]
126    pub fn get_quick_fix(&self, violation: &Violation, _content: &str) -> Option<QuickFix> {
127        perlcritic_quick_fix(violation)
128    }
129}
130
131fn build_perlcritic_args(config: &CriticConfig, path_str: &str) -> Vec<String> {
132    let mut args = vec![format!("--severity={}", config.severity)];
133
134    if let Some(profile) = &config.profile {
135        args.push(format!("--profile={profile}"));
136    }
137    if let Some(theme) = &config.theme {
138        args.push(format!("--theme={theme}"));
139    }
140    for policy in &config.include {
141        args.push(format!("--include={policy}"));
142    }
143    for policy in &config.exclude {
144        args.push(format!("--exclude={policy}"));
145    }
146
147    args.push("--verbose=%f:%l:%c:%s:%p:%m\\n".to_string());
148    args.push("--".to_string());
149    args.push(path_str.to_string());
150    args
151}