Skip to main content

normalize_languages/
swift.rs

1//! Swift language support.
2
3use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8/// Swift language support.
9pub struct Swift;
10
11impl Language for Swift {
12    fn name(&self) -> &'static str {
13        "Swift"
14    }
15    fn extensions(&self) -> &'static [&'static str] {
16        &["swift"]
17    }
18    fn grammar_name(&self) -> &'static str {
19        "swift"
20    }
21
22    fn has_symbols(&self) -> bool {
23        true
24    }
25
26    fn container_kinds(&self) -> &'static [&'static str] {
27        &["class_declaration", "protocol_declaration"]
28    }
29
30    fn function_kinds(&self) -> &'static [&'static str] {
31        &[
32            "function_declaration",
33            "init_declaration",
34            "subscript_declaration",
35            "computed_property",
36            "lambda_literal",
37        ]
38    }
39
40    fn type_kinds(&self) -> &'static [&'static str] {
41        &[
42            "class_declaration",
43            "protocol_declaration",
44            "typealias_declaration",
45        ]
46    }
47
48    fn import_kinds(&self) -> &'static [&'static str] {
49        &["import_declaration"]
50    }
51
52    fn public_symbol_kinds(&self) -> &'static [&'static str] {
53        &[
54            "class_declaration",
55            "protocol_declaration",
56            "function_declaration",
57        ]
58    }
59
60    fn visibility_mechanism(&self) -> VisibilityMechanism {
61        VisibilityMechanism::AccessModifier
62    }
63
64    fn extract_public_symbols(&self, node: &Node, content: &str) -> Vec<Export> {
65        if self.get_visibility(node, content) != Visibility::Public {
66            return Vec::new();
67        }
68
69        let name = match self.node_name(node, content) {
70            Some(n) => n.to_string(),
71            None => return Vec::new(),
72        };
73
74        let kind = match node.kind() {
75            "class_declaration" => SymbolKind::Class,
76            "struct_declaration" => SymbolKind::Struct,
77            "protocol_declaration" => SymbolKind::Interface,
78            "enum_declaration" => SymbolKind::Enum,
79            "actor_declaration" => SymbolKind::Class,
80            "function_declaration" | "init_declaration" => SymbolKind::Function,
81            _ => return Vec::new(),
82        };
83
84        vec![Export {
85            name,
86            kind,
87            line: node.start_position().row + 1,
88        }]
89    }
90
91    fn scope_creating_kinds(&self) -> &'static [&'static str] {
92        &[
93            "for_statement",
94            "while_statement",
95            "repeat_while_statement",
96            "do_statement",
97            "catch_block",
98            "switch_statement",
99            "guard_statement",
100        ]
101    }
102
103    fn control_flow_kinds(&self) -> &'static [&'static str] {
104        &[
105            "if_statement",
106            "for_statement",
107            "while_statement",
108            "repeat_while_statement",
109            "switch_statement",
110            "guard_statement",
111            "do_statement",
112            "control_transfer_statement",
113        ]
114    }
115
116    fn complexity_nodes(&self) -> &'static [&'static str] {
117        &[
118            "if_statement",
119            "for_statement",
120            "while_statement",
121            "repeat_while_statement",
122            "switch_statement",
123            "catch_block",
124            "ternary_expression",
125            "nil_coalescing_expression",
126        ]
127    }
128
129    fn nesting_nodes(&self) -> &'static [&'static str] {
130        &[
131            "if_statement",
132            "for_statement",
133            "while_statement",
134            "repeat_while_statement",
135            "switch_statement",
136            "do_statement",
137            "function_declaration",
138            "class_declaration",
139            "lambda_literal",
140        ]
141    }
142
143    fn signature_suffix(&self) -> &'static str {
144        " {}"
145    }
146
147    fn extract_function(&self, node: &Node, content: &str, _in_container: bool) -> Option<Symbol> {
148        let name = self.node_name(node, content)?;
149
150        let params = node
151            .child_by_field_name("parameters")
152            .map(|p| content[p.byte_range()].to_string())
153            .unwrap_or_else(|| "()".to_string());
154
155        let return_type = node
156            .child_by_field_name("return_type")
157            .map(|t| format!(" -> {}", content[t.byte_range()].trim()));
158
159        let signature = format!("func {}{}{}", name, params, return_type.unwrap_or_default());
160
161        // Check for override modifier
162        let is_override = if let Some(mods) = node.child_by_field_name("modifiers") {
163            let mut cursor = mods.walk();
164            let children: Vec<_> = mods.children(&mut cursor).collect();
165            children.iter().any(|child| {
166                child.kind() == "member_modifier"
167                    && child.child(0).map(|c| c.kind()) == Some("override")
168            })
169        } else {
170            false
171        };
172
173        Some(Symbol {
174            name: name.to_string(),
175            kind: SymbolKind::Function,
176            signature,
177            docstring: self.extract_docstring(node, content),
178            attributes: Vec::new(),
179            start_line: node.start_position().row + 1,
180            end_line: node.end_position().row + 1,
181            visibility: self.get_visibility(node, content),
182            children: Vec::new(),
183            is_interface_impl: is_override,
184            implements: Vec::new(),
185        })
186    }
187
188    fn extract_container(&self, node: &Node, content: &str) -> Option<Symbol> {
189        let name = self.node_name(node, content)?;
190        let (kind, keyword) = match node.kind() {
191            "struct_declaration" => (SymbolKind::Struct, "struct"),
192            "protocol_declaration" => (SymbolKind::Interface, "protocol"),
193            "enum_declaration" => (SymbolKind::Enum, "enum"),
194            "extension_declaration" => (SymbolKind::Module, "extension"),
195            "actor_declaration" => (SymbolKind::Class, "actor"),
196            _ => (SymbolKind::Class, "class"),
197        };
198
199        Some(Symbol {
200            name: name.to_string(),
201            kind,
202            signature: format!("{} {}", keyword, name),
203            docstring: self.extract_docstring(node, content),
204            attributes: Vec::new(),
205            start_line: node.start_position().row + 1,
206            end_line: node.end_position().row + 1,
207            visibility: self.get_visibility(node, content),
208            children: Vec::new(),
209            is_interface_impl: false,
210            implements: Vec::new(),
211        })
212    }
213
214    fn extract_type(&self, node: &Node, content: &str) -> Option<Symbol> {
215        if node.kind() == "typealias_declaration" {
216            let name = self.node_name(node, content)?;
217            let target = node
218                .child_by_field_name("value")
219                .map(|t| content[t.byte_range()].to_string())
220                .unwrap_or_default();
221            return Some(Symbol {
222                name: name.to_string(),
223                kind: SymbolKind::Type,
224                signature: format!("typealias {} = {}", name, target),
225                docstring: None,
226                attributes: Vec::new(),
227                start_line: node.start_position().row + 1,
228                end_line: node.end_position().row + 1,
229                visibility: self.get_visibility(node, content),
230                children: Vec::new(),
231                is_interface_impl: false,
232                implements: Vec::new(),
233            });
234        }
235        self.extract_container(node, content)
236    }
237
238    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
239        // Swift uses /// or /** */ for documentation
240        let mut prev = node.prev_sibling();
241        let mut doc_lines = Vec::new();
242
243        while let Some(sibling) = prev {
244            let text = &content[sibling.byte_range()];
245            if sibling.kind() == "comment" || sibling.kind() == "multiline_comment" {
246                if text.starts_with("///") {
247                    let line = text.strip_prefix("///").unwrap_or(text).trim();
248                    if !line.is_empty() {
249                        doc_lines.insert(0, line.to_string());
250                    }
251                } else if text.starts_with("/**") {
252                    let inner = text
253                        .strip_prefix("/**")
254                        .unwrap_or(text)
255                        .strip_suffix("*/")
256                        .unwrap_or(text);
257                    for line in inner.lines() {
258                        let clean = line.trim().strip_prefix("*").unwrap_or(line).trim();
259                        if !clean.is_empty() {
260                            doc_lines.push(clean.to_string());
261                        }
262                    }
263                    break;
264                } else {
265                    break;
266                }
267            } else {
268                break;
269            }
270            prev = sibling.prev_sibling();
271        }
272
273        if doc_lines.is_empty() {
274            None
275        } else {
276            Some(doc_lines.join(" "))
277        }
278    }
279
280    fn extract_attributes(&self, _node: &Node, _content: &str) -> Vec<String> {
281        Vec::new()
282    }
283
284    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
285        if node.kind() != "import_declaration" {
286            return Vec::new();
287        }
288
289        let line = node.start_position().row + 1;
290
291        // Get the module name
292        let mut cursor = node.walk();
293        for child in node.children(&mut cursor) {
294            if child.kind() == "identifier" || child.kind() == "simple_identifier" {
295                let module = content[child.byte_range()].to_string();
296                return vec![Import {
297                    module,
298                    names: Vec::new(),
299                    alias: None,
300                    is_wildcard: false,
301                    is_relative: false,
302                    line,
303                }];
304            }
305        }
306
307        Vec::new()
308    }
309
310    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
311        // Swift: import Module
312        format!("import {}", import.module)
313    }
314
315    fn is_public(&self, node: &Node, content: &str) -> bool {
316        self.get_visibility(node, content) == Visibility::Public
317    }
318
319    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
320        node.child_by_field_name("body")
321    }
322
323    fn body_has_docstring(&self, _body: &Node, _content: &str) -> bool {
324        false
325    }
326
327    fn node_name<'a>(&self, node: &Node, content: &'a str) -> Option<&'a str> {
328        node.child_by_field_name("name")
329            .map(|n| &content[n.byte_range()])
330    }
331
332    fn file_path_to_module_name(&self, path: &Path) -> Option<String> {
333        if path.extension()?.to_str()? != "swift" {
334            return None;
335        }
336        let stem = path.file_stem()?.to_str()?;
337        Some(stem.to_string())
338    }
339
340    fn module_name_to_paths(&self, module: &str) -> Vec<String> {
341        vec![
342            format!("{}.swift", module),
343            format!("Sources/{}.swift", module),
344            format!("Sources/{}/{}.swift", module, module),
345        ]
346    }
347
348    fn is_stdlib_import(&self, import_name: &str, _project_root: &Path) -> bool {
349        matches!(
350            import_name,
351            "Foundation"
352                | "UIKit"
353                | "AppKit"
354                | "SwiftUI"
355                | "Combine"
356                | "CoreData"
357                | "CoreGraphics"
358                | "CoreFoundation"
359                | "Darwin"
360                | "Dispatch"
361                | "ObjectiveC"
362                | "Swift"
363        )
364    }
365
366    fn find_stdlib(&self, _project_root: &Path) -> Option<PathBuf> {
367        None
368    }
369
370    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
371        let mut cursor = node.walk();
372        for child in node.children(&mut cursor) {
373            if child.kind() == "modifiers" || child.kind() == "modifier" {
374                let mod_text = &content[child.byte_range()];
375                if mod_text.contains("private") || mod_text.contains("fileprivate") {
376                    return Visibility::Private;
377                }
378                if mod_text.contains("internal") {
379                    return Visibility::Protected;
380                }
381                if mod_text.contains("public") || mod_text.contains("open") {
382                    return Visibility::Public;
383                }
384            }
385        }
386        // Swift default is internal
387        Visibility::Protected
388    }
389
390    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
391        let name = symbol.name.as_str();
392        match symbol.kind {
393            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
394            crate::SymbolKind::Module => name == "tests" || name == "test",
395            _ => false,
396        }
397    }
398
399    fn embedded_content(&self, _node: &Node, _content: &str) -> Option<crate::EmbeddedBlock> {
400        None
401    }
402
403    fn lang_key(&self) -> &'static str {
404        "swift"
405    }
406
407    fn resolve_local_import(
408        &self,
409        import: &str,
410        _current_file: &Path,
411        project_root: &Path,
412    ) -> Option<PathBuf> {
413        // Swift Package Manager structure
414        let paths = [
415            format!("Sources/{}/{}.swift", import, import),
416            format!("Sources/{}.swift", import),
417            format!("{}.swift", import),
418        ];
419
420        for path in &paths {
421            let full_path = project_root.join(path);
422            if full_path.is_file() {
423                return Some(full_path);
424            }
425        }
426
427        None
428    }
429
430    fn resolve_external_import(
431        &self,
432        _import_name: &str,
433        _project_root: &Path,
434    ) -> Option<ResolvedPackage> {
435        // Swift Package Manager resolution would go here
436        None
437    }
438
439    fn get_version(&self, project_root: &Path) -> Option<String> {
440        // Check Package.swift for swift-tools-version
441        let package_swift = project_root.join("Package.swift");
442        if package_swift.is_file()
443            && let Ok(content) = std::fs::read_to_string(&package_swift)
444            && let Some(line) = content.lines().next()
445            && line.contains("swift-tools-version:")
446        {
447            let version = line.split(':').nth(1)?.trim();
448            return Some(version.to_string());
449        }
450        None
451    }
452
453    fn find_package_cache(&self, _project_root: &Path) -> Option<PathBuf> {
454        // Swift Package Manager cache
455        if let Ok(home) = std::env::var("HOME") {
456            let cache = PathBuf::from(home).join("Library/Caches/org.swift.swiftpm");
457            if cache.is_dir() {
458                return Some(cache);
459            }
460        }
461        None
462    }
463
464    fn indexable_extensions(&self) -> &'static [&'static str] {
465        &["swift"]
466    }
467
468    fn package_sources(&self, _project_root: &Path) -> Vec<crate::PackageSource> {
469        Vec::new()
470    }
471
472    fn should_skip_package_entry(&self, name: &str, is_dir: bool) -> bool {
473        use crate::traits::{has_extension, skip_dotfiles};
474        if skip_dotfiles(name) {
475            return true;
476        }
477        if is_dir && (name == "build" || name == ".build" || name == "Pods") {
478            return true;
479        }
480        !is_dir && !has_extension(name, self.indexable_extensions())
481    }
482
483    fn discover_packages(&self, _source: &crate::PackageSource) -> Vec<(String, PathBuf)> {
484        Vec::new()
485    }
486
487    fn package_module_name(&self, entry_name: &str) -> String {
488        entry_name
489            .strip_suffix(".swift")
490            .unwrap_or(entry_name)
491            .to_string()
492    }
493
494    fn find_package_entry(&self, path: &Path) -> Option<PathBuf> {
495        if path.is_file() {
496            return Some(path.to_path_buf());
497        }
498        None
499    }
500}
501
502#[cfg(test)]
503mod tests {
504    use super::*;
505    use crate::validate_unused_kinds_audit;
506
507    #[test]
508    fn unused_node_kinds_audit() {
509        #[rustfmt::skip]
510        let documented_unused: &[&str] = &[
511            // STRUCTURAL
512            "as_operator", "associatedtype_declaration", "catch_keyword", "class_body",
513            "computed_modify", "constructor_expression", "constructor_suffix", "custom_operator",
514            "deinit_declaration", "deprecated_operator_declaration_body", "didset_clause",
515            "else", "enum_class_body", "enum_entry", "enum_type_parameters",
516            "existential_type", "external_macro_definition", "function_body", "function_modifier",
517            "getter_specifier", "identifier", "inheritance_modifier", "inheritance_specifier",
518            "interpolated_expression", "key_path_expression", "key_path_string_expression",
519            "lambda_function_type", "lambda_function_type_parameters", "lambda_parameter",
520            "macro_declaration", "macro_definition", "member_modifier", "metatype", "modifiers",
521            "modify_specifier", "mutation_modifier", "opaque_type", "operator_declaration",
522            "optional_type", "ownership_modifier", "parameter_modifier", "parameter_modifiers",
523            "precedence_group_declaration", "property_behavior_modifier", "property_declaration",
524            "property_modifier", "protocol_body", "protocol_composition_type",
525            "protocol_function_declaration", "protocol_property_declaration", "self_expression",
526            "setter_specifier", "simple_identifier", "statement_label", "statements",
527            "super_expression", "switch_entry", "throw_keyword", "throws", "try_operator",
528            "tuple_expression", "tuple_type", "tuple_type_item", "type_annotation",
529            "type_arguments", "type_constraint", "type_constraints", "type_identifier",
530            "type_modifiers", "type_pack_expansion", "type_parameter", "type_parameter_modifiers",
531            "type_parameter_pack", "type_parameters", "user_type", "visibility_modifier",
532            "where_clause", "willset_clause", "willset_didset_block",
533            // EXPRESSION
534            "additive_expression", "as_expression", "await_expression", "call_expression",
535            "check_expression", "comparison_expression", "conjunction_expression",
536            "directly_assignable_expression", "disjunction_expression", "equality_expression",
537            "infix_expression", "multiplicative_expression", "navigation_expression",
538            "open_end_range_expression", "open_start_range_expression", "postfix_expression",
539            "prefix_expression", "range_expression", "selector_expression", "try_expression",
540            // TYPE
541            "array_type", "dictionary_type", "function_type",
542        ];
543
544        validate_unused_kinds_audit(&Swift, documented_unused)
545            .expect("Swift unused node kinds audit failed");
546    }
547}