diff --git a/README.md b/README.md index 2cf84826f..eb04cdde6 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,29 @@ # GitNexus -abhigyanpatwari%2FGitNexus | Trendshift +
-**Building git for agent context.** + + abhigyanpatwari%2FGitNexus | Trendshift + + +

Join the official Discord to discuss ideas, issues etc!

+ + + Discord + + + npm version + + + License: PolyForm Noncommercial + + +
+ +**Building nervous system for agent context.** Indexes any codebase into a knowledge graph — every dependency, call chain, cluster, and execution flow — then exposes it through smart tools so AI agents never miss code. -[![npm version](https://img.shields.io/npm/v/gitnexus.svg)](https://www.npmjs.com/package/gitnexus) -[![License: PolyForm Noncommercial](https://img.shields.io/badge/License-PolyForm%20Noncommercial-blue.svg)](https://polyformproject.org/licenses/noncommercial/1.0.0/) @@ -303,7 +319,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 +481,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-lock.json b/gitnexus/package-lock.json index b57c2208f..a229f0564 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -1,12 +1,13 @@ { "name": "gitnexus", - "version": "1.3.4", + "version": "1.3.5", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "gitnexus", - "version": "1.3.4", + "version": "1.3.5", + "hasInstallScript": true, "license": "PolyForm-Noncommercial-1.0.0", "dependencies": { "@huggingface/transformers": "^3.0.0", @@ -34,6 +35,7 @@ "tree-sitter-php": "^0.23.12", "tree-sitter-python": "^0.21.0", "tree-sitter-rust": "^0.21.0", + "tree-sitter-swift": "^0.6.0", "tree-sitter-typescript": "^0.21.0", "typescript": "^5.4.5", "uuid": "^13.0.0" @@ -4268,6 +4270,19 @@ "node": "^18 || ^20 || >= 21" } }, + "node_modules/tree-sitter-cli": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/tree-sitter-cli/-/tree-sitter-cli-0.23.2.tgz", + "integrity": "sha512-kPPXprOqREX+C/FgUp2Qpt9jd0vSwn+hOgjzVv/7hapdoWpa+VeWId53rf4oNNd29ikheF12BYtGD/W90feMbA==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "tree-sitter": "cli.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/tree-sitter-cpp": { "version": "0.22.3", "resolved": "https://registry.npmjs.org/tree-sitter-cpp/-/tree-sitter-cpp-0.22.3.tgz", @@ -4483,6 +4498,36 @@ "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", "license": "MIT" }, + "node_modules/tree-sitter-swift": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/tree-sitter-swift/-/tree-sitter-swift-0.6.0.tgz", + "integrity": "sha512-9vOJZes4/UFjBr4COHtp6ZHVuZYwfChSQbpneXQog04dAstfx5px3ybVX2cN+ylvLqsvVpmXLpidxxgF2rDQ7A==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^8.0.0", + "node-gyp-build": "^4.8.0", + "tree-sitter-cli": "^0.23", + "which": "2.0.2" + }, + "peerDependencies": { + "tree-sitter": "^0.21.1" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-swift/node_modules/node-addon-api": { + "version": "8.6.0", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.6.0.tgz", + "integrity": "sha512-gBVjCaqDlRUk0EwoPNKzIr9KkS9041G/q31IBShPs1Xz6UTA+EXdZADbzqAJQrpDRq71CIMnOP5VMut3SL0z5Q==", + "license": "MIT", + "engines": { + "node": "^18 || ^20 || >= 21" + } + }, "node_modules/tree-sitter-typescript": { "version": "0.21.2", "resolved": "https://registry.npmjs.org/tree-sitter-typescript/-/tree-sitter-typescript-0.21.2.tgz", diff --git a/gitnexus/package.json b/gitnexus/package.json index 0845899e2..5b11308ae 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -1,6 +1,6 @@ { "name": "gitnexus", - "version": "1.3.4", + "version": "1.3.5", "description": "Graph-powered code intelligence for AI agents. Index any codebase, query via MCP or CLI.", "author": "Abhigyan Patwari", "license": "PolyForm-Noncommercial-1.0.0", @@ -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", @@ -64,6 +66,7 @@ "tree-sitter-javascript": "^0.21.0", "tree-sitter-kotlin": "^0.3.8", "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/cli/index.ts b/gitnexus/src/cli/index.ts index f86aac4e5..e7b7bd194 100644 --- a/gitnexus/src/cli/index.ts +++ b/gitnexus/src/cli/index.ts @@ -32,12 +32,15 @@ import { augmentCommand } from './augment.js'; import { wikiCommand } from './wiki.js'; import { queryCommand, contextCommand, impactCommand, cypherCommand } from './tool.js'; import { evalServerCommand } from './eval-server.js'; +import { createRequire } from 'node:module'; +const _require = createRequire(import.meta.url); +const pkg = _require('../../package.json'); const program = new Command(); program .name('gitnexus') .description('GitNexus local CLI and MCP server') - .version('1.2.0'); + .version(pkg.version); program .command('setup') diff --git a/gitnexus/src/config/supported-languages.ts b/gitnexus/src/config/supported-languages.ts index 7f72bc112..9d67eaf05 100644 --- a/gitnexus/src/config/supported-languages.ts +++ b/gitnexus/src/config/supported-languages.ts @@ -11,5 +11,5 @@ export enum SupportedLanguages { PHP = 'php', Kotlin = 'kotlin', // 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 e82236e51..c5fc3f8df 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -40,6 +40,9 @@ const FUNCTION_NODE_TYPES = new Set([ // Kotlin (function_declaration already included above via JS/TS) 'anonymous_function', 'lambda_literal', + // Swift + 'init_declaration', + 'deinit_declaration', ]); /** @@ -60,7 +63,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' || @@ -355,6 +364,37 @@ const BUILT_IN_NAMES = new Set([ 'stateIn', 'shareIn', 'launchIn', // Kotlin infix stdlib functions 'to', 'until', 'downTo', 'step', + // 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 c3ab00bca..aecff1126 100644 --- a/gitnexus/src/core/ingestion/framework-detection.ts +++ b/gitnexus/src/core/ingestion/framework-detection.ts @@ -302,6 +302,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 @@ -351,6 +398,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 e3e4f74c0..990f968af 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 // ============================================================================ @@ -178,6 +214,8 @@ const EXTENSIONS = [ '.rs', '/mod.rs', // PHP '.php', '.phtml', + // Swift + '.swift', ]; /** @@ -710,6 +748,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) => { @@ -859,6 +898,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, @@ -909,6 +967,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); @@ -1032,6 +1091,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 7d753aae5..8c60802c2 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -164,6 +164,17 @@ const isNodeExported = (node: any, name: string, language: string): boolean => { // No visibility modifier = public (Kotlin default) return true; + // 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 b98a1d653..7eeeb73e0 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -476,6 +476,59 @@ export const KOTLIN_QUERIES = ` (user_type (type_identifier) @heritage.extends)))) @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, @@ -488,5 +541,6 @@ export const LANGUAGE_QUERIES: Record = { [SupportedLanguages.Rust]: RUST_QUERIES, [SupportedLanguages.PHP]: PHP_QUERIES, [SupportedLanguages.Kotlin]: KOTLIN_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 927e32e60..47fa8cbe4 100644 --- a/gitnexus/src/core/ingestion/utils.ts +++ b/gitnexus/src/core/ingestion/utils.ts @@ -56,6 +56,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 cd9c03eaf..d31c980fe 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -11,6 +11,7 @@ import Go from 'tree-sitter-go'; import Rust from 'tree-sitter-rust'; import PHP from 'tree-sitter-php'; import Kotlin from 'tree-sitter-kotlin'; +import Swift from 'tree-sitter-swift'; import { SupportedLanguages } from '../../../config/supported-languages.js'; import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js'; import { findSiblingChild, getLanguageFromFilename } from '../utils.js'; @@ -108,6 +109,7 @@ const languageMap: Record = { [SupportedLanguages.Rust]: Rust, [SupportedLanguages.PHP]: PHP.php_only, [SupportedLanguages.Kotlin]: Kotlin, + [SupportedLanguages.Swift]: Swift, }; const setLanguage = (language: SupportedLanguages, filePath: string): void => { @@ -193,6 +195,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 @@ -246,6 +258,7 @@ const FUNCTION_NODE_TYPES = new Set([ 'anonymous_function_creation_expression', // PHP anonymous functions // Kotlin (function_declaration already included above via JS/TS) 'anonymous_function', 'lambda_literal', + 'init_declaration', 'deinit_declaration', // Swift initializers/deinitializers ]); /** Walk up AST to find enclosing function, return its generateId or null for top-level */ @@ -256,6 +269,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') || @@ -376,6 +395,37 @@ const BUILT_INS = new Set([ 'stateIn', 'shareIn', 'launchIn', // Kotlin infix stdlib functions 'to', 'until', 'downTo', 'step', + // 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 fb3a0ae93..f0133e5e2 100644 --- a/gitnexus/src/core/tree-sitter/parser-loader.ts +++ b/gitnexus/src/core/tree-sitter/parser-loader.ts @@ -10,6 +10,7 @@ import Go from 'tree-sitter-go'; import Rust from 'tree-sitter-rust'; import PHP from 'tree-sitter-php'; import Kotlin from 'tree-sitter-kotlin'; +import Swift from 'tree-sitter-swift'; import { SupportedLanguages } from '../../config/supported-languages.js'; let parser: Parser | null = null; @@ -27,6 +28,7 @@ const languageMap: Record = { [SupportedLanguages.Rust]: Rust, [SupportedLanguages.PHP]: PHP.php_only, [SupportedLanguages.Kotlin]: Kotlin, + [SupportedLanguages.Swift]: Swift, }; export const loadParser = async (): Promise => {