mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-08-28 05:25:25 +00:00
Merge pull request #94 from jandyx/feat/swift-language-support
feat(swift): full Swift / iOS language support with SPM import resolution
This commit is contained in:
commit
b7c582de76
22 changed files with 534 additions and 9 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
BIN
gitnexus-web/public/wasm/swift/tree-sitter-swift.wasm
Executable file
BIN
gitnexus-web/public/wasm/swift/tree-sitter-swift.wasm
Executable file
Binary file not shown.
|
|
@ -10,5 +10,5 @@ export enum SupportedLanguages {
|
|||
Rust = 'rust',
|
||||
PHP = 'php',
|
||||
// Ruby = 'ruby',
|
||||
// Swift = 'swift',
|
||||
Swift = 'swift',
|
||||
}
|
||||
|
|
@ -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') ||
|
||||
|
|
|
|||
|
|
@ -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'],
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
74
gitnexus/scripts/patch-tree-sitter-swift.cjs
Normal file
74
gitnexus/scripts/patch-tree-sitter-swift.cjs
Normal 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');
|
||||
}
|
||||
|
|
@ -10,5 +10,5 @@ export enum SupportedLanguages {
|
|||
Rust = 'rust',
|
||||
PHP = 'php',
|
||||
// Ruby = 'ruby',
|
||||
// Swift = 'swift',
|
||||
Swift = 'swift',
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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') ||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
// ============================================================================
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
};
|
||||
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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<string, any> = {
|
|||
[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',
|
||||
]);
|
||||
|
||||
// ============================================================================
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<string, any> = {
|
|||
[SupportedLanguages.Go]: Go,
|
||||
[SupportedLanguages.Rust]: Rust,
|
||||
[SupportedLanguages.PHP]: PHP.php_only,
|
||||
[SupportedLanguages.Swift]: Swift,
|
||||
};
|
||||
|
||||
export const loadParser = async (): Promise<Parser> => {
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue