gitnexus 1.3.11 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/README.md +22 -2
  2. package/dist/cli/ai-context.d.ts +2 -1
  3. package/dist/cli/ai-context.js +15 -6
  4. package/dist/cli/analyze.d.ts +2 -0
  5. package/dist/cli/analyze.js +12 -2
  6. package/dist/cli/index.js +2 -0
  7. package/dist/cli/skill-gen.d.ts +26 -0
  8. package/dist/cli/skill-gen.js +549 -0
  9. package/dist/core/graph/types.d.ts +5 -2
  10. package/dist/core/ingestion/call-processor.d.ts +5 -5
  11. package/dist/core/ingestion/call-processor.js +173 -260
  12. package/dist/core/ingestion/constants.d.ts +16 -0
  13. package/dist/core/ingestion/constants.js +16 -0
  14. package/dist/core/ingestion/entry-point-scoring.d.ts +2 -1
  15. package/dist/core/ingestion/entry-point-scoring.js +81 -22
  16. package/dist/core/ingestion/export-detection.d.ts +18 -0
  17. package/dist/core/ingestion/export-detection.js +230 -0
  18. package/dist/core/ingestion/framework-detection.d.ts +5 -1
  19. package/dist/core/ingestion/framework-detection.js +39 -8
  20. package/dist/core/ingestion/heritage-processor.d.ts +13 -4
  21. package/dist/core/ingestion/heritage-processor.js +92 -28
  22. package/dist/core/ingestion/import-processor.d.ts +17 -19
  23. package/dist/core/ingestion/import-processor.js +170 -695
  24. package/dist/core/ingestion/language-config.d.ts +46 -0
  25. package/dist/core/ingestion/language-config.js +167 -0
  26. package/dist/core/ingestion/mro-processor.d.ts +45 -0
  27. package/dist/core/ingestion/mro-processor.js +369 -0
  28. package/dist/core/ingestion/named-binding-extraction.d.ts +61 -0
  29. package/dist/core/ingestion/named-binding-extraction.js +363 -0
  30. package/dist/core/ingestion/parsing-processor.d.ts +1 -10
  31. package/dist/core/ingestion/parsing-processor.js +41 -177
  32. package/dist/core/ingestion/pipeline.js +26 -24
  33. package/dist/core/ingestion/process-processor.js +2 -1
  34. package/dist/core/ingestion/resolvers/csharp.d.ts +22 -0
  35. package/dist/core/ingestion/resolvers/csharp.js +109 -0
  36. package/dist/core/ingestion/resolvers/go.d.ts +19 -0
  37. package/dist/core/ingestion/resolvers/go.js +42 -0
  38. package/dist/core/ingestion/resolvers/index.d.ts +16 -0
  39. package/dist/core/ingestion/resolvers/index.js +11 -0
  40. package/dist/core/ingestion/resolvers/jvm.d.ts +23 -0
  41. package/dist/core/ingestion/resolvers/jvm.js +87 -0
  42. package/dist/core/ingestion/resolvers/php.d.ts +15 -0
  43. package/dist/core/ingestion/resolvers/php.js +35 -0
  44. package/dist/core/ingestion/resolvers/rust.d.ts +15 -0
  45. package/dist/core/ingestion/resolvers/rust.js +73 -0
  46. package/dist/core/ingestion/resolvers/standard.d.ts +28 -0
  47. package/dist/core/ingestion/resolvers/standard.js +145 -0
  48. package/dist/core/ingestion/resolvers/utils.d.ts +33 -0
  49. package/dist/core/ingestion/resolvers/utils.js +120 -0
  50. package/dist/core/ingestion/symbol-resolver.d.ts +32 -0
  51. package/dist/core/ingestion/symbol-resolver.js +83 -0
  52. package/dist/core/ingestion/symbol-table.d.ts +12 -1
  53. package/dist/core/ingestion/symbol-table.js +19 -12
  54. package/dist/core/ingestion/tree-sitter-queries.d.ts +11 -11
  55. package/dist/core/ingestion/tree-sitter-queries.js +114 -9
  56. package/dist/core/ingestion/type-env.d.ts +27 -0
  57. package/dist/core/ingestion/type-env.js +86 -0
  58. package/dist/core/ingestion/type-extractors/c-cpp.d.ts +2 -0
  59. package/dist/core/ingestion/type-extractors/c-cpp.js +60 -0
  60. package/dist/core/ingestion/type-extractors/csharp.d.ts +2 -0
  61. package/dist/core/ingestion/type-extractors/csharp.js +89 -0
  62. package/dist/core/ingestion/type-extractors/go.d.ts +2 -0
  63. package/dist/core/ingestion/type-extractors/go.js +105 -0
  64. package/dist/core/ingestion/type-extractors/index.d.ts +21 -0
  65. package/dist/core/ingestion/type-extractors/index.js +29 -0
  66. package/dist/core/ingestion/type-extractors/jvm.d.ts +3 -0
  67. package/dist/core/ingestion/type-extractors/jvm.js +121 -0
  68. package/dist/core/ingestion/type-extractors/php.d.ts +2 -0
  69. package/dist/core/ingestion/type-extractors/php.js +31 -0
  70. package/dist/core/ingestion/type-extractors/python.d.ts +2 -0
  71. package/dist/core/ingestion/type-extractors/python.js +41 -0
  72. package/dist/core/ingestion/type-extractors/rust.d.ts +2 -0
  73. package/dist/core/ingestion/type-extractors/rust.js +39 -0
  74. package/dist/core/ingestion/type-extractors/shared.d.ts +17 -0
  75. package/dist/core/ingestion/type-extractors/shared.js +97 -0
  76. package/dist/core/ingestion/type-extractors/swift.d.ts +2 -0
  77. package/dist/core/ingestion/type-extractors/swift.js +43 -0
  78. package/dist/core/ingestion/type-extractors/types.d.ts +14 -0
  79. package/dist/core/ingestion/type-extractors/types.js +1 -0
  80. package/dist/core/ingestion/type-extractors/typescript.d.ts +2 -0
  81. package/dist/core/ingestion/type-extractors/typescript.js +46 -0
  82. package/dist/core/ingestion/utils.d.ts +67 -0
  83. package/dist/core/ingestion/utils.js +691 -4
  84. package/dist/core/ingestion/workers/parse-worker.d.ts +20 -3
  85. package/dist/core/ingestion/workers/parse-worker.js +84 -345
  86. package/dist/core/kuzu/csv-generator.js +19 -3
  87. package/dist/core/kuzu/kuzu-adapter.js +3 -0
  88. package/dist/core/kuzu/schema.d.ts +3 -3
  89. package/dist/core/kuzu/schema.js +16 -1
  90. package/dist/mcp/tools.js +12 -3
  91. package/package.json +1 -1
