diff --git a/README.md b/README.md index 2cf84826f..a23c45ea3 100644 --- a/README.md +++ b/README.md @@ -303,7 +303,7 @@ GitNexus builds a complete knowledge graph of your codebase through a multi-phas ### Supported Languages -TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust +TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Swift --- @@ -465,7 +465,7 @@ The wiki generator reads the indexed graph structure, groups files into modules - [X] Wiki Generation, Multi-File Rename, Git-Diff Impact Analysis - [X] Process-Grouped Search, 360-Degree Context, Claude Code Hooks -- [X] Multi-Repo MCP, Zero-Config Setup, 9 Language Support +- [X] Multi-Repo MCP, Zero-Config Setup, 11 Language Support - [X] Community Detection, Process Detection, Confidence Scoring - [X] Hybrid Search, Vector Index diff --git a/gitnexus-web/public/wasm/swift/tree-sitter-swift.wasm b/gitnexus-web/public/wasm/swift/tree-sitter-swift.wasm new file mode 100755 index 000000000..87282f216 Binary files /dev/null and b/gitnexus-web/public/wasm/swift/tree-sitter-swift.wasm differ diff --git a/gitnexus-web/src/config/supported-languages.ts b/gitnexus-web/src/config/supported-languages.ts index a9bcd8248..5df70ed83 100644 --- a/gitnexus-web/src/config/supported-languages.ts +++ b/gitnexus-web/src/config/supported-languages.ts @@ -10,5 +10,5 @@ export enum SupportedLanguages { Rust = 'rust', PHP = 'php', // Ruby = 'ruby', - // Swift = 'swift', + Swift = 'swift', } \ No newline at end of file diff --git a/gitnexus-web/src/core/ingestion/entry-point-scoring.ts b/gitnexus-web/src/core/ingestion/entry-point-scoring.ts index 9285e407d..89645769e 100644 --- a/gitnexus-web/src/core/ingestion/entry-point-scoring.ts +++ b/gitnexus-web/src/core/ingestion/entry-point-scoring.ts @@ -103,6 +103,26 @@ const ENTRY_POINT_PATTERNS: Record = { /^Start$/, // Start methods ], + // Swift / iOS + 'swift': [ + /^viewDidLoad$/, // UIKit lifecycle + /^viewWillAppear$/, // UIKit lifecycle + /^viewDidAppear$/, // UIKit lifecycle + /^viewWillDisappear$/, // UIKit lifecycle + /^viewDidDisappear$/, // UIKit lifecycle + /^application\(/, // AppDelegate methods + /^scene\(/, // SceneDelegate methods + /^body$/, // SwiftUI View.body + /Coordinator$/, // Coordinator pattern + /^sceneDidBecomeActive$/, // SceneDelegate lifecycle + /^sceneWillResignActive$/, // SceneDelegate lifecycle + /^didFinishLaunchingWithOptions$/, // AppDelegate + /ViewController$/, // ViewController classes + /^configure[A-Z]/, // Configuration methods + /^setup[A-Z]/, // Setup methods + /^makeBody$/, // SwiftUI ViewModifier + ], + // PHP / Laravel 'php': [ /Controller$/, // UserController (class name convention) @@ -271,6 +291,10 @@ export function isTestFile(filePath: string): boolean { p.includes('/src/test/') || // Rust test patterns (inline tests are different, but test files) p.includes('/tests/') || + // Swift/iOS test patterns + p.endsWith('tests.swift') || + p.endsWith('test.swift') || + p.includes('uitests/') || // C# test patterns p.includes('.tests/') || p.includes('tests.cs') || diff --git a/gitnexus-web/src/core/ingestion/framework-detection.ts b/gitnexus-web/src/core/ingestion/framework-detection.ts index 4aec27f7c..67f773574 100644 --- a/gitnexus-web/src/core/ingestion/framework-detection.ts +++ b/gitnexus-web/src/core/ingestion/framework-detection.ts @@ -257,16 +257,63 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null return { framework: 'laravel', entryPointMultiplier: 1.5, reason: 'laravel-repository' }; } + // ========== SWIFT / iOS ========== + + // iOS App entry points (highest priority) + if (p.endsWith('/appdelegate.swift') || p.endsWith('/scenedelegate.swift') || p.endsWith('/app.swift')) { + return { framework: 'ios', entryPointMultiplier: 3.0, reason: 'ios-app-entry' }; + } + + // SwiftUI App entry (@main) + if (p.endsWith('app.swift') && p.includes('/sources/')) { + return { framework: 'swiftui', entryPointMultiplier: 3.0, reason: 'swiftui-app' }; + } + + // UIKit ViewControllers (high priority - screen entry points) + if ((p.includes('/viewcontrollers/') || p.includes('/controllers/') || p.includes('/screens/')) && p.endsWith('.swift')) { + return { framework: 'uikit', entryPointMultiplier: 2.5, reason: 'uikit-viewcontroller' }; + } + + // ViewController by filename convention + if (p.endsWith('viewcontroller.swift') || p.endsWith('vc.swift')) { + return { framework: 'uikit', entryPointMultiplier: 2.5, reason: 'uikit-viewcontroller-file' }; + } + + // Coordinator pattern (navigation entry points) + if (p.includes('/coordinators/') && p.endsWith('.swift')) { + return { framework: 'ios-coordinator', entryPointMultiplier: 2.5, reason: 'ios-coordinator' }; + } + + // Coordinator by filename + if (p.endsWith('coordinator.swift')) { + return { framework: 'ios-coordinator', entryPointMultiplier: 2.5, reason: 'ios-coordinator-file' }; + } + + // SwiftUI Views (moderate - reusable components) + if ((p.includes('/views/') || p.includes('/scenes/')) && p.endsWith('.swift')) { + return { framework: 'swiftui', entryPointMultiplier: 1.8, reason: 'swiftui-view' }; + } + + // Service layer + if (p.includes('/services/') && p.endsWith('.swift')) { + return { framework: 'ios-service', entryPointMultiplier: 1.8, reason: 'ios-service' }; + } + + // Router / navigation + if (p.includes('/router/') && p.endsWith('.swift')) { + return { framework: 'ios-router', entryPointMultiplier: 2.0, reason: 'ios-router' }; + } + // ========== GENERIC PATTERNS ========== // Any language: index files in API folders if (p.includes('/api/') && ( - p.endsWith('/index.ts') || p.endsWith('/index.js') || + p.endsWith('/index.ts') || p.endsWith('/index.js') || p.endsWith('/__init__.py') )) { return { framework: 'api', entryPointMultiplier: 1.8, reason: 'api-index' }; } - + // No framework detected - return null for graceful fallback (1.0 multiplier) return null; } @@ -306,4 +353,9 @@ export const FRAMEWORK_AST_PATTERNS = { 'actix': ['#[get', '#[post', '#[put', '#[delete'], 'axum': ['Router::new'], 'rocket': ['#[get', '#[post'], + + // Swift/iOS + 'uikit': ['viewDidLoad', 'viewWillAppear', 'viewDidAppear', 'UIViewController'], + 'swiftui': ['@main', 'WindowGroup', 'ContentView', '@StateObject', '@ObservedObject'], + 'combine': ['sink', 'assign', 'Publisher', 'Subscriber'], }; diff --git a/gitnexus-web/src/core/ingestion/tree-sitter-queries.ts b/gitnexus-web/src/core/ingestion/tree-sitter-queries.ts index 5cb2d46ff..3ba476ba3 100644 --- a/gitnexus-web/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus-web/src/core/ingestion/tree-sitter-queries.ts @@ -396,6 +396,59 @@ export const PHP_QUERIES = ` [(name) (qualified_name)] @heritage.trait))) @heritage `; +// Swift queries - works with tree-sitter-swift +export const SWIFT_QUERIES = ` +; Classes +(class_declaration "class" name: (type_identifier) @name) @definition.class + +; Structs +(class_declaration "struct" name: (type_identifier) @name) @definition.struct + +; Enums +(class_declaration "enum" name: (type_identifier) @name) @definition.enum + +; Extensions (mapped to class — no dedicated label in schema) +(class_declaration "extension" name: (user_type (type_identifier) @name)) @definition.class + +; Actors +(class_declaration "actor" name: (type_identifier) @name) @definition.class + +; Protocols (mapped to interface) +(protocol_declaration name: (type_identifier) @name) @definition.interface + +; Type aliases +(typealias_declaration name: (type_identifier) @name) @definition.type + +; Functions (top-level and methods) +(function_declaration name: (simple_identifier) @name) @definition.function + +; Protocol method declarations +(protocol_function_declaration name: (simple_identifier) @name) @definition.method + +; Initializers +(init_declaration) @definition.constructor + +; Properties (stored and computed) +(property_declaration (pattern (simple_identifier) @name)) @definition.property + +; Imports +(import_declaration (identifier (simple_identifier) @import.source)) @import + +; Calls - direct function calls +(call_expression (simple_identifier) @call.name) @call + +; Calls - member/navigation calls (obj.method()) +(call_expression (navigation_expression (navigation_suffix (simple_identifier) @call.name))) @call + +; Heritage - class/struct/enum inheritance and protocol conformance +(class_declaration name: (type_identifier) @heritage.class + (inheritance_specifier inherits_from: (user_type (type_identifier) @heritage.extends))) @heritage + +; Heritage - protocol inheritance +(protocol_declaration name: (type_identifier) @heritage.class + (inheritance_specifier inherits_from: (user_type (type_identifier) @heritage.extends))) @heritage +`; + export const LANGUAGE_QUERIES: Record = { [SupportedLanguages.TypeScript]: TYPESCRIPT_QUERIES, [SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES, @@ -407,5 +460,6 @@ export const LANGUAGE_QUERIES: Record = { [SupportedLanguages.CSharp]: CSHARP_QUERIES, [SupportedLanguages.Rust]: RUST_QUERIES, [SupportedLanguages.PHP]: PHP_QUERIES, + [SupportedLanguages.Swift]: SWIFT_QUERIES, }; \ No newline at end of file diff --git a/gitnexus-web/src/core/ingestion/utils.ts b/gitnexus-web/src/core/ingestion/utils.ts index c53fa4248..c7479aaa6 100644 --- a/gitnexus-web/src/core/ingestion/utils.ts +++ b/gitnexus-web/src/core/ingestion/utils.ts @@ -31,6 +31,8 @@ export const getLanguageFromFilename = (filename: string): SupportedLanguages | filename.endsWith('.php5') || filename.endsWith('.php8')) { return SupportedLanguages.PHP; } + // Swift + if (filename.endsWith('.swift')) return SupportedLanguages.Swift; return null; }; diff --git a/gitnexus-web/src/core/tree-sitter/parser-loader.ts b/gitnexus-web/src/core/tree-sitter/parser-loader.ts index e38e8d5d2..e434874c4 100644 --- a/gitnexus-web/src/core/tree-sitter/parser-loader.ts +++ b/gitnexus-web/src/core/tree-sitter/parser-loader.ts @@ -40,6 +40,7 @@ const getWasmPath = (language: SupportedLanguages, filePath?: string): string => [SupportedLanguages.Go]: '/wasm/go/tree-sitter-go.wasm', [SupportedLanguages.Rust]: '/wasm/rust/tree-sitter-rust.wasm', [SupportedLanguages.PHP]: '/wasm/php/tree-sitter-php.wasm', + [SupportedLanguages.Swift]: '/wasm/swift/tree-sitter-swift.wasm', }; return languageFileMap[language]; diff --git a/gitnexus/README.md b/gitnexus/README.md index e6aa62940..d66f94fe2 100644 --- a/gitnexus/README.md +++ b/gitnexus/README.md @@ -156,7 +156,7 @@ GitNexus supports indexing multiple repositories. Each `gitnexus analyze` regist ## Supported Languages -TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust +TypeScript, JavaScript, Python, Java, C, C++, C#, Go, Rust, PHP, Swift ## Agent Skills diff --git a/gitnexus/package.json b/gitnexus/package.json index 44a78b131..16f5248e5 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -32,13 +32,15 @@ "files": [ "dist", "hooks", + "scripts", "skills", "vendor" ], "scripts": { "build": "tsc", "dev": "tsx watch src/cli/index.ts", - "prepare": "npm run build" + "prepare": "npm run build", + "postinstall": "node scripts/patch-tree-sitter-swift.cjs" }, "dependencies": { "@huggingface/transformers": "^3.0.0", @@ -63,6 +65,7 @@ "tree-sitter-java": "^0.21.0", "tree-sitter-javascript": "^0.21.0", "tree-sitter-php": "^0.23.12", + "tree-sitter-swift": "^0.6.0", "tree-sitter-python": "^0.21.0", "tree-sitter-rust": "^0.21.0", "tree-sitter-typescript": "^0.21.0", diff --git a/gitnexus/scripts/patch-tree-sitter-swift.cjs b/gitnexus/scripts/patch-tree-sitter-swift.cjs new file mode 100644 index 000000000..3c3dcad50 --- /dev/null +++ b/gitnexus/scripts/patch-tree-sitter-swift.cjs @@ -0,0 +1,74 @@ +#!/usr/bin/env node +/** + * WORKAROUND: tree-sitter-swift@0.6.0 binding.gyp build failure + * + * Background: + * tree-sitter-swift@0.6.0's binding.gyp contains an "actions" array that + * invokes `tree-sitter generate` to regenerate parser.c from grammar.js. + * This is intended for grammar developers, but the published npm package + * already ships pre-generated parser files (parser.c, scanner.c), so the + * actions are unnecessary for consumers. Since consumers don't have + * tree-sitter-cli installed, the actions always fail during `npm install`. + * + * Why we can't just upgrade: + * tree-sitter-swift@0.7.1 fixes this (removes postinstall, ships prebuilds), + * but it requires tree-sitter@^0.22.1. The upstream project pins tree-sitter + * to ^0.21.0 and all other grammar packages depend on that version. + * Upgrading tree-sitter would be a separate breaking change. + * + * How this workaround works: + * 1. tree-sitter-swift's own postinstall fails (npm warns but continues) + * 2. This script runs as gitnexus's postinstall + * 3. It removes the "actions" array from binding.gyp + * 4. It rebuilds the native binding with the cleaned binding.gyp + * + * TODO: Remove this script when tree-sitter is upgraded to ^0.22.x, + * which allows using tree-sitter-swift@0.7.1+ directly. + */ +const fs = require('fs'); +const path = require('path'); +const { execSync } = require('child_process'); + +const swiftDir = path.join(__dirname, '..', 'node_modules', 'tree-sitter-swift'); +const bindingPath = path.join(swiftDir, 'binding.gyp'); + +try { + if (!fs.existsSync(bindingPath)) { + process.exit(0); + } + + const content = fs.readFileSync(bindingPath, 'utf8'); + let needsRebuild = false; + + if (content.includes('"actions"')) { + // Strip Python-style comments (#) before JSON parsing + const cleaned = content.replace(/#[^\n]*/g, ''); + const gyp = JSON.parse(cleaned); + + if (gyp.targets && gyp.targets[0] && gyp.targets[0].actions) { + delete gyp.targets[0].actions; + fs.writeFileSync(bindingPath, JSON.stringify(gyp, null, 2) + '\n'); + console.log('[tree-sitter-swift] Patched binding.gyp (removed actions array)'); + needsRebuild = true; + } + } + + // Check if native binding exists + const bindingNode = path.join(swiftDir, 'build', 'Release', 'tree_sitter_swift_binding.node'); + if (!fs.existsSync(bindingNode)) { + needsRebuild = true; + } + + if (needsRebuild) { + console.log('[tree-sitter-swift] Rebuilding native binding...'); + execSync('npx node-gyp rebuild', { + cwd: swiftDir, + stdio: 'pipe', + timeout: 120000, + }); + console.log('[tree-sitter-swift] Native binding built successfully'); + } +} catch (err) { + console.warn('[tree-sitter-swift] Could not build native binding:', err.message); + console.warn('[tree-sitter-swift] You may need to manually run: cd node_modules/tree-sitter-swift && npx node-gyp rebuild'); +} diff --git a/gitnexus/src/config/supported-languages.ts b/gitnexus/src/config/supported-languages.ts index a9bcd8248..5df70ed83 100644 --- a/gitnexus/src/config/supported-languages.ts +++ b/gitnexus/src/config/supported-languages.ts @@ -10,5 +10,5 @@ export enum SupportedLanguages { Rust = 'rust', PHP = 'php', // Ruby = 'ruby', - // Swift = 'swift', + Swift = 'swift', } \ No newline at end of file diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index 421c6ed0c..e54d43dd9 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -37,6 +37,9 @@ const FUNCTION_NODE_TYPES = new Set([ // Rust 'function_item', 'impl_item', // Methods inside impl blocks + // Swift + 'init_declaration', + 'deinit_declaration', ]); /** @@ -57,7 +60,13 @@ const findEnclosingFunction = ( let label = 'Function'; // Different node types have different name locations - if (current.type === 'function_declaration' || + // Swift init/deinit — handle before generic cases (more specific) + if (current.type === 'init_declaration' || current.type === 'deinit_declaration') { + const funcName = current.type === 'init_declaration' ? 'init' : 'deinit'; + return generateId('Constructor', `${filePath}:${funcName}`); + } + + if (current.type === 'function_declaration' || current.type === 'function_definition' || current.type === 'async_function_declaration' || current.type === 'generator_function_declaration' || @@ -336,6 +345,37 @@ const BUILT_IN_NAMES = new Set([ 'mutex_lock', 'mutex_unlock', 'mutex_init', 'kfree', 'kmalloc', 'kzalloc', 'kcalloc', 'krealloc', 'kvmalloc', 'kvfree', 'get', 'put', + // Swift/iOS built-ins and standard library + 'print', 'debugPrint', 'dump', 'fatalError', 'precondition', 'preconditionFailure', + 'assert', 'assertionFailure', 'NSLog', + 'abs', 'min', 'max', 'zip', 'stride', 'sequence', 'repeatElement', + 'swap', 'withUnsafePointer', 'withUnsafeMutablePointer', 'withUnsafeBytes', + 'autoreleasepool', 'unsafeBitCast', 'unsafeDowncast', 'numericCast', + 'type', 'MemoryLayout', + // Swift collection/string methods (common noise) + 'map', 'flatMap', 'compactMap', 'filter', 'reduce', 'forEach', 'contains', + 'first', 'last', 'prefix', 'suffix', 'dropFirst', 'dropLast', + 'sorted', 'reversed', 'enumerated', 'joined', 'split', + 'append', 'insert', 'remove', 'removeAll', 'removeFirst', 'removeLast', + 'isEmpty', 'count', 'index', 'startIndex', 'endIndex', + // UIKit/Foundation common methods (noise in call graph) + 'addSubview', 'removeFromSuperview', 'layoutSubviews', 'setNeedsLayout', + 'layoutIfNeeded', 'setNeedsDisplay', 'invalidateIntrinsicContentSize', + 'addTarget', 'removeTarget', 'addGestureRecognizer', + 'addConstraint', 'addConstraints', 'removeConstraint', 'removeConstraints', + 'NSLocalizedString', 'Bundle', + 'reloadData', 'reloadSections', 'reloadRows', 'performBatchUpdates', + 'register', 'dequeueReusableCell', 'dequeueReusableSupplementaryView', + 'beginUpdates', 'endUpdates', 'insertRows', 'deleteRows', 'insertSections', 'deleteSections', + 'present', 'dismiss', 'pushViewController', 'popViewController', 'popToRootViewController', + 'performSegue', 'prepare', + // GCD / async + 'DispatchQueue', 'async', 'sync', 'asyncAfter', + 'Task', 'withCheckedContinuation', 'withCheckedThrowingContinuation', + // Combine + 'sink', 'store', 'assign', 'receive', 'subscribe', + // Notification / KVO + 'addObserver', 'removeObserver', 'post', 'NotificationCenter', ]); const isBuiltInOrNoise = (name: string): boolean => BUILT_IN_NAMES.has(name); diff --git a/gitnexus/src/core/ingestion/entry-point-scoring.ts b/gitnexus/src/core/ingestion/entry-point-scoring.ts index ed328cc13..b7b9d457e 100644 --- a/gitnexus/src/core/ingestion/entry-point-scoring.ts +++ b/gitnexus/src/core/ingestion/entry-point-scoring.ts @@ -103,6 +103,26 @@ const ENTRY_POINT_PATTERNS: Record = { /^Start$/, // Start methods ], + // Swift / iOS + 'swift': [ + /^viewDidLoad$/, // UIKit lifecycle + /^viewWillAppear$/, // UIKit lifecycle + /^viewDidAppear$/, // UIKit lifecycle + /^viewWillDisappear$/, // UIKit lifecycle + /^viewDidDisappear$/, // UIKit lifecycle + /^application\(/, // AppDelegate methods + /^scene\(/, // SceneDelegate methods + /^body$/, // SwiftUI View.body + /Coordinator$/, // Coordinator pattern + /^sceneDidBecomeActive$/, // SceneDelegate lifecycle + /^sceneWillResignActive$/, // SceneDelegate lifecycle + /^didFinishLaunchingWithOptions$/, // AppDelegate + /ViewController$/, // ViewController classes + /^configure[A-Z]/, // Configuration methods + /^setup[A-Z]/, // Setup methods + /^makeBody$/, // SwiftUI ViewModifier + ], + // PHP / Laravel 'php': [ /Controller$/, // UserController (class name convention) @@ -271,6 +291,10 @@ export function isTestFile(filePath: string): boolean { p.includes('/src/test/') || // Rust test patterns (inline tests are different, but test files) p.includes('/tests/') || + // Swift/iOS test patterns + p.endsWith('tests.swift') || + p.endsWith('test.swift') || + p.includes('uitests/') || // C# test patterns p.includes('.tests/') || p.includes('tests.cs') || diff --git a/gitnexus/src/core/ingestion/framework-detection.ts b/gitnexus/src/core/ingestion/framework-detection.ts index 299966c7e..71a706aff 100644 --- a/gitnexus/src/core/ingestion/framework-detection.ts +++ b/gitnexus/src/core/ingestion/framework-detection.ts @@ -259,6 +259,53 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null return { framework: 'laravel', entryPointMultiplier: 1.5, reason: 'laravel-repository' }; } + // ========== SWIFT / iOS ========== + + // iOS App entry points (highest priority) + if (p.endsWith('/appdelegate.swift') || p.endsWith('/scenedelegate.swift') || p.endsWith('/app.swift')) { + return { framework: 'ios', entryPointMultiplier: 3.0, reason: 'ios-app-entry' }; + } + + // SwiftUI App entry (@main) + if (p.endsWith('app.swift') && p.includes('/sources/')) { + return { framework: 'swiftui', entryPointMultiplier: 3.0, reason: 'swiftui-app' }; + } + + // UIKit ViewControllers (high priority - screen entry points) + if ((p.includes('/viewcontrollers/') || p.includes('/controllers/') || p.includes('/screens/')) && p.endsWith('.swift')) { + return { framework: 'uikit', entryPointMultiplier: 2.5, reason: 'uikit-viewcontroller' }; + } + + // ViewController by filename convention + if (p.endsWith('viewcontroller.swift') || p.endsWith('vc.swift')) { + return { framework: 'uikit', entryPointMultiplier: 2.5, reason: 'uikit-viewcontroller-file' }; + } + + // Coordinator pattern (navigation entry points) + if (p.includes('/coordinators/') && p.endsWith('.swift')) { + return { framework: 'ios-coordinator', entryPointMultiplier: 2.5, reason: 'ios-coordinator' }; + } + + // Coordinator by filename + if (p.endsWith('coordinator.swift')) { + return { framework: 'ios-coordinator', entryPointMultiplier: 2.5, reason: 'ios-coordinator-file' }; + } + + // SwiftUI Views (moderate - reusable components) + if ((p.includes('/views/') || p.includes('/scenes/')) && p.endsWith('.swift')) { + return { framework: 'swiftui', entryPointMultiplier: 1.8, reason: 'swiftui-view' }; + } + + // Service layer + if (p.includes('/services/') && p.endsWith('.swift')) { + return { framework: 'ios-service', entryPointMultiplier: 1.8, reason: 'ios-service' }; + } + + // Router / navigation + if (p.includes('/router/') && p.endsWith('.swift')) { + return { framework: 'ios-router', entryPointMultiplier: 2.0, reason: 'ios-router' }; + } + // ========== GENERIC PATTERNS ========== // Any language: index files in API folders @@ -308,6 +355,11 @@ export const FRAMEWORK_AST_PATTERNS = { 'actix': ['#[get', '#[post', '#[put', '#[delete'], 'axum': ['Router::new'], 'rocket': ['#[get', '#[post'], + + // Swift/iOS + 'uikit': ['viewDidLoad', 'viewWillAppear', 'viewDidAppear', 'UIViewController'], + 'swiftui': ['@main', 'WindowGroup', 'ContentView', '@StateObject', '@ObservedObject'], + 'combine': ['sink', 'assign', 'Publisher', 'Subscriber'], }; interface AstFrameworkPatternConfig { diff --git a/gitnexus/src/core/ingestion/import-processor.ts b/gitnexus/src/core/ingestion/import-processor.ts index 6ab4213d9..0ad666bba 100644 --- a/gitnexus/src/core/ingestion/import-processor.ts +++ b/gitnexus/src/core/ingestion/import-processor.ts @@ -153,6 +153,42 @@ async function loadComposerConfig(repoRoot: string): Promise source directory path (e.g., "SiuperModel" -> "Package/Sources/SiuperModel") */ + targets: Map; +} + +async function loadSwiftPackageConfig(repoRoot: string): Promise { + // Swift imports are module-name based (e.g., `import SiuperModel`) + // SPM convention: Sources// or Package/Sources// + // We scan for these directories to build a target map + const targets = new Map(); + + const sourceDirs = ['Sources', 'Package/Sources', 'src']; + for (const sourceDir of sourceDirs) { + try { + const fullPath = path.join(repoRoot, sourceDir); + const entries = await fs.readdir(fullPath, { withFileTypes: true }); + for (const entry of entries) { + if (entry.isDirectory()) { + targets.set(entry.name, sourceDir + '/' + entry.name); + } + } + } catch { + // Directory doesn't exist + } + } + + if (targets.size > 0) { + if (isDev) { + console.log(`📦 Loaded ${targets.size} Swift package targets`); + } + return { targets }; + } + return null; +} + // ============================================================================ // IMPORT PATH RESOLUTION // ============================================================================ @@ -176,6 +212,8 @@ const EXTENSIONS = [ '.rs', '/mod.rs', // PHP '.php', '.phtml', + // Swift + '.swift', ]; /** @@ -688,6 +726,7 @@ export const processImports = async ( const tsconfigPaths = await loadTsconfigPaths(effectiveRoot); const goModule = await loadGoModulePath(effectiveRoot); const composerConfig = await loadComposerConfig(effectiveRoot); + const swiftPackageConfig = await loadSwiftPackageConfig(effectiveRoot); // Helper: add an IMPORTS edge + update import map const addImportEdge = (filePath: string, resolvedPath: string) => { @@ -821,6 +860,25 @@ export const processImports = async ( return; } + // ---- Swift: handle module imports ---- + if (language === SupportedLanguages.Swift && swiftPackageConfig) { + // Swift imports are module names: `import SiuperModel` + // Resolve to the module's source directory → all .swift files in it + const targetDir = swiftPackageConfig.targets.get(rawImportPath); + if (targetDir) { + // Find all .swift files in this target directory + const dirPrefix = targetDir + '/'; + for (const filePath2 of allFileList) { + if (filePath2.startsWith(dirPrefix) && filePath2.endsWith('.swift')) { + addImportEdge(file.path, filePath2); + } + } + return; + } + // External framework (Foundation, UIKit, etc.) — skip + return; + } + // ---- Standard single-file resolution ---- const resolvedPath = resolveImportPath( file.path, @@ -871,6 +929,7 @@ export const processImportsFromExtracted = async ( const tsconfigPaths = await loadTsconfigPaths(effectiveRoot); const goModule = await loadGoModulePath(effectiveRoot); const composerConfig = await loadComposerConfig(effectiveRoot); + const swiftPackageConfig = await loadSwiftPackageConfig(effectiveRoot); const addImportEdge = (filePath: string, resolvedPath: string) => { const sourceId = generateId('File', filePath); @@ -980,6 +1039,20 @@ export const processImportsFromExtracted = async ( continue; } + // Swift: handle module imports + if (language === SupportedLanguages.Swift && swiftPackageConfig) { + const targetDir = swiftPackageConfig.targets.get(rawImportPath); + if (targetDir) { + const dirPrefix = targetDir + '/'; + for (const fp of allFileList) { + if (fp.startsWith(dirPrefix) && fp.endsWith('.swift')) { + addImportEdge(filePath, fp); + } + } + } + continue; + } + // Standard resolution (has its own internal cache) const resolvedPath = resolveImportPath( filePath, diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index 7647f0c37..f024ac566 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -147,6 +147,17 @@ const isNodeExported = (node: any, name: string, language: string): boolean => { case 'cpp': return false; + // Swift: Check for 'public' or 'open' access modifiers + case 'swift': + while (current) { + if (current.type === 'modifiers' || current.type === 'visibility_modifier') { + const text = current.text || ''; + if (text.includes('public') || text.includes('open')) return true; + } + current = current.parent; + } + return false; + default: return false; } diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index ff4f8f28f..a32685278 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -396,6 +396,59 @@ export const PHP_QUERIES = ` [(name) (qualified_name)] @heritage.trait))) @heritage `; +// Swift queries - works with tree-sitter-swift +export const SWIFT_QUERIES = ` +; Classes +(class_declaration "class" name: (type_identifier) @name) @definition.class + +; Structs +(class_declaration "struct" name: (type_identifier) @name) @definition.struct + +; Enums +(class_declaration "enum" name: (type_identifier) @name) @definition.enum + +; Extensions (mapped to class — no dedicated label in schema) +(class_declaration "extension" name: (user_type (type_identifier) @name)) @definition.class + +; Actors +(class_declaration "actor" name: (type_identifier) @name) @definition.class + +; Protocols (mapped to interface) +(protocol_declaration name: (type_identifier) @name) @definition.interface + +; Type aliases +(typealias_declaration name: (type_identifier) @name) @definition.type + +; Functions (top-level and methods) +(function_declaration name: (simple_identifier) @name) @definition.function + +; Protocol method declarations +(protocol_function_declaration name: (simple_identifier) @name) @definition.method + +; Initializers +(init_declaration) @definition.constructor + +; Properties (stored and computed) +(property_declaration (pattern (simple_identifier) @name)) @definition.property + +; Imports +(import_declaration (identifier (simple_identifier) @import.source)) @import + +; Calls - direct function calls +(call_expression (simple_identifier) @call.name) @call + +; Calls - member/navigation calls (obj.method()) +(call_expression (navigation_expression (navigation_suffix (simple_identifier) @call.name))) @call + +; Heritage - class/struct/enum inheritance and protocol conformance +(class_declaration name: (type_identifier) @heritage.class + (inheritance_specifier inherits_from: (user_type (type_identifier) @heritage.extends))) @heritage + +; Heritage - protocol inheritance +(protocol_declaration name: (type_identifier) @heritage.class + (inheritance_specifier inherits_from: (user_type (type_identifier) @heritage.extends))) @heritage +`; + export const LANGUAGE_QUERIES: Record = { [SupportedLanguages.TypeScript]: TYPESCRIPT_QUERIES, [SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES, @@ -407,5 +460,6 @@ export const LANGUAGE_QUERIES: Record = { [SupportedLanguages.CSharp]: CSHARP_QUERIES, [SupportedLanguages.Rust]: RUST_QUERIES, [SupportedLanguages.PHP]: PHP_QUERIES, + [SupportedLanguages.Swift]: SWIFT_QUERIES, }; \ No newline at end of file diff --git a/gitnexus/src/core/ingestion/utils.ts b/gitnexus/src/core/ingestion/utils.ts index 12b4c6b3e..d975ed293 100644 --- a/gitnexus/src/core/ingestion/utils.ts +++ b/gitnexus/src/core/ingestion/utils.ts @@ -37,6 +37,7 @@ export const getLanguageFromFilename = (filename: string): SupportedLanguages | filename.endsWith('.php5') || filename.endsWith('.php8')) { return SupportedLanguages.PHP; } + if (filename.endsWith('.swift')) return SupportedLanguages.Swift; return null; }; diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 4b31fd9c3..c700bcee2 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -10,6 +10,7 @@ import CSharp from 'tree-sitter-c-sharp'; import Go from 'tree-sitter-go'; import Rust from 'tree-sitter-rust'; import PHP from 'tree-sitter-php'; +import Swift from 'tree-sitter-swift'; import { SupportedLanguages } from '../../../config/supported-languages.js'; import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js'; import { getLanguageFromFilename } from '../utils.js'; @@ -106,6 +107,7 @@ const languageMap: Record = { [SupportedLanguages.Go]: Go, [SupportedLanguages.Rust]: Rust, [SupportedLanguages.PHP]: PHP.php_only, + [SupportedLanguages.Swift]: Swift, }; const setLanguage = (language: SupportedLanguages, filePath: string): void => { @@ -191,6 +193,16 @@ const isNodeExported = (node: any, name: string, language: string): boolean => { case 'cpp': return false; + case 'swift': + while (current) { + if (current.type === 'modifiers' || current.type === 'visibility_modifier') { + const text = current.text || ''; + if (text.includes('public') || text.includes('open')) return true; + } + current = current.parent; + } + return false; + case 'php': // Top-level classes/interfaces/traits are always accessible // Methods/properties are exported only if they have 'public' modifier @@ -225,6 +237,7 @@ const FUNCTION_NODE_TYPES = new Set([ 'method_declaration', 'constructor_declaration', 'local_function_statement', 'function_item', 'impl_item', 'anonymous_function_creation_expression', // PHP anonymous functions + 'init_declaration', 'deinit_declaration', // Swift initializers/deinitializers ]); /** Walk up AST to find enclosing function, return its generateId or null for top-level */ @@ -235,6 +248,12 @@ const findEnclosingFunctionId = (node: any, filePath: string): string | null => let funcName: string | null = null; let label = 'Function'; + if (current.type === 'init_declaration' || current.type === 'deinit_declaration') { + const funcName = current.type === 'init_declaration' ? 'init' : 'deinit'; + const label = 'Constructor'; + return generateId(label, `${filePath}:${funcName}`); + } + if (['function_declaration', 'function_definition', 'async_function_declaration', 'generator_function_declaration', 'function_item'].includes(current.type)) { const nameNode = current.childForFieldName?.('name') || @@ -339,6 +358,37 @@ const BUILT_INS = new Set([ 'preg_match', 'preg_match_all', 'preg_replace', 'preg_split', 'header', 'session_start', 'session_destroy', 'ob_start', 'ob_end_clean', 'ob_get_clean', 'dd', 'dump', + // Swift/iOS built-ins and standard library + 'print', 'debugPrint', 'dump', 'fatalError', 'precondition', 'preconditionFailure', + 'assert', 'assertionFailure', 'NSLog', + 'abs', 'min', 'max', 'zip', 'stride', 'sequence', 'repeatElement', + 'swap', 'withUnsafePointer', 'withUnsafeMutablePointer', 'withUnsafeBytes', + 'autoreleasepool', 'unsafeBitCast', 'unsafeDowncast', 'numericCast', + 'type', 'MemoryLayout', + // Swift collection/string methods (common noise) + 'map', 'flatMap', 'compactMap', 'filter', 'reduce', 'forEach', 'contains', + 'first', 'last', 'prefix', 'suffix', 'dropFirst', 'dropLast', + 'sorted', 'reversed', 'enumerated', 'joined', 'split', + 'append', 'insert', 'remove', 'removeAll', 'removeFirst', 'removeLast', + 'isEmpty', 'count', 'index', 'startIndex', 'endIndex', + // UIKit/Foundation common methods (noise in call graph) + 'addSubview', 'removeFromSuperview', 'layoutSubviews', 'setNeedsLayout', + 'layoutIfNeeded', 'setNeedsDisplay', 'invalidateIntrinsicContentSize', + 'addTarget', 'removeTarget', 'addGestureRecognizer', + 'addConstraint', 'addConstraints', 'removeConstraint', 'removeConstraints', + 'NSLocalizedString', 'Bundle', + 'reloadData', 'reloadSections', 'reloadRows', 'performBatchUpdates', + 'register', 'dequeueReusableCell', 'dequeueReusableSupplementaryView', + 'beginUpdates', 'endUpdates', 'insertRows', 'deleteRows', 'insertSections', 'deleteSections', + 'present', 'dismiss', 'pushViewController', 'popViewController', 'popToRootViewController', + 'performSegue', 'prepare', + // GCD / async + 'DispatchQueue', 'async', 'sync', 'asyncAfter', + 'Task', 'withCheckedContinuation', 'withCheckedThrowingContinuation', + // Combine + 'sink', 'store', 'assign', 'receive', 'subscribe', + // Notification / KVO + 'addObserver', 'removeObserver', 'post', 'NotificationCenter', ]); // ============================================================================ diff --git a/gitnexus/src/core/kuzu/schema.ts b/gitnexus/src/core/kuzu/schema.ts index 5d634cba8..9989bd6c1 100644 --- a/gitnexus/src/core/kuzu/schema.ts +++ b/gitnexus/src/core/kuzu/schema.ts @@ -242,6 +242,7 @@ CREATE REL TABLE ${REL_TABLE_NAME} ( FROM Function TO \`Const\`, FROM Function TO \`Typedef\`, FROM Function TO \`Union\`, + FROM Function TO \`Property\`, FROM Class TO Method, FROM Class TO Function, FROM Class TO Class, @@ -301,7 +302,11 @@ CREATE REL TABLE ${REL_TABLE_NAME} ( FROM \`Struct\` TO \`Enum\`, FROM \`Struct\` TO Function, FROM \`Struct\` TO Method, + FROM \`Struct\` TO Interface, + FROM \`Enum\` TO \`Enum\`, FROM \`Enum\` TO Community, + FROM \`Enum\` TO Class, + FROM \`Enum\` TO Interface, FROM \`Macro\` TO Community, FROM \`Macro\` TO Function, FROM \`Macro\` TO Method, @@ -318,6 +323,7 @@ CREATE REL TABLE ${REL_TABLE_NAME} ( FROM \`Impl\` TO \`Impl\`, FROM \`TypeAlias\` TO Community, FROM \`TypeAlias\` TO \`Trait\`, + FROM \`TypeAlias\` TO Class, FROM \`Const\` TO Community, FROM \`Static\` TO Community, FROM \`Property\` TO Community, @@ -339,6 +345,8 @@ CREATE REL TABLE ${REL_TABLE_NAME} ( FROM \`Constructor\` TO \`Impl\`, FROM \`Constructor\` TO \`Namespace\`, FROM \`Constructor\` TO \`Module\`, + FROM \`Constructor\` TO \`Property\`, + FROM \`Constructor\` TO \`Typedef\`, FROM \`Template\` TO Community, FROM \`Module\` TO Community, FROM Function TO Process, diff --git a/gitnexus/src/core/tree-sitter/parser-loader.ts b/gitnexus/src/core/tree-sitter/parser-loader.ts index e92898424..8c02ecc35 100644 --- a/gitnexus/src/core/tree-sitter/parser-loader.ts +++ b/gitnexus/src/core/tree-sitter/parser-loader.ts @@ -9,6 +9,7 @@ import CSharp from 'tree-sitter-c-sharp'; import Go from 'tree-sitter-go'; import Rust from 'tree-sitter-rust'; import PHP from 'tree-sitter-php'; +import Swift from 'tree-sitter-swift'; import { SupportedLanguages } from '../../config/supported-languages.js'; let parser: Parser | null = null; @@ -25,6 +26,7 @@ const languageMap: Record = { [SupportedLanguages.Go]: Go, [SupportedLanguages.Rust]: Rust, [SupportedLanguages.PHP]: PHP.php_only, + [SupportedLanguages.Swift]: Swift, }; export const loadParser = async (): Promise => {