Skip to main content

garbage_code_hunter/language/adapter/
swift.rs

1//! SwiftAdapter — Swift language adapter.
2
3use 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}