From c37b63ae8b45b3dfd4ef709ec9c4757a60b53883 Mon Sep 17 00:00:00 2001 From: Gary Magyar Date: Thu, 26 Feb 2026 17:01:35 +0000 Subject: [PATCH 1/6] feat: add Kotlin language support Add end-to-end Kotlin parsing, symbol extraction, and visibility detection. Extract findSiblingChild helper into utils.ts for clean AST traversal of Kotlin's modifiers/visibility_modifier sibling pattern. Fix pre-existing duplicate ftsLoaded declaration in kuzu-adapter.ts. Files changed: - supported-languages.ts: add Kotlin enum member - parser-loader.ts, parse-worker.ts: register tree-sitter-kotlin - tree-sitter-queries.ts: add Kotlin queries for classes, interfaces, objects, functions, properties, imports, calls, and heritage - parsing-processor.ts, parse-worker.ts: add Kotlin visibility detection - call-processor.ts, parse-worker.ts: add Kotlin builtins and node types - utils.ts: add .kt/.kts extension mapping and findSiblingChild helper - package.json: add tree-sitter-kotlin dependency --- gitnexus/package-lock.json | 26 ++++++++ gitnexus/package.json | 1 + gitnexus/src/config/supported-languages.ts | 1 + gitnexus/src/core/ingestion/call-processor.ts | 9 +++ .../src/core/ingestion/parsing-processor.ts | 19 +++++- .../src/core/ingestion/tree-sitter-queries.ts | 63 +++++++++++++++++++ gitnexus/src/core/ingestion/utils.ts | 19 ++++++ .../core/ingestion/workers/parse-worker.ts | 29 ++++++++- gitnexus/src/core/kuzu/kuzu-adapter.ts | 1 - .../src/core/tree-sitter/parser-loader.ts | 2 + 10 files changed, 167 insertions(+), 3 deletions(-) diff --git a/gitnexus/package-lock.json b/gitnexus/package-lock.json index b2f14dea1..66a291cb8 100644 --- a/gitnexus/package-lock.json +++ b/gitnexus/package-lock.json @@ -30,6 +30,7 @@ "tree-sitter-go": "^0.21.0", "tree-sitter-java": "^0.21.0", "tree-sitter-javascript": "^0.21.0", + "tree-sitter-kotlin": "^0.3.8", "tree-sitter-php": "^0.23.12", "tree-sitter-python": "^0.21.0", "tree-sitter-rust": "^0.21.0", @@ -4379,6 +4380,31 @@ "node": "^18 || ^20 || >= 21" } }, + "node_modules/tree-sitter-kotlin": { + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/tree-sitter-kotlin/-/tree-sitter-kotlin-0.3.8.tgz", + "integrity": "sha512-A4obq6bjzmYrA+F0JLLoheFPcofFkctNaZSpnDd+GPn1SfVZLY4/GG4C0cYVBTOShuPBGGAOPLM1JWLZQV4m1g==", + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "node-addon-api": "^7.1.0", + "node-gyp-build": "^4.8.0" + }, + "peerDependencies": { + "tree-sitter": "^0.21.0" + }, + "peerDependenciesMeta": { + "tree_sitter": { + "optional": true + } + } + }, + "node_modules/tree-sitter-kotlin/node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "license": "MIT" + }, "node_modules/tree-sitter-php": { "version": "0.23.12", "resolved": "https://registry.npmjs.org/tree-sitter-php/-/tree-sitter-php-0.23.12.tgz", diff --git a/gitnexus/package.json b/gitnexus/package.json index 04b20e88a..16de567bb 100644 --- a/gitnexus/package.json +++ b/gitnexus/package.json @@ -62,6 +62,7 @@ "tree-sitter-go": "^0.21.0", "tree-sitter-java": "^0.21.0", "tree-sitter-javascript": "^0.21.0", + "tree-sitter-kotlin": "^0.3.8", "tree-sitter-php": "^0.23.12", "tree-sitter-python": "^0.21.0", "tree-sitter-rust": "^0.21.0", diff --git a/gitnexus/src/config/supported-languages.ts b/gitnexus/src/config/supported-languages.ts index a9bcd8248..7f72bc112 100644 --- a/gitnexus/src/config/supported-languages.ts +++ b/gitnexus/src/config/supported-languages.ts @@ -9,6 +9,7 @@ export enum SupportedLanguages { Go = 'go', Rust = 'rust', PHP = 'php', + Kotlin = 'kotlin', // Ruby = 'ruby', // Swift = 'swift', } \ No newline at end of file diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index 421c6ed0c..05148981b 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -37,6 +37,9 @@ const FUNCTION_NODE_TYPES = new Set([ // Rust 'function_item', 'impl_item', // Methods inside impl blocks + // Kotlin (function_declaration already included above via JS/TS) + 'anonymous_function', + 'lambda_literal', ]); /** @@ -336,6 +339,12 @@ const BUILT_IN_NAMES = new Set([ 'mutex_lock', 'mutex_unlock', 'mutex_init', 'kfree', 'kmalloc', 'kzalloc', 'kcalloc', 'krealloc', 'kvmalloc', 'kvfree', 'get', 'put', + // Kotlin stdlib + 'println', 'print', 'readLine', 'require', 'requireNotNull', 'check', 'assert', 'lazy', 'error', + 'listOf', 'mapOf', 'setOf', 'mutableListOf', 'mutableMapOf', 'mutableSetOf', + 'arrayOf', 'sequenceOf', 'also', 'apply', 'run', 'with', 'takeIf', 'takeUnless', + 'TODO', 'buildString', 'buildList', 'buildMap', 'buildSet', + 'repeat', 'synchronized', ]); const isBuiltInOrNoise = (name: string): boolean => BUILT_IN_NAMES.has(name); diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index c15d39a17..b447afef8 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -5,7 +5,7 @@ import { LANGUAGE_QUERIES } from './tree-sitter-queries.js'; import { generateId } from '../../lib/utils.js'; import { SymbolTable } from './symbol-table.js'; import { ASTCache } from './ast-cache.js'; -import { getLanguageFromFilename, yieldToEventLoop } from './utils.js'; +import { findSiblingChild, getLanguageFromFilename, yieldToEventLoop } from './utils.js'; import { WorkerPool } from './workers/worker-pool.js'; import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedHeritage } from './workers/parse-worker.js'; @@ -114,6 +114,23 @@ const isNodeExported = (node: any, name: string, language: string): boolean => { case 'cpp': return false; + // Kotlin: Default visibility is public (unlike Java) + // visibility_modifier is inside modifiers, a sibling of the name node within the declaration + case 'kotlin': + while (current) { + if (current.parent) { + const visMod = findSiblingChild(current.parent, 'modifiers', 'visibility_modifier'); + if (visMod) { + const text = visMod.text; + if (text === 'private' || text === 'internal' || text === 'protected') return false; + if (text === 'public') return true; + } + } + current = current.parent; + } + // No visibility modifier = public (Kotlin default) + return true; + default: return false; } diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index ff4f8f28f..cea3824e3 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -396,6 +396,68 @@ export const PHP_QUERIES = ` [(name) (qualified_name)] @heritage.trait))) @heritage `; +// Kotlin queries - works with tree-sitter-kotlin (fwcd/tree-sitter-kotlin) +// Based on official tags.scm; functions use simple_identifier, classes use type_identifier +export const KOTLIN_QUERIES = ` +; ── Classes (regular, data, sealed, enum) ──────────────────────────────── +(class_declaration + (type_identifier) @name) @definition.class + +; ── Interfaces ───────────────────────────────────────────────────────────── +(interface_declaration + (type_identifier) @name) @definition.interface + +; ── Object declarations (Kotlin singletons) ────────────────────────────── +(object_declaration + (type_identifier) @name) @definition.class + +; ── Companion objects (named only) ─────────────────────────────────────── +(companion_object + (type_identifier) @name) @definition.class + +; ── Functions (top-level, member, extension) ────────────────────────────── +(function_declaration + (simple_identifier) @name) @definition.function + +; ── Properties ─────────────────────────────────────────────────────────── +(property_declaration + (variable_declaration + (simple_identifier) @name)) @definition.property + +; ── Enum entries ───────────────────────────────────────────────────────── +(enum_entry + (simple_identifier) @name) @definition.property + +; ── Type aliases ───────────────────────────────────────────────────────── +(type_alias + (type_identifier) @name) @definition.type + +; ── Imports ────────────────────────────────────────────────────────────── +(import_header + (identifier) @import.source) @import + +; ── Function calls (direct) ────────────────────────────────────────────── +(call_expression + (simple_identifier) @call.name) @call + +; ── Method calls (via navigation: obj.method()) ────────────────────────── +(call_expression + (navigation_expression + (navigation_suffix + (simple_identifier) @call.name))) @call + +; ── Constructor invocations ────────────────────────────────────────────── +(constructor_invocation + (user_type + (type_identifier) @call.name)) @call + +; ── Heritage: extends / implements via delegation_specifier ────────────── +(class_declaration + (type_identifier) @heritage.class + (delegation_specifier + (user_type (type_identifier) @heritage.extends))) @heritage +`; + export const LANGUAGE_QUERIES: Record = { [SupportedLanguages.TypeScript]: TYPESCRIPT_QUERIES, [SupportedLanguages.JavaScript]: JAVASCRIPT_QUERIES, @@ -407,5 +469,6 @@ export const LANGUAGE_QUERIES: Record = { [SupportedLanguages.CSharp]: CSHARP_QUERIES, [SupportedLanguages.Rust]: RUST_QUERIES, [SupportedLanguages.PHP]: PHP_QUERIES, + [SupportedLanguages.Kotlin]: KOTLIN_QUERIES, }; \ No newline at end of file diff --git a/gitnexus/src/core/ingestion/utils.ts b/gitnexus/src/core/ingestion/utils.ts index 12b4c6b3e..927e32e60 100644 --- a/gitnexus/src/core/ingestion/utils.ts +++ b/gitnexus/src/core/ingestion/utils.ts @@ -6,6 +6,23 @@ import { SupportedLanguages } from '../../config/supported-languages.js'; */ export const yieldToEventLoop = (): Promise => new Promise(resolve => setImmediate(resolve)); +/** + * Find a child of `childType` within a sibling node of `siblingType`. + * Used for Kotlin AST traversal where visibility_modifier lives inside a modifiers sibling. + */ +export const findSiblingChild = (parent: any, siblingType: string, childType: string): any | null => { + for (let i = 0; i < parent.childCount; i++) { + const sibling = parent.child(i); + if (sibling?.type === siblingType) { + for (let j = 0; j < sibling.childCount; j++) { + const child = sibling.child(j); + if (child?.type === childType) return child; + } + } + } + return null; +}; + /** * Map file extension to SupportedLanguage enum */ @@ -31,6 +48,8 @@ export const getLanguageFromFilename = (filename: string): SupportedLanguages | if (filename.endsWith('.go')) return SupportedLanguages.Go; // Rust if (filename.endsWith('.rs')) return SupportedLanguages.Rust; + // Kotlin + if (filename.endsWith('.kt') || filename.endsWith('.kts')) return SupportedLanguages.Kotlin; // PHP (all common extensions) if (filename.endsWith('.php') || filename.endsWith('.phtml') || filename.endsWith('.php3') || filename.endsWith('.php4') || diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index ff985ad4c..20bbde7a8 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -10,9 +10,10 @@ 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 Kotlin from 'tree-sitter-kotlin'; import { SupportedLanguages } from '../../../config/supported-languages.js'; import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js'; -import { getLanguageFromFilename } from '../utils.js'; +import { findSiblingChild, getLanguageFromFilename } from '../utils.js'; import { generateId } from '../../../lib/utils.js'; // ============================================================================ @@ -103,6 +104,7 @@ const languageMap: Record = { [SupportedLanguages.Go]: Go, [SupportedLanguages.Rust]: Rust, [SupportedLanguages.PHP]: PHP.php_only, + [SupportedLanguages.Kotlin]: Kotlin, }; const setLanguage = (language: SupportedLanguages, filePath: string): void => { @@ -206,6 +208,23 @@ const isNodeExported = (node: any, name: string, language: string): boolean => { // Top-level functions (no parent class) are globally accessible return true; + // Kotlin: Default visibility is public (unlike Java) + // visibility_modifier is inside modifiers, a sibling of the name node within the declaration + case 'kotlin': + while (current) { + if (current.parent) { + const visMod = findSiblingChild(current.parent, 'modifiers', 'visibility_modifier'); + if (visMod) { + const text = visMod.text; + if (text === 'private' || text === 'internal' || text === 'protected') return false; + if (text === 'public') return true; + } + } + current = current.parent; + } + // No visibility modifier = public (Kotlin default) + return true; + default: return false; } @@ -222,6 +241,8 @@ const FUNCTION_NODE_TYPES = new Set([ 'method_declaration', 'constructor_declaration', 'local_function_statement', 'function_item', 'impl_item', 'anonymous_function_creation_expression', // PHP anonymous functions + // Kotlin (function_declaration already included above via JS/TS) + 'anonymous_function', 'lambda_literal', ]); /** Walk up AST to find enclosing function, return its generateId or null for top-level */ @@ -336,6 +357,12 @@ 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', + // Kotlin stdlib + 'println', 'print', 'readLine', 'require', 'requireNotNull', 'check', 'assert', 'lazy', 'error', + 'listOf', 'mapOf', 'setOf', 'mutableListOf', 'mutableMapOf', 'mutableSetOf', + 'arrayOf', 'sequenceOf', 'also', 'apply', 'run', 'with', 'takeIf', 'takeUnless', + 'TODO', 'buildString', 'buildList', 'buildMap', 'buildSet', + 'repeat', 'synchronized', ]); // ============================================================================ diff --git a/gitnexus/src/core/kuzu/kuzu-adapter.ts b/gitnexus/src/core/kuzu/kuzu-adapter.ts index 1ba30d15b..b42978ae7 100644 --- a/gitnexus/src/core/kuzu/kuzu-adapter.ts +++ b/gitnexus/src/core/kuzu/kuzu-adapter.ts @@ -676,7 +676,6 @@ export const getEmbeddingTableName = (): string => EMBEDDING_TABLE_NAME; * Load the FTS extension (required before using FTS functions). * Safe to call multiple times — tracks loaded state. */ -let ftsLoaded = false; export const loadFTSExtension = async (): Promise => { if (ftsLoaded) return; if (!conn) { diff --git a/gitnexus/src/core/tree-sitter/parser-loader.ts b/gitnexus/src/core/tree-sitter/parser-loader.ts index e92898424..fb3a0ae93 100644 --- a/gitnexus/src/core/tree-sitter/parser-loader.ts +++ b/gitnexus/src/core/tree-sitter/parser-loader.ts @@ -9,6 +9,7 @@ import CSharp from 'tree-sitter-c-sharp'; import Go from 'tree-sitter-go'; import Rust from 'tree-sitter-rust'; import PHP from 'tree-sitter-php'; +import Kotlin from 'tree-sitter-kotlin'; import { SupportedLanguages } from '../../config/supported-languages.js'; let parser: Parser | null = null; @@ -25,6 +26,7 @@ const languageMap: Record = { [SupportedLanguages.Go]: Go, [SupportedLanguages.Rust]: Rust, [SupportedLanguages.PHP]: PHP.php_only, + [SupportedLanguages.Kotlin]: Kotlin, }; export const loadParser = async (): Promise => { From 1b8c3c77afe986742fbad5c95ab56607969be01b Mon Sep 17 00:00:00 2001 From: Gary Magyar Date: Fri, 27 Feb 2026 09:09:26 +0000 Subject: [PATCH 2/6] feat(kotlin): distinguish interfaces from classes in knowledge graph MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit tree-sitter-kotlin (fwcd) has no interface_declaration node — both interfaces and classes are class_declaration nodes. Use anonymous keyword literal matching ("interface" vs "class") to produce the correct @definition.interface / @definition.class captures. Verified against two real Kotlin repos: a small one (3 Interface, 92 Class) and a large one (35 Interface, 677 Class, 5998 Function). --- .../src/core/ingestion/tree-sitter-queries.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index cea3824e3..747836617 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -399,14 +399,20 @@ export const PHP_QUERIES = ` // Kotlin queries - works with tree-sitter-kotlin (fwcd/tree-sitter-kotlin) // Based on official tags.scm; functions use simple_identifier, classes use type_identifier export const KOTLIN_QUERIES = ` -; ── Classes (regular, data, sealed, enum) ──────────────────────────────── -(class_declaration - (type_identifier) @name) @definition.class - ; ── Interfaces ───────────────────────────────────────────────────────────── -(interface_declaration +; tree-sitter-kotlin (fwcd) has no interface_declaration node type. +; Interfaces are class_declaration nodes with an anonymous "interface" keyword child. +(class_declaration + "interface" (type_identifier) @name) @definition.interface +; ── Classes (regular, data, sealed, enum) ──────────────────────────────── +; All have the anonymous "class" keyword child. enum class has both +; "enum" and "class" children — the "class" child still matches. +(class_declaration + "class" + (type_identifier) @name) @definition.class + ; ── Object declarations (Kotlin singletons) ────────────────────────────── (object_declaration (type_identifier) @name) @definition.class From ee6753bf055ee862eb42562fa709c8fbfeccdb09 Mon Sep 17 00:00:00 2001 From: Gary Magyar Date: Fri, 27 Feb 2026 09:28:58 +0000 Subject: [PATCH 3/6] fix(kotlin): capture constructor-based heritage (class Foo : Bar()) The heritage query only matched bare user_type delegation specifiers (interface implementation), missing constructor_invocation patterns used for class extension. Adds a second heritage pattern for constructor invocations, capturing ~3x more heritage edges. --- gitnexus/src/core/ingestion/tree-sitter-queries.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index 747836617..66644a6d0 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -458,10 +458,18 @@ export const KOTLIN_QUERIES = ` (type_identifier) @call.name)) @call ; ── Heritage: extends / implements via delegation_specifier ────────────── +; Interface implementation (bare user_type): class Foo : Bar (class_declaration (type_identifier) @heritage.class (delegation_specifier (user_type (type_identifier) @heritage.extends))) @heritage + +; Class extension (constructor_invocation): class Foo : Bar() +(class_declaration + (type_identifier) @heritage.class + (delegation_specifier + (constructor_invocation + (user_type (type_identifier) @heritage.extends)))) @heritage `; export const LANGUAGE_QUERIES: Record = { From e2a8bfa5ab2fc27f2ae2fdaa50cf1a3aa38395d6 Mon Sep 17 00:00:00 2001 From: Gary Magyar Date: Fri, 27 Feb 2026 10:26:23 +0000 Subject: [PATCH 4/6] fix(kotlin): enable import dependency tree resolution for Kotlin files Add .kt/.kts to EXTENSIONS array, parameterize Java resolvers into JVM resolvers (resolveJvmWildcard, resolveJvmMemberImport), and unify Java+Kotlin dispatch in both import processing paths. Detect wildcard imports via AST child node inspection in parse worker. Validated against okhttp repo: 524 .kt files detected, imports resolve correctly to .kt files (e.g. okhttp3.OkHttpClient -> OkHttpClient.kt). --- .../src/core/ingestion/import-processor.ts | 104 +++++++++++------- .../core/ingestion/workers/parse-worker.ts | 13 ++- 2 files changed, 76 insertions(+), 41 deletions(-) diff --git a/gitnexus/src/core/ingestion/import-processor.ts b/gitnexus/src/core/ingestion/import-processor.ts index 6ab4213d9..9f26a39fe 100644 --- a/gitnexus/src/core/ingestion/import-processor.ts +++ b/gitnexus/src/core/ingestion/import-processor.ts @@ -166,6 +166,8 @@ const EXTENSIONS = [ '.py', '/__init__.py', // Java '.java', + // Kotlin + '.kt', '.kts', // C/C++ '.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.hxx', '.hh', // C# @@ -494,25 +496,28 @@ function tryRustModulePath(modulePath: string, allFiles: Set): string | } // ============================================================================ -// JAVA MULTI-FILE RESOLUTION +// JVM MULTI-FILE RESOLUTION (Java + Kotlin) // ============================================================================ +/** Kotlin file extensions for JVM resolver reuse */ +const KOTLIN_EXTENSIONS: readonly string[] = ['.kt', '.kts']; + /** - * Resolve a Java wildcard import (com.example.*) to all matching .java files. - * Returns an array of file paths. + * Resolve a JVM wildcard import (com.example.*) to all matching files. + * Works for both Java (.java) and Kotlin (.kt, .kts). */ -function resolveJavaWildcard( +function resolveJvmWildcard( importPath: string, normalizedFileList: string[], allFileList: string[], + extensions: readonly string[], index?: SuffixIndex, ): string[] { // "com.example.util.*" -> "com/example/util" const packagePath = importPath.slice(0, -2).replace(/\./g, '/'); if (index) { - // Use directory index: get all .java files in this package directory - const candidates = index.getFilesInDir(packagePath, '.java'); + const candidates = extensions.flatMap(ext => index.getFilesInDir(packagePath, ext)); // Filter to only direct children (no subdirectories) const packageSuffix = '/' + packagePath + '/'; return candidates.filter(f => { @@ -529,7 +534,8 @@ function resolveJavaWildcard( const matches: string[] = []; for (let i = 0; i < normalizedFileList.length; i++) { const normalized = normalizedFileList[i]; - if (normalized.includes(packageSuffix) && normalized.endsWith('.java')) { + if (normalized.includes(packageSuffix) && + extensions.some(ext => normalized.endsWith(ext))) { const afterPackage = normalized.substring(normalized.indexOf(packageSuffix) + packageSuffix.length); if (!afterPackage.includes('/')) { matches.push(allFileList[i]); @@ -540,36 +546,39 @@ function resolveJavaWildcard( } /** - * Try to resolve a Java static import by stripping the member name. - * "com.example.Constants.VALUE" -> resolve "com.example.Constants" + * Try to resolve a JVM member/static import by stripping the member name. + * Java: "com.example.Constants.VALUE" -> resolve "com.example.Constants" + * Kotlin: "com.example.Constants.VALUE" -> resolve "com.example.Constants" */ -function resolveJavaStaticImport( +function resolveJvmMemberImport( importPath: string, normalizedFileList: string[], allFileList: string[], + extensions: readonly string[], index?: SuffixIndex, ): string | null { - // Static imports look like: com.example.Constants.VALUE or com.example.Constants.* - // The last segment is a member name (field/method) if it starts with lowercase or is ALL_CAPS + // Member imports: com.example.Constants.VALUE or com.example.Constants.* + // The last segment is a member name if it starts with lowercase, is ALL_CAPS, or is a wildcard const segments = importPath.split('.'); if (segments.length < 3) return null; const lastSeg = segments[segments.length - 1]; - // If last segment is a wildcard or ALL_CAPS constant or starts with lowercase, strip it if (lastSeg === '*' || /^[a-z]/.test(lastSeg) || /^[A-Z_]+$/.test(lastSeg)) { const classPath = segments.slice(0, -1).join('/'); - const classSuffix = classPath + '.java'; - if (index) { - return index.get(classSuffix) || index.getInsensitive(classSuffix) || null; - } - - // Fallback: linear scan - const fullSuffix = '/' + classSuffix; - for (let i = 0; i < normalizedFileList.length; i++) { - if (normalizedFileList[i].endsWith(fullSuffix) || - normalizedFileList[i].toLowerCase().endsWith(fullSuffix.toLowerCase())) { - return allFileList[i]; + for (const ext of extensions) { + const classSuffix = classPath + ext; + if (index) { + const result = index.get(classSuffix) || index.getInsensitive(classSuffix); + if (result) return result; + } else { + const fullSuffix = '/' + classSuffix; + for (let i = 0; i < normalizedFileList.length; i++) { + if (normalizedFileList[i].endsWith(fullSuffix) || + normalizedFileList[i].toLowerCase().endsWith(fullSuffix.toLowerCase())) { + return allFileList[i]; + } + } } } } @@ -778,26 +787,39 @@ export const processImports = async ( } // Clean path (remove quotes and angle brackets for C/C++ includes) - const rawImportPath = sourceNode.text.replace(/['"<>]/g, ''); + let rawImportPath = sourceNode.text.replace(/['"<>]/g, ''); + // Kotlin wildcard imports: wildcard_import is a separate AST node + // sibling to identifier, so check the import_header for it and append .* + if (language === SupportedLanguages.Kotlin) { + const importNode = captureMap['import']; + for (let ci = 0; ci < importNode.childCount; ci++) { + if (importNode.child(ci)?.type === 'wildcard_import') { + rawImportPath += '.*'; + break; + } + } + } totalImportsFound++; - // ---- Java: handle wildcards and static imports specially ---- - if (language === SupportedLanguages.Java) { + // ---- JVM languages (Java + Kotlin): handle wildcards and member imports ---- + if (language === SupportedLanguages.Java || language === SupportedLanguages.Kotlin) { + const exts = language === SupportedLanguages.Java ? ['.java'] : KOTLIN_EXTENSIONS; + if (rawImportPath.endsWith('.*')) { - const matchedFiles = resolveJavaWildcard(rawImportPath, normalizedFileList, allFileList, index); + const matchedFiles = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, exts, index); for (const matchedFile of matchedFiles) { addImportEdge(file.path, matchedFile); } return; // skip single-file resolution } - // Try static import resolution (strip member name) - const staticResolved = resolveJavaStaticImport(rawImportPath, normalizedFileList, allFileList, index); - if (staticResolved) { - addImportEdge(file.path, staticResolved); + // Try member/static import resolution (strip member name) + const memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, exts, index); + if (memberResolved) { + addImportEdge(file.path, memberResolved); return; } - // Fall through to normal resolution for regular Java imports + // Fall through to normal resolution for regular imports } // ---- Go: handle package-level imports ---- @@ -941,20 +963,22 @@ export const processImportsFromExtracted = async ( continue; } - // Java: handle wildcards and static imports - if (language === SupportedLanguages.Java) { + // JVM languages (Java + Kotlin): handle wildcards and member imports + if (language === SupportedLanguages.Java || language === SupportedLanguages.Kotlin) { + const exts = language === SupportedLanguages.Java ? ['.java'] : KOTLIN_EXTENSIONS; + if (rawImportPath.endsWith('.*')) { - const matchedFiles = resolveJavaWildcard(rawImportPath, normalizedFileList, allFileList, index); + const matchedFiles = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, exts, index); for (const matchedFile of matchedFiles) { addImportEdge(filePath, matchedFile); } continue; } - const staticResolved = resolveJavaStaticImport(rawImportPath, normalizedFileList, allFileList, index); - if (staticResolved) { - resolveCache.set(cacheKey, staticResolved); - addImportEdge(filePath, staticResolved); + const memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, exts, index); + if (memberResolved) { + resolveCache.set(cacheKey, memberResolved); + addImportEdge(filePath, memberResolved); continue; } } diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 20bbde7a8..9b76186ea 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -622,7 +622,18 @@ const processFileGroup = ( // Extract import paths before skipping if (captureMap['import'] && captureMap['import.source']) { - const rawImportPath = captureMap['import.source'].text.replace(/['"<>]/g, ''); + let rawImportPath = captureMap['import.source'].text.replace(/['"<>]/g, ''); + // Kotlin wildcard imports: wildcard_import is a separate AST node + // sibling to identifier, so check the import_header for it and append .* + if (language === SupportedLanguages.Kotlin) { + const importNode = captureMap['import']; + for (let i = 0; i < importNode.childCount; i++) { + if (importNode.child(i)?.type === 'wildcard_import') { + rawImportPath += '.*'; + break; + } + } + } result.imports.push({ filePath: file.path, rawImportPath, From 43f525d056967b24c94a362680162d923ed5403e Mon Sep 17 00:00:00 2001 From: Gary Magyar Date: Fri, 27 Feb 2026 10:28:39 +0000 Subject: [PATCH 5/6] fix(kotlin): guard against double-appending .* to wildcard import paths Add endsWith('.*') check before appending wildcard suffix to prevent possible double-append if grammar returns identifier text that already includes the wildcard. --- gitnexus/src/core/ingestion/import-processor.ts | 4 +++- gitnexus/src/core/ingestion/workers/parse-worker.ts | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/gitnexus/src/core/ingestion/import-processor.ts b/gitnexus/src/core/ingestion/import-processor.ts index 9f26a39fe..85b150b3d 100644 --- a/gitnexus/src/core/ingestion/import-processor.ts +++ b/gitnexus/src/core/ingestion/import-processor.ts @@ -794,7 +794,9 @@ export const processImports = async ( const importNode = captureMap['import']; for (let ci = 0; ci < importNode.childCount; ci++) { if (importNode.child(ci)?.type === 'wildcard_import') { - rawImportPath += '.*'; + if (!rawImportPath.endsWith('.*')) { + rawImportPath += '.*'; + } break; } } diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 9b76186ea..24c63d0a2 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -629,7 +629,9 @@ const processFileGroup = ( const importNode = captureMap['import']; for (let i = 0; i < importNode.childCount; i++) { if (importNode.child(i)?.type === 'wildcard_import') { - rawImportPath += '.*'; + if (!rawImportPath.endsWith('.*')) { + rawImportPath += '.*'; + } break; } } From 508402fd4a65f953258653bb1cf47cab572a43fd Mon Sep 17 00:00:00 2001 From: Gary Magyar Date: Sat, 28 Feb 2026 10:06:52 +0000 Subject: [PATCH 6/6] feat: add full Kotlin language support --- gitnexus/src/core/ingestion/call-processor.ts | 12 ++- .../src/core/ingestion/framework-detection.ts | 63 +++++++++++- .../src/core/ingestion/import-processor.ts | 58 +++++++---- .../src/core/ingestion/parsing-processor.ts | 68 +++++++------ .../src/core/ingestion/tree-sitter-queries.ts | 6 +- .../core/ingestion/workers/parse-worker.ts | 96 +++++++++++-------- 6 files changed, 206 insertions(+), 97 deletions(-) diff --git a/gitnexus/src/core/ingestion/call-processor.ts b/gitnexus/src/core/ingestion/call-processor.ts index 05148981b..e82236e51 100644 --- a/gitnexus/src/core/ingestion/call-processor.ts +++ b/gitnexus/src/core/ingestion/call-processor.ts @@ -339,12 +339,22 @@ const BUILT_IN_NAMES = new Set([ 'mutex_lock', 'mutex_unlock', 'mutex_init', 'kfree', 'kmalloc', 'kzalloc', 'kcalloc', 'krealloc', 'kvmalloc', 'kvfree', 'get', 'put', - // Kotlin stdlib + // Kotlin stdlib (IMPORTANT: keep in sync with parse-worker.ts BUILT_IN_NAMES) 'println', 'print', 'readLine', 'require', 'requireNotNull', 'check', 'assert', 'lazy', 'error', 'listOf', 'mapOf', 'setOf', 'mutableListOf', 'mutableMapOf', 'mutableSetOf', 'arrayOf', 'sequenceOf', 'also', 'apply', 'run', 'with', 'takeIf', 'takeUnless', 'TODO', 'buildString', 'buildList', 'buildMap', 'buildSet', 'repeat', 'synchronized', + // Kotlin coroutine builders & scope functions + 'launch', 'async', 'runBlocking', 'withContext', 'coroutineScope', + 'supervisorScope', 'delay', + // Kotlin Flow operators + 'flow', 'flowOf', 'collect', 'emit', 'onEach', 'catch', + 'buffer', 'conflate', 'distinctUntilChanged', + 'flatMapLatest', 'flatMapMerge', 'combine', + 'stateIn', 'shareIn', 'launchIn', + // Kotlin infix stdlib functions + 'to', 'until', 'downTo', 'step', ]); const isBuiltInOrNoise = (name: string): boolean => BUILT_IN_NAMES.has(name); diff --git a/gitnexus/src/core/ingestion/framework-detection.ts b/gitnexus/src/core/ingestion/framework-detection.ts index 299966c7e..c3ab00bca 100644 --- a/gitnexus/src/core/ingestion/framework-detection.ts +++ b/gitnexus/src/core/ingestion/framework-detection.ts @@ -129,6 +129,49 @@ export function detectFrameworkFromPath(filePath: string): FrameworkHint | null return { framework: 'java-service', entryPointMultiplier: 1.8, reason: 'java-service' }; } + // ========== KOTLIN FRAMEWORKS ========== + + // Spring Boot Kotlin controllers + if ((p.includes('/controller/') || p.includes('/controllers/')) && p.endsWith('.kt')) { + return { framework: 'spring-kotlin', entryPointMultiplier: 3.0, reason: 'spring-kotlin-controller' }; + } + + // Spring Boot - files ending in Controller.kt + if (p.endsWith('controller.kt')) { + return { framework: 'spring-kotlin', entryPointMultiplier: 3.0, reason: 'spring-kotlin-controller-file' }; + } + + // Ktor routes + if (p.includes('/routes/') && p.endsWith('.kt')) { + return { framework: 'ktor', entryPointMultiplier: 2.5, reason: 'ktor-routes' }; + } + + // Ktor plugins folder or Routing.kt files + if (p.includes('/plugins/') && p.endsWith('.kt')) { + return { framework: 'ktor', entryPointMultiplier: 2.0, reason: 'ktor-plugin' }; + } + if (p.endsWith('routing.kt') || p.endsWith('routes.kt')) { + return { framework: 'ktor', entryPointMultiplier: 2.5, reason: 'ktor-routing-file' }; + } + + // Android Activities, Fragments + if ((p.includes('/activity/') || p.includes('/ui/')) && p.endsWith('.kt')) { + return { framework: 'android-kotlin', entryPointMultiplier: 2.5, reason: 'android-ui' }; + } + if (p.endsWith('activity.kt') || p.endsWith('fragment.kt')) { + return { framework: 'android-kotlin', entryPointMultiplier: 2.5, reason: 'android-component' }; + } + + // Kotlin main entry point + if (p.endsWith('/main.kt')) { + return { framework: 'kotlin', entryPointMultiplier: 3.0, reason: 'kotlin-main' }; + } + + // Kotlin Application entry point (common naming) + if (p.endsWith('/application.kt')) { + return { framework: 'kotlin', entryPointMultiplier: 2.5, reason: 'kotlin-application' }; + } + // ========== C# / .NET FRAMEWORKS ========== // ASP.NET Controllers @@ -332,6 +375,12 @@ const AST_FRAMEWORK_PATTERNS_BY_LANGUAGE: Record> = + Object.fromEntries( + Object.entries(AST_FRAMEWORK_PATTERNS_BY_LANGUAGE).map(([lang, cfgs]) => [ + lang, + cfgs.map(cfg => ({ ...cfg, patterns: cfg.patterns.map(p => p.toLowerCase()) })), + ]) + ); + /** * Detect framework entry points from AST definition text (decorators/annotations/attributes). * Returns null if no known pattern is found. + * Note: callers should slice definitionText to ~300 chars since annotations appear at the start. */ export function detectFrameworkFromAST( language: string, @@ -350,14 +409,14 @@ export function detectFrameworkFromAST( ): FrameworkHint | null { if (!language || !definitionText) return null; - const configs = AST_FRAMEWORK_PATTERNS_BY_LANGUAGE[language.toLowerCase()]; + const configs = AST_PATTERNS_LOWERED[language.toLowerCase()]; if (!configs || configs.length === 0) return null; const normalized = definitionText.toLowerCase(); for (const cfg of configs) { for (const pattern of cfg.patterns) { - if (normalized.includes(pattern.toLowerCase())) { + if (normalized.includes(pattern)) { return { framework: cfg.framework, entryPointMultiplier: cfg.entryPointMultiplier, diff --git a/gitnexus/src/core/ingestion/import-processor.ts b/gitnexus/src/core/ingestion/import-processor.ts index 85b150b3d..e3e4f74c0 100644 --- a/gitnexus/src/core/ingestion/import-processor.ts +++ b/gitnexus/src/core/ingestion/import-processor.ts @@ -495,6 +495,19 @@ function tryRustModulePath(modulePath: string, allFiles: Set): string | return null; } +/** + * Append .* to a Kotlin import path if the AST has a wildcard_import sibling node. + * Pure function — returns a new string without mutating the input. + */ +const appendKotlinWildcard = (importPath: string, importNode: any): string => { + for (let i = 0; i < importNode.childCount; i++) { + if (importNode.child(i)?.type === 'wildcard_import') { + return importPath.endsWith('.*') ? importPath : `${importPath}.*`; + } + } + return importPath; +}; + // ============================================================================ // JVM MULTI-FILE RESOLUTION (Java + Kotlin) // ============================================================================ @@ -787,20 +800,9 @@ export const processImports = async ( } // Clean path (remove quotes and angle brackets for C/C++ includes) - let rawImportPath = sourceNode.text.replace(/['"<>]/g, ''); - // Kotlin wildcard imports: wildcard_import is a separate AST node - // sibling to identifier, so check the import_header for it and append .* - if (language === SupportedLanguages.Kotlin) { - const importNode = captureMap['import']; - for (let ci = 0; ci < importNode.childCount; ci++) { - if (importNode.child(ci)?.type === 'wildcard_import') { - if (!rawImportPath.endsWith('.*')) { - rawImportPath += '.*'; - } - break; - } - } - } + const rawImportPath = language === SupportedLanguages.Kotlin + ? appendKotlinWildcard(sourceNode.text.replace(/['"<>]/g, ''), captureMap['import']) + : sourceNode.text.replace(/['"<>]/g, ''); totalImportsFound++; // ---- JVM languages (Java + Kotlin): handle wildcards and member imports ---- @@ -809,6 +811,14 @@ export const processImports = async ( if (rawImportPath.endsWith('.*')) { const matchedFiles = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, exts, index); + // Kotlin can import Java files in mixed codebases — try .java as fallback + if (matchedFiles.length === 0 && language === SupportedLanguages.Kotlin) { + const javaMatches = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, ['.java'], index); + for (const matchedFile of javaMatches) { + addImportEdge(file.path, matchedFile); + } + if (javaMatches.length > 0) return; + } for (const matchedFile of matchedFiles) { addImportEdge(file.path, matchedFile); } @@ -816,7 +826,11 @@ export const processImports = async ( } // Try member/static import resolution (strip member name) - const memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, exts, index); + let memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, exts, index); + // Kotlin can import Java files in mixed codebases — try .java as fallback + if (!memberResolved && language === SupportedLanguages.Kotlin) { + memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, ['.java'], index); + } if (memberResolved) { addImportEdge(file.path, memberResolved); return; @@ -971,13 +985,25 @@ export const processImportsFromExtracted = async ( if (rawImportPath.endsWith('.*')) { const matchedFiles = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, exts, index); + // Kotlin can import Java files in mixed codebases — try .java as fallback + if (matchedFiles.length === 0 && language === SupportedLanguages.Kotlin) { + const javaMatches = resolveJvmWildcard(rawImportPath, normalizedFileList, allFileList, ['.java'], index); + for (const matchedFile of javaMatches) { + addImportEdge(filePath, matchedFile); + } + if (javaMatches.length > 0) continue; + } for (const matchedFile of matchedFiles) { addImportEdge(filePath, matchedFile); } continue; } - const memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, exts, index); + let memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, exts, index); + // Kotlin can import Java files in mixed codebases — try .java as fallback + if (!memberResolved && language === SupportedLanguages.Kotlin) { + memberResolved = resolveJvmMemberImport(rawImportPath, normalizedFileList, allFileList, ['.java'], index); + } if (memberResolved) { resolveCache.set(cacheKey, memberResolved); addImportEdge(filePath, memberResolved); diff --git a/gitnexus/src/core/ingestion/parsing-processor.ts b/gitnexus/src/core/ingestion/parsing-processor.ts index a8242c58f..7d753aae5 100644 --- a/gitnexus/src/core/ingestion/parsing-processor.ts +++ b/gitnexus/src/core/ingestion/parsing-processor.ts @@ -18,33 +18,33 @@ export interface WorkerExtractedData { heritage: ExtractedHeritage[]; } -const getDefinitionNodeFromCaptures = (captureMap: Record): any | null => { - const definitionKeys = [ - 'definition.function', - 'definition.class', - 'definition.interface', - 'definition.method', - 'definition.struct', - 'definition.enum', - 'definition.namespace', - 'definition.module', - 'definition.trait', - 'definition.impl', - 'definition.type', - 'definition.const', - 'definition.static', - 'definition.typedef', - 'definition.macro', - 'definition.union', - 'definition.property', - 'definition.record', - 'definition.delegate', - 'definition.annotation', - 'definition.constructor', - 'definition.template', - ]; +const DEFINITION_CAPTURE_KEYS = [ + 'definition.function', + 'definition.class', + 'definition.interface', + 'definition.method', + 'definition.struct', + 'definition.enum', + 'definition.namespace', + 'definition.module', + 'definition.trait', + 'definition.impl', + 'definition.type', + 'definition.const', + 'definition.static', + 'definition.typedef', + 'definition.macro', + 'definition.union', + 'definition.property', + 'definition.record', + 'definition.delegate', + 'definition.annotation', + 'definition.constructor', + 'definition.template', +] as const; - for (const key of definitionKeys) { +const getDefinitionNodeFromCaptures = (captureMap: Record): any | null => { + for (const key of DEFINITION_CAPTURE_KEYS) { if (captureMap[key]) return captureMap[key]; } return null; @@ -334,16 +334,15 @@ const processParsingSequential = async ( const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}`); + const definitionNode = getDefinitionNodeFromCaptures(captureMap); + const frameworkHint = definitionNode + ? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300)) + : null; + const node: GraphNode = { id: nodeId, label: nodeLabel as any, - properties: (() => { - const definitionNode = getDefinitionNodeFromCaptures(captureMap); - const frameworkHint = definitionNode - ? detectFrameworkFromAST(language, definitionNode.text || '') - : null; - - return { + properties: { name: nodeName, filePath: file.path, startLine: nameNode.startPosition.row, @@ -354,8 +353,7 @@ const processParsingSequential = async ( astFrameworkMultiplier: frameworkHint.entryPointMultiplier, astFrameworkReason: frameworkHint.reason, } : {}), - }; - })() + }, }; graph.addNode(node); diff --git a/gitnexus/src/core/ingestion/tree-sitter-queries.ts b/gitnexus/src/core/ingestion/tree-sitter-queries.ts index 66644a6d0..b98a1d653 100644 --- a/gitnexus/src/core/ingestion/tree-sitter-queries.ts +++ b/gitnexus/src/core/ingestion/tree-sitter-queries.ts @@ -432,7 +432,7 @@ export const KOTLIN_QUERIES = ` ; ── Enum entries ───────────────────────────────────────────────────────── (enum_entry - (simple_identifier) @name) @definition.property + (simple_identifier) @name) @definition.enum ; ── Type aliases ───────────────────────────────────────────────────────── (type_alias @@ -457,6 +457,10 @@ export const KOTLIN_QUERIES = ` (user_type (type_identifier) @call.name)) @call +; ── Infix function calls (e.g., a to b, x until y) ────────────────────── +(infix_expression + (simple_identifier) @call.name) @call + ; ── Heritage: extends / implements via delegation_specifier ────────────── ; Interface implementation (bare user_type): class Foo : Bar (class_declaration diff --git a/gitnexus/src/core/ingestion/workers/parse-worker.ts b/gitnexus/src/core/ingestion/workers/parse-worker.ts index 3cb3fc3eb..cd9c03eaf 100644 --- a/gitnexus/src/core/ingestion/workers/parse-worker.ts +++ b/gitnexus/src/core/ingestion/workers/parse-worker.ts @@ -360,12 +360,22 @@ 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', - // Kotlin stdlib + // Kotlin stdlib (IMPORTANT: keep in sync with call-processor.ts BUILT_IN_NAMES) 'println', 'print', 'readLine', 'require', 'requireNotNull', 'check', 'assert', 'lazy', 'error', 'listOf', 'mapOf', 'setOf', 'mutableListOf', 'mutableMapOf', 'mutableSetOf', 'arrayOf', 'sequenceOf', 'also', 'apply', 'run', 'with', 'takeIf', 'takeUnless', 'TODO', 'buildString', 'buildList', 'buildMap', 'buildSet', 'repeat', 'synchronized', + // Kotlin coroutine builders & scope functions + 'launch', 'async', 'runBlocking', 'withContext', 'coroutineScope', + 'supervisorScope', 'delay', + // Kotlin Flow operators + 'flow', 'flowOf', 'collect', 'emit', 'onEach', 'catch', + 'buffer', 'conflate', 'distinctUntilChanged', + 'flatMapLatest', 'flatMapMerge', 'combine', + 'stateIn', 'shareIn', 'launchIn', + // Kotlin infix stdlib functions + 'to', 'until', 'downTo', 'step', ]); // ============================================================================ @@ -402,38 +412,51 @@ const getLabelFromCaptures = (captureMap: Record): string | null => return 'CodeElement'; }; -const getDefinitionNodeFromCaptures = (captureMap: Record): any | null => { - const definitionKeys = [ - 'definition.function', - 'definition.class', - 'definition.interface', - 'definition.method', - 'definition.struct', - 'definition.enum', - 'definition.namespace', - 'definition.module', - 'definition.trait', - 'definition.impl', - 'definition.type', - 'definition.const', - 'definition.static', - 'definition.typedef', - 'definition.macro', - 'definition.union', - 'definition.property', - 'definition.record', - 'definition.delegate', - 'definition.annotation', - 'definition.constructor', - 'definition.template', - ]; +const DEFINITION_CAPTURE_KEYS = [ + 'definition.function', + 'definition.class', + 'definition.interface', + 'definition.method', + 'definition.struct', + 'definition.enum', + 'definition.namespace', + 'definition.module', + 'definition.trait', + 'definition.impl', + 'definition.type', + 'definition.const', + 'definition.static', + 'definition.typedef', + 'definition.macro', + 'definition.union', + 'definition.property', + 'definition.record', + 'definition.delegate', + 'definition.annotation', + 'definition.constructor', + 'definition.template', +] as const; - for (const key of definitionKeys) { +const getDefinitionNodeFromCaptures = (captureMap: Record): any | null => { + for (const key of DEFINITION_CAPTURE_KEYS) { if (captureMap[key]) return captureMap[key]; } return null; }; +/** + * Append .* to a Kotlin import path if the AST has a wildcard_import sibling node. + * Pure function — returns a new string without mutating the input. + */ +const appendKotlinWildcard = (importPath: string, importNode: any): string => { + for (let i = 0; i < importNode.childCount; i++) { + if (importNode.child(i)?.type === 'wildcard_import') { + return importPath.endsWith('.*') ? importPath : `${importPath}.*`; + } + } + return importPath; +}; + // ============================================================================ // Process a batch of files // ============================================================================ @@ -657,20 +680,9 @@ const processFileGroup = ( // Extract import paths before skipping if (captureMap['import'] && captureMap['import.source']) { - let rawImportPath = captureMap['import.source'].text.replace(/['"<>]/g, ''); - // Kotlin wildcard imports: wildcard_import is a separate AST node - // sibling to identifier, so check the import_header for it and append .* - if (language === SupportedLanguages.Kotlin) { - const importNode = captureMap['import']; - for (let i = 0; i < importNode.childCount; i++) { - if (importNode.child(i)?.type === 'wildcard_import') { - if (!rawImportPath.endsWith('.*')) { - rawImportPath += '.*'; - } - break; - } - } - } + const rawImportPath = language === SupportedLanguages.Kotlin + ? appendKotlinWildcard(captureMap['import.source'].text.replace(/['"<>]/g, ''), captureMap['import']) + : captureMap['import.source'].text.replace(/['"<>]/g, ''); result.imports.push({ filePath: file.path, rawImportPath, @@ -743,7 +755,7 @@ const processFileGroup = ( const definitionNode = getDefinitionNodeFromCaptures(captureMap); const frameworkHint = definitionNode - ? detectFrameworkFromAST(language, definitionNode.text || '') + ? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300)) : null; result.nodes.push({