use crate::ast::{Node, NodeKind, ParseError};
use crate::position::{Position, Range};
use lsp_types::*;
use std::collections::VecDeque;
use url::Url;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DiagnosticSeverity {
Error,
Warning,
Information,
Hint,
}
impl From<DiagnosticSeverity> for lsp_types::DiagnosticSeverity {
fn from(severity: DiagnosticSeverity) -> Self {
match severity {
DiagnosticSeverity::Error => lsp_types::DiagnosticSeverity::ERROR,
DiagnosticSeverity::Warning => lsp_types::DiagnosticSeverity::WARNING,
DiagnosticSeverity::Information => lsp_types::DiagnosticSeverity::INFORMATION,
DiagnosticSeverity::Hint => lsp_types::DiagnosticSeverity::HINT,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DiagnosticCategory {
Syntax,
Runtime,
Style,
Deprecated,
Performance,
Security,
}
#[derive(Debug, Clone)]
pub struct DiagnosticProvider {
severity_config: DiagnosticConfig,
cached_diagnostics: VecDeque<Diagnostic>,
}
#[derive(Debug, Clone)]
pub struct DiagnosticConfig {
pub syntax_errors: bool,
pub style_warnings: bool,
pub performance_hints: bool,
pub security_warnings: bool,
pub max_diagnostics: usize,
}
impl Default for DiagnosticConfig {
fn default() -> Self {
Self {
syntax_errors: true,
style_warnings: true,
performance_hints: false, security_warnings: true,
max_diagnostics: 100,
}
}
}
impl DiagnosticProvider {
pub fn new() -> Self {
Self {
severity_config: DiagnosticConfig::default(),
cached_diagnostics: VecDeque::new(),
}
}
pub fn with_config(config: DiagnosticConfig) -> Self {
Self {
severity_config: config,
cached_diagnostics: VecDeque::new(),
}
}
pub fn generate_diagnostics(
&self,
result: &crate::ParseResult,
uri: Url,
) -> Vec<Diagnostic> {
let mut diagnostics = Vec::new();
if self.severity_config.syntax_errors {
for error in &result.errors {
diagnostics.push(self.convert_parse_error(error, &uri));
}
}
if let Some(ast) = &result.ast {
self.analyze_ast_for_diagnostics(ast, &uri, &mut diagnostics);
}
diagnostics.sort_by_key(|d| (d.range.start.line, d.range.start.character));
diagnostics.truncate(self.severity_config.max_diagnostics);
diagnostics
}
fn convert_parse_error(&self, error: &ParseError, uri: &Url) -> Diagnostic {
Diagnostic {
range: self.convert_range(&error.location),
severity: Some(lsp_types::DiagnosticSeverity::ERROR),
code: Some(NumberOrString::String("syntax-error".to_string())),
code_description: None,
source: Some("perl-parser".to_string()),
message: error.message.clone(),
related_information: None,
tags: None,
data: None,
}
}
fn analyze_ast_for_diagnostics(
&self,
ast: &Node,
uri: &Url,
diagnostics: &mut Vec<Diagnostic>,
) {
self.walk_ast_for_diagnostics(ast, uri, diagnostics);
}
fn walk_ast_for_diagnostics(
&self,
node: &Node,
uri: &Url,
diagnostics: &mut Vec<Diagnostic>,
) {
match &node.kind {
NodeKind::Variable { sigil, name } => {
self.check_variable_usage(sigil, name, node, uri, diagnostics);
}
NodeKind::FunctionCall { name, arguments } => {
self.check_function_call(name, arguments, node, uri, diagnostics);
}
NodeKind::Binary { op, .. } => {
self.check_binary_operation(op, node, uri, diagnostics);
}
_ => {}
}
for child in &node.children {
self.walk_ast_for_diagnostics(child, uri, diagnostics);
}
}
fn check_variable_usage(
&self,
sigil: &str,
name: &str,
node: &Node,
uri: &Url,
diagnostics: &mut Vec<Diagnostic>,
) {
if name.starts_with('_') && self.severity_config.style_warnings {
diagnostics.push(Diagnostic {
range: self.convert_node_range(node),
severity: Some(lsp_types::DiagnosticSeverity::HINT),
code: Some(NumberOrString::String("style".to_string())),
source: Some("perl-parser".to_string()),
message: format!("Variable '{}' starts with underscore, consider using 'my' to declare it", name),
tags: Some(vec![DiagnosticTag::UNNECESSARY]),
..Default::default()
});
}
}
fn check_function_call(
&self,
name: &str,
arguments: &[Node],
node: &Node,
uri: &Url,
diagnostics: &mut Vec<Diagnostic>,
) {
if self.severity_config.security_warnings {
let dangerous_functions = ["eval", "system", "exec", "backtick"];
if dangerous_functions.contains(&name) {
diagnostics.push(Diagnostic {
range: self.convert_node_range(node),
severity: Some(lsp_types::DiagnosticSeverity::WARNING),
code: Some(NumberOrString::String("security".to_string())),
source: Some("perl-parser".to_string()),
message: format!("Potentially dangerous function '{}' detected", name),
tags: None,
..Default::default()
});
}
}
}
fn check_binary_operation(
&self,
op: &str,
node: &Node,
uri: &Url,
diagnostics: &mut Vec<Diagnostic>,
) {
if self.severity_config.style_warnings && op == "eq" {
diagnostics.push(Diagnostic {
range: self.convert_node_range(node),
severity: Some(lsp_types::DiagnosticSeverity::HINT),
code: Some(NumberOrString::String("style".to_string())),
source: Some("perl-parser".to_string()),
message: "Consider using '==' instead of 'eq' for numeric comparison".to_string(),
tags: None,
..Default::default()
});
}
}
fn convert_range(&self, location: &crate::position::Location) -> Range {
Range {
start: Position {
line: location.line,
character: location.column,
},
end: Position {
line: location.line,
character: location.column + 1, },
}
}
fn convert_node_range(&self, node: &Node) -> Range {
Range {
start: Position {
line: 0,
character: 0,
},
end: Position {
line: 0,
character: 1,
},
}
}
}
impl Default for DiagnosticProvider {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_diagnostic_provider_creation() {
let provider = DiagnosticProvider::new();
assert!(provider.severity_config.syntax_errors);
assert!(provider.severity_config.style_warnings);
}
#[test]
fn test_custom_config() {
let config = DiagnosticConfig {
syntax_errors: false,
style_warnings: true,
performance_hints: true,
security_warnings: false,
max_diagnostics: 50,
};
let provider = DiagnosticProvider::with_config(config);
assert!(!provider.severity_config.syntax_errors);
assert!(provider.severity_config.style_warnings);
assert!(provider.severity_config.performance_hints);
assert!(!provider.severity_config.security_warnings);
assert_eq!(provider.severity_config.max_diagnostics, 50);
}
#[test]
fn test_severity_conversion() {
assert_eq!(
lsp_types::DiagnosticSeverity::ERROR,
DiagnosticSeverity::Error.into()
);
assert_eq!(
lsp_types::DiagnosticSeverity::WARNING,
DiagnosticSeverity::Warning.into()
);
assert_eq!(
lsp_types::DiagnosticSeverity::INFORMATION,
DiagnosticSeverity::Information.into()
);
assert_eq!(
lsp_types::DiagnosticSeverity::HINT,
DiagnosticSeverity::Hint.into()
);
}
#[test]
fn test_diagnostic_categories() {
assert_eq!(DiagnosticCategory::Syntax, DiagnosticCategory::Syntax);
assert_ne!(DiagnosticCategory::Syntax, DiagnosticCategory::Runtime);
assert_ne!(DiagnosticCategory::Style, DiagnosticCategory::Security);
}
}