@@ -0,0 +1,363 @@
1
+ import { SupportedLanguages } from '../../config/supported-languages.js';
2
+ /**
3
+ * Walk a named-binding re-export chain through NamedImportMap.
4
+ *
5
+ * When file A imports { User } from B, and B re-exports { User } from C,
6
+ * the NamedImportMap for A points to B, but B has no User definition.
7
+ * This function follows the chain: A→B→C until a definition is found.
8
+ *
9
+ * Returns the definitions found at the end of the chain, or null if the
10
+ * chain breaks (missing binding, circular reference, or depth exceeded).
11
+ * Max depth 5 to prevent infinite loops.
12
+ *
13
+ * @param allDefs Pre-computed `symbolTable.lookupFuzzy(name)` result — must be the
14
+ * complete unfiltered result. Passing a file-filtered subset will cause
15
+ * silent misses at depth=0 for non-aliased bindings.
16
+ */
17
+ export function walkBindingChain(name, currentFilePath, symbolTable, namedImportMap, allDefs) {
18
+ let lookupFile = currentFilePath;
19
+ let lookupName = name;
20
+ const visited = new Set();
21
+ for (let depth = 0; depth < 5; depth++) {
22
+ const bindings = namedImportMap.get(lookupFile);
23
+ if (!bindings)
24
+ return null;
25
+ const binding = bindings.get(lookupName);
26
+ if (!binding)
27
+ return null;
28
+ const key = `${binding.sourcePath}:${binding.exportedName}`;
29
+ if (visited.has(key))
30
+ return null; // circular
31
+ visited.add(key);
32
+ const targetName = binding.exportedName;
33
+ const resolvedDefs = targetName !== lookupName || depth > 0
34
+ ? symbolTable.lookupFuzzy(targetName).filter(def => def.filePath === binding.sourcePath)
35
+ : allDefs.filter(def => def.filePath === binding.sourcePath);
36
+ if (resolvedDefs.length > 0)
37
+ return resolvedDefs;
38
+ // No definition in source file → follow re-export chain
39
+ lookupFile = binding.sourcePath;
40
+ lookupName = targetName;
41
+ }
42
+ return null;
43
+ }
44
+ /**
45
+ * Extract named bindings from an import AST node.
46
+ * Returns undefined if the import is not a named import (e.g., import * or default).
47
+ *
48
+ * TS: import { User, Repo as R } from './models'
49
+ * → [{local:'User', exported:'User'}, {local:'R', exported:'Repo'}]
50
+ *
51
+ * Python: from models import User, Repo as R
52
+ * → [{local:'User', exported:'User'}, {local:'R', exported:'Repo'}]
53
+ */
54
+ export function extractNamedBindings(importNode, language) {
55
+ if (language === SupportedLanguages.TypeScript || language === SupportedLanguages.JavaScript) {
56
+ return extractTsNamedBindings(importNode);
57
+ }
58
+ if (language === SupportedLanguages.Python) {
59
+ return extractPythonNamedBindings(importNode);
60
+ }
61
+ if (language === SupportedLanguages.Kotlin) {
62
+ return extractKotlinNamedBindings(importNode);
63
+ }
64
+ if (language === SupportedLanguages.Rust) {
65
+ return extractRustNamedBindings(importNode);
66
+ }
67
+ if (language === SupportedLanguages.PHP) {
68
+ return extractPhpNamedBindings(importNode);
69
+ }
70
+ if (language === SupportedLanguages.CSharp) {
71
+ return extractCsharpNamedBindings(importNode);
72
+ }
73
+ if (language === SupportedLanguages.Java) {
74
+ return extractJavaNamedBindings(importNode);
75
+ }
76
+ return undefined;
77
+ }
78
+ export function extractTsNamedBindings(importNode) {
79
+ // import_statement > import_clause > named_imports > import_specifier*
80
+ const importClause = findChild(importNode, 'import_clause');
81
+ if (importClause) {
82
+ const namedImports = findChild(importClause, 'named_imports');
83
+ if (!namedImports)
84
+ return undefined; // default import, namespace import, or side-effect
85
+ const bindings = [];
86
+ for (let i = 0; i < namedImports.namedChildCount; i++) {
87
+ const specifier = namedImports.namedChild(i);
88
+ if (specifier?.type !== 'import_specifier')
89
+ continue;
90
+ const identifiers = [];
91
+ for (let j = 0; j < specifier.namedChildCount; j++) {
92
+ const child = specifier.namedChild(j);
93
+ if (child?.type === 'identifier')
94
+ identifiers.push(child.text);
95
+ }
96
+ if (identifiers.length === 1) {
97
+ bindings.push({ local: identifiers[0], exported: identifiers[0] });
98
+ }
99
+ else if (identifiers.length === 2) {
100
+ // import { Foo as Bar } → exported='Foo', local='Bar'
101
+ bindings.push({ local: identifiers[1], exported: identifiers[0] });
102
+ }
103
+ }
104
+ return bindings.length > 0 ? bindings : undefined;
105
+ }
106
+ // Re-export: export { X } from './y' → export_statement > export_clause > export_specifier
107
+ const exportClause = findChild(importNode, 'export_clause');
108
+ if (exportClause) {
109
+ const bindings = [];
110
+ for (let i = 0; i < exportClause.namedChildCount; i++) {
111
+ const specifier = exportClause.namedChild(i);
112
+ if (specifier?.type !== 'export_specifier')
113
+ continue;
114
+ const identifiers = [];
115
+ for (let j = 0; j < specifier.namedChildCount; j++) {
116
+ const child = specifier.namedChild(j);
117
+ if (child?.type === 'identifier')
118
+ identifiers.push(child.text);
119
+ }
120
+ if (identifiers.length === 1) {
121
+ // export { User } from './base' → re-exports User as User
122
+ bindings.push({ local: identifiers[0], exported: identifiers[0] });
123
+ }
124
+ else if (identifiers.length === 2) {
125
+ // export { Repo as Repository } from './models' → name=Repo, alias=Repository
126
+ // For re-exports, the first id is the source name, second is what's exported
127
+ // When another file imports { Repository }, they get Repo from the source
128
+ bindings.push({ local: identifiers[1], exported: identifiers[0] });
129
+ }
130
+ }
131
+ return bindings.length > 0 ? bindings : undefined;
132
+ }
133
+ return undefined;
134
+ }
135
+ export function extractPythonNamedBindings(importNode) {
136
+ // Only from import_from_statement, not plain import_statement
137
+ if (importNode.type !== 'import_from_statement')
138
+ return undefined;
139
+ const bindings = [];
140
+ for (let i = 0; i < importNode.namedChildCount; i++) {
141
+ const child = importNode.namedChild(i);
142
+ if (!child)
143
+ continue;
144
+ if (child.type === 'dotted_name') {
145
+ // Skip the module_name (first dotted_name is the source module)
146
+ const fieldName = importNode.childForFieldName?.('module_name');
147
+ if (fieldName && child.startIndex === fieldName.startIndex)
148
+ continue;
149
+ // This is an imported name: from x import User
150
+ const name = child.text;
151
+ if (name)
152
+ bindings.push({ local: name, exported: name });
153
+ }
154
+ if (child.type === 'aliased_import') {
155
+ // from x import Repo as R
156
+ const dottedName = findChild(child, 'dotted_name');
157
+ const aliasIdent = findChild(child, 'identifier');
158
+ if (dottedName && aliasIdent) {
159
+ bindings.push({ local: aliasIdent.text, exported: dottedName.text });
160
+ }
161
+ }
162
+ }
163
+ return bindings.length > 0 ? bindings : undefined;
164
+ }
165
+ export function extractKotlinNamedBindings(importNode) {
166
+ // import_header > identifier + import_alias > simple_identifier
167
+ if (importNode.type !== 'import_header')
168
+ return undefined;
169
+ const fullIdent = findChild(importNode, 'identifier');
170
+ if (!fullIdent)
171
+ return undefined;
172
+ const fullText = fullIdent.text;
173
+ const exportedName = fullText.includes('.') ? fullText.split('.').pop() : fullText;
174
+ const importAlias = findChild(importNode, 'import_alias');
175
+ if (importAlias) {
176
+ // Aliased: import com.example.User as U
177
+ const aliasIdent = findChild(importAlias, 'simple_identifier');
178
+ if (!aliasIdent)
179
+ return undefined;
180
+ return [{ local: aliasIdent.text, exported: exportedName }];
181
+ }
182
+ // Non-aliased: import com.example.User → local="User", exported="User"
183
+ // Skip wildcard imports (ending in *)
184
+ if (fullText.endsWith('.*') || fullText.endsWith('*'))
185
+ return undefined;
186
+ // Skip lowercase last segments — those are member/function imports (e.g.,
187
+ // import util.OneArg.writeAudit), not class imports. Multiple member imports
188
+ // with the same function name would collide in NamedImportMap, breaking
189
+ // arity-based disambiguation.
190
+ if (exportedName[0] && exportedName[0] === exportedName[0].toLowerCase())
191
+ return undefined;
192
+ return [{ local: exportedName, exported: exportedName }];
193
+ }
194
+ export function extractRustNamedBindings(importNode) {
195
+ // use_declaration may contain use_as_clause at any depth
196
+ if (importNode.type !== 'use_declaration')
197
+ return undefined;
198
+ const bindings = [];
199
+ collectRustBindings(importNode, bindings);
200
+ return bindings.length > 0 ? bindings : undefined;
201
+ }
202
+ function collectRustBindings(node, bindings) {
203
+ if (node.type === 'use_as_clause') {
204
+ // First identifier = exported name, second identifier = local alias
205
+ const idents = [];
206
+ for (let i = 0; i < node.namedChildCount; i++) {
207
+ const child = node.namedChild(i);
208
+ if (child?.type === 'identifier')
209
+ idents.push(child.text);
210
+ // For scoped_identifier, extract the last segment
211
+ if (child?.type === 'scoped_identifier') {
212
+ const nameNode = child.childForFieldName?.('name');
213
+ if (nameNode)
214
+ idents.push(nameNode.text);
215
+ }
216
+ }
217
+ if (idents.length === 2) {
218
+ bindings.push({ local: idents[1], exported: idents[0] });
219
+ }
220
+ return;
221
+ }
222
+ // Terminal identifier in a use_list: use crate::models::{User, Repo}
223
+ if (node.type === 'identifier' && node.parent?.type === 'use_list') {
224
+ bindings.push({ local: node.text, exported: node.text });
225
+ return;
226
+ }
227
+ // Skip scoped_identifier that serves as path prefix in scoped_use_list
228
+ // e.g. use crate::models::{User, Repo} — the path node "crate::models" is not an importable symbol
229
+ if (node.type === 'scoped_identifier' && node.parent?.type === 'scoped_use_list') {
230
+ return; // path prefix — the use_list sibling handles the actual symbols
231
+ }
232
+ // Terminal scoped_identifier: use crate::models::User;
233
+ // Only extract if this is a leaf (no deeper use_list/use_as_clause/scoped_use_list)
234
+ if (node.type === 'scoped_identifier') {
235
+ let hasDeeper = false;
236
+ for (let i = 0; i < node.namedChildCount; i++) {
237
+ const child = node.namedChild(i);
238
+ if (child?.type === 'use_list' || child?.type === 'use_as_clause' || child?.type === 'scoped_use_list') {
239
+ hasDeeper = true;
240
+ break;
241
+ }
242
+ }
243
+ if (!hasDeeper) {
244
+ const nameNode = node.childForFieldName?.('name');
245
+ if (nameNode) {
246
+ bindings.push({ local: nameNode.text, exported: nameNode.text });
247
+ }
248
+ return;
249
+ }
250
+ }
251
+ // Recurse into children
252
+ for (let i = 0; i < node.namedChildCount; i++) {
253
+ const child = node.namedChild(i);
254
+ if (child)
255
+ collectRustBindings(child, bindings);
256
+ }
257
+ }
258
+ export function extractPhpNamedBindings(importNode) {
259
+ // namespace_use_declaration > namespace_use_clause* (flat)
260
+ // namespace_use_declaration > namespace_use_group > namespace_use_clause* (grouped)
261
+ if (importNode.type !== 'namespace_use_declaration')
262
+ return undefined;
263
+ const bindings = [];
264
+ // Collect all clauses — from direct children AND from namespace_use_group
265
+ const clauses = [];
266
+ for (let i = 0; i < importNode.namedChildCount; i++) {
267
+ const child = importNode.namedChild(i);
268
+ if (child?.type === 'namespace_use_clause') {
269
+ clauses.push(child);
270
+ }
271
+ else if (child?.type === 'namespace_use_group') {
272
+ for (let j = 0; j < child.namedChildCount; j++) {
273
+ const groupChild = child.namedChild(j);
274
+ if (groupChild?.type === 'namespace_use_clause')
275
+ clauses.push(groupChild);
276
+ }
277
+ }
278
+ }
279
+ for (const clause of clauses) {
280
+ // Flat imports: qualified_name + name (alias)
281
+ let qualifiedName = null;
282
+ const names = [];
283
+ for (let j = 0; j < clause.namedChildCount; j++) {
284
+ const child = clause.namedChild(j);
285
+ if (child?.type === 'qualified_name')
286
+ qualifiedName = child;
287
+ else if (child?.type === 'name')
288
+ names.push(child);
289
+ }
290
+ if (qualifiedName && names.length > 0) {
291
+ // Flat aliased import: use App\Models\Repo as R;
292
+ const fullText = qualifiedName.text;
293
+ const exportedName = fullText.includes('\\') ? fullText.split('\\').pop() : fullText;
294
+ bindings.push({ local: names[0].text, exported: exportedName });
295
+ }
296
+ else if (qualifiedName && names.length === 0) {
297
+ // Flat non-aliased import: use App\Models\User;
298
+ const fullText = qualifiedName.text;
299
+ const lastSegment = fullText.includes('\\') ? fullText.split('\\').pop() : fullText;
300
+ bindings.push({ local: lastSegment, exported: lastSegment });
301
+ }
302
+ else if (!qualifiedName && names.length >= 2) {
303
+ // Grouped aliased import: {Repo as R} — first name = exported, second = alias
304
+ bindings.push({ local: names[1].text, exported: names[0].text });
305
+ }
306
+ else if (!qualifiedName && names.length === 1) {
307
+ // Grouped non-aliased import: {User} in use App\Models\{User, Repo as R}
308
+ bindings.push({ local: names[0].text, exported: names[0].text });
309
+ }
310
+ }
311
+ return bindings.length > 0 ? bindings : undefined;
312
+ }
313
+ export function extractCsharpNamedBindings(importNode) {
314
+ // using_directive with identifier (alias) + qualified_name (target)
315
+ if (importNode.type !== 'using_directive')
316
+ return undefined;
317
+ let aliasIdent = null;
318
+ let qualifiedName = null;
319
+ for (let i = 0; i < importNode.namedChildCount; i++) {
320
+ const child = importNode.namedChild(i);
321
+ if (child?.type === 'identifier' && !aliasIdent)
322
+ aliasIdent = child;
323
+ else if (child?.type === 'qualified_name')
324
+ qualifiedName = child;
325
+ }
326
+ if (!aliasIdent || !qualifiedName)
327
+ return undefined;
328
+ const fullText = qualifiedName.text;
329
+ const exportedName = fullText.includes('.') ? fullText.split('.').pop() : fullText;
330
+ return [{ local: aliasIdent.text, exported: exportedName }];
331
+ }
332
+ export function extractJavaNamedBindings(importNode) {
333
+ // import_declaration > scoped_identifier "com.example.models.User"
334
+ // Wildcard imports (.*) don't produce named bindings
335
+ if (importNode.type !== 'import_declaration')
336
+ return undefined;
337
+ // Check for asterisk (wildcard import) — skip those
338
+ for (let i = 0; i < importNode.childCount; i++) {
339
+ const child = importNode.child(i);
340
+ if (child?.type === 'asterisk')
341
+ return undefined;
342
+ }
343
+ const scopedId = findChild(importNode, 'scoped_identifier');
344
+ if (!scopedId)
345
+ return undefined;
346
+ const fullText = scopedId.text;
347
+ const lastDot = fullText.lastIndexOf('.');
348
+ if (lastDot === -1)
349
+ return undefined;
350
+ const className = fullText.slice(lastDot + 1);
351
+ // Skip lowercase names — those are package imports, not class imports
352
+ if (className[0] && className[0] === className[0].toLowerCase())
353
+ return undefined;
354
+ return [{ local: className, exported: className }];
355
+ }
356
+ function findChild(node, type) {
357
+ for (let i = 0; i < node.namedChildCount; i++) {
358
+ const child = node.namedChild(i);
359
+ if (child?.type === type)
360
+ return child;
361
+ }
362
+ return null;
363
+ }
@@ -10,16 +10,7 @@ export interface WorkerExtractedData {
10
10
  heritage: ExtractedHeritage[];
11
11
  routes: ExtractedRoute[];
12
12
  }
