Merge pull request #84 from magyargergo/feat/kotlin-language-support

feat: add Kotlin language support
This commit is contained in:
abhigyanpatwari 2026-03-01 23:11:25 +05:30
commit ee95808478
10 changed files with 422 additions and 117 deletions

View file

@ -69,6 +69,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",

View file

@ -9,6 +9,7 @@ export enum SupportedLanguages {
Go = 'go',
Rust = 'rust',
PHP = 'php',
Kotlin = 'kotlin',
// Ruby = 'ruby',
Swift = 'swift',
}

View file

@ -37,6 +37,10 @@ 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',
// PHP — no additional node types needed
// Swift
'init_declaration',
'deinit_declaration',
@ -324,6 +328,22 @@ const BUILT_IN_NAMES = new Set([
'open', 'read', 'write', 'close', 'append', 'extend', 'update',
'super', 'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr',
'enumerate', 'zip', 'sorted', 'reversed', 'min', 'max', 'sum', 'abs',
// 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',
// C/C++ standard library and common kernel helpers
'printf', 'fprintf', 'sprintf', 'snprintf', 'vprintf', 'vfprintf', 'vsprintf', 'vsnprintf',
'scanf', 'fscanf', 'sscanf',

View file

@ -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
@ -384,6 +427,12 @@ const AST_FRAMEWORK_PATTERNS_BY_LANGUAGE: Record<string, AstFrameworkPatternConf
{ framework: 'spring', entryPointMultiplier: 3.2, reason: 'spring-annotation', patterns: FRAMEWORK_AST_PATTERNS.spring },
{ framework: 'jaxrs', entryPointMultiplier: 3.0, reason: 'jaxrs-annotation', patterns: FRAMEWORK_AST_PATTERNS.jaxrs },
],
kotlin: [
{ framework: 'spring-kotlin', entryPointMultiplier: 3.2, reason: 'spring-kotlin-annotation', patterns: FRAMEWORK_AST_PATTERNS.spring },
{ framework: 'jaxrs', entryPointMultiplier: 3.0, reason: 'jaxrs-annotation', patterns: FRAMEWORK_AST_PATTERNS.jaxrs },
{ framework: 'ktor', entryPointMultiplier: 2.8, reason: 'ktor-routing', patterns: ['routing', 'embeddedServer', 'Application.module'] },
{ framework: 'android-kotlin', entryPointMultiplier: 2.5, reason: 'android-annotation', patterns: ['@AndroidEntryPoint', 'AppCompatActivity', 'Fragment('] },
],
csharp: [
{ framework: 'aspnet', entryPointMultiplier: 3.2, reason: 'aspnet-attribute', patterns: FRAMEWORK_AST_PATTERNS.aspnet },
],
@ -392,9 +441,19 @@ const AST_FRAMEWORK_PATTERNS_BY_LANGUAGE: Record<string, AstFrameworkPatternConf
],
};
/** Pre-lowercased patterns for O(1) pattern matching at runtime */
const AST_PATTERNS_LOWERED: Record<string, Array<{ framework: string; entryPointMultiplier: number; reason: string; patterns: string[] }>> =
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,
@ -402,14 +461,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,

View file

