ricecoder_lsp/diagnostics/
rust_rules.rs

1//! Rust-specific diagnostic rules
2
3use super::DiagnosticsResult;
4use crate::types::{Diagnostic, DiagnosticSeverity, Position, Range};
5
6/// Generate diagnostics for Rust code
7pub fn generate_rust_diagnostics(code: &str) -> DiagnosticsResult<Vec<Diagnostic>> {
8    let mut diagnostics = Vec::new();
9
10    // Check for unused imports
11    diagnostics.extend(check_unused_imports(code));
12
13    // Check for unreachable code patterns
14    diagnostics.extend(check_unreachable_code(code));
15
16    // Check for naming conventions
17    diagnostics.extend(check_naming_conventions(code));
18
19    Ok(diagnostics)
20}
21
22/// Check for unused imports
23fn check_unused_imports(code: &str) -> Vec<Diagnostic> {
24    let mut diagnostics = Vec::new();
25
26    for (line_num, line) in code.lines().enumerate() {
27        // Simple heuristic: look for use statements that might be unused
28        if line.trim().starts_with("use ") && line.contains("::") {
29            // Check if the imported name appears elsewhere in the code
30            let parts: Vec<&str> = line.split("::").collect();
31            if let Some(last_part) = parts.last() {
32                let imported_name = last_part.trim().trim_end_matches(';').trim_end_matches(',');
33
34                // Count occurrences of the imported name (excluding the import line itself)
35                let occurrences = code
36                    .lines()
37                    .enumerate()
38                    .filter(|(i, l)| *i != line_num && l.contains(imported_name))
39                    .count();
40
41                // If not used elsewhere, it might be unused
42                if occurrences == 0 && !imported_name.is_empty() {
43                    let range = Range::new(
44                        Position::new(line_num as u32, 0),
45                        Position::new(line_num as u32, line.len() as u32),
46                    );
47
48                    let diagnostic = Diagnostic::new(
49                        range,
50                        DiagnosticSeverity::Warning,
51                        format!("Unused import: `{}`", imported_name),
52                    );
53
54                    diagnostics.push(diagnostic);
55                }
56            }
57        }
58    }
59
60    diagnostics
61}
62
63/// Check for unreachable code patterns
64fn check_unreachable_code(code: &str) -> Vec<Diagnostic> {
65    let mut diagnostics = Vec::new();
66
67    for (line_num, line) in code.lines().enumerate() {
68        let trimmed = line.trim();
69
70        // Check for code after return/panic/unreachable
71        if trimmed.starts_with("return")
72            || trimmed.starts_with("panic!")
73            || trimmed.starts_with("unreachable!")
74        {
75            // Check if there's code on the same line after the statement
76            if let Some(pos) = line.find("return") {
77                let after_return = &line[pos + 6..].trim();
78                if !after_return.is_empty()
79                    && !after_return.starts_with(';')
80                    && !after_return.starts_with("(")
81                {
82                    let range = Range::new(
83                        Position::new(line_num as u32, (pos + 6) as u32),
84                        Position::new(line_num as u32, line.len() as u32),
85                    );
86
87                    let diagnostic = Diagnostic::new(
88                        range,
89                        DiagnosticSeverity::Warning,
90                        "Unreachable code after return statement".to_string(),
91                    );
92
93                    diagnostics.push(diagnostic);
94                }
95            }
96        }
97    }
98
99    diagnostics
100}
101
102/// Check for naming convention violations
103fn check_naming_conventions(code: &str) -> Vec<Diagnostic> {
104    let mut diagnostics = Vec::new();
105
106    for (line_num, line) in code.lines().enumerate() {
107        // Check for function definitions with non-snake_case names
108        if line.contains("fn ") {
109            if let Some(fn_pos) = line.find("fn ") {
110                let after_fn = &line[fn_pos + 3..];
111                if let Some(paren_pos) = after_fn.find('(') {
112                    let fn_name = after_fn[..paren_pos].trim();
113
114                    // Check if name contains uppercase letters (not snake_case)
115                    if fn_name.contains(|c: char| c.is_uppercase()) {
116                        let range = Range::new(
117                            Position::new(line_num as u32, (fn_pos + 3) as u32),
118                            Position::new(line_num as u32, (fn_pos + 3 + fn_name.len()) as u32),
119                        );
120
121                        let diagnostic = Diagnostic::new(
122                            range,
123                            DiagnosticSeverity::Hint,
124                            format!("Function name `{}` should be in snake_case", fn_name),
125                        );
126
127                        diagnostics.push(diagnostic);
128                    }
129                }
130            }
131        }
132
133        // Check for const definitions with non-UPPER_CASE names
134        if line.contains("const ") {
135            if let Some(const_pos) = line.find("const ") {
136                let after_const = &line[const_pos + 6..];
137                if let Some(colon_pos) = after_const.find(':') {
138                    let const_name = after_const[..colon_pos].trim();
139
140                    // Check if name is not all uppercase
141                    if !const_name
142                        .chars()
143                        .all(|c| c.is_uppercase() || c == '_' || c.is_numeric())
144                    {
145                        let range = Range::new(
146                            Position::new(line_num as u32, (const_pos + 6) as u32),
147                            Position::new(
148                                line_num as u32,
149                                (const_pos + 6 + const_name.len()) as u32,
150                            ),
151                        );
152
153                        let diagnostic = Diagnostic::new(
154                            range,
155                            DiagnosticSeverity::Hint,
156                            format!("Constant name `{}` should be in UPPER_CASE", const_name),
157                        );
158
159                        diagnostics.push(diagnostic);
160                    }
161                }
162            }
163        }
164    }
165
166    diagnostics
167}