garbage_code_hunter/language/adapter/
swift.rs1use super::{
4 count_dead_code_with, count_duplicate_imports_with, is_boolean_or_null, is_common_safe_number,
5 is_inside_declaration, is_repeating_chars, FunctionNode, LanguageAdapter, MEANINGLESS_NAMES,
6};
7use crate::language::Language;
8use crate::treesitter::engine::ParsedFile;
9use crate::treesitter::query::QueryCapture;
10use regex::Regex;
11use std::sync::LazyLock;
12
13const SWIFT_PATTERNS: &[&str] = &[
14 "(call_expression (simple_identifier) @pc_f (#match? @pc_f \"^(fatalError|preconditionFailure|assert|assertionFailure|precondition)$\"))",
15 "(function_declaration (simple_identifier) @ex_name) @ex_fn",
16 "(property_declaration (pattern (simple_identifier) @nv_var))",
17 "(call_expression (simple_identifier) @dp_f (#match? @dp_f \"^(print|debugPrint|dump|NSLog)$\"))",
18 "(function_declaration) @ep_fn",
19 "[(integer_literal) @mn_num (real_literal) @mn_num]",
20];
21
22pub struct SwiftAdapter;
23
24impl LanguageAdapter for SwiftAdapter {
25 fn language(&self) -> Language {
26 Language::Swift
27 }
28
29 fn query_patterns(&self) -> &[&str] {
30 SWIFT_PATTERNS
31 }
32
33 fn count_panic_calls(&self, file: &ParsedFile) -> usize {
34 self.count_panic_from_batch(file, &self.batch_captures(file))
35 }
36
37 fn extract_functions(&self, file: &ParsedFile) -> Vec<FunctionNode> {
38 self.extract_functions_from_batch(file, &self.batch_captures(file))
39 }
40
41 fn max_nesting_depth(&self, file: &ParsedFile) -> usize {
42 fn swift_scope_depth(node: tree_sitter::Node, depth: usize) -> usize {
43 let mut max = depth;
44 for i in 0..node.child_count() {
45 if let Some(child) = node.child(i as u32) {
46 let child_depth = match child.kind() {
47 "function_body" => depth + 1,
48 _ => depth,
49 };
50 max = max.max(swift_scope_depth(child, child_depth));
51 }
52 }
53 max
54 }
55 swift_scope_depth(file.root_node(), 0)
56 }
57
58 fn count_naming_violations(&self, file: &ParsedFile) -> usize {
59 self.count_naming_from_batch(file, &self.batch_captures(file))
60 }
61
62 fn count_deeply_nested_blocks(&self, file: &ParsedFile) -> usize {
63 fn walk_swift_nodes(
64 node: tree_sitter::Node,
65 depth: usize,
66 threshold: usize,
67 count: &mut usize,
68 ) {
69 if node.kind() == "function_body" && depth >= threshold {
70 *count += 1;
71 }
72 let child_depth = match node.kind() {
73 "function_body" => depth + 1,
74 _ => depth,
75 };
76 for i in 0..node.child_count() {
77 if let Some(child) = node.child(i as u32) {
78 walk_swift_nodes(child, child_depth, threshold, count);
79 }
80 }
81 }
82 let mut count = 0;
83 walk_swift_nodes(file.root_node(), 0, 5, &mut count);
84 count
85 }
86
87 fn count_debug_calls(&self, file: &ParsedFile) -> usize {
88 self.count_debug_from_batch(file, &self.batch_captures(file))
89 }
90
91 fn count_excessive_params(&self, file: &ParsedFile, threshold: usize) -> usize {
92 self.count_excessive_from_batch_with(file, &self.batch_captures(file), threshold)
93 }
94
95 fn count_magic_numbers(&self, file: &ParsedFile) -> usize {
96 self.count_magic_from_batch(file, &self.batch_captures(file))
97 }
98
99 fn count_dead_code(&self, file: &ParsedFile) -> usize {
100 count_dead_code_with(
101 file,
102 &["return", "break", "continue"],
103 &["return ", "throw ", "fatalError(", "preconditionFailure("],
104 "//",
105 )
106 }
107
108 fn count_duplicate_imports(&self, file: &ParsedFile) -> usize {
109 count_duplicate_imports_with(file, &["import "])
110 }
111
112 fn count_swift_issues(&self, file: &ParsedFile) -> usize {
113 let mut count = 0;
114 for line in file.content.lines() {
115 let t = line.trim();
116 if t.starts_with("//") || t.starts_with("/*") || t.starts_with("*") {
117 continue;
118 }
119 if t.contains("try!") {
120 count += 1;
121 }
122 if t.contains("as!") {
123 count += 1;
124 }
125 }
126 count
127 }
128
129 fn count_panic_from_batch<'a>(
130 &self,
131 _file: &ParsedFile,
132 batch: &[Vec<QueryCapture<'a>>],
133 ) -> usize {
134 batch
135 .iter()
136 .filter(|m| m.iter().any(|c| c.name == "pc_f"))
137 .count()
138 }
139
140 fn extract_functions_from_batch<'a>(
141 &self,
142 _file: &ParsedFile,
143 batch: &[Vec<QueryCapture<'a>>],
144 ) -> Vec<FunctionNode> {
145 let mut functions = Vec::new();
146 for m in batch {
147 let has_ex = m.iter().any(|c| c.name.starts_with("ex_"));
148 if !has_ex {
149 continue;
150 }
151 let mut name = String::new();
152 let mut start_line = 0usize;
153 let mut end_line = 0usize;
154 for c in m {
155 match c.name.as_str() {
156 "ex_name" => name = c.text.to_string(),
157 "ex_fn" => {
158 start_line = c.node.start_position().row + 1;
159 end_line = c.node.end_position().row + 1;
160 }
161 _ => {}
162 }
163 }
164 if !name.is_empty() {
165 functions.push(FunctionNode {
166 name,
167 start_line,
168 end_line,
169 nesting_depth: 0,
170 });
171 }
172 }
173 functions
174 }
175
176 fn count_naming_from_batch<'a>(
177 &self,
178 _file: &ParsedFile,
179 batch: &[Vec<QueryCapture<'a>>],
180 ) -> usize {
181 let mut count = 0usize;
182 static TERRIBLE_RE: LazyLock<Option<Regex>> = LazyLock::new(|| {
183 Regex::new(r"^(data|info|temp|tmp|val|value|thing|stuff|obj|object|manager|handler|helper|util|utils)(\d+)?$").ok()
184 });
185 let terrible_re = TERRIBLE_RE.as_ref();
186 let idiomatic_single: &[&str] = &["i", "j", "k", "n", "e", "x"];
187
188 for m in batch {
189 for c in m {
190 if c.name == "nv_var" {
191 let name = c.text;
192 if name.len() == 1 && name.chars().all(|ch| ch.is_ascii_lowercase()) {
193 if !idiomatic_single.contains(&name) {
194 count += 1;
195 }
196 continue;
197 }
198 if let Some(re) = terrible_re {
199 if re.is_match(&name.to_lowercase()) {
200 count += 1;
201 continue;
202 }
203 }
204 if MEANINGLESS_NAMES.contains(&name) || is_repeating_chars(name) {
205 count += 1;
206 continue;
207 }
208 }
209 }
210 }
211 count
212 }
213
214 fn count_debug_from_batch<'a>(
215 &self,
216 _file: &ParsedFile,
217 batch: &[Vec<QueryCapture<'a>>],
218 ) -> usize {
219 batch
220 .iter()
221 .filter(|m| m.iter().any(|c| c.name == "dp_f"))
222 .count()
223 }
224
225 fn count_excessive_from_batch<'a>(
226 &self,
227 _file: &ParsedFile,
228 batch: &[Vec<QueryCapture<'a>>],
229 ) -> usize {
230 self.count_excessive_from_batch_with(_file, batch, 5)
231 }
232
233 fn count_magic_from_batch<'a>(
234 &self,
235 _file: &ParsedFile,
236 batch: &[Vec<QueryCapture<'a>>],
237 ) -> usize {
238 let mut count = 0;
239 for m in batch {
240 for c in m {
241 if c.name == "mn_num" && !is_inside_declaration(c.node) {
242 let text = c.text;
243 if text != "0"
244 && text != "1"
245 && text != "-1"
246 && !is_common_safe_number(text)
247 && !is_boolean_or_null(text)
248 {
249 count += 1;
250 }
251 }
252 }
253 }
254 count
255 }
256}
257
258impl SwiftAdapter {
259 fn count_excessive_from_batch_with<'a>(
260 &self,
261 _file: &ParsedFile,
262 batch: &[Vec<QueryCapture<'a>>],
263 threshold: usize,
264 ) -> usize {
265 let mut count = 0;
266 for m in batch {
267 for c in m {
268 if c.name == "ep_fn" {
269 let text = c.text;
270 let params = text.split('(').nth(1).and_then(|s| s.rsplit(')').nth(1));
271 if let Some(p) = params {
272 let p = p.trim();
273 if p.is_empty() {
274 continue;
275 }
276 let param_count = p.split(',').count();
277 if param_count > threshold {
278 count += 1;
279 }
280 }
281 }
282 }
283 }
284 count
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::super::parse_code;
291 use super::*;
292
293 fn parse_swift(code: &str) -> ParsedFile {
294 parse_code(code, "test.swift").expect("parse")
295 }
296
297 #[test]
298 fn test_swift_count_panic_fatal_error() {
299 let code = r#"
300func main() {
301 fatalError("boom")
302 preconditionFailure("bad")
303}
304"#;
305 let file = parse_swift(code);
306 let adapter = SwiftAdapter;
307 assert_eq!(adapter.count_panic_calls(&file), 2);
308 }
309
310 #[test]
311 fn test_swift_count_panic_clean() {
312 let code = "func add(x: Int) -> Int { return x + 1 }\n";
313 let file = parse_swift(code);
314 let adapter = SwiftAdapter;
315 assert_eq!(adapter.count_panic_calls(&file), 0);
316 }
317
318 #[test]
319 fn test_swift_extract_functions() {
320 let code = r#"
321func foo() {}
322func bar(x: Int) -> Int { return x }
323"#;
324 let file = parse_swift(code);
325 let adapter = SwiftAdapter;
326 let fns = adapter.extract_functions(&file);
327 assert_eq!(fns.len(), 2);
328 assert_eq!(fns[0].name, "foo");
329 assert_eq!(fns[1].name, "bar");
330 }
331
332 #[test]
333 fn test_swift_naming_single_letter() {
334 let code = r#"
335func main() {
336 let a = 1
337 var b = 2
338}
339"#;
340 let file = parse_swift(code);
341 let adapter = SwiftAdapter;
342 assert_eq!(adapter.count_naming_violations(&file), 2);
343 }
344
345 #[test]
346 fn test_swift_debug_print() {
347 let code = r#"
348print("hello")
349debugPrint(x)
350dump(obj)
351"#;
352 let file = parse_swift(code);
353 let adapter = SwiftAdapter;
354 assert_eq!(adapter.count_debug_calls(&file), 3);
355 }
356
357 #[test]
358 fn test_swift_excessive_params() {
359 let code = "func process(a: Int, b: Int, c: Int, d: Int, e: Int, f: Int) {}\n";
360 let file = parse_swift(code);
361 let adapter = SwiftAdapter;
362 assert_eq!(adapter.count_excessive_params(&file, 5), 1);
363 }
364
365 #[test]
366 fn test_swift_magic_numbers() {
367 let code = r#"
368func main() {
369 foo(41)
370 bar(100)
371}
372"#;
373 let file = parse_swift(code);
374 let adapter = SwiftAdapter;
375 assert_eq!(adapter.count_magic_numbers(&file), 2);
376 }
377
378 #[test]
379 fn test_swift_magic_numbers_skips_trivial() {
380 let code = "func main() { foo(0); bar(1) }\n";
381 let file = parse_swift(code);
382 let adapter = SwiftAdapter;
383 assert_eq!(adapter.count_magic_numbers(&file), 0);
384 }
385
386 #[test]
387 fn test_swift_panic_assert() {
388 let code = "func main() { assert(x > 0); precondition(y != nil) }\n";
389 let file = parse_swift(code);
390 let adapter = SwiftAdapter;
391 assert_eq!(adapter.count_panic_calls(&file), 2);
392 }
393
394 #[test]
395 fn test_swift_debug_nslog() {
396 let code = "NSLog(\"hello\")\n";
397 let file = parse_swift(code);
398 let adapter = SwiftAdapter;
399 assert_eq!(adapter.count_debug_calls(&file), 1);
400 }
401
402 #[test]
403 fn test_swift_issues_try_bang() {
404 let code = "let data = try! Data(contentsOf: url)\n";
405 let file = parse_swift(code);
406 let adapter = SwiftAdapter;
407 assert_eq!(adapter.count_swift_issues(&file), 1);
408 }
409
410 #[test]
411 fn test_swift_issues_clean() {
412 let code = "let x = 1\nlet y = 2\n";
413 let file = parse_swift(code);
414 let adapter = SwiftAdapter;
415 assert_eq!(adapter.count_swift_issues(&file), 0);
416 }
417
418 #[test]
419 fn test_swift_dead_code_after_return() {
420 let code = r#"
421func foo() -> Int {
422 return 42
423 print("dead")
424}
425"#;
426 let file = parse_swift(code);
427 let adapter = SwiftAdapter;
428 assert_eq!(adapter.count_dead_code(&file), 1);
429 }
430}