Skip to main content

normalize_languages/
swift.rs

1//! Swift language support.
2
3use crate::traits::{ImportSpec, ModuleId, ModuleResolver, Resolution, ResolverConfig};
4use crate::{ContainerBody, Import, Language, LanguageSymbols, Visibility};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8/// Swift language support.
9pub struct Swift;
10
11impl Swift {
12    /// Find the first type_identifier in an inheritance_specifier subtree.
13    fn find_type_identifier(node: &Node, content: &str, out: &mut Vec<String>) {
14        let before = out.len();
15        if node.kind() == "type_identifier" {
16            out.push(content[node.byte_range()].to_string());
17            return;
18        }
19        let mut cursor = node.walk();
20        for child in node.children(&mut cursor) {
21            Self::find_type_identifier(&child, content, out);
22            if out.len() > before {
23                return;
24            }
25        }
26    }
27}
28
29impl Language for Swift {
30    fn name(&self) -> &'static str {
31        "Swift"
32    }
33    fn extensions(&self) -> &'static [&'static str] {
34        &["swift"]
35    }
36    fn grammar_name(&self) -> &'static str {
37        "swift"
38    }
39
40    fn as_symbols(&self) -> Option<&dyn LanguageSymbols> {
41        Some(self)
42    }
43
44    fn signature_suffix(&self) -> &'static str {
45        " {}"
46    }
47
48    fn refine_kind(
49        &self,
50        node: &Node,
51        content: &str,
52        tag_kind: crate::SymbolKind,
53    ) -> crate::SymbolKind {
54        // Swift uses class_declaration for class/struct/enum/actor,
55        // distinguished by the declaration_kind field.
56        if node.kind() == "class_declaration"
57            && let Some(kind_node) = node.child_by_field_name("declaration_kind")
58        {
59            let kind_text = &content[kind_node.byte_range()];
60            return match kind_text {
61                "struct" => crate::SymbolKind::Struct,
62                "enum" => crate::SymbolKind::Enum,
63                "class" | "actor" => crate::SymbolKind::Class,
64                _ => tag_kind,
65            };
66        }
67        tag_kind
68    }
69
70    fn extract_docstring(&self, node: &Node, content: &str) -> Option<String> {
71        // Swift doc comments use triple-slash `///` lines or `/** */` blocks.
72        let mut doc_lines: Vec<String> = Vec::new();
73        let mut prev = node.prev_sibling();
74
75        while let Some(sibling) = prev {
76            match sibling.kind() {
77                "comment" => {
78                    let text = &content[sibling.byte_range()];
79                    if text.starts_with("///") {
80                        let line = text.strip_prefix("///").unwrap_or("").trim().to_string();
81                        doc_lines.push(line);
82                    } else {
83                        break;
84                    }
85                }
86                "multiline_comment" => {
87                    let text = &content[sibling.byte_range()];
88                    if text.starts_with("/**") {
89                        let lines: Vec<&str> = text
90                            .strip_prefix("/**")
91                            .unwrap_or(text)
92                            .strip_suffix("*/")
93                            .unwrap_or(text)
94                            .lines()
95                            .map(|l| l.trim().strip_prefix('*').unwrap_or(l).trim())
96                            .filter(|l| !l.is_empty())
97                            .collect();
98                        if lines.is_empty() {
99                            return None;
100                        }
101                        return Some(lines.join(" "));
102                    }
103                    break;
104                }
105                "attribute" => {
106                    // Skip attributes between doc comment and declaration
107                }
108                _ => break,
109            }
110            prev = sibling.prev_sibling();
111        }
112
113        if doc_lines.is_empty() {
114            return None;
115        }
116        doc_lines.reverse();
117        let joined = doc_lines.join(" ");
118        let trimmed = joined.trim().to_string();
119        if trimmed.is_empty() {
120            None
121        } else {
122            Some(trimmed)
123        }
124    }
125
126    fn extract_attributes(&self, node: &Node, content: &str) -> Vec<String> {
127        let mut attrs = Vec::new();
128        if let Some(mods) = node.child_by_field_name("modifiers") {
129            let mut cursor = mods.walk();
130            for child in mods.children(&mut cursor) {
131                if child.kind() == "attribute" {
132                    let text = content[child.byte_range()].trim().to_string();
133                    if !text.is_empty() {
134                        attrs.push(text);
135                    }
136                }
137            }
138        }
139        let mut prev = node.prev_sibling();
140        while let Some(sibling) = prev {
141            if sibling.kind() == "attribute" {
142                let text = content[sibling.byte_range()].trim().to_string();
143                if !text.is_empty() {
144                    attrs.insert(0, text);
145                }
146                prev = sibling.prev_sibling();
147            } else {
148                break;
149            }
150        }
151        attrs
152    }
153
154    fn extract_implements(&self, node: &Node, content: &str) -> crate::ImplementsInfo {
155        let mut implements = Vec::new();
156        let mut cursor = node.walk();
157        for child in node.children(&mut cursor) {
158            if child.kind() == "inheritance_specifier" {
159                Self::find_type_identifier(&child, content, &mut implements);
160            }
161        }
162        crate::ImplementsInfo {
163            is_interface: false,
164            implements,
165        }
166    }
167
168    fn build_signature(&self, node: &Node, content: &str) -> String {
169        let name = match self.node_name(node, content) {
170            Some(n) => n,
171            None => {
172                return content[node.byte_range()]
173                    .lines()
174                    .next()
175                    .unwrap_or("")
176                    .trim()
177                    .to_string();
178            }
179        };
180        match node.kind() {
181            "function_declaration" => {
182                let params = node
183                    .child_by_field_name("parameters")
184                    .map(|p| content[p.byte_range()].to_string())
185                    .unwrap_or_else(|| "()".to_string());
186                let return_type = node
187                    .child_by_field_name("return_type")
188                    .map(|t| format!(" -> {}", content[t.byte_range()].trim()))
189                    .unwrap_or_default();
190                format!("func {}{}{}", name, params, return_type)
191            }
192            "class_declaration" => format!("class {}", name),
193            "struct_declaration" => format!("struct {}", name),
194            "protocol_declaration" => format!("protocol {}", name),
195            "enum_declaration" => format!("enum {}", name),
196            "extension_declaration" => format!("extension {}", name),
197            "actor_declaration" => format!("actor {}", name),
198            "typealias_declaration" => {
199                let target = node
200                    .child_by_field_name("value")
201                    .map(|t| content[t.byte_range()].to_string())
202                    .unwrap_or_default();
203                format!("typealias {} = {}", name, target)
204            }
205            _ => {
206                let text = &content[node.byte_range()];
207                text.lines().next().unwrap_or(text).trim().to_string()
208            }
209        }
210    }
211
212    fn extract_imports(&self, node: &Node, content: &str) -> Vec<Import> {
213        if node.kind() != "import_declaration" {
214            return Vec::new();
215        }
216
217        let line = node.start_position().row + 1;
218
219        // Get the module name
220        let mut cursor = node.walk();
221        for child in node.children(&mut cursor) {
222            if child.kind() == "identifier" || child.kind() == "simple_identifier" {
223                let module = content[child.byte_range()].to_string();
224                return vec![Import {
225                    module,
226                    names: Vec::new(),
227                    alias: None,
228                    is_wildcard: false,
229                    is_relative: false,
230                    line,
231                }];
232            }
233        }
234
235        Vec::new()
236    }
237
238    fn format_import(&self, import: &Import, _names: Option<&[&str]>) -> String {
239        // Swift: import Module
240        format!("import {}", import.module)
241    }
242
243    fn container_body<'a>(&self, node: &'a Node<'a>) -> Option<Node<'a>> {
244        node.child_by_field_name("body")
245    }
246
247    fn analyze_container_body(
248        &self,
249        body_node: &Node,
250        content: &str,
251        inner_indent: &str,
252    ) -> Option<ContainerBody> {
253        crate::body::analyze_brace_body(body_node, content, inner_indent)
254    }
255
256    fn get_visibility(&self, node: &Node, content: &str) -> Visibility {
257        let mut cursor = node.walk();
258        for child in node.children(&mut cursor) {
259            if child.kind() == "modifiers" || child.kind() == "modifier" {
260                let mod_text = &content[child.byte_range()];
261                if mod_text.contains("private") || mod_text.contains("fileprivate") {
262                    return Visibility::Private;
263                }
264                if mod_text.contains("internal") {
265                    return Visibility::Protected;
266                }
267                if mod_text.contains("public") || mod_text.contains("open") {
268                    return Visibility::Public;
269                }
270            }
271        }
272        // Swift default is internal
273        Visibility::Protected
274    }
275
276    fn is_test_symbol(&self, symbol: &crate::Symbol) -> bool {
277        let name = symbol.name.as_str();
278        match symbol.kind {
279            crate::SymbolKind::Function | crate::SymbolKind::Method => name.starts_with("test_"),
280            crate::SymbolKind::Module => name == "tests" || name == "test",
281            _ => false,
282        }
283    }
284
285    fn test_file_globs(&self) -> &'static [&'static str] {
286        &["**/*Tests.swift", "**/*Test.swift"]
287    }
288
289    fn module_resolver(&self) -> Option<&dyn ModuleResolver> {
290        static RESOLVER: SwiftModuleResolver = SwiftModuleResolver;
291        Some(&RESOLVER)
292    }
293}
294
295impl LanguageSymbols for Swift {}
296
297// =============================================================================
298// Swift Module Resolver
299// =============================================================================
300
301/// Module resolver for Swift (Swift Package Manager conventions).
302///
303/// Each target in `Sources/<TargetName>/` is a module.
304/// `import TargetName` → `Sources/TargetName/` directory.
305pub struct SwiftModuleResolver;
306
307impl ModuleResolver for SwiftModuleResolver {
308    fn workspace_config(&self, root: &Path) -> ResolverConfig {
309        let mut path_mappings: Vec<(String, PathBuf)> = Vec::new();
310
311        let package_swift = root.join("Package.swift");
312        if let Ok(content) = std::fs::read_to_string(&package_swift) {
313            // Parse `.target(name: "TargetName"` patterns
314            let mut search_start = 0;
315            while let Some(pos) = content[search_start..].find(".target(name:") {
316                let abs_pos = search_start + pos;
317                let after = &content[abs_pos + 13..]; // after ".target(name:"
318                // Find the quoted name
319                if let Some(q_start) = after.find('"') {
320                    let rest = &after[q_start + 1..];
321                    if let Some(q_end) = rest.find('"') {
322                        let target_name = &rest[..q_end];
323                        let target_dir = root.join("Sources").join(target_name);
324                        path_mappings.push((target_name.to_string(), target_dir));
325                    }
326                }
327                search_start = abs_pos + 13;
328            }
329        }
330
331        // If no Package.swift or no targets found, scan Sources/ for directories
332        if path_mappings.is_empty() {
333            let sources_dir = root.join("Sources");
334            if let Ok(entries) = std::fs::read_dir(&sources_dir) {
335                for entry in entries.flatten() {
336                    if entry.path().is_dir()
337                        && let Some(name) = entry.file_name().to_str()
338                    {
339                        path_mappings.push((name.to_string(), entry.path()));
340                    }
341                }
342            }
343        }
344
345        ResolverConfig {
346            workspace_root: root.to_path_buf(),
347            path_mappings,
348            search_roots: vec![root.join("Sources")],
349        }
350    }
351
352    fn module_of_file(&self, _root: &Path, file: &Path, cfg: &ResolverConfig) -> Vec<ModuleId> {
353        let ext = file.extension().and_then(|e| e.to_str()).unwrap_or("");
354        if ext != "swift" {
355            return Vec::new();
356        }
357        for (target_name, target_dir) in &cfg.path_mappings {
358            if file.starts_with(target_dir) {
359                return vec![ModuleId {
360                    canonical_path: target_name.clone(),
361                }];
362            }
363        }
364        Vec::new()
365    }
366
367    fn resolve(&self, from_file: &Path, spec: &ImportSpec, cfg: &ResolverConfig) -> Resolution {
368        let ext = from_file.extension().and_then(|e| e.to_str()).unwrap_or("");
369        if ext != "swift" {
370            return Resolution::NotApplicable;
371        }
372        let raw = &spec.raw;
373        // Swift `import TargetName` → look in path_mappings
374        for (target_name, target_dir) in &cfg.path_mappings {
375            if target_name == raw {
376                return Resolution::Resolved(target_dir.clone(), String::new());
377            }
378        }
379        Resolution::NotFound
380    }
381}
382
383#[cfg(test)]
384mod tests {
385    use super::*;
386    use crate::validate_unused_kinds_audit;
387
388    #[test]
389    fn unused_node_kinds_audit() {
390        #[rustfmt::skip]
391        let documented_unused: &[&str] = &[
392            // STRUCTURAL
393            "as_operator", "associatedtype_declaration", "catch_keyword", "class_body",
394            "computed_modify", "constructor_expression", "constructor_suffix", "custom_operator",
395            "deinit_declaration", "deprecated_operator_declaration_body", "didset_clause",
396            "else", "enum_class_body", "enum_entry", "enum_type_parameters",
397            "existential_type", "external_macro_definition", "function_body", "function_modifier",
398            "getter_specifier", "identifier", "inheritance_modifier", "inheritance_specifier",
399            "interpolated_expression", "key_path_expression", "key_path_string_expression",
400            "lambda_function_type", "lambda_function_type_parameters", "lambda_parameter",
401            "macro_declaration", "macro_definition", "member_modifier", "metatype", "modifiers",
402            "modify_specifier", "mutation_modifier", "opaque_type", "operator_declaration",
403            "optional_type", "ownership_modifier", "parameter_modifier", "parameter_modifiers",
404            "precedence_group_declaration", "property_behavior_modifier", "property_declaration",
405            "property_modifier", "protocol_body", "protocol_composition_type",
406            "protocol_function_declaration", "protocol_property_declaration", "self_expression",
407            "setter_specifier", "simple_identifier", "statement_label", "statements",
408            "super_expression", "switch_entry", "throw_keyword", "throws", "try_operator",
409            "tuple_expression", "tuple_type", "tuple_type_item", "type_annotation",
410            "type_arguments", "type_constraint", "type_constraints", "type_identifier",
411            "type_modifiers", "type_pack_expansion", "type_parameter", "type_parameter_modifiers",
412            "type_parameter_pack", "type_parameters", "user_type", "visibility_modifier",
413            "where_clause", "willset_clause", "willset_didset_block",
414            // EXPRESSION
415            "additive_expression", "as_expression", "await_expression", "call_expression",
416            "check_expression", "comparison_expression", "conjunction_expression",
417            "directly_assignable_expression", "disjunction_expression", "equality_expression",
418            "infix_expression", "multiplicative_expression", "navigation_expression",
419            "open_end_range_expression", "open_start_range_expression", "postfix_expression",
420            "prefix_expression", "range_expression", "selector_expression", "try_expression",
421            // TYPE
422            "array_type", "dictionary_type", "function_type",
423            // covered by tags.scm
424            "init_declaration",
425            "repeat_while_statement",
426            "while_statement",
427            "import_declaration",
428            "subscript_declaration",
429            "lambda_literal",
430            "for_statement",
431            "if_statement",
432            "nil_coalescing_expression",
433            "do_statement",
434            "ternary_expression",
435            "catch_block",
436            "control_transfer_statement",
437            "switch_statement",
438            "guard_statement",
439        ];
440
441        validate_unused_kinds_audit(&Swift, documented_unused)
442            .expect("Swift unused node kinds audit failed");
443    }
444}