@ -202,6 +202,8 @@ const EXTENSIONS = [
'.py', '/__init__.py',
// Java
'.java',
// Kotlin
'.kt', '.kts',
// C/C++
'.c', '.h', '.cpp', '.hpp', '.cc', '.cxx', '.hxx', '.hh',
// C#
@ -531,26 +533,42 @@ function tryRustModulePath(modulePath: string, allFiles: Set<string>): 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;
};
// ============================================================================
// 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 => {
@ -567,7 +585,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]);
@ -578,36 +597,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];
}
}
}
}
}
@ -817,26 +839,42 @@ export const processImports = async (
}
// Clean path (remove quotes and angle brackets for C/C++ includes)
const rawImportPath = sourceNode.text.replace(/['"<>]/g, '');
const rawImportPath = language === SupportedLanguages.Kotlin
? appendKotlinWildcard(sourceNode.text.replace(/['"<>]/g, ''), captureMap['import'])
: sourceNode.text.replace(/['"<>]/g, '');
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);
// 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);
}
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)
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;
}
// Fall through to normal resolution for regular Java imports
// Fall through to normal resolution for regular imports
}
// ---- Go: handle package-level imports ----
@ -1000,20 +1038,34 @@ 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);
// 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 staticResolved = resolveJavaStaticImport(rawImportPath, normalizedFileList, allFileList, index);
if (staticResolved) {
resolveCache.set(cacheKey, staticResolved);
addImportEdge(filePath, staticResolved);
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);
continue;
}
}

View file

