gitnexus 1.3.6 → 1.3.8
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 +4 -11
- package/dist/cli/eval-server.d.ts +7 -0
- package/dist/cli/eval-server.js +16 -7
- package/dist/cli/index.js +2 -20
- 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 +473 -51
- 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 +3 -0
- 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 +11 -3
- 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,9 +9,11 @@ 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
14
|
import { createRequire } from 'node:module';
|
|
14
15
|
import { SupportedLanguages } from '../../../config/supported-languages.js';
|
|
16
|
+
import { LANGUAGE_QUERIES } from '../tree-sitter-queries.js';
|
|
15
17
|
// tree-sitter-swift is an optionalDependency — may not be installed
|
|
16
18
|
const _require = createRequire(import.meta.url);
|
|
17
19
|
let Swift = null;
|
|
@@ -19,8 +21,7 @@ try {
|
|
|
19
21
|
Swift = _require('tree-sitter-swift');
|
|
20
22
|
}
|
|
21
23
|
catch { }
|
|
22
|
-
import {
|
|
23
|
-
import { getLanguageFromFilename } from '../utils.js';
|
|
24
|
+
import { findSiblingChild, getLanguageFromFilename } from '../utils.js';
|
|
24
25
|
import { detectFrameworkFromAST } from '../framework-detection.js';
|
|
25
26
|
import { generateId } from '../../../lib/utils.js';
|
|
26
27
|
// ============================================================================
|
|
@@ -38,6 +39,7 @@ const languageMap = {
|
|
|
38
39
|
[SupportedLanguages.CSharp]: CSharp,
|
|
39
40
|
[SupportedLanguages.Go]: Go,
|
|
40
41
|
[SupportedLanguages.Rust]: Rust,
|
|
42
|
+
[SupportedLanguages.Kotlin]: Kotlin,
|
|
41
43
|
[SupportedLanguages.PHP]: PHP.php_only,
|
|
42
44
|
...(Swift ? { [SupportedLanguages.Swift]: Swift } : {}),
|
|
43
45
|
};
|
|
@@ -115,18 +117,26 @@ const isNodeExported = (node, name, language) => {
|
|
|
115
117
|
current = current.parent;
|
|
116
118
|
}
|
|
117
119
|
return false;
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
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':
|
|
122
123
|
while (current) {
|
|
123
|
-
if (current.
|
|
124
|
-
const
|
|
125
|
-
if (
|
|
126
|
-
|
|
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
|
+
}
|
|
127
133
|
}
|
|
128
134
|
current = current.parent;
|
|
129
135
|
}
|
|
136
|
+
// No visibility modifier = public (Kotlin default)
|
|
137
|
+
return true;
|
|
138
|
+
case 'c':
|
|
139
|
+
case 'cpp':
|
|
130
140
|
return false;
|
|
131
141
|
case 'php':
|
|
132
142
|
// Top-level classes/interfaces/traits are always accessible
|
|
@@ -145,6 +155,16 @@ const isNodeExported = (node, name, language) => {
|
|
|
145
155
|
}
|
|
146
156
|
// Top-level functions (no parent class) are globally accessible
|
|
147
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;
|
|
148
168
|
default:
|
|
149
169
|
return false;
|
|
150
170
|
}
|
|
@@ -158,8 +178,12 @@ const FUNCTION_NODE_TYPES = new Set([
|
|
|
158
178
|
'function_definition', 'async_function_declaration', 'async_arrow_function',
|
|
159
179
|
'method_declaration', 'constructor_declaration',
|
|
160
180
|
'local_function_statement', 'function_item', 'impl_item',
|
|
161
|
-
|
|
162
|
-
'
|
|
181
|
+
// Kotlin
|
|
182
|
+
'lambda_literal',
|
|
183
|
+
// PHP
|
|
184
|
+
'anonymous_function',
|
|
185
|
+
// Swift initializers/deinitializers
|
|
186
|
+
'init_declaration', 'deinit_declaration',
|
|
163
187
|
]);
|
|
164
188
|
/** Walk up AST to find enclosing function, return its generateId or null for top-level */
|
|
165
189
|
const findEnclosingFunctionId = (node, filePath) => {
|
|
@@ -241,6 +265,22 @@ const BUILT_INS = new Set([
|
|
|
241
265
|
'open', 'read', 'write', 'close', 'append', 'extend', 'update',
|
|
242
266
|
'super', 'type', 'isinstance', 'issubclass', 'getattr', 'setattr', 'hasattr',
|
|
243
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',
|
|
244
284
|
// C/C++ standard library
|
|
245
285
|
'printf', 'fprintf', 'sprintf', 'snprintf', 'vprintf', 'vfprintf', 'vsprintf', 'vsnprintf',
|
|
246
286
|
'scanf', 'fscanf', 'sscanf',
|
|
@@ -366,37 +406,49 @@ const getLabelFromCaptures = (captureMap) => {
|
|
|
366
406
|
return 'Template';
|
|
367
407
|
return 'CodeElement';
|
|
368
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
|
+
];
|
|
369
433
|
const getDefinitionNodeFromCaptures = (captureMap) => {
|
|
370
|
-
const
|
|
371
|
-
'definition.function',
|
|
372
|
-
'definition.class',
|
|
373
|
-
'definition.interface',
|
|
374
|
-
'definition.method',
|
|
375
|
-
'definition.struct',
|
|
376
|
-
'definition.enum',
|
|
377
|
-
'definition.namespace',
|
|
378
|
-
'definition.module',
|
|
379
|
-
'definition.trait',
|
|
380
|
-
'definition.impl',
|
|
381
|
-
'definition.type',
|
|
382
|
-
'definition.const',
|
|
383
|
-
'definition.static',
|
|
384
|
-
'definition.typedef',
|
|
385
|
-
'definition.macro',
|
|
386
|
-
'definition.union',
|
|
387
|
-
'definition.property',
|
|
388
|
-
'definition.record',
|
|
389
|
-
'definition.delegate',
|
|
390
|
-
'definition.annotation',
|
|
391
|
-
'definition.constructor',
|
|
392
|
-
'definition.template',
|
|
393
|
-
];
|
|
394
|
-
for (const key of definitionKeys) {
|
|
434
|
+
for (const key of DEFINITION_CAPTURE_KEYS) {
|
|
395
435
|
if (captureMap[key])
|
|
396
436
|
return captureMap[key];
|
|
397
437
|
}
|
|
398
438
|
return null;
|
|
399
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
|
+
};
|
|
400
452
|
// ============================================================================
|
|
401
453
|
// Process a batch of files
|
|
402
454
|
// ============================================================================
|
|
@@ -408,6 +460,7 @@ const processBatch = (files, onProgress) => {
|
|
|
408
460
|
imports: [],
|
|
409
461
|
calls: [],
|
|
410
462
|
heritage: [],
|
|
463
|
+
routes: [],
|
|
411
464
|
fileCount: 0,
|
|
412
465
|
};
|
|
413
466
|
// Group by language to minimize setLanguage calls
|
|
@@ -455,13 +508,23 @@ const processBatch = (files, onProgress) => {
|
|
|
455
508
|
}
|
|
456
509
|
// Process regular files for this language
|
|
457
510
|
if (regularFiles.length > 0) {
|
|
458
|
-
|
|
459
|
-
|
|
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
|
+
}
|
|
460
518
|
}
|
|
461
519
|
// Process tsx files separately (different grammar)
|
|
462
520
|
if (tsxFiles.length > 0) {
|
|
463
|
-
|
|
464
|
-
|
|
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
|
+
}
|
|
465
528
|
}
|
|
466
529
|
}
|
|
467
530
|
return result;
|
|
@@ -570,6 +633,353 @@ function extractEloquentRelationDescription(methodNode) {
|
|
|
570
633
|
return relType;
|
|
571
634
|
return null;
|
|
572
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
|
+
}
|
|
573
983
|
const processFileGroup = (files, language, queryString, result, onFileProcessed) => {
|
|
574
984
|
let query;
|
|
575
985
|
try {
|
|
@@ -606,7 +1016,9 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
606
1016
|
}
|
|
607
1017
|
// Extract import paths before skipping
|
|
608
1018
|
if (captureMap['import'] && captureMap['import.source']) {
|
|
609
|
-
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, '');
|
|
610
1022
|
result.imports.push({
|
|
611
1023
|
filePath: file.path,
|
|
612
1024
|
rawImportPath,
|
|
@@ -662,8 +1074,13 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
662
1074
|
if (!nodeLabel)
|
|
663
1075
|
continue;
|
|
664
1076
|
const nameNode = captureMap['name'];
|
|
665
|
-
|
|
666
|
-
|
|
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}`);
|
|
667
1084
|
let description;
|
|
668
1085
|
if (language === SupportedLanguages.PHP) {
|
|
669
1086
|
if (nodeLabel === 'Property' && captureMap['definition.property']) {
|
|
@@ -673,9 +1090,8 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
673
1090
|
description = extractEloquentRelationDescription(captureMap['definition.method']) ?? undefined;
|
|
674
1091
|
}
|
|
675
1092
|
}
|
|
676
|
-
const definitionNode = getDefinitionNodeFromCaptures(captureMap);
|
|
677
1093
|
const frameworkHint = definitionNode
|
|
678
|
-
? detectFrameworkFromAST(language, definitionNode.text || '')
|
|
1094
|
+
? detectFrameworkFromAST(language, (definitionNode.text || '').slice(0, 300))
|
|
679
1095
|
: null;
|
|
680
1096
|
result.nodes.push({
|
|
681
1097
|
id: nodeId,
|
|
@@ -683,10 +1099,10 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
683
1099
|
properties: {
|
|
684
1100
|
name: nodeName,
|
|
685
1101
|
filePath: file.path,
|
|
686
|
-
startLine:
|
|
687
|
-
endLine:
|
|
1102
|
+
startLine: definitionNode ? definitionNode.startPosition.row : startLine,
|
|
1103
|
+
endLine: definitionNode ? definitionNode.endPosition.row : startLine,
|
|
688
1104
|
language: language,
|
|
689
|
-
isExported: isNodeExported(nameNode, nodeName, language),
|
|
1105
|
+
isExported: isNodeExported(nameNode || definitionNode, nodeName, language),
|
|
690
1106
|
...(frameworkHint ? {
|
|
691
1107
|
astFrameworkMultiplier: frameworkHint.entryPointMultiplier,
|
|
692
1108
|
astFrameworkReason: frameworkHint.reason,
|
|
@@ -711,6 +1127,11 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
711
1127
|
reason: '',
|
|
712
1128
|
});
|
|
713
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
|
+
}
|
|
714
1135
|
}
|
|
715
1136
|
};
|
|
716
1137
|
// ============================================================================
|
|
@@ -719,7 +1140,7 @@ const processFileGroup = (files, language, queryString, result, onFileProcessed)
|
|
|
719
1140
|
/** Accumulated result across sub-batches */
|
|
720
1141
|
let accumulated = {
|
|
721
1142
|
nodes: [], relationships: [], symbols: [],
|
|
722
|
-
imports: [], calls: [], heritage: [], fileCount: 0,
|
|
1143
|
+
imports: [], calls: [], heritage: [], routes: [], fileCount: 0,
|
|
723
1144
|
};
|
|
724
1145
|
let cumulativeProcessed = 0;
|
|
725
1146
|
const mergeResult = (target, src) => {
|
|
@@ -729,6 +1150,7 @@ const mergeResult = (target, src) => {
|
|
|
729
1150
|
target.imports.push(...src.imports);
|
|
730
1151
|
target.calls.push(...src.calls);
|
|
731
1152
|
target.heritage.push(...src.heritage);
|
|
1153
|
+
target.routes.push(...src.routes);
|
|
732
1154
|
target.fileCount += src.fileCount;
|
|
733
1155
|
};
|
|
734
1156
|
parentPort.on('message', (msg) => {
|
|
@@ -748,7 +1170,7 @@ parentPort.on('message', (msg) => {
|
|
|
748
1170
|
if (msg && msg.type === 'flush') {
|
|
749
1171
|
parentPort.postMessage({ type: 'result', data: accumulated });
|
|
750
1172
|
// Reset for potential reuse
|
|
751
|
-
accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], heritage: [], fileCount: 0 };
|
|
1173
|
+
accumulated = { nodes: [], relationships: [], symbols: [], imports: [], calls: [], heritage: [], routes: [], fileCount: 0 };
|
|
752
1174
|
cumulativeProcessed = 0;
|
|
753
1175
|
return;
|
|
754
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;
|