13
- /**
14
- * Check if a symbol (function, class, etc.) is exported/public
15
- * Handles all 9 supported languages with explicit logic
16
- *
17
- * @param node - The AST node for the symbol name
18
- * @param name - The symbol name
19
- * @param language - The programming language
20
- * @returns true if the symbol is exported/public
21
- */
22
- export declare const isNodeExported: (node: any, name: string, language: string) => boolean;
13
+ export { isNodeExported } from './export-detection.js';
23
14
  export declare const processParsing: (graph: KnowledgeGraph, files: {
24
15
  path: string;
25
16
  content: string;
@@ -2,178 +2,13 @@ import Parser from 'tree-sitter';
2
2
  import { loadParser, loadLanguage } from '../tree-sitter/parser-loader.js';
3
3
  import { LANGUAGE_QUERIES } from './tree-sitter-queries.js';
4
4
  import { generateId } from '../../lib/utils.js';
5
- import { findSiblingChild, getLanguageFromFilename, yieldToEventLoop } from './utils.js';
5
+ import { getLanguageFromFilename, yieldToEventLoop, getDefinitionNodeFromCaptures, findEnclosingClassId, extractMethodSignature } from './utils.js';
6
+ import { isNodeExported } from './export-detection.js';
6
7
  import { detectFrameworkFromAST } from './framework-detection.js';
7
- const DEFINITION_CAPTURE_KEYS = [
8
- 'definition.function',
9
- 'definition.class',
10
- 'definition.interface',
11
- 'definition.method',
12
- 'definition.struct',
13
- 'definition.enum',
14
- 'definition.namespace',
15
- 'definition.module',
16
- 'definition.trait',
17
- 'definition.impl',
18
- 'definition.type',
19
- 'definition.const',
20
- 'definition.static',
21
- 'definition.typedef',
22
- 'definition.macro',
23
- 'definition.union',
24
- 'definition.property',
25
- 'definition.record',
26
- 'definition.delegate',
27
- 'definition.annotation',
28
- 'definition.constructor',
29
- 'definition.template',
30
- ];
31
- const getDefinitionNodeFromCaptures = (captureMap) => {
32
- for (const key of DEFINITION_CAPTURE_KEYS) {
33
- if (captureMap[key])
34
- return captureMap[key];
35
- }
36
- return null;
37
- };
38
- // ============================================================================
39
- // EXPORT DETECTION - Language-specific visibility detection
40
- // ============================================================================
41
- /**
42
- * Check if a symbol (function, class, etc.) is exported/public
43
- * Handles all 9 supported languages with explicit logic
44
- *
45
- * @param node - The AST node for the symbol name
46
- * @param name - The symbol name
47
- * @param language - The programming language
48
- * @returns true if the symbol is exported/public
49
- */
50
- export const isNodeExported = (node, name, language) => {
51
- let current = node;
52
- switch (language) {
53
- // JavaScript/TypeScript: Check for export keyword in ancestors
54
- case 'javascript':
55
- case 'typescript':
56
- while (current) {
57
- const type = current.type;
58
- if (type === 'export_statement' ||
59
- type === 'export_specifier' ||
60
- type === 'lexical_declaration' && current.parent?.type === 'export_statement') {
61
- return true;
62
- }
63
- // Also check if text starts with 'export '
64
- if (current.text?.startsWith('export ')) {
65
- return true;
66
- }
67
- current = current.parent;
68
- }
69
- return false;
70
- // Python: Public if no leading underscore (convention)
71
- case 'python':
72
- return !name.startsWith('_');
73
- // Java: Check for 'public' modifier
74
- // In tree-sitter Java, modifiers are siblings of the name node, not parents
75
- case 'java':
76
- while (current) {
77
- // Check if this node or any sibling is a 'modifiers' node containing 'public'
78
- if (current.parent) {
79
- const parent = current.parent;
80
- // Check all children of the parent for modifiers
81
- for (let i = 0; i < parent.childCount; i++) {
82
- const child = parent.child(i);
83
- if (child?.type === 'modifiers' && child.text?.includes('public')) {
84
- return true;
85
- }
86
- }
87
- // Also check if the parent's text starts with 'public' (fallback)
88
- if (parent.type === 'method_declaration' || parent.type === 'constructor_declaration') {
89
- if (parent.text?.trimStart().startsWith('public')) {
90
- return true;
91
- }
92
- }
93
- }
94
- current = current.parent;
95
- }
96
- return false;
97
- // C#: Check for 'public' modifier in ancestors
98
- case 'csharp':
99
- while (current) {
100
- if (current.type === 'modifier' || current.type === 'modifiers') {
101
- if (current.text?.includes('public'))
102
- return true;
103
- }
104
- current = current.parent;
105
- }
106
- return false;
107
- // Go: Uppercase first letter = exported
108
- case 'go':
109
- if (name.length === 0)
110
- return false;
111
- const first = name[0];
112
- // Must be uppercase letter (not a number or symbol)
113
- return first === first.toUpperCase() && first !== first.toLowerCase();
114
- // Rust: Check for 'pub' visibility modifier
115
- case 'rust':
116
- while (current) {
117
- if (current.type === 'visibility_modifier') {
118
- if (current.text?.includes('pub'))
119
- return true;
120
- }
121
- current = current.parent;
122
- }
123
- return false;
124
- // Kotlin: Default visibility is public (unlike Java)
125
- // visibility_modifier is inside modifiers, a sibling of the name node within the declaration
126
- case 'kotlin':
127
- while (current) {
128
- if (current.parent) {
129
- const visMod = findSiblingChild(current.parent, 'modifiers', 'visibility_modifier');
130
- if (visMod) {
131
- const text = visMod.text;
132
- if (text === 'private' || text === 'internal' || text === 'protected')
133
- return false;
134
- if (text === 'public')
135
- return true;
136
- }
137
- }
138
- current = current.parent;
139
- }
140
- // No visibility modifier = public (Kotlin default)
141
- return true;
142
- // C/C++: No native export concept at language level
143
- // Entry points will be detected via name patterns (main, etc.)
144
- case 'c':
145
- case 'cpp':
146
- return false;
147
- // Swift: Check for 'public' or 'open' access modifiers
148
- case 'swift':
149
- while (current) {
150
- if (current.type === 'modifiers' || current.type === 'visibility_modifier') {
151
- const text = current.text || '';
152
- if (text.includes('public') || text.includes('open'))
153
- return true;
154
- }
155
- current = current.parent;
156
- }
157
- return false;
158
- // PHP: Check for visibility modifier or top-level scope
159
- case 'php':
160
- while (current) {
161
- if (current.type === 'class_declaration' ||
162
- current.type === 'interface_declaration' ||
163
- current.type === 'trait_declaration' ||
164
- current.type === 'enum_declaration') {
165
- return true;
166
- }
167
- if (current.type === 'visibility_modifier') {
168
- return current.text === 'public';
169
- }
170
- current = current.parent;
171
- }
172
- return true; // Top-level functions are globally accessible
173
- default:
174
- return false;
175
- }
176
- };
8
+ import { getTreeSitterBufferSize, TREE_SITTER_MAX_BUFFER } from './constants.js';
9
+ // isNodeExported imported from ./export-detection.js (shared module)
10
+ // Re-export for backward compatibility with any external consumers
11
+ export { isNodeExported } from './export-detection.js';
177
12
  // ============================================================================
178
13
  // Worker-based parallel parsing
179
14
  // ============================================================================
@@ -209,7 +44,10 @@ const processParsingWithWorkers = async (graph, files, symbolTable, astCache, wo
209
44
  graph.addRelationship(rel);
210
45
  }
211
46
  for (const sym of result.symbols) {
212
- symbolTable.add(sym.filePath, sym.name, sym.nodeId, sym.type);
47
+ symbolTable.add(sym.filePath, sym.name, sym.nodeId, sym.type, {
48
+ parameterCount: sym.parameterCount,
49
+ ownerId: sym.ownerId,
50
+ });
213
51
  }
214
52
  allImports.push(...result.imports);
215
53
  allCalls.push(...result.calls);
@@ -234,8 +72,8 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
234
72
  const language = getLanguageFromFilename(file.path);
235
73
  if (!language)
236
74
  continue;
237
- // Skip very large files they can crash tree-sitter or cause OOM
238
- if (file.content.length > 512 * 1024)
75
+ // Skip files larger than the max tree-sitter buffer (32 MB)
76
+ if (file.content.length > TREE_SITTER_MAX_BUFFER)
239
77
  continue;
240
78
  try {
241
79
  await loadLanguage(language, file.path);
@@ -245,7 +83,7 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
245
83
  }
246
84
  let tree;
247
85
  try {
248
- tree = parser.parse(file.content, undefined, { bufferSize: 1024 * 256 });
86
+ tree = parser.parse(file.content, undefined, { bufferSize: getTreeSitterBufferSize(file.content.length) });
249
87
  }
250
88
  catch (parseError) {
251
89
  console.warn(`Skipping unparseable file: ${file.path}`);
@@ -330,11 +168,15 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
330
168
  nodeLabel = 'Template';
331
169
  const definitionNodeForRange = getDefinitionNodeFromCaptures(captureMap);
332
170
  const startLine = definitionNodeForRange ? definitionNodeForRange.startPosition.row : (nameNode ? nameNode.startPosition.row : 0);
333
- const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}:${startLine}`);
171
+ const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}`);
334
172
  const definitionNode = getDefinitionNodeFromCaptures(captureMap);
335
173
  const frameworkHint = definitionNode
336
174
  ? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
337
175
  : null;
176
+ // Extract method signature for Method/Constructor nodes
177
+ const methodSig = (nodeLabel === 'Function' || nodeLabel === 'Method' || nodeLabel === 'Constructor')
178
+ ? extractMethodSignature(definitionNode)
179
+ : undefined;
338
180
  const node = {
339
181
  id: nodeId,
340
182
  label: nodeLabel,
@@ -349,10 +191,21 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
349
191
  astFrameworkMultiplier: frameworkHint.entryPointMultiplier,
350
192
  astFrameworkReason: frameworkHint.reason,
351
193
  } : {}),