@ -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 { detectFrameworkFromAST } from './framework-detection.js';
import { WorkerPool } from './workers/worker-pool.js';
import type { ParseWorkerResult, ParseWorkerInput, ExtractedImport, ExtractedCall, ExtractedHeritage } from './workers/parse-worker.js';
@ -18,33 +18,33 @@ export interface WorkerExtractedData {
heritage: ExtractedHeritage[];
}
const getDefinitionNodeFromCaptures = (captureMap: Record<string, any>): 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<string, any>): any | null => {
for (const key of DEFINITION_CAPTURE_KEYS) {
if (captureMap[key]) return captureMap[key];
}
return null;
@ -141,6 +141,23 @@ export const isNodeExported = (node: any, name: string, language: string): boole
}
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;
// C/C++: No native export concept at language level
// Entry points will be detected via name patterns (main, etc.)
case 'c':
@ -346,15 +363,15 @@ const processParsingSequential = async (
const startLine = definitionNodeForRange ? definitionNodeForRange.startPosition.row : (nameNode ? nameNode.startPosition.row : 0);
const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}:${startLine}`);
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 frameworkHint = definitionNodeForRange
? detectFrameworkFromAST(language, definitionNodeForRange.text || '')
: null;
return {
properties: {
name: nodeName,
filePath: file.path,
startLine: definitionNodeForRange ? definitionNodeForRange.startPosition.row : startLine,
@ -365,8 +382,7 @@ const processParsingSequential = async (
astFrameworkMultiplier: frameworkHint.entryPointMultiplier,
astFrameworkReason: frameworkHint.reason,
} : {}),
};
})()
},
};
graph.addNode(node);

View file

@ -396,6 +396,86 @@ 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 = `
; Interfaces
; 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
; 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.enum
; 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
; 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
(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
`;
// Swift queries - works with tree-sitter-swift
export const SWIFT_QUERIES = `
; Classes
@ -460,6 +540,7 @@ export const LANGUAGE_QUERIES: Record<SupportedLanguages, string> = {
[SupportedLanguages.CSharp]: CSHARP_QUERIES,
[SupportedLanguages.Rust]: RUST_QUERIES,
[SupportedLanguages.PHP]: PHP_QUERIES,
[SupportedLanguages.Kotlin]: KOTLIN_QUERIES,
[SupportedLanguages.Swift]: SWIFT_QUERIES,
};

View file

@ -6,6 +6,23 @@ import { SupportedLanguages } from '../../config/supported-languages.js';
*/
export const yieldToEventLoop = (): Promise<void> => 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') ||

View file

@ -9,16 +9,17 @@ import CPP from 'tree-sitter-cpp';
import CSharp from 'tree-sitter-c-sharp';
import Go from 'tree-sitter-go';
import Rust from 'tree-sitter-rust';
import Kotlin from 'tree-sitter-kotlin';
import PHP from 'tree-sitter-php';
import { createRequire } from 'node:module';
import { SupportedLanguages } from '../../../config/supported-languages.js';
import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js';
// tree-sitter-swift is an optionalDependency — may not be installed
const _require = createRequire(import.meta.url);
let Swift: any = null;
try { Swift = _require('tree-sitter-swift'); } catch {}
import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js';
import { getLanguageFromFilename } from '../utils.js';
import { findSiblingChild, getLanguageFromFilename } from '../utils.js';
import { detectFrameworkFromAST } from '../framework-detection.js';
import { generateId } from '../../../lib/utils.js';
@ -111,6 +112,7 @@ const languageMap: Record<string, any> = {
[SupportedLanguages.CSharp]: CSharp,
[SupportedLanguages.Go]: Go,
[SupportedLanguages.Rust]: Rust,
[SupportedLanguages.Kotlin]: Kotlin,
[SupportedLanguages.PHP]: PHP.php_only,
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
};
@ -194,18 +196,25 @@ const isNodeExported = (node: any, name: string, language: string): boolean => {
}
return false;
case 'c':
case 'cpp':
return false;
case 'swift':
// 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.type === 'modifiers' || current.type === 'visibility_modifier') {
const text = current.text || '';
if (text.includes('public') || text.includes('open')) return true;
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;
case 'c':
case 'cpp':
return false;
case 'php':
@ -226,6 +235,16 @@ const isNodeExported = (node: any, name: string, language: string): boolean => {
// Top-level functions (no parent class) are globally accessible
return true;
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;
}
@ -241,8 +260,12 @@ const FUNCTION_NODE_TYPES = new Set([
'function_definition', 'async_function_declaration', 'async_arrow_function',
'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
// Kotlin (function_declaration already included above via JS/TS)
'anonymous_function', 'lambda_literal',
// PHP
'anonymous_function_creation_expression',
// Swift initializers/deinitializers
'init_declaration', 'deinit_declaration',
]);
/** Walk up AST to find enclosing function, return its generateId or null for top-level */
@ -325,6 +348,22 @@ const BUILT_INS = new Set([
'open', 'read', 'write', 'close', 'append', 'extend', 'update',
'super', 'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr',
'enumerate', 'zip', 'sorted', 'reversed', 'min', 'max', 'sum', 'abs',
// 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',
// C/C++ standard library
'printf', 'fprintf', 'sprintf', 'snprintf', 'vprintf', 'vfprintf', 'vsprintf', 'vsnprintf',
'scanf', 'fscanf', 'sscanf',
@ -430,38 +469,51 @@ const getLabelFromCaptures = (captureMap: Record<string, any>): string | null =>
return 'CodeElement';
};
const getDefinitionNodeFromCaptures = (captureMap: Record<string, any>): 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<string, any>): 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
// ============================================================================
@ -685,7 +737,9 @@ const processFileGroup = (
// Extract import paths before skipping
if (captureMap['import'] && captureMap['import.source']) {
const rawImportPath = captureMap['import.source'].text.replace(/['"<>]/g, '');
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,
@ -761,7 +815,7 @@ const processFileGroup = (
}
const frameworkHint = definitionNode
? detectFrameworkFromAST(language, definitionNode.text || '')
? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
: null;
result.nodes.push({

View file

@ -8,6 +8,7 @@ import CPP from 'tree-sitter-cpp';
import CSharp from 'tree-sitter-c-sharp';
import Go from 'tree-sitter-go';
import Rust from 'tree-sitter-rust';
import Kotlin from 'tree-sitter-kotlin';
import PHP from 'tree-sitter-php';
import { createRequire } from 'node:module';
import { SupportedLanguages } from '../../config/supported-languages.js';
@ -30,6 +31,7 @@ const languageMap: Record<string, any> = {
[SupportedLanguages.CSharp]: CSharp,
[SupportedLanguages.Go]: Go,
[SupportedLanguages.Rust]: Rust,
[SupportedLanguages.Kotlin]: Kotlin,
[SupportedLanguages.PHP]: PHP.php_only,
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
};