gitnexus 1.3.5 → 1.3.7
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.
- package/dist/cli/ai-context.js +77 -23
- package/dist/cli/analyze.js +0 -5
- package/dist/cli/eval-server.d.ts +7 -0
- package/dist/cli/eval-server.js +16 -7
- package/dist/cli/index.js +6 -21
- package/dist/cli/mcp.js +2 -0
- package/dist/cli/setup.js +6 -1
- package/dist/config/supported-languages.d.ts +1 -0
- package/dist/config/supported-languages.js +1 -0
- package/dist/core/ingestion/call-processor.d.ts +5 -1
- package/dist/core/ingestion/call-processor.js +78 -0
- package/dist/core/ingestion/framework-detection.d.ts +1 -0
- package/dist/core/ingestion/framework-detection.js +49 -2
- package/dist/core/ingestion/import-processor.js +90 -39
- package/dist/core/ingestion/parsing-processor.d.ts +12 -1
- package/dist/core/ingestion/parsing-processor.js +92 -51
- package/dist/core/ingestion/pipeline.js +21 -2
- package/dist/core/ingestion/process-processor.js +0 -1
- package/dist/core/ingestion/tree-sitter-queries.d.ts +1 -0
- package/dist/core/ingestion/tree-sitter-queries.js +80 -0
- package/dist/core/ingestion/utils.d.ts +5 -0
- package/dist/core/ingestion/utils.js +20 -0
- package/dist/core/ingestion/workers/parse-worker.d.ts +11 -0
- package/dist/core/ingestion/workers/parse-worker.js +481 -52
- package/dist/core/kuzu/csv-generator.d.ts +4 -0
- package/dist/core/kuzu/csv-generator.js +23 -9
- package/dist/core/kuzu/kuzu-adapter.js +9 -3
- package/dist/core/tree-sitter/parser-loader.d.ts +1 -0
- package/dist/core/tree-sitter/parser-loader.js +12 -2
- package/dist/mcp/core/kuzu-adapter.d.ts +4 -3
- package/dist/mcp/core/kuzu-adapter.js +79 -16
- package/dist/mcp/local/local-backend.d.ts +13 -0
- package/dist/mcp/local/local-backend.js +148 -105
- package/dist/mcp/server.js +26 -11
- package/dist/storage/git.js +4 -1
- package/dist/storage/repo-manager.js +16 -2
- package/hooks/claude/gitnexus-hook.cjs +28 -8
- package/hooks/claude/pre-tool-use.sh +2 -1
- package/package.json +14 -4
- package/dist/cli/claude-hooks.d.ts +0 -22
- package/dist/cli/claude-hooks.js +0 -97
- package/dist/cli/view.d.ts +0 -13
- package/dist/cli/view.js +0 -59
- package/dist/core/graph/html-graph-viewer.d.ts +0 -15
- package/dist/core/graph/html-graph-viewer.js +0 -542
- package/dist/core/graph/html-graph-viewer.test.d.ts +0 -1
- package/dist/core/graph/html-graph-viewer.test.js +0 -67
|
@@ -9,11 +9,19 @@ import CPP from 'tree-sitter-cpp';
|
|
|
9
9
|
import CSharp from 'tree-sitter-c-sharp';
|
|
10
10
|
import Go from 'tree-sitter-go';
|
|
11
11
|
import Rust from 'tree-sitter-rust';
|
|
12
|
+
import Kotlin from 'tree-sitter-kotlin';
|
|
12
13
|
import PHP from 'tree-sitter-php';
|
|
13
|
-
import
|
|
14
|
+
import { createRequire } from 'node:module';
|
|
14
15
|
import { SupportedLanguages } from '../../../config/supported-languages.js';
|
|
15
16
|
import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js';
|
|
16
|
-
|
|
17
|
+
// tree-sitter-swift is an optionalDependency — may not be installed
|
|
18
|
+
const _require = createRequire(import.meta.url);
|
|
19
|
+
let Swift = null;
|
|
20
|
+
try {
|
|
21
|
+
Swift = _require('tree-sitter-swift');
|
|
22
|
+
}
|
|
23
|
+
catch { }
|
|
24
|
+
import { findSiblingChild, getLanguageFromFilename } from '../utils.js';
|
|
17
25
|
import { detectFrameworkFromAST } from '../framework-detection.js';
|
|
18
26
|
import { generateId } from '../../../lib/utils.js';
|
|
19
27
|
// ============================================================================
|
|
@@ -31,8 +39,9 @@ const languageMap = {
|
|
|
31
39
|
[SupportedLanguages.CSharp]: CSharp,
|
|
32
40
|
[SupportedLanguages.Go]: Go,
|
|
33
41
|
[SupportedLanguages.Rust]: Rust,
|
|
42
|
+
[SupportedLanguages.Kotlin]: Kotlin,
|
|
34
43
|
[SupportedLanguages.PHP]: PHP.php_only,
|
|
35
|
-
[SupportedLanguages.Swift]: Swift,
|
|
44
|
+
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
|
|
36
45
|
};
|
|
37
46
|
const setLanguage = (language, filePath) => {
|
|
38
47
|
const key = language === SupportedLanguages.TypeScript && filePath.endsWith('.tsx')
|
|
@@ -108,18 +117,26 @@ const isNodeExported = (node, name, language) => {
|
|
|
108
117
|
current = current.parent;
|
|
109
118
|
}
|
|
110
119
|
return false;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
case 'swift':
|
|
120
|
+
// Kotlin: Default visibility is public (unlike Java)
|
|
121
|
+
// visibility_modifier is inside modifiers, a sibling of the name node within the declaration
|
|
122
|
+
case 'kotlin':
|
|
115
123
|
while (current) {
|
|
116
|
-
if (current.
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
|
|
124
|
+
if (current.parent) {
|
|
125
|
+
const visMod = findSiblingChild(current.parent, 'modifiers', 'visibility_modifier');
|
|
126
|
+
if (visMod) {
|
|
127
|
+
const text = visMod.text;
|
|
128
|
+
if (text === 'private' || text === 'internal' || text === 'protected')
|
|
129
|
+
return false;
|
|
130
|
+
if (text === 'public')
|
|
131
|
+
return true;
|
|
132
|
+
}
|
|
120
133
|
}
|
|
121
134
|
current = current.parent;
|
|
122
135
|
}
|
|
136
|
+
// No visibility modifier = public (Kotlin default)
|
|
137
|
+
return true;
|
|
138
|
+
case 'c':
|
|
139
|
+
case 'cpp':
|
|
123
140
|
return false;
|
|
124
141
|
case 'php':
|
|
125
142
|
// Top-level classes/interfaces/traits are always accessible
|
|
@@ -138,6 +155,16 @@ const isNodeExported = (node, name, language) => {
|
|
|
138
155
|
}
|
|
139
156
|
// Top-level functions (no parent class) are globally accessible
|
|
140
157
|
return true;
|
|
158
|
+
case 'swift':
|
|
159
|
+
while (current) {
|
|
160
|
+
if (current.type === 'modifiers' || current.type === 'visibility_modifier') {
|
|
161
|
+
const text = current.text || '';
|
|
162
|
+
if (text.includes('public') || text.includes('open'))
|
|
163
|
+
return true;
|
|
164
|
+
}
|
|
165
|
+
current = current.parent;
|
|
166
|
+
}
|
|
167
|
+
return false;
|
|
141
168
|
default:
|
|
142
169
|
return false;
|
|
143
170
|
}
|
|
@@ -151,8 +178,12 @@ const FUNCTION_NODE_TYPES = new Set([
|
|
|
151
178
|
'function_definition', 'async_function_declaration', 'async_arrow_function',
|
|
152
179
|
'method_declaration', 'constructor_declaration',
|
|
153
180
|
'local_function_statement', 'function_item', 'impl_item',
|
|
154
|
-
|
|
155
|
-
'
|
|
181
|
+
// Kotlin
|
|
182
|
+
'lambda_literal',
|
|
183
|
+
// PHP
|
|
184
|
+
'anonymous_function',
|
|
185
|
+
// Swift initializers/deinitializers
|
|
186
|
+
'init_declaration', 'deinit_declaration',
|
|
156
187
|
]);
|
|
157
188
|
/** Walk up AST to find enclosing function, return its generateId or null for top-level */
|
|
158
189
|
const findEnclosingFunctionId = (node, filePath) => {
|
|
@@ -234,6 +265,22 @@ const BUILT_INS = new Set([
|
|
|
234
265
|
'open', 'read', 'write', 'close', 'append', 'extend', 'update',
|
|
235
266
|
'super', 'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr',
|
|
236
267
|
'enumerate', 'zip', 'sorted', 'reversed', 'min', 'max', 'sum', 'abs',
|
|
268
|
+
// Kotlin stdlib (IMPORTANT: keep in sync with call-processor.ts BUILT_IN_NAMES)
|
|
269
|
+
'println', 'print', 'readLine', 'require', 'requireNotNull', 'check', 'assert', 'lazy', 'error',
|
|
270
|
+
'listOf', 'mapOf', 'setOf', 'mutableListOf', 'mutableMapOf', 'mutableSetOf',
|
|
271
|
+
'arrayOf', 'sequenceOf', 'also', 'apply', 'run', 'with', 'takeIf', 'takeUnless',
|
|
272
|
+
'TODO', 'buildString', 'buildList', 'buildMap', 'buildSet',
|
|
273
|
+
'repeat', 'synchronized',
|
|
274
|
+
// Kotlin coroutine builders & scope functions
|
|
275
|
+
'launch', 'async', 'runBlocking', 'withContext', 'coroutineScope',
|
|
276
|
+
'supervisorScope', 'delay',
|
|
277
|
+
// Kotlin Flow operators
|
|
278
|
+
'flow', 'flowOf', 'collect', 'emit', 'onEach', 'catch',
|
|
279
|
+
'buffer', 'conflate', 'distinctUntilChanged',
|
|
280
|
+
'flatMapLatest', 'flatMapMerge', 'combine',
|
|
281
|
+
'stateIn', 'shareIn', 'launchIn',
|
|
282
|
+
// Kotlin infix stdlib functions
|
|
283
|
+
'to', 'until', 'downTo', 'step',
|
|
237
284
|
// C/C++ standard library
|
|
238
285
|
'printf', 'fprintf', 'sprintf', 'snprintf', 'vprintf', 'vfprintf', 'vsprintf', 'vsnprintf',
|
|
239
286
|
'scanf', 'fscanf', 'sscanf',
|
|
@@ -359,37 +406,49 @@ const getLabelFromCaptures = (captureMap) => {
|
|
|
359
406
|
return 'Template';
|
|
360
407
|
return 'CodeElement';
|
|
361
408
|
};
|
|
409
|
+
const DEFINITION_CAPTURE_KEYS = [
|
|
410
|
+
'definition.function',
|
|
411
|
+
'definition.class',
|
|
412
|
+
'definition.interface',
|
|
413
|
+
'definition.method',
|
|
414
|
+
'definition.struct',
|
|
415
|
+
'definition.enum',
|
|
416
|
+
'definition.namespace',
|
|
417
|
+
'definition.module',
|
|
418
|
+
'definition.trait',
|
|
419
|
+
'definition.impl',
|
|
420
|
+
'definition.type',
|
|
421
|
+
'definition.const',
|
|
422
|
+
'definition.static',
|
|
423
|
+
'definition.typedef',
|
|
424
|
+
'definition.macro',
|
|
425
|
+
'definition.union',
|
|
426
|
+
'definition.property',
|
|
427
|
+
'definition.record',
|
|
428
|
+
'definition.delegate',
|
|
429
|
+
'definition.annotation',
|
|
430
|
+
'definition.constructor',
|
|
431
|
+
'definition.template',
|
|
432
|
+
];
|
|
362
433
|
const getDefinitionNodeFromCaptures = (captureMap) => {
|
|
363
|
-
const
|
|
364
|
-
'definition.function',
|
|
365
|
-
'definition.class',
|
|
366
|
-
'definition.interface',
|
|
367
|
-
'definition.method',
|
|
368
|
-
'definition.struct',
|
|
369
|
-
'definition.enum',
|
|
370
|
-
'definition.namespace',
|
|
371
|
-
'definition.module',
|
|
372
|
-
'definition.trait',
|
|
373
|
-
'definition.impl',
|
|
374
|
-
'definition.type',
|
|
375
|
-
'definition.const',
|
|
376
|
-
'definition.static',
|
|
377
|
-
'definition.typedef',
|
|
378
|
-
'definition.macro',
|
|
379
|
-
'definition.union',
|
|
380
|
-
'definition.property',
|
|
381
|
-
'definition.record',
|
|
382
|
-
'definition.delegate',
|
|
383
|
-
'definition.annotation',
|
|
384
|
-
'definition.constructor',
|
|
385
|
-
'definition.template',
|
|
386
|
-
];
|
|
387
|
-
for (const key of definitionKeys) {
|
|
434
|
+
for (const key of DEFINITION_CAPTURE_KEYS) {
|
|
388
435
|
if (captureMap[key])
|
|
389
436
|
return captureMap[key];
|
|
390
437
|
}
|
|
391
438
|
return null;
|
|
392
439
|
};
|
|
440
|
+
/**
|
|
441
|
+
* Append .* to a Kotlin import path if the AST has a wildcard_import sibling node.
|
|
442
|
+
* Pure function — returns a new string without mutating the input.
|
|
443
|
+
*/
|
|
444
|
+
const appendKotlinWildcard = (importPath, importNode) => {
|
|
445
|
+
for (let i = 0; i < importNode.childCount; i++) {
|
|
446
|
+
if (importNode.child(i)?.type === 'wildcard_import') {
|
|
447
|
+
return importPath.endsWith('.*') ? importPath : `${importPath}.*`;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
return importPath;
|
|
451
|
+
};
|
|
393
452
|
// ============================================================================
|
|
394
453
|
// Process a batch of files
|
|
395
454
|
// ============================================================================
|
|
@@ -401,6 +460,7 @@ const processBatch = (files, onProgress) => {
|
|
|
401
460
|
imports: [],
|
|
402
461
|
calls: [],
|
|
403
462
|
heritage: [],
|
|
463
|
+
routes: [],
|
|
404
464
|
fileCount: 0,
|
|
405
465
|
};
|
|
406
466
|
// Group by language to minimize setLanguage calls
|
|
@@ -448,13 +508,23 @@ const processBatch = (files, onProgress) => {
|
|
|
448
508
|
}
|
|
449
509
|
// Process regular files for this language
|
|
450
510
|
if (regularFiles.length > 0) {
|
|
451
|
-
|
|
452
|
-
|
|
511
|
+
try {
|
|
512
|
+
setLanguage(language, regularFiles[0].path);
|
|
513
|
+
processFileGroup(regularFiles, language, queryString, result, onFileProcessed);
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
// parser unavailable — skip this language group
|
|
517
|
+
}
|
|
453
518
|
}
|
|
454
519
|
// Process tsx files separately (different grammar)
|
|
455
520
|
if (tsxFiles.length > 0) {
|
|
456
|
-
|
|
457
|
-
|
|
521
|
+
try {
|
|
522
|
+
setLanguage(language, tsxFiles[0].path);
|
|
523
|
+
processFileGroup(tsxFiles, language, queryString, result, onFileProcessed);
|
|
524
|
+
}
|
|
525
|
+
catch {
|
|
526
|
+
// parser unavailable — skip this language group
|
|
527
|
+
}
|
|
458
528
|
}
|
|
459
529
|
}
|
|
460
530
|
return result;
|
|
@@ -563,6 +633,353 @@ function extractEloquentRelationDescription(methodNode) {
|
|
|
563
633
|
return relType;
|
|
564
634
|
return null;
|
|
565
635
|
}
|
|
636
|
+
const ROUTE_HTTP_METHODS = new Set([
|
|
637
|
+
'get', 'post', 'put', 'patch', 'delete', 'options', 'any', 'match',
|
|
638
|
+
]);
|
|
639
|
+
const ROUTE_RESOURCE_METHODS = new Set(['resource', 'apiResource']);
|
|
640
|
+
const RESOURCE_ACTIONS = ['index', 'create', 'store', 'show', 'edit', 'update', 'destroy'];
|
|
641
|
+
const API_RESOURCE_ACTIONS = ['index', 'store', 'show', 'update', 'destroy'];
|
|
642
|
+
/** Check if node is a scoped_call_expression with object 'Route' */
|
|
643
|
+
function isRouteStaticCall(node) {
|
|
644
|
+
if (node.type !== 'scoped_call_expression')
|
|
645
|
+
return false;
|
|
646
|
+
const obj = node.childForFieldName?.('object') ?? node.children?.[0];
|
|
647
|
+
return obj?.text === 'Route';
|
|
648
|
+
}
|
|
649
|
+
/** Get the method name from a scoped_call_expression or member_call_expression */
|
|
650
|
+
function getCallMethodName(node) {
|
|
651
|
+
const nameNode = node.childForFieldName?.('name') ??
|
|
652
|
+
node.children?.find((c) => c.type === 'name');
|
|
653
|
+
return nameNode?.text ?? null;
|
|
654
|
+
}
|
|
655
|
+
/** Get the arguments node from a call expression */
|
|
656
|
+
function getArguments(node) {
|
|
657
|
+
return node.children?.find((c) => c.type === 'arguments') ?? null;
|
|
658
|
+
}
|
|
659
|
+
/** Find the closure body inside arguments */
|
|
660
|
+
function findClosureBody(argsNode) {
|
|
661
|
+
if (!argsNode)
|
|
662
|
+
return null;
|
|
663
|
+
for (const child of argsNode.children ?? []) {
|
|
664
|
+
if (child.type === 'argument') {
|
|
665
|
+
for (const inner of child.children ?? []) {
|
|
666
|
+
if (inner.type === 'anonymous_function' ||
|
|
667
|
+
inner.type === 'arrow_function') {
|
|
668
|
+
return inner.childForFieldName?.('body') ??
|
|
669
|
+
inner.children?.find((c) => c.type === 'compound_statement');
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
if (child.type === 'anonymous_function' ||
|
|
674
|
+
child.type === 'arrow_function') {
|
|
675
|
+
return child.childForFieldName?.('body') ??
|
|
676
|
+
child.children?.find((c) => c.type === 'compound_statement');
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
return null;
|
|
680
|
+
}
|
|
681
|
+
/** Extract first string argument from arguments node */
|
|
682
|
+
function extractFirstStringArg(argsNode) {
|
|
683
|
+
if (!argsNode)
|
|
684
|
+
return null;
|
|
685
|
+
for (const child of argsNode.children ?? []) {
|
|
686
|
+
const target = child.type === 'argument' ? child.children?.[0] : child;
|
|
687
|
+
if (!target)
|
|
688
|
+
continue;
|
|
689
|
+
if (target.type === 'string' || target.type === 'encapsed_string') {
|
|
690
|
+
return extractStringContent(target);
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return null;
|
|
694
|
+
}
|
|
695
|
+
/** Extract middleware from arguments — handles string or array */
|
|
696
|
+
function extractMiddlewareArg(argsNode) {
|
|
697
|
+
if (!argsNode)
|
|
698
|
+
return [];
|
|
699
|
+
for (const child of argsNode.children ?? []) {
|
|
700
|
+
const target = child.type === 'argument' ? child.children?.[0] : child;
|
|
701
|
+
if (!target)
|
|
702
|
+
continue;
|
|
703
|
+
if (target.type === 'string' || target.type === 'encapsed_string') {
|
|
704
|
+
const val = extractStringContent(target);
|
|
705
|
+
return val ? [val] : [];
|
|
706
|
+
}
|
|
707
|
+
if (target.type === 'array_creation_expression') {
|
|
708
|
+
const items = [];
|
|
709
|
+
for (const el of target.children ?? []) {
|
|
710
|
+
if (el.type === 'array_element_initializer') {
|
|
711
|
+
const str = el.children?.find((c) => c.type === 'string' || c.type === 'encapsed_string');
|
|
712
|
+
const val = str ? extractStringContent(str) : null;
|
|
713
|
+
if (val)
|
|
714
|
+
items.push(val);
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
return items;
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
return [];
|
|
721
|
+
}
|
|
722
|
+
/** Extract Controller::class from arguments */
|
|
723
|
+
function extractClassArg(argsNode) {
|
|
724
|
+
if (!argsNode)
|
|
725
|
+
return null;
|
|
726
|
+
for (const child of argsNode.children ?? []) {
|
|
727
|
+
const target = child.type === 'argument' ? child.children?.[0] : child;
|
|
728
|
+
if (target?.type === 'class_constant_access_expression') {
|
|
729
|
+
return target.children?.find((c) => c.type === 'name')?.text ?? null;
|
|
730
|
+
}
|
|
731
|
+
}
|
|
732
|
+
return null;
|
|
733
|
+
}
|
|
734
|
+
/** Extract controller class name from arguments: [Controller::class, 'method'] or 'Controller@method' */
|
|
735
|
+
function extractControllerTarget(argsNode) {
|
|
736
|
+
if (!argsNode)
|
|
737
|
+
return { controller: null, method: null };
|
|
738
|
+
const args = [];
|
|
739
|
+
for (const child of argsNode.children ?? []) {
|
|
740
|
+
if (child.type === 'argument')
|
|
741
|
+
args.push(child.children?.[0]);
|
|
742
|
+
else if (child.type !== '(' && child.type !== ')' && child.type !== ',')
|
|
743
|
+
args.push(child);
|
|
744
|
+
}
|
|
745
|
+
// Second arg is the handler
|
|
746
|
+
const handlerNode = args[1];
|
|
747
|
+
if (!handlerNode)
|
|
748
|
+
return { controller: null, method: null };
|
|
749
|
+
// Array syntax: [UserController::class, 'index']
|
|
750
|
+
if (handlerNode.type === 'array_creation_expression') {
|
|
751
|
+
let controller = null;
|
|
752
|
+
let method = null;
|
|
753
|
+
const elements = [];
|
|
754
|
+
for (const el of handlerNode.children ?? []) {
|
|
755
|
+
if (el.type === 'array_element_initializer')
|
|
756
|
+
elements.push(el);
|
|
757
|
+
}
|
|
758
|
+
if (elements[0]) {
|
|
759
|
+
const classAccess = findDescendant(elements[0], 'class_constant_access_expression');
|
|
760
|
+
if (classAccess) {
|
|
761
|
+
controller = classAccess.children?.find((c) => c.type === 'name')?.text ?? null;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
if (elements[1]) {
|
|
765
|
+
const str = findDescendant(elements[1], 'string');
|
|
766
|
+
method = str ? extractStringContent(str) : null;
|
|
767
|
+
}
|
|
768
|
+
return { controller, method };
|
|
769
|
+
}
|
|
770
|
+
// String syntax: 'UserController@index'
|
|
771
|
+
if (handlerNode.type === 'string' || handlerNode.type === 'encapsed_string') {
|
|
772
|
+
const text = extractStringContent(handlerNode);
|
|
773
|
+
if (text?.includes('@')) {
|
|
774
|
+
const [controller, method] = text.split('@');
|
|
775
|
+
return { controller, method };
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// Class reference: UserController::class (invokable controller)
|
|
779
|
+
if (handlerNode.type === 'class_constant_access_expression') {
|
|
780
|
+
const controller = handlerNode.children?.find((c) => c.type === 'name')?.text ?? null;
|
|
781
|
+
return { controller, method: '__invoke' };
|
|
782
|
+
}
|
|
783
|
+
return { controller: null, method: null };
|
|
784
|
+
}
|
|
785
|
+
/**
|
|
786
|
+
* Unwrap a chained call like Route::middleware('auth')->prefix('api')->group(fn)
|
|
787
|
+
*/
|
|
788
|
+
function unwrapRouteChain(node) {
|
|
789
|
+
if (node.type !== 'member_call_expression')
|
|
790
|
+
return null;
|
|
791
|
+
const terminalMethod = getCallMethodName(node);
|
|
792
|
+
if (!terminalMethod)
|
|
793
|
+
return null;
|
|
794
|
+
const terminalArgs = getArguments(node);
|
|
795
|
+
const attributes = [];
|
|
796
|
+
let current = node.children?.[0];
|
|
797
|
+
while (current) {
|
|
798
|
+
if (current.type === 'member_call_expression') {
|
|
799
|
+
const method = getCallMethodName(current);
|
|
800
|
+
const args = getArguments(current);
|
|
801
|
+
if (method)
|
|
802
|
+
attributes.unshift({ method, argsNode: args });
|
|
803
|
+
current = current.children?.[0];
|
|
804
|
+
}
|
|
805
|
+
else if (current.type === 'scoped_call_expression') {
|
|
806
|
+
const obj = current.childForFieldName?.('object') ?? current.children?.[0];
|
|
807
|
+
if (obj?.text !== 'Route')
|
|
808
|
+
return null;
|
|
809
|
+
const method = getCallMethodName(current);
|
|
810
|
+
const args = getArguments(current);
|
|
811
|
+
if (method)
|
|
812
|
+
attributes.unshift({ method, argsNode: args });
|
|
813
|
+
return { isRouteFacade: true, terminalMethod, attributes, terminalArgs, node };
|
|
814
|
+
}
|
|
815
|
+
else {
|
|
816
|
+
break;
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
return null;
|
|
820
|
+
}
|
|
821
|
+
/** Parse Route::group(['middleware' => ..., 'prefix' => ...], fn) array syntax */
|
|
822
|
+
function parseArrayGroupArgs(argsNode) {
|
|
823
|
+
const ctx = { middleware: [], prefix: null, controller: null };
|
|
824
|
+
if (!argsNode)
|
|
825
|
+
return ctx;
|
|
826
|
+
for (const child of argsNode.children ?? []) {
|
|
827
|
+
const target = child.type === 'argument' ? child.children?.[0] : child;
|
|
828
|
+
if (target?.type === 'array_creation_expression') {
|
|
829
|
+
for (const el of target.children ?? []) {
|
|
830
|
+
if (el.type !== 'array_element_initializer')
|
|
831
|
+
continue;
|
|
832
|
+
const children = el.children ?? [];
|
|
833
|
+
const arrowIdx = children.findIndex((c) => c.type === '=>');
|
|
834
|
+
if (arrowIdx === -1)
|
|
835
|
+
continue;
|
|
836
|
+
const key = extractStringContent(children[arrowIdx - 1]);
|
|
837
|
+
const val = children[arrowIdx + 1];
|
|
838
|
+
if (key === 'middleware') {
|
|
839
|
+
if (val?.type === 'string') {
|
|
840
|
+
const s = extractStringContent(val);
|
|
841
|
+
if (s)
|
|
842
|
+
ctx.middleware.push(s);
|
|
843
|
+
}
|
|
844
|
+
else if (val?.type === 'array_creation_expression') {
|
|
845
|
+
for (const item of val.children ?? []) {
|
|
846
|
+
if (item.type === 'array_element_initializer') {
|
|
847
|
+
const str = item.children?.find((c) => c.type === 'string');
|
|
848
|
+
const s = str ? extractStringContent(str) : null;
|
|
849
|
+
if (s)
|
|
850
|
+
ctx.middleware.push(s);
|
|
851
|
+
}
|
|
852
|
+
}
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
else if (key === 'prefix') {
|
|
856
|
+
ctx.prefix = extractStringContent(val) ?? null;
|
|
857
|
+
}
|
|
858
|
+
else if (key === 'controller') {
|
|
859
|
+
if (val?.type === 'class_constant_access_expression') {
|
|
860
|
+
ctx.controller = val.children?.find((c) => c.type === 'name')?.text ?? null;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
}
|
|
866
|
+
return ctx;
|
|
867
|
+
}
|
|
868
|
+
function extractLaravelRoutes(tree, filePath) {
|
|
869
|
+
const routes = [];
|
|
870
|
+
function resolveStack(stack) {
|
|
871
|
+
const middleware = [];
|
|
872
|
+
let prefix = null;
|
|
873
|
+
let controller = null;
|
|
874
|
+
for (const ctx of stack) {
|
|
875
|
+
middleware.push(...ctx.middleware);
|
|
876
|
+
if (ctx.prefix)
|
|
877
|
+
prefix = prefix ? `${prefix}/${ctx.prefix}`.replace(/\/+/g, '/') : ctx.prefix;
|
|
878
|
+
if (ctx.controller)
|
|
879
|
+
controller = ctx.controller;
|
|
880
|
+
}
|
|
881
|
+
return { middleware, prefix, controller };
|
|
882
|
+
}
|
|
883
|
+
function emitRoute(httpMethod, argsNode, lineNumber, groupStack, chainAttrs) {
|
|
884
|
+
const effective = resolveStack(groupStack);
|
|
885
|
+
for (const attr of chainAttrs) {
|
|
886
|
+
if (attr.method === 'middleware')
|
|
887
|
+
effective.middleware.push(...extractMiddlewareArg(attr.argsNode));
|
|
888
|
+
if (attr.method === 'prefix') {
|
|
889
|
+
const p = extractFirstStringArg(attr.argsNode);
|
|
890
|
+
if (p)
|
|
891
|
+
effective.prefix = effective.prefix ? `${effective.prefix}/${p}` : p;
|
|
892
|
+
}
|
|
893
|
+
if (attr.method === 'controller') {
|
|
894
|
+
const cls = extractClassArg(attr.argsNode);
|
|
895
|
+
if (cls)
|
|
896
|
+
effective.controller = cls;
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
const routePath = extractFirstStringArg(argsNode);
|
|
900
|
+
if (ROUTE_RESOURCE_METHODS.has(httpMethod)) {
|
|
901
|
+
const target = extractControllerTarget(argsNode);
|
|
902
|
+
const actions = httpMethod === 'apiResource' ? API_RESOURCE_ACTIONS : RESOURCE_ACTIONS;
|
|
903
|
+
for (const action of actions) {
|
|
904
|
+
routes.push({
|
|
905
|
+
filePath, httpMethod, routePath,
|
|
906
|
+
controllerName: target.controller ?? effective.controller,
|
|
907
|
+
methodName: action,
|
|
908
|
+
middleware: [...effective.middleware],
|
|
909
|
+
prefix: effective.prefix,
|
|
910
|
+
lineNumber,
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
else {
|
|
915
|
+
const target = extractControllerTarget(argsNode);
|
|
916
|
+
routes.push({
|
|
917
|
+
filePath, httpMethod, routePath,
|
|
918
|
+
controllerName: target.controller ?? effective.controller,
|
|
919
|
+
methodName: target.method,
|
|
920
|
+
middleware: [...effective.middleware],
|
|
921
|
+
prefix: effective.prefix,
|
|
922
|
+
lineNumber,
|
|
923
|
+
});
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
function walk(node, groupStack) {
|
|
927
|
+
// Case 1: Simple Route::get(...), Route::post(...), etc.
|
|
928
|
+
if (isRouteStaticCall(node)) {
|
|
929
|
+
const method = getCallMethodName(node);
|
|
930
|
+
if (method && (ROUTE_HTTP_METHODS.has(method) || ROUTE_RESOURCE_METHODS.has(method))) {
|
|
931
|
+
emitRoute(method, getArguments(node), node.startPosition.row, groupStack, []);
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
if (method === 'group') {
|
|
935
|
+
const argsNode = getArguments(node);
|
|
936
|
+
const groupCtx = parseArrayGroupArgs(argsNode);
|
|
937
|
+
const body = findClosureBody(argsNode);
|
|
938
|
+
if (body) {
|
|
939
|
+
groupStack.push(groupCtx);
|
|
940
|
+
walkChildren(body, groupStack);
|
|
941
|
+
groupStack.pop();
|
|
942
|
+
}
|
|
943
|
+
return;
|
|
944
|
+
}
|
|
945
|
+
}
|
|
946
|
+
// Case 2: Fluent chain — Route::middleware(...)->group(...) or Route::middleware(...)->get(...)
|
|
947
|
+
const chain = unwrapRouteChain(node);
|
|
948
|
+
if (chain) {
|
|
949
|
+
if (chain.terminalMethod === 'group') {
|
|
950
|
+
const groupCtx = { middleware: [], prefix: null, controller: null };
|
|
951
|
+
for (const attr of chain.attributes) {
|
|
952
|
+
if (attr.method === 'middleware')
|
|
953
|
+
groupCtx.middleware.push(...extractMiddlewareArg(attr.argsNode));
|
|
954
|
+
if (attr.method === 'prefix')
|
|
955
|
+
groupCtx.prefix = extractFirstStringArg(attr.argsNode);
|
|
956
|
+
if (attr.method === 'controller')
|
|
957
|
+
groupCtx.controller = extractClassArg(attr.argsNode);
|
|
958
|
+
}
|
|
959
|
+
const body = findClosureBody(chain.terminalArgs);
|
|
960
|
+
if (body) {
|
|
961
|
+
groupStack.push(groupCtx);
|
|
962
|
+
walkChildren(body, groupStack);
|
|
963
|
+
groupStack.pop();
|
|
964
|
+
}
|
|
965
|
+
return;
|
|
966
|
+
}
|
|
967
|
+
if (ROUTE_HTTP_METHODS.has(chain.terminalMethod) || ROUTE_RESOURCE_METHODS.has(chain.terminalMethod)) {
|
|
968
|
+
emitRoute(chain.terminalMethod, chain.terminalArgs, node.startPosition.row, groupStack, chain.attributes);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
// Default: recurse into children
|
|
973
|
+
walkChildren(node, groupStack);
|
|
974
|
+
}
|
|
975
|
+
function walkChildren(node, groupStack) {
|
|
976
|
+
for (const child of node.children ?? []) {
|
|
977
|
+
walk(child, groupStack);
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
walk(tree.rootNode, []);
|
|
981
|
+
return routes;
|
|
982
|
+
}
|
|
566
983
|
const processFileGroup = (files, language, queryString, result, onFileProcessed) => {
|
|
567
984
|
let query;
|
|
568
985
|
try {
|
|
@@ -599,7 +1016,9 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
599
1016
|
}
|
|
600
1017
|
// Extract import paths before skipping
|
|
601
1018
|
if (captureMap['import'] && captureMap['import.source']) {
|
|
602
|
-
const rawImportPath =
|
|
1019
|
+
const rawImportPath = language === SupportedLanguages.Kotlin
|
|
1020
|
+
? appendKotlinWildcard(captureMap['import.source'].text.replace(/['"<>]/g, ''), captureMap['import'])
|
|
1021
|
+
: captureMap['import.source'].text.replace(/['"<>]/g, '');
|
|
603
1022
|
result.imports.push({
|
|
604
1023
|
filePath: file.path,
|
|
605
1024
|
rawImportPath,
|
|
@@ -655,8 +1074,13 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
655
1074
|
if (!nodeLabel)
|
|
656
1075
|
continue;
|
|
657
1076
|
const nameNode = captureMap['name'];
|
|
658
|
-
|
|
659
|
-
|
|
1077
|
+
// Synthesize name for constructors without explicit @name capture (e.g. Swift init)
|
|
1078
|
+
if (!nameNode && nodeLabel !== 'Constructor')
|
|
1079
|
+
continue;
|
|
1080
|
+
const nodeName = nameNode ? nameNode.text : 'init';
|
|
1081
|
+
const definitionNode = getDefinitionNodeFromCaptures(captureMap);
|
|
1082
|
+
const startLine = definitionNode ? definitionNode.startPosition.row : (nameNode ? nameNode.startPosition.row : 0);
|
|
1083
|
+
const nodeId = generateId(nodeLabel, `${file.path}:${nodeName}:${startLine}`);
|
|
660
1084
|
let description;
|
|
661
1085
|
if (language === SupportedLanguages.PHP) {
|
|
662
1086
|
if (nodeLabel === 'Property' && captureMap['definition.property']) {
|
|
@@ -666,9 +1090,8 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
666
1090
|
description = extractEloquentRelationDescription(captureMap['definition.method']) ?? undefined;
|
|
667
1091
|
}
|
|
668
1092
|
}
|
|
669
|
-
const definitionNode = getDefinitionNodeFromCaptures(captureMap);
|
|
670
1093
|
const frameworkHint = definitionNode
|
|
671
|
-
? detectFrameworkFromAST(language, definitionNode.text || '')
|
|
1094
|
+
? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
|
|
672
1095
|
: null;
|
|
673
1096
|
result.nodes.push({
|
|
674
1097
|
id: nodeId,
|
|
@@ -676,10 +1099,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
676
1099
|
properties: {
|
|
677
1100
|
name: nodeName,
|
|
678
1101
|
filePath: file.path,
|
|
679
|
-
startLine:
|
|
680
|
-
endLine:
|
|
1102
|
+
startLine: definitionNode ? definitionNode.startPosition.row : startLine,
|
|
1103
|
+
endLine: definitionNode ? definitionNode.endPosition.row : startLine,
|
|
681
1104
|
language: language,
|
|
682
|
-
isExported: isNodeExported(nameNode, nodeName, language),
|
|
1105
|
+
isExported: isNodeExported(nameNode || definitionNode, nodeName, language),
|
|
683
1106
|
...(frameworkHint ? {
|
|
684
1107
|
astFrameworkMultiplier: frameworkHint.entryPointMultiplier,
|
|
685
1108
|
astFrameworkReason: frameworkHint.reason,
|
|
@@ -704,6 +1127,11 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
704
1127
|
reason: '',
|
|
705
1128
|
});
|
|
706
1129
|
}
|
|
1130
|
+
// Extract Laravel routes from route files via procedural AST walk
|
|
1131
|
+
if (language === SupportedLanguages.PHP && (file.path.includes('/routes/') || file.path.startsWith('routes/')) && file.path.endsWith('.php')) {
|
|
1132
|
+
const extractedRoutes = extractLaravelRoutes(tree, file.path);
|
|
1133
|
+
result.routes.push(...extractedRoutes);
|
|
1134
|
+
}
|
|
707
1135
|
}
|
|
708
1136
|
};
|
|
709
1137
|
// ============================================================================
|
|
@@ -712,7 +1140,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
712
1140
|
/** Accumulated result across sub-batches */
|
|
713
1141
|
let accumulated = {
|
|
714
1142
|
nodes: [], relationships: [], symbols: [],
|
|
715
|
-
imports: [], calls: [], heritage: [], fileCount: 0,
|
|
1143
|
+
imports: [], calls: [], heritage: [], routes: [], fileCount: 0,
|
|
716
1144
|
};
|
|
717
1145
|
let cumulativeProcessed = 0;
|
|
718
1146
|
const mergeResult = (target, src) => {
|
|
@@ -722,6 +1150,7 @@ const mergeResult = (target, src) => {
|
|
|
722
1150
|
target.imports.push(...src.imports);
|
|
723
1151
|
target.calls.push(...src.calls);
|
|
724
1152
|
target.heritage.push(...src.heritage);
|
|
1153
|
+
target.routes.push(...src.routes);
|
|
725
1154
|
target.fileCount += src.fileCount;
|
|
726
1155
|
};
|
|
727
1156
|
parentPort.on('message', (msg) => {
|
|
@@ -741,7 +1170,7 @@ parentPort.on('message', (msg) => {
|
|
|
741
1170
|
if (msg && msg.type === 'flush') {
|
|
742
1171
|
parentPort.postMessage({ type: 'result', data: accumulated });
|
|
743
1172
|
// Reset for potential reuse
|
|
744
|
-
accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], heritage: [], fileCount: 0 };
|
|
1173
|
+
accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], heritage: [], routes: [], fileCount: 0 };
|
|
745
1174
|
cumulativeProcessed = 0;
|
|
746
1175
|
return;
|
|
747
1176
|
}
|
|
@@ -13,6 +13,10 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { KnowledgeGraph } from '../graph/types.js';
|
|
15
15
|
import { NodeTableName } from './schema.js';
|
|
16
|
+
export declare const sanitizeUTF8: (str: string) => string;
|
|
17
|
+
export declare const escapeCSVField: (value: string | number | undefined | null) => string;
|
|
18
|
+
export declare const escapeCSVNumber: (value: number | undefined | null, defaultValue?: number) => string;
|
|
19
|
+
export declare const isBinaryContent: (content: string) => boolean;
|
|
16
20
|
export interface StreamedCSVResult {
|
|
17
21
|
nodeFiles: Map<NodeTableName, {
|
|
18
22
|
csvPath: string;
|