chore: merge upstream main into feat/kotlin-language-support

Resolve conflicts between Kotlin and Swift language support additions.
Both languages are now fully supported side by side.
This commit is contained in:
Gary Magyar 2026-02-28 12:26:43 +00:00
commit e046bf734d
24 changed files with 606 additions and 17 deletions

View file

@ -1,13 +1,29 @@
# GitNexus
<a href="https://trendshift.io/repositories/19809" target="_blank"><img src="https://trendshift.io/api/badge/repositories/19809" alt="abhigyanpatwari%2FGitNexus | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
<div align="center">
**Building git for agent context.**
<a href="https://trendshift.io/repositories/19809" target="_blank">
<img src="https://trendshift.io/api/badge/repositories/19809" alt="abhigyanpatwari%2FGitNexus | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/>
</a>
<h2>Join the official Discord to discuss ideas, issues etc!</h2>
<a href="https://discord.gg/AAsRVT6fGb">
<img src="https://img.shields.io/discord/1477255801545429032?color=5865F2&logo=discord&logoColor=white" alt="Discord"/>
</a>
<a href="https://www.npmjs.com/package/gitnexus">
<img src="https://img.shields.io/npm/v/gitnexus.svg" alt="npm version"/>
</a>
<a href="https://polyformproject.org/licenses/noncommercial/1.0.0/">
<img src="https://img.shields.io/badge/License-PolyForm%20Noncommercial-blue.svg" alt="License: PolyForm Noncommercial"/>
</a>
</div>
**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

Binary file not shown.

View file

@ -10,5 +10,5 @@ export enum SupportedLanguages {
Rust = 'rust',
PHP = 'php',
// Ruby = 'ruby',
// Swift = 'swift',
Swift = 'swift',
}

View file

@ -103,6 +103,26 @@ const ENTRY_POINT_PATTERNS: Record<string, RegExp[]> = {
/^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') ||

View file

@ -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'],
};

View file

@ -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, string> = {
[SupportedLanguages.TypeScript]: TYPESCRIPT_QUERIES,
[SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES,
@ -407,5 +460,6 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.CSharp]: CSHARP_QUERIES,
[SupportedLanguages.Rust]: RUST_QUERIES,
[SupportedLanguages.PHP]: PHP_QUERIES,
[SupportedLanguages.Swift]: SWIFT_QUERIES,
};

View file

@ -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;
};

View file

@ -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];

View file

@ -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

View file

@ -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",

View file

@ -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",

View file

@ -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');
}

View file

@ -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')

View file

@ -11,5 +11,5 @@ export enum SupportedLanguages {
PHP = 'php',
Kotlin = 'kotlin',
// Ruby = 'ruby',
// Swift = 'swift',
Swift = 'swift',
}

View file

@ -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);

View file

@ -103,6 +103,26 @@ const ENTRY_POINT_PATTERNS: Record<string, RegExp[]> = {
/^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') ||

View file

@ -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 {

View file

@ -153,6 +153,42 @@ async function loadComposerConfig(repoRoot: string): Promise<ComposerConfig | nu
}
}
/** Swift Package Manager module config */
interface SwiftPackageConfig {
/** Map of target name -> source directory path (e.g., "SiuperModel" -> "Package/Sources/SiuperModel") */
targets: Map<string, string>;
}
async function loadSwiftPackageConfig(repoRoot: string): Promise<SwiftPackageConfig | null> {
// Swift imports are module-name based (e.g., `import SiuperModel`)
// SPM convention: Sources/<TargetName>/ or Package/Sources/<TargetName>/
// We scan for these directories to build a target map
const targets = new Map<string, string>();
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,

View file

@ -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;
}

View file

@ -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, string> = {
[SupportedLanguages.TypeScript]: TYPESCRIPT_QUERIES,
[SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES,
@ -488,5 +541,6 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.Rust]: RUST_QUERIES,
[SupportedLanguages.PHP]: PHP_QUERIES,
[SupportedLanguages.Kotlin]: KOTLIN_QUERIES,
[SupportedLanguages.Swift]: SWIFT_QUERIES,
};

View file

@ -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;
};

View file

@ -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<string, any> = {
[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',
]);
// ============================================================================

View file

@ -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,

View file

@ -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<string, any> = {
[SupportedLanguages.Rust]: Rust,
[SupportedLanguages.PHP]: PHP.php_only,
[SupportedLanguages.Kotlin]: Kotlin,
[SupportedLanguages.Swift]: Swift,
};
export const loadParser = async (): Promise<Parser> => {