ricecoder_lsp/diagnostics/
python_rules.rs1use super::DiagnosticsResult;
4use crate::types::{Diagnostic, DiagnosticSeverity, Position, Range};
5
6pub fn generate_python_diagnostics(code: &str) -> DiagnosticsResult<Vec<Diagnostic>> {
8 let mut diagnostics = Vec::new();
9
10 diagnostics.extend(check_unused_imports(code));
12
13 diagnostics.extend(check_unreachable_code(code));
15
16 diagnostics.extend(check_naming_conventions(code));
18
19 Ok(diagnostics)
20}
21
22fn check_unused_imports(code: &str) -> Vec<Diagnostic> {
24 let mut diagnostics = Vec::new();
25
26 for (line_num, line) in code.lines().enumerate() {
27 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 let occurrences = code
34 .lines()
35 .enumerate()
36 .filter(|(i, l)| *i != line_num && l.contains(&name))
37 .count();
38
39 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
61fn extract_import_names(line: &str) -> Vec<(String, usize)> {
63 let mut names = Vec::new();
64
65 let trimmed = line.trim();
66
67 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 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 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 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
113fn 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 if trimmed.starts_with("return") || trimmed.starts_with("raise ") {
122 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
146fn check_naming_conventions(code: &str) -> Vec<Diagnostic> {
148 let mut diagnostics = Vec::new();
149
150 for (line_num, line) in code.lines().enumerate() {
151 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 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 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 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}