ricecoder_lsp/diagnostics/
python_rules.rs

1//! Python-specific diagnostic rules
2
3use super::DiagnosticsResult;
4use crate::types::{Diagnostic, DiagnosticSeverity, Position, Range};
5
6/// Generate diagnostics for Python code
7pub fn generate_python_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        // Check for import statements
28        if line.trim().starts_with("import ") || line.trim().starts_with("from ") {
29            let imported_names = extract_import_names(line);
30
31            for (name, start_pos) in imported_names {
32                // Count occurrences of the imported name (excluding the import line itself)
33                let occurrences = code
34                    .lines()
35                    .enumerate()
36                    .filter(|(i, l)| *i != line_num && l.contains(&name))
37                    .count();
38
39                // If not used elsewhere, it might be unused
40                if occurrences == 0 {
41                    let range = Range::new(
42                        Position::new(line_num as u32, start_pos as u32),
43                        Position::new(line_num as u32, (start_pos + name.len()) as u32),
44                    );
45
46                    let diagnostic = Diagnostic::new(
47                        range,
48                        DiagnosticSeverity::Warning,
49                        format!("Unused import: `{}`", name),
50                    );
51
52                    diagnostics.push(diagnostic);
53                }
54            }
55        }
56    }
57
58    diagnostics
59}
60
61/// Extract imported names from an import statement
62fn extract_import_names(line: &str) -> Vec<(String, usize)> {
63    let mut names = Vec::new();
64
65    let trimmed = line.trim();
66
67    // Handle: from module import name1, name2
68    if trimmed.starts_with("from ") {
69        if let Some(import_pos) = trimmed.find(" import ") {
70            let imports_str = &trimmed[import_pos + 8..];
71            let mut current_pos = import_pos + 8;
72
73            for part in imports_str.split(',') {
74                let trimmed_part = part.trim();
75                if !trimmed_part.is_empty() && trimmed_part != "*" {
76                    // Handle "name as alias"
77                    let name = if let Some(as_pos) = trimmed_part.find(" as ") {
78                        trimmed_part[as_pos + 4..].trim().to_string()
79                    } else {
80                        trimmed_part.to_string()
81                    };
82
83                    names.push((name, current_pos));
84                    current_pos += part.len() + 1;
85                }
86            }
87        }
88    }
89
90    // Handle: import name1, name2
91    if let Some(imports_str) = trimmed.strip_prefix("import ") {
92        let mut current_pos = 7;
93
94        for part in imports_str.split(',') {
95            let trimmed_part = part.trim();
96            if !trimmed_part.is_empty() {
97                // Handle "name as alias"
98                let name = if let Some(as_pos) = trimmed_part.find(" as ") {
99                    trimmed_part[as_pos + 4..].trim().to_string()
100                } else {
101                    trimmed_part.to_string()
102                };
103
104                names.push((name, current_pos));
105                current_pos += part.len() + 1;
106            }
107        }
108    }
109
110    names
111}
112
113/// Check for unreachable code patterns
114fn check_unreachable_code(code: &str) -> Vec<Diagnostic> {
115    let mut diagnostics = Vec::new();
116
117    for (line_num, line) in code.lines().enumerate() {
118        let trimmed = line.trim();
119
120        // Check for code after return/raise
121        if trimmed.starts_with("return") || trimmed.starts_with("raise ") {
122            // Check if there's code on the same line after the statement
123            if let Some(pos) = line.find("return") {
124                let after_return = &line[pos + 6..].trim();
125                if !after_return.is_empty() && !after_return.starts_with('#') {
126                    let range = Range::new(
127                        Position::new(line_num as u32, (pos + 6) as u32),
128                        Position::new(line_num as u32, line.len() as u32),
129                    );
130
131                    let diagnostic = Diagnostic::new(
132                        range,
133                        DiagnosticSeverity::Warning,
134                        "Unreachable code after return statement".to_string(),
135                    );
136
137                    diagnostics.push(diagnostic);
138                }
139            }
140        }
141    }
142
143    diagnostics
144}
145
146/// Check for naming convention violations
147fn check_naming_conventions(code: &str) -> Vec<Diagnostic> {
148    let mut diagnostics = Vec::new();
149
150    for (line_num, line) in code.lines().enumerate() {
151        // Check for function definitions with non-snake_case names
152        if line.contains("def ") {
153            if let Some(def_pos) = line.find("def ") {
154                let after_def = &line[def_pos + 4..];
155                if let Some(paren_pos) = after_def.find('(') {
156                    let fn_name = after_def[..paren_pos].trim();
157
158                    // Check if name contains uppercase letters (not snake_case)
159                    if fn_name.contains(|c: char| c.is_uppercase()) {
160                        let range = Range::new(
161                            Position::new(line_num as u32, (def_pos + 4) as u32),
162                            Position::new(line_num as u32, (def_pos + 4 + fn_name.len()) as u32),
163                        );
164
165                        let diagnostic = Diagnostic::new(
166                            range,
167                            DiagnosticSeverity::Hint,
168                            format!("Function name `{}` should be in snake_case", fn_name),
169                        );
170
171                        diagnostics.push(diagnostic);
172                    }
173                }
174            }
175        }
176
177        // Check for class definitions with non-PascalCase names
178        if line.contains("class ") {
179            if let Some(class_pos) = line.find("class ") {
180                let after_class = &line[class_pos + 6..];
181                if let Some(paren_or_colon) = after_class.find(['(', ':']) {
182                    let class_name = after_class[..paren_or_colon].trim();
183
184                    // Check if name starts with lowercase (should be PascalCase)
185                    if class_name.chars().next().is_some_and(|c| c.is_lowercase()) {
186                        let range = Range::new(
187                            Position::new(line_num as u32, (class_pos + 6) as u32),
188                            Position::new(
189                                line_num as u32,
190                                (class_pos + 6 + class_name.len()) as u32,
191                            ),
192                        );
193
194                        let diagnostic = Diagnostic::new(
195                            range,
196                            DiagnosticSeverity::Hint,
197                            format!("Class name `{}` should be in PascalCase", class_name),
198                        );
199
200                        diagnostics.push(diagnostic);
201                    }
202                }
203            }
204        }
205    }
206
207    diagnostics
208}