ricecoder_lsp/diagnostics/
typescript_rules.rs1use super::DiagnosticsResult;
4use crate::types::{Diagnostic, DiagnosticSeverity, Position, Range};
5
6pub fn generate_typescript_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 ") {
29 let imported_names = extract_import_names(line);
31
32 for (name, start_pos) in imported_names {
33 let occurrences = code
35 .lines()
36 .enumerate()
37 .filter(|(i, l)| *i != line_num && l.contains(&name))
38 .count();
39
40 if occurrences == 0 {
42 let range = Range::new(
43 Position::new(line_num as u32, start_pos as u32),
44 Position::new(line_num as u32, (start_pos + name.len()) as u32),
45 );
46
47 let diagnostic = Diagnostic::new(
48 range,
49 DiagnosticSeverity::Warning,
50 format!("Unused import: `{}`", name),
51 );
52
53 diagnostics.push(diagnostic);
54 }
55 }
56 }
57 }
58
59 diagnostics
60}
61
62fn extract_import_names(line: &str) -> Vec<(String, usize)> {
64 let mut names = Vec::new();
65
66 if let Some(start) = line.find('{') {
68 if let Some(end) = line.find('}') {
69 let imports_str = &line[start + 1..end];
70 let mut current_pos = start + 1;
71
72 for part in imports_str.split(',') {
73 let trimmed = part.trim();
74 if !trimmed.is_empty() {
75 let name = if let Some(as_pos) = trimmed.find(" as ") {
77 trimmed[as_pos + 4..].trim().to_string()
78 } else {
79 trimmed.to_string()
80 };
81
82 names.push((name, current_pos));
83 current_pos += part.len() + 1;
84 }
85 }
86 }
87 }
88
89 if let Some(from_pos) = line.find(" from ") {
91 let before_from = &line[7..from_pos]; let name = before_from.trim().to_string();
93 if !name.is_empty() && !name.contains('{') {
94 names.push((name, 7));
95 }
96 }
97
98 names
99}
100
101fn check_unreachable_code(code: &str) -> Vec<Diagnostic> {
103 let mut diagnostics = Vec::new();
104
105 for (line_num, line) in code.lines().enumerate() {
106 let trimmed = line.trim();
107
108 if trimmed.starts_with("return") || trimmed.starts_with("throw ") {
110 if let Some(pos) = line.find("return") {
112 let after_return = &line[pos + 6..].trim();
113 if !after_return.is_empty() && !after_return.starts_with(';') {
114 let range = Range::new(
115 Position::new(line_num as u32, (pos + 6) as u32),
116 Position::new(line_num as u32, line.len() as u32),
117 );
118
119 let diagnostic = Diagnostic::new(
120 range,
121 DiagnosticSeverity::Warning,
122 "Unreachable code after return statement".to_string(),
123 );
124
125 diagnostics.push(diagnostic);
126 }
127 }
128 }
129 }
130
131 diagnostics
132}
133
134fn check_naming_conventions(code: &str) -> Vec<Diagnostic> {
136 let mut diagnostics = Vec::new();
137
138 for (line_num, line) in code.lines().enumerate() {
139 if line.contains("function ") {
141 if let Some(fn_pos) = line.find("function ") {
142 let after_fn = &line[fn_pos + 9..];
143 if let Some(paren_pos) = after_fn.find('(') {
144 let fn_name = after_fn[..paren_pos].trim();
145
146 if fn_name.chars().next().is_some_and(|c| c.is_uppercase()) {
148 let range = Range::new(
149 Position::new(line_num as u32, (fn_pos + 9) as u32),
150 Position::new(line_num as u32, (fn_pos + 9 + fn_name.len()) as u32),
151 );
152
153 let diagnostic = Diagnostic::new(
154 range,
155 DiagnosticSeverity::Hint,
156 format!("Function name `{}` should be in camelCase", fn_name),
157 );
158
159 diagnostics.push(diagnostic);
160 }
161 }
162 }
163 }
164
165 if line.contains("class ") {
167 if let Some(class_pos) = line.find("class ") {
168 let after_class = &line[class_pos + 6..];
169 if let Some(space_or_brace) =
170 after_class.find(|c: char| c.is_whitespace() || c == '{')
171 {
172 let class_name = after_class[..space_or_brace].trim();
173
174 if class_name.chars().next().is_some_and(|c| c.is_lowercase()) {
176 let range = Range::new(
177 Position::new(line_num as u32, (class_pos + 6) as u32),
178 Position::new(
179 line_num as u32,
180 (class_pos + 6 + class_name.len()) as u32,
181 ),
182 );
183
184 let diagnostic = Diagnostic::new(
185 range,
186 DiagnosticSeverity::Hint,
187 format!("Class name `{}` should be in PascalCase", class_name),
188 );
189
190 diagnostics.push(diagnostic);
191 }
192 }
193 }
194 }
195 }
196
197 diagnostics
198}