use once_cell::sync::Lazy;
use regex::Regex;
use crate::types::{
ExportSymbol, FileAnalysis, ImportEntry, ImportKind, ImportResolutionKind, SymbolUsage,
};
static RE_SWIFT_DECL: Lazy<Regex> = Lazy::new(|| {
Regex::new(
r"^\s*(?:@objc\s*(?:\([^)]+\)\s*)?)?(?:(?:public|internal|private|fileprivate|open|final|override|static|class|mutating|nonmutating|lazy|weak|unowned)\s+)*(class|struct|enum|protocol|extension|func|var|let)\s+([A-Za-z_][A-Za-z0-9_]*)",
)
.expect("valid swift decl regex")
});
static RE_IMPORT: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^\s*(?:@testable\s+)?import\s+(?:class\s+|struct\s+|enum\s+|protocol\s+|func\s+|var\s+|let\s+)?([A-Za-z0-9_.]+)")
.expect("valid swift import regex")
});
static RE_WORD: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"\b([A-Z][A-Za-z0-9_]*|[a-z][A-Za-z0-9_]*)\b").expect("valid swift word regex")
});
pub fn analyze_swift_file(content: &str, relative: String) -> FileAnalysis {
let mut analysis = FileAnalysis::new(relative);
analysis.imports = parse_imports(content);
analysis.exports = parse_exports(content);
analysis.symbol_usages = parse_symbol_usages(content, &analysis.exports);
analysis.local_uses = parse_local_uses(content, &analysis.exports);
apply_runtime_dispatch_signals(content, &mut analysis);
credit_uniffi_generated_glue(content, &mut analysis);
analysis
}
const UNIFFI_FFICONVERTER_DENSITY_THRESHOLD: usize = 4;
fn credit_uniffi_generated_glue(content: &str, analysis: &mut FileAnalysis) {
if !is_uniffi_generated_bridge(content) {
return;
}
let existing: std::collections::HashSet<&str> =
analysis.local_uses.iter().map(|u| u.as_str()).collect();
let credited: Vec<String> = analysis
.exports
.iter()
.map(|e| e.name.clone())
.filter(|name| !existing.contains(name.as_str()))
.collect();
let mut seen = std::collections::HashSet::new();
for name in credited {
if seen.insert(name.clone()) {
analysis.local_uses.push(name);
}
}
}
fn is_uniffi_generated_bridge(content: &str) -> bool {
let head: String = content
.lines()
.take(12)
.collect::<Vec<_>>()
.join("\n")
.to_ascii_lowercase();
let has_autogen_header = head.contains("autogenerated") && head.contains("hand-written");
if has_autogen_header {
return true;
}
content.matches("FfiConverter").count() >= UNIFFI_FFICONVERTER_DENSITY_THRESHOLD
}
const SWIFT_FRAMEWORK_DISPATCH_METHODS: &[&str] = &[
"applicationDidFinishLaunching",
"applicationWillFinishLaunching",
"applicationWillTerminate",
"applicationShouldTerminateAfterLastWindowClosed",
"applicationShouldTerminate",
"applicationSupportsSecureRestorableState",
"applicationDidBecomeActive",
"applicationWillBecomeActive",
"applicationDidResignActive",
"applicationWillResignActive",
"applicationDidHide",
"applicationDidUnhide",
"applicationShouldHandleReopen",
"applicationDockMenu",
"applicationOpenUntitledFile",
"applicationShouldOpenUntitledFile",
"applicationDidChangeScreenParameters",
"application",
"sceneDidDisconnect",
"sceneWillEnterForeground",
"sceneDidEnterBackground",
"sceneWillResignActive",
"sceneDidBecomeActive",
"observeValue",
"windowWillClose",
"windowDidResize",
"windowShouldClose",
];
fn apply_runtime_dispatch_signals(content: &str, analysis: &mut FileAnalysis) {
let mut is_entry = false;
let mut pending_objc = false;
for raw_line in content.lines() {
let line = strip_line_comment(raw_line);
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
if trimmed.starts_with("@main")
|| trimmed.starts_with("@NSApplicationMain")
|| trimmed.starts_with("@UIApplicationMain")
|| trimmed.contains("NSApplicationMain(")
|| trimmed.contains("UIApplicationMain(")
{
is_entry = true;
}
if trimmed.starts_with("@objc") {
pending_objc = true;
}
if let Some(caps) = RE_SWIFT_DECL.captures(line) {
let is_func = caps.get(1).map(|m| m.as_str()) == Some("func");
if is_func && let Some(name) = caps.get(2).map(|m| m.as_str()) {
let has_override = trimmed.contains("override ");
let has_objc = trimmed.contains("@objc") || pending_objc;
let is_lifecycle = SWIFT_FRAMEWORK_DISPATCH_METHODS.contains(&name);
if (has_override || has_objc || is_lifecycle)
&& !analysis.local_uses.iter().any(|u| u == name)
{
analysis.local_uses.push(name.to_string());
}
}
pending_objc = false;
}
}
if is_entry && !analysis.entry_points.iter().any(|e| e == "swift-main") {
analysis.entry_points.push("swift-main".to_string());
}
}
fn parse_local_uses(content: &str, exports: &[ExportSymbol]) -> Vec<String> {
use std::collections::{HashMap, HashSet};
if exports.is_empty() {
return Vec::new();
}
let mut decl_lines: HashMap<&str, HashSet<usize>> = HashMap::new();
for e in exports {
if let Some(line) = e.line {
decl_lines.entry(e.name.as_str()).or_default().insert(line);
}
}
let export_names: HashSet<&str> = exports.iter().map(|e| e.name.as_str()).collect();
let mut used: Vec<String> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
for (idx, line) in content.lines().enumerate() {
let effective = strip_line_comment(line);
if effective.trim().is_empty() {
continue;
}
let lineno = idx + 1;
for caps in RE_WORD.captures_iter(effective) {
let Some(m) = caps.get(1) else { continue };
let word = m.as_str();
if !export_names.contains(word) {
continue;
}
if decl_lines
.get(word)
.is_some_and(|lines| lines.contains(&lineno))
{
continue;
}
if seen.insert(word.to_string()) {
used.push(word.to_string());
}
}
}
used
}
fn parse_imports(content: &str) -> Vec<ImportEntry> {
let mut imports: Vec<ImportEntry> = Vec::new();
for (idx, line) in content.lines().enumerate() {
let effective = strip_line_comment(line);
if let Some(caps) = RE_IMPORT.captures(effective)
&& let Some(m) = caps.get(1)
{
let path = m.as_str().trim();
if path.is_empty() {
continue;
}
if imports.iter().any(|i| i.source == path) {
continue;
}
let mut entry = ImportEntry::new(path.to_string(), ImportKind::Static);
entry.line = Some(idx + 1);
entry.resolution = ImportResolutionKind::Unknown;
imports.push(entry);
}
}
imports
}
fn parse_exports(content: &str) -> Vec<ExportSymbol> {
let mut out: Vec<ExportSymbol> = Vec::new();
for (idx, line) in content.lines().enumerate() {
let effective = strip_line_comment(line);
if let Some(caps) = RE_SWIFT_DECL.captures(effective) {
let keyword = caps.get(1).map(|m| m.as_str()).unwrap_or("");
let name = caps.get(2).map(|m| m.as_str()).unwrap_or("").to_string();
if name.is_empty() {
continue;
}
if !out.iter().any(|e| e.name == name && e.kind == keyword) {
out.push(ExportSymbol::new(name, keyword, "named", Some(idx + 1)));
}
}
}
out
}
fn parse_symbol_usages(content: &str, exports: &[ExportSymbol]) -> Vec<SymbolUsage> {
let mut out: Vec<SymbolUsage> = Vec::new();
let export_names: std::collections::HashSet<&str> =
exports.iter().map(|e| e.name.as_str()).collect();
for (idx, line) in content.lines().enumerate() {
let effective = strip_line_comment(line);
if effective.trim().is_empty() {
continue;
}
for caps in RE_WORD.captures_iter(effective) {
if let Some(m) = caps.get(1) {
let word = m.as_str();
if word.is_empty() || is_swift_keyword(word) || export_names.contains(word) {
continue;
}
if out.len() >= 1500 {
return out;
}
out.push(SymbolUsage {
name: word.to_string(),
line: idx + 1,
context: effective.trim().to_string(),
});
}
}
}
out.sort_by(|a, b| a.name.cmp(&b.name).then(a.line.cmp(&b.line)));
out.dedup_by(|a, b| a.name == b.name && a.line == b.line);
out
}
fn strip_line_comment(line: &str) -> &str {
let mut in_str = false;
let bytes = line.as_bytes();
let mut idx = 0;
while idx + 1 < bytes.len() {
let ch = bytes[idx] as char;
match ch {
'\\' => {
idx += 2;
continue;
}
'"' => in_str = !in_str,
'/' if !in_str && bytes[idx + 1] == b'/' => {
return &line[..idx];
}
_ => {}
}
idx += 1;
}
line
}
fn is_swift_keyword(word: &str) -> bool {
matches!(
word,
"import"
| "struct"
| "class"
| "enum"
| "protocol"
| "extension"
| "func"
| "var"
| "let"
| "public"
| "internal"
| "private"
| "fileprivate"
| "open"
| "final"
| "override"
| "static"
| "mutating"
| "nonmutating"
| "lazy"
| "weak"
| "unowned"
| "if"
| "else"
| "guard"
| "switch"
| "case"
| "default"
| "for"
| "in"
| "while"
| "repeat"
| "do"
| "catch"
| "throw"
| "throws"
| "rethrows"
| "try"
| "return"
| "break"
| "continue"
| "fallthrough"
| "defer"
| "true"
| "false"
| "nil"
| "self"
| "super"
| "init"
| "deinit"
| "subscript"
| "typealias"
| "associatedtype"
| "String"
| "Int"
| "Bool"
| "Double"
| "Float"
| "Optional"
| "Array"
| "Dictionary"
| "Set"
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_swift_decls() {
let src = r#"
import Foundation
public final class WorkspaceCacheStore {
let id: String
private var data: [String: Any]
init() {}
}
struct DocumentRecord {}
protocol Searchable {}
extension WorkspaceCacheStore: Searchable {}
"#;
let analysis = analyze_swift_file(src, "main.swift".to_string());
let classes: Vec<_> = analysis
.exports
.iter()
.filter(|e| e.kind == "class")
.map(|e| e.name.clone())
.collect();
assert!(classes.contains(&"WorkspaceCacheStore".to_string()));
let structs: Vec<_> = analysis
.exports
.iter()
.filter(|e| e.kind == "struct")
.map(|e| e.name.clone())
.collect();
assert!(structs.contains(&"DocumentRecord".to_string()));
let extensions: Vec<_> = analysis
.exports
.iter()
.filter(|e| e.kind == "extension")
.map(|e| e.name.clone())
.collect();
assert!(extensions.contains(&"WorkspaceCacheStore".to_string()));
}
#[test]
fn marks_nsapplicationmain_file_as_entry_point() {
let src = "import AppKit\n\nlet delegate = AppDelegate()\nNSApplication.shared.delegate = delegate\n_ = NSApplicationMain(CommandLine.argc, CommandLine.unsafeArgv)\n";
let analysis = analyze_swift_file(src, "main.swift".to_string());
assert!(
!analysis.entry_points.is_empty(),
"a file calling NSApplicationMain must be a runtime entry point"
);
}
#[test]
fn marks_main_attribute_file_as_entry_point() {
let src = "import SwiftUI\n\n@main\nstruct MyApp: App {\n var body: some Scene { WindowGroup {} }\n}\n";
let analysis = analyze_swift_file(src, "MyApp.swift".to_string());
assert!(
!analysis.entry_points.is_empty(),
"a @main type must mark its file as an entry point"
);
}
#[test]
fn credits_appkit_lifecycle_and_override_methods_as_used() {
let src = "import AppKit\n\n@MainActor\nclass AppDelegate: NSObject, NSApplicationDelegate {\n func applicationDidFinishLaunching(_ notification: Notification) {}\n func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { true }\n override func observeValue(forKeyPath keyPath: String?) {}\n @objc func handleClick() {}\n private func internalHelper() {}\n}\n";
let analysis = analyze_swift_file(src, "AppDelegate.swift".to_string());
for invoked in [
"applicationDidFinishLaunching",
"applicationSupportsSecureRestorableState",
"observeValue",
"handleClick",
] {
assert!(
analysis.local_uses.iter().any(|u| u == invoked),
"framework-dispatched method `{invoked}` must be credited as used"
);
}
assert!(
!analysis.local_uses.iter().any(|u| u == "internalHelper"),
"plain private helper must not be force-credited"
);
}
#[test]
fn credits_uniffi_generated_glue_via_header() {
let src = "// This file was autogenerated by some hand-written code.\n// Trust me, you don't want to mess with it!\nimport Foundation\n\npublic func uniffiTraitInterfaceCallWithError() {}\npublic let UNIFFI_CALLBACK_SUCCESS = 0\npublic struct FfiConverterTypeFoo {}\n";
let analysis = analyze_swift_file(src, "vibecrafted_shell_ffi.swift".to_string());
for credited in [
"uniffiTraitInterfaceCallWithError",
"UNIFFI_CALLBACK_SUCCESS",
"FfiConverterTypeFoo",
] {
assert!(
analysis.local_uses.iter().any(|u| u == credited),
"UniFFI generated symbol `{credited}` must be credited as used"
);
}
}
#[test]
fn credits_uniffi_generated_glue_via_ffi_converter_density() {
let src = "import Foundation\n\npublic struct FfiConverterUInt8 {}\npublic struct FfiConverterString {}\npublic struct FfiConverterData {}\npublic struct FfiConverterBool {}\npublic func lift() {}\n";
let analysis = analyze_swift_file(src, "bindings.swift".to_string());
assert!(
analysis.local_uses.iter().any(|u| u == "lift"),
"a high-FfiConverter-density file must credit its exports as used"
);
}
#[test]
fn does_not_credit_hand_written_ffi_named_file() {
let src = "import Foundation\n\npublic func myHandWrittenHelper() {}\npublic struct FfiBridge {}\n";
let analysis = analyze_swift_file(src, "custom_ffi.swift".to_string());
assert!(
!analysis
.local_uses
.iter()
.any(|u| u == "myHandWrittenHelper"),
"hand-written *_ffi.swift must not be force-credited as generated"
);
}
#[test]
fn parses_swift_imports() {
let src = r#"
import Foundation
@testable import MyApp
import struct Module.MyStruct
"#;
let analysis = analyze_swift_file(src, "main.swift".to_string());
let imports: Vec<_> = analysis.imports.iter().map(|i| i.source.clone()).collect();
assert!(imports.contains(&"Foundation".to_string()));
assert!(imports.contains(&"MyApp".to_string()));
assert!(imports.contains(&"Module.MyStruct".to_string()));
}
}