ricecoder_lsp/diagnostics/
typescript_rules.rs

1//! TypeScript-specific diagnostic rules
2
3use super::DiagnosticsResult;
4use crate::types::{Diagnostic, DiagnosticSeverity, Position, Range};
5
6/// Generate diagnostics for TypeScript code
7pub fn generate_typescript_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 ") {
29            // Extract imported names
30            let imported_names = extract_import_names(line);
31
32            for (name, start_pos) in imported_names {
33                // Count occurrences of the imported name (excluding the import line itself)
34                let occurrences = code
35                    .lines()
36                    .enumerate()
37                    .filter(|(i, l)| *i != line_num && l.contains(&name))
38                    .count();
39
40                // If not used elsewhere, it might be unused
41                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
62/// Extract imported names from an import statement
63fn extract_import_names(line: &str) -> Vec<(String, usize)> {
64    let mut names = Vec::new();
65
66    // Handle: import { name1, name2 } from "module"
67    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                    // Handle "name as alias"
76                    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    // Handle: import name from "module"
90    if let Some(from_pos) = line.find(" from ") {
91        let before_from = &line[7..from_pos]; // Skip "import "
92        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
101/// Check for unreachable code patterns
102fn 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        // Check for code after return/throw
109        if trimmed.starts_with("return") || trimmed.starts_with("throw ") {
110            // Check if there's code on the same line after the statement
111            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
134/// Check for naming convention violations
135fn check_naming_conventions(code: &str) -> Vec<Diagnostic> {
136    let mut diagnostics = Vec::new();
137
138    for (line_num, line) in code.lines().enumerate() {
139        // Check for function definitions with non-camelCase names
140        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                    // Check if name starts with uppercase (should be camelCase)
147                    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        // Check for class definitions with non-PascalCase names
166        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                    // Check if name starts with lowercase (should be PascalCase)
175                    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}