1use crate::external_packages::ResolvedPackage;
4use crate::{Export, Import, Language, Symbol, SymbolKind, Visibility, VisibilityMechanism};
5use std::path::{Path, PathBuf};
6use tree_sitter::Node;
7
8pub 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 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 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 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 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 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 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 None
437 }
438
439 fn get_version(&self, project_root: &Path) -> Option<String> {
440 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 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 "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 "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 "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}