ricecoder_lsp/diagnostics/
mod.rs

1//! Diagnostics Engine Module
2//!
3//! This module provides the diagnostics engine for analyzing code and generating
4//! diagnostics (errors, warnings, hints) for identified issues.
5//!
6//! # Architecture
7//!
8//! The diagnostics engine is organized into:
9//! - `DiagnosticsEngine`: Main trait for generating diagnostics
10//! - Language-specific rule modules: `rust_rules`, `typescript_rules`, `python_rules`
11//! - `Diagnostic` types: Error, warning, and hint severity levels
12//!
13//! # External LSP Integration
14//!
15//! The diagnostics engine integrates with external LSP servers for semantic diagnostics:
16//!
17//! 1. **External LSP First**: If an external LSP server is configured for the language,
18//!    it provides semantic diagnostics (compiler errors, type errors, etc.)
19//! 2. **Merge Results**: External diagnostics are merged with internal diagnostics
20//!    (external takes priority)
21//! 3. **Fallback**: If the external LSP is unavailable, the system falls back to
22//!    internal diagnostics engine
23//!
24//! # Fallback Behavior
25//!
26//! When external LSP is unavailable, the internal diagnostics engine provides:
27//!
28//! - **Syntax Errors**: Basic syntax validation
29//! - **Pattern-Based Warnings**: Common coding patterns and anti-patterns
30//! - **Style Issues**: Code style and formatting issues
31//! - **Language-Specific Rules**: Language-specific rules (Rust, TypeScript, Python)
32//!
33//! However, the internal engine lacks:
34//!
35//! - **Type Checking**: Cannot perform type inference or type checking
36//! - **Semantic Analysis**: Cannot resolve references or perform semantic analysis
37//! - **Project Context**: Cannot access project configuration or dependencies
38//! - **Compiler Errors**: Cannot provide actual compiler errors
39//!
40//! # Example
41//!
42//! ```ignore
43//! use ricecoder_lsp::diagnostics::DiagnosticsEngine;
44//! use ricecoder_lsp::types::Language;
45//!
46//! let engine = DefaultDiagnosticsEngine::new();
47//! let diagnostics = engine.generate_diagnostics(code, Language::Rust)?;
48//! ```
49
50pub mod adapters;
51pub mod generic_engine;
52pub mod python_rules;
53pub mod rust_rules;
54pub mod typescript_rules;
55
56pub use adapters::{
57    PythonDiagnosticsAdapter, RustDiagnosticsAdapter, TypeScriptDiagnosticsAdapter,
58};
59pub use generic_engine::GenericDiagnosticsEngine;
60
61use crate::types::{Diagnostic, Language, Range};
62use std::error::Error;
63use std::fmt;
64
65/// Error type for diagnostics operations
66#[derive(Debug, Clone)]
67pub enum DiagnosticsError {
68    /// Analysis failed
69    AnalysisFailed(String),
70    /// Invalid input
71    InvalidInput(String),
72    /// Unsupported language
73    UnsupportedLanguage(String),
74}
75
76impl fmt::Display for DiagnosticsError {
77    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
78        match self {
79            DiagnosticsError::AnalysisFailed(msg) => write!(f, "Analysis failed: {}", msg),
80            DiagnosticsError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg),
81            DiagnosticsError::UnsupportedLanguage(lang) => {
82                write!(f, "Unsupported language: {}", lang)
83            }
84        }
85    }
86}
87
88impl Error for DiagnosticsError {}
89
90/// Result type for diagnostics operations
91pub type DiagnosticsResult<T> = Result<T, DiagnosticsError>;
92
93/// Trait for generating diagnostics from code
94pub trait DiagnosticsEngine: Send + Sync {
95    /// Generate diagnostics for the given code
96    fn generate_diagnostics(
97        &self,
98        code: &str,
99        language: Language,
100    ) -> DiagnosticsResult<Vec<Diagnostic>>;
101
102    /// Generate diagnostics for a specific range
103    fn generate_diagnostics_for_range(
104        &self,
105        code: &str,
106        language: Language,
107        range: Range,
108    ) -> DiagnosticsResult<Vec<Diagnostic>>;
109}
110
111/// Default diagnostics engine implementation
112pub struct DefaultDiagnosticsEngine;
113
114impl DefaultDiagnosticsEngine {
115    /// Create a new diagnostics engine
116    pub fn new() -> Self {
117        Self
118    }
119}
120
121impl Default for DefaultDiagnosticsEngine {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127impl DiagnosticsEngine for DefaultDiagnosticsEngine {
128    fn generate_diagnostics(
129        &self,
130        code: &str,
131        language: Language,
132    ) -> DiagnosticsResult<Vec<Diagnostic>> {
133        if code.is_empty() {
134            return Ok(Vec::new());
135        }
136
137        match language {
138            Language::Rust => rust_rules::generate_rust_diagnostics(code),
139            Language::TypeScript => typescript_rules::generate_typescript_diagnostics(code),
140            Language::Python => python_rules::generate_python_diagnostics(code),
141            Language::Unknown => {
142                // Gracefully degrade for unknown languages
143                Ok(Vec::new())
144            }
145        }
146    }
147
148    fn generate_diagnostics_for_range(
149        &self,
150        code: &str,
151        language: Language,
152        range: Range,
153    ) -> DiagnosticsResult<Vec<Diagnostic>> {
154        let all_diagnostics = self.generate_diagnostics(code, language)?;
155
156        // Filter diagnostics that fall within the specified range
157        let filtered = all_diagnostics
158            .into_iter()
159            .filter(|diag| {
160                diag.range.start.line >= range.start.line && diag.range.end.line <= range.end.line
161            })
162            .collect();
163
164        Ok(filtered)
165    }
166}
167
168#[cfg(test)]
169mod tests {
170    use super::*;
171
172    #[test]
173    fn test_diagnostics_engine_empty_code() {
174        let engine = DefaultDiagnosticsEngine::new();
175        let result = engine.generate_diagnostics("", Language::Rust);
176        assert!(result.is_ok());
177        assert!(result.unwrap().is_empty());
178    }
179
180    #[test]
181    fn test_diagnostics_engine_unknown_language() {
182        let engine = DefaultDiagnosticsEngine::new();
183        let result = engine.generate_diagnostics("some code", Language::Unknown);
184        assert!(result.is_ok());
185        assert!(result.unwrap().is_empty());
186    }
187
188    #[test]
189    fn test_diagnostics_error_display() {
190        let err = DiagnosticsError::AnalysisFailed("test error".to_string());
191        assert_eq!(err.to_string(), "Analysis failed: test error");
192    }
193}