194
+ ...(methodSig ? {
195
+ parameterCount: methodSig.parameterCount,
196
+ returnType: methodSig.returnType,
197
+ } : {}),
352
198
  },
353
199
  };
354
200
  graph.addNode(node);
355
- symbolTable.add(file.path, nodeName, nodeId, nodeLabel);
201
+ // Compute enclosing class for Method/Constructor/Property/Function — used for both ownerId and HAS_METHOD
202
+ // Function is included because Kotlin/Rust/Python capture class methods as Function nodes
203
+ const needsOwner = nodeLabel === 'Method' || nodeLabel === 'Constructor' || nodeLabel === 'Property' || nodeLabel === 'Function';
204
+ const enclosingClassId = needsOwner ? findEnclosingClassId(nameNode || definitionNodeForRange, file.path) : null;
205
+ symbolTable.add(file.path, nodeName, nodeId, nodeLabel, {
206
+ parameterCount: methodSig?.parameterCount,
207
+ ownerId: enclosingClassId ?? undefined,
208
+ });
356
209
  const fileId = generateId('File', file.path);
357
210
  const relId = generateId('DEFINES', `${fileId}->${nodeId}`);
358
211
  const relationship = {
@@ -364,6 +217,17 @@ const processParsingSequential = async (graph, files, symbolTable, astCache, onF
364
217
  reason: '',
365
218
  };
366
219
  graph.addRelationship(relationship);
220
+ // ── HAS_METHOD: link method/constructor/property to enclosing class ──
221
+ if (enclosingClassId) {
222
+ graph.addRelationship({
223
+ id: generateId('HAS_METHOD', `${enclosingClassId}->${nodeId}`),
224
+ sourceId: enclosingClassId,
225
+ targetId: nodeId,
226
+ type: 'HAS_METHOD',
227
+ confidence: 1.0,
228
+ reason: '',
229
+ });
230
+ }
367
231
  });
368
232
  }
369
233
  };