codesearch-mcp 0.2.0-alpha → 0.2.1-alpha
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/CHANGELOG.md +16 -0
- package/README.md +5 -2
- package/dist/app.js +2 -17
- package/dist/src/commands/IndexCommand.js +27 -7
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +2 -1
- package/dist/src/db/schema.d.ts +75 -0
- package/dist/src/db/schema.js +10 -1
- package/dist/src/enriched-prompt.d.ts +8 -0
- package/dist/src/enriched-prompt.js +87 -0
- package/dist/src/interfaces/IAstParser.d.ts +18 -2
- package/dist/src/interfaces/IDatabase.d.ts +14 -0
- package/dist/src/interfaces/ISearcher.d.ts +3 -0
- package/dist/src/services/AstParserService.d.ts +39 -7
- package/dist/src/services/AstParserService.js +504 -104
- package/dist/src/services/DatabaseService.d.ts +13 -0
- package/dist/src/services/DatabaseService.js +84 -2
- package/dist/src/services/IndexerService.d.ts +0 -14
- package/dist/src/services/IndexerService.js +29 -151
- package/dist/src/services/McpServer.js +3 -3
- package/dist/src/services/SearcherService.d.ts +24 -3
- package/dist/src/services/SearcherService.js +149 -58
- package/dist/src/symbol-name.d.ts +44 -0
- package/dist/src/symbol-name.js +68 -0
- package/dist/src/symbol-rows.d.ts +12 -0
- package/dist/src/symbol-rows.js +38 -0
- package/dist/src/tools/FindSymbolTool.js +30 -7
- package/dist/src/tools/GetCallersTool.js +20 -5
- package/docs/architecture.svg +103 -0
- package/package.json +1 -2
- package/dist/src/commands/BackfillCommand.d.ts +0 -7
- package/dist/src/commands/BackfillCommand.js +0 -37
|
@@ -41,6 +41,7 @@ const child_process_1 = require("child_process");
|
|
|
41
41
|
const file_utils_1 = require("../file-utils");
|
|
42
42
|
const MonorepoAwareDiscoveryService_1 = require("./MonorepoAwareDiscoveryService");
|
|
43
43
|
const legacy_ollama_index_1 = require("./legacy-ollama-index");
|
|
44
|
+
const symbol_name_1 = require("../symbol-name");
|
|
44
45
|
const SILENT_LOGGER = {
|
|
45
46
|
debug() { },
|
|
46
47
|
info() { },
|
|
@@ -472,7 +473,22 @@ class SearcherService {
|
|
|
472
473
|
async getCallers(symbol, project, calleeFile = null) {
|
|
473
474
|
const t0 = Date.now();
|
|
474
475
|
this.logger.debug('SearcherService: getCallers', { symbol, project, calleeFile });
|
|
475
|
-
const
|
|
476
|
+
const parsed = (0, symbol_name_1.parseCallerSymbol)(symbol);
|
|
477
|
+
if (!parsed)
|
|
478
|
+
throw new Error('Invalid symbol name');
|
|
479
|
+
const params = [project];
|
|
480
|
+
let nameFilter;
|
|
481
|
+
if (parsed.form === 'bare') {
|
|
482
|
+
params.push(parsed.name);
|
|
483
|
+
nameFilter = `"calleeName" = $${params.length}`;
|
|
484
|
+
}
|
|
485
|
+
else {
|
|
486
|
+
params.push(`${parsed.container}.${parsed.name}`);
|
|
487
|
+
const exactParam = params.length;
|
|
488
|
+
params.push(`%.${(0, symbol_name_1.escapeLike)(parsed.container)}.${(0, symbol_name_1.escapeLike)(parsed.name)}`);
|
|
489
|
+
const suffixParam = params.length;
|
|
490
|
+
nameFilter = `("calleeSymbol" = $${exactParam} OR "calleeSymbol" LIKE $${suffixParam} ESCAPE '\\')`;
|
|
491
|
+
}
|
|
476
492
|
let fileFilter = '';
|
|
477
493
|
if (calleeFile) {
|
|
478
494
|
params.push(calleeFile);
|
|
@@ -480,13 +496,32 @@ class SearcherService {
|
|
|
480
496
|
}
|
|
481
497
|
const rows = await this.db.query(`SELECT "fromFile", "fromSymbol", "calleeName", "calleeFile", line
|
|
482
498
|
FROM code_call
|
|
483
|
-
WHERE
|
|
499
|
+
WHERE project = $1 AND ${nameFilter}
|
|
484
500
|
${fileFilter}
|
|
485
501
|
ORDER BY "fromFile", line
|
|
486
502
|
LIMIT 200`, params);
|
|
487
503
|
this.logger.debug('SearcherService: getCallers result', { count: rows.length, ms: Date.now() - t0 });
|
|
488
504
|
return rows;
|
|
489
505
|
}
|
|
506
|
+
/**
|
|
507
|
+
* One-line warning when any project in `scope` (the given `project`, or
|
|
508
|
+
* every project with `code_chunk`/`code_symbol` rows) is outdated — shown
|
|
509
|
+
* first in `find_symbol`/`get_callers` output. Any failure to determine the
|
|
510
|
+
* scope or read markers only drops the notice (warn logged); the lookup
|
|
511
|
+
* itself is unaffected.
|
|
512
|
+
*/
|
|
513
|
+
async outdatedNotice(project) {
|
|
514
|
+
try {
|
|
515
|
+
const outdated = await this.db.listOutdatedProjects(project ? [project] : null);
|
|
516
|
+
if (outdated.length === 0)
|
|
517
|
+
return null;
|
|
518
|
+
return `⚠ Indexed before member indexing: ${[...outdated].sort().join(', ')} — run rag-index --reindex; methods and qualified names may be missing.`;
|
|
519
|
+
}
|
|
520
|
+
catch (err) {
|
|
521
|
+
this.logger.warn('SearcherService: failed to determine outdated-index scope', { reason: err.message });
|
|
522
|
+
return null;
|
|
523
|
+
}
|
|
524
|
+
}
|
|
490
525
|
async grepSymbol(symbol, project, excludeFile) {
|
|
491
526
|
const t0 = Date.now();
|
|
492
527
|
this.logger.debug('SearcherService: grepSymbol', { symbol, project, excludeFile });
|
|
@@ -499,11 +534,11 @@ class SearcherService {
|
|
|
499
534
|
symbol,
|
|
500
535
|
projectDir,
|
|
501
536
|
]);
|
|
502
|
-
if (!result) {
|
|
537
|
+
if (!result.ok || result.output === null) {
|
|
503
538
|
return { matches: [], truncated: false };
|
|
504
539
|
}
|
|
505
540
|
const excludeAbsPath = path.join(this.config.workspaceRoot, excludeFile);
|
|
506
|
-
const all = result.split('\n').flatMap(line => {
|
|
541
|
+
const all = result.output.split('\n').flatMap(line => {
|
|
507
542
|
const match = line.match(/^(.+?):(\d+):/);
|
|
508
543
|
if (!match)
|
|
509
544
|
return [];
|
|
@@ -517,19 +552,29 @@ class SearcherService {
|
|
|
517
552
|
return { matches: all.slice(0, GREP_MAX_RESULTS), truncated };
|
|
518
553
|
}
|
|
519
554
|
// Sync wrapper: validates name (throws synchronously), then returns Promise.
|
|
520
|
-
//
|
|
555
|
+
// Every kind, including 'method', is looked up in the index first.
|
|
521
556
|
findSymbol(name, project, kind) {
|
|
522
|
-
|
|
557
|
+
const parsed = (0, symbol_name_1.parseFindSymbolName)(name);
|
|
558
|
+
if (!parsed)
|
|
523
559
|
throw new Error('Invalid symbol name');
|
|
524
|
-
|
|
525
|
-
return this.grepFindSymbol(name, project, kind);
|
|
526
|
-
return this.findSymbolFromIndex(name, project, kind);
|
|
560
|
+
return this.findSymbolFromIndex(parsed, project, kind);
|
|
527
561
|
}
|
|
528
|
-
async findSymbolFromIndex(
|
|
562
|
+
async findSymbolFromIndex(parsed, project, kind) {
|
|
529
563
|
const t0 = Date.now();
|
|
530
|
-
this.logger.debug('SearcherService: findSymbol', {
|
|
564
|
+
this.logger.debug('SearcherService: findSymbol', { parsed, project, kind, source: 'index' });
|
|
565
|
+
const container = 'container' in parsed ? parsed.container : null;
|
|
531
566
|
try {
|
|
532
|
-
const params = [name];
|
|
567
|
+
const params = [parsed.name];
|
|
568
|
+
let nameFilter = 'name = $1';
|
|
569
|
+
let orderPrefix = '';
|
|
570
|
+
if (container !== null) {
|
|
571
|
+
params.push(container);
|
|
572
|
+
const exactParam = params.length;
|
|
573
|
+
params.push(`%.${(0, symbol_name_1.escapeLike)(container)}`);
|
|
574
|
+
const suffixParam = params.length;
|
|
575
|
+
nameFilter += ` AND (container = $${exactParam} OR container LIKE $${suffixParam} ESCAPE '\\')`;
|
|
576
|
+
orderPrefix = `(CASE WHEN container = $${exactParam} THEN 0 ELSE 1 END), `;
|
|
577
|
+
}
|
|
533
578
|
let projectFilter = '';
|
|
534
579
|
if (project) {
|
|
535
580
|
params.push(project);
|
|
@@ -541,11 +586,11 @@ class SearcherService {
|
|
|
541
586
|
kindFilter = `AND kind = $${params.length}`;
|
|
542
587
|
}
|
|
543
588
|
params.push(FIND_SYMBOL_LIMIT + 1);
|
|
544
|
-
const rows = await this.db.query(`SELECT "filePath", name, kind, signature, line, col
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
589
|
+
const rows = await this.db.query(`SELECT "filePath", name, kind, signature, line, col, container, "isStatic"
|
|
590
|
+
FROM code_symbol
|
|
591
|
+
WHERE ${nameFilter} ${projectFilter} ${kindFilter}
|
|
592
|
+
ORDER BY ${orderPrefix}project, "filePath", line
|
|
593
|
+
LIMIT $${params.length}`, params);
|
|
549
594
|
const truncated = rows.length > FIND_SYMBOL_LIMIT;
|
|
550
595
|
const hits = rows.slice(0, FIND_SYMBOL_LIMIT).map(row => ({
|
|
551
596
|
filePath: row.filePath,
|
|
@@ -553,22 +598,34 @@ class SearcherService {
|
|
|
553
598
|
column: row.col ?? 1,
|
|
554
599
|
kind: row.kind,
|
|
555
600
|
signature: row.signature ?? row.name,
|
|
601
|
+
qualified: row.container ? `${row.container}.${row.name}` : row.name,
|
|
602
|
+
isStatic: !!row.isStatic,
|
|
603
|
+
source: 'index',
|
|
556
604
|
}));
|
|
557
605
|
this.logger.debug('SearcherService: findSymbol result', { source: 'index', hits: hits.length, truncated, ms: Date.now() - t0 });
|
|
558
|
-
|
|
606
|
+
if (hits.length > 0)
|
|
607
|
+
return { hits, truncated, grepFailed: false };
|
|
608
|
+
if (parsed.form === 'quoted' || parsed.form === 'computed')
|
|
609
|
+
return { hits: [], truncated: false, grepFailed: false };
|
|
610
|
+
return this.grepFindSymbol(parsed, project, kind);
|
|
559
611
|
}
|
|
560
|
-
catch {
|
|
561
|
-
this.logger.warn('SearcherService: symbol index unavailable, falling back to grep', {
|
|
562
|
-
|
|
612
|
+
catch (err) {
|
|
613
|
+
this.logger.warn('SearcherService: symbol index unavailable, falling back to grep', { reason: err.message, ms: Date.now() - t0 });
|
|
614
|
+
if (parsed.form === 'quoted' || parsed.form === 'computed')
|
|
615
|
+
return { hits: [], truncated: false, grepFailed: false };
|
|
616
|
+
return this.grepFindSymbol(parsed, project, kind);
|
|
563
617
|
}
|
|
564
618
|
}
|
|
565
|
-
async grepFindSymbol(
|
|
619
|
+
async grepFindSymbol(parsed, project, kind) {
|
|
566
620
|
const t0 = Date.now();
|
|
567
|
-
|
|
621
|
+
const grepName = parsed.name; // qualified names are grepped by their last segment, no container filter
|
|
622
|
+
this.logger.debug('SearcherService: findSymbol', { grepName, project, kind, source: 'grep' });
|
|
623
|
+
const pattern = this.buildFindSymbolPattern(grepName, kind);
|
|
624
|
+
if (pattern === null)
|
|
625
|
+
return { hits: [], truncated: false, grepFailed: false };
|
|
568
626
|
const searchDir = project
|
|
569
627
|
? this.resolveProjectRoot(project)
|
|
570
628
|
: this.config.workspaceRoot;
|
|
571
|
-
const pattern = this.buildFindSymbolPattern(name, kind);
|
|
572
629
|
const result = await this.spawnGrep([
|
|
573
630
|
'-rn', '-E',
|
|
574
631
|
'-m', String(FIND_SYMBOL_RAW_LIMIT),
|
|
@@ -577,12 +634,14 @@ class SearcherService {
|
|
|
577
634
|
pattern,
|
|
578
635
|
searchDir,
|
|
579
636
|
]);
|
|
580
|
-
if (!result)
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
637
|
+
if (!result.ok) {
|
|
638
|
+
this.logger.warn('SearcherService: findSymbol grep failed', { grepName, project });
|
|
639
|
+
return { hits: [], truncated: false, grepFailed: true };
|
|
640
|
+
}
|
|
641
|
+
if (result.output === null)
|
|
642
|
+
return { hits: [], truncated: false, grepFailed: false };
|
|
643
|
+
const parsedHits = [];
|
|
644
|
+
for (const raw of result.output.split('\n')) {
|
|
586
645
|
const m = raw.match(/^(.+?):(\d+):(.*)$/);
|
|
587
646
|
if (!m)
|
|
588
647
|
continue;
|
|
@@ -590,19 +649,23 @@ class SearcherService {
|
|
|
590
649
|
const lineNum = parseInt(lineNumStr, 10);
|
|
591
650
|
if (isNaN(lineNum))
|
|
592
651
|
continue;
|
|
593
|
-
const col =
|
|
652
|
+
const col = SearcherService.wholeWordIndex(matchedLine, grepName);
|
|
594
653
|
if (col === -1)
|
|
595
654
|
continue;
|
|
596
|
-
|
|
655
|
+
parsedHits.push({
|
|
597
656
|
filePath: path.relative(this.config.workspaceRoot, absFile),
|
|
598
657
|
line: lineNum,
|
|
599
658
|
column: col + 1,
|
|
600
|
-
kind: this.inferKind(matchedLine,
|
|
659
|
+
kind: this.inferKind(matchedLine, grepName),
|
|
601
660
|
signature: matchedLine.trim(),
|
|
661
|
+
qualified: grepName,
|
|
662
|
+
isStatic: false,
|
|
663
|
+
source: 'grep',
|
|
602
664
|
});
|
|
603
665
|
}
|
|
604
|
-
|
|
605
|
-
|
|
666
|
+
const truncated = parsedHits.length > FIND_SYMBOL_LIMIT;
|
|
667
|
+
this.logger.debug('SearcherService: findSymbol result', { source: 'grep', hits: Math.min(parsedHits.length, FIND_SYMBOL_LIMIT), truncated, ms: Date.now() - t0 });
|
|
668
|
+
return { hits: parsedHits.slice(0, FIND_SYMBOL_LIMIT), truncated, grepFailed: false };
|
|
606
669
|
}
|
|
607
670
|
listFiles(relativePath, project, depth, pattern) {
|
|
608
671
|
const t0 = Date.now();
|
|
@@ -740,36 +803,67 @@ class SearcherService {
|
|
|
740
803
|
}
|
|
741
804
|
}
|
|
742
805
|
}
|
|
806
|
+
/**
|
|
807
|
+
* ERE pattern for a grep-fallback kind, or null when that kind is never
|
|
808
|
+
* grepped (`function_property`/`method_signature` are index-only; a bare
|
|
809
|
+
* `constructor` search only makes sense when the name itself is `constructor`).
|
|
810
|
+
*/
|
|
743
811
|
buildFindSymbolPattern(name, kind) {
|
|
812
|
+
if (kind === 'function_property' || kind === 'method_signature')
|
|
813
|
+
return null;
|
|
814
|
+
if (kind === 'constructor')
|
|
815
|
+
return name === 'constructor' ? `(^|[[:space:]])constructor[[:space:]]*\\(` : null;
|
|
816
|
+
const esc = name.replace(/\$/g, '\\$'); // ERE end-of-line metachar
|
|
817
|
+
const MODIFIER = '(public|private|protected|static|abstract|async|override)';
|
|
818
|
+
const BOUNDARY = '(^|[^[:alnum:]_$])';
|
|
744
819
|
const patterns = {
|
|
745
|
-
function: `(export[[:space:]]+)?(async[[:space:]]+)?function[[:space:]]+${
|
|
746
|
-
class: `(export[[:space:]]+)?(abstract[[:space:]]+)?class[[:space:]]+${
|
|
747
|
-
type: `(export[[:space:]]+)?type[[:space:]]+${
|
|
748
|
-
interface: `(export[[:space:]]+)?interface[[:space:]]+${
|
|
749
|
-
variable: `(export[[:space:]]+)?(const|let|var)[[:space:]]+${
|
|
750
|
-
method:
|
|
820
|
+
function: `(export[[:space:]]+)?(async[[:space:]]+)?function[[:space:]]+${esc}[[:space:](<]`,
|
|
821
|
+
class: `(export[[:space:]]+)?(abstract[[:space:]]+)?class[[:space:]]+${esc}[[:space:]{<]`,
|
|
822
|
+
type: `(export[[:space:]]+)?type[[:space:]]+${esc}[[:space:]=<]`,
|
|
823
|
+
interface: `(export[[:space:]]+)?interface[[:space:]]+${esc}[[:space:]{<]`,
|
|
824
|
+
variable: `(export[[:space:]]+)?(const|let|var)[[:space:]]+${esc}[[:space:]=:]`,
|
|
825
|
+
method: `${BOUNDARY}(${MODIFIER}[[:space:]]+)+\\*?[[:space:]]*${esc}[[:space:]]*[(<]`,
|
|
826
|
+
get_accessor: `(^|[[:space:]])get[[:space:]]+${esc}[[:space:]]*\\(`,
|
|
827
|
+
set_accessor: `(^|[[:space:]])set[[:space:]]+${esc}[[:space:]]*\\(`,
|
|
751
828
|
};
|
|
752
829
|
if (kind === 'all')
|
|
753
830
|
return Object.values(patterns).join('|');
|
|
754
831
|
return patterns[kind];
|
|
755
832
|
}
|
|
833
|
+
/** 1-based index of the first whole-word occurrence of `name` in `line`, or -1. */
|
|
834
|
+
static wholeWordIndex(line, name) {
|
|
835
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
836
|
+
const re = new RegExp(`(?<![\\w$#])${escaped}(?![\\w$#])`);
|
|
837
|
+
const m = re.exec(line);
|
|
838
|
+
return m ? m.index : -1;
|
|
839
|
+
}
|
|
756
840
|
inferKind(line, name) {
|
|
757
|
-
|
|
758
|
-
|
|
841
|
+
if (name === 'constructor')
|
|
842
|
+
return 'constructor';
|
|
843
|
+
const idx = SearcherService.wholeWordIndex(line, name);
|
|
844
|
+
const before = idx === -1 ? line : line.slice(0, idx);
|
|
845
|
+
const tokens = before.trim().split(/\s+/).filter(Boolean);
|
|
846
|
+
const last = (tokens[tokens.length - 1] ?? '').replace(/\*$/, '');
|
|
847
|
+
if (last === 'get')
|
|
848
|
+
return 'get_accessor';
|
|
849
|
+
if (last === 'set')
|
|
850
|
+
return 'set_accessor';
|
|
851
|
+
if (last === 'function')
|
|
759
852
|
return 'function';
|
|
760
|
-
if (
|
|
853
|
+
if (last === 'class')
|
|
761
854
|
return 'class';
|
|
762
|
-
if (
|
|
855
|
+
if (last === 'interface')
|
|
763
856
|
return 'interface';
|
|
764
|
-
if (
|
|
857
|
+
if (last === 'type')
|
|
765
858
|
return 'type';
|
|
766
|
-
if (
|
|
859
|
+
if (['public', 'private', 'protected', 'static', 'abstract', 'async', 'override'].includes(last))
|
|
767
860
|
return 'method';
|
|
768
861
|
return 'variable';
|
|
769
862
|
}
|
|
770
863
|
/**
|
|
771
|
-
* Async wrapper around grep to avoid blocking the event loop.
|
|
772
|
-
*
|
|
864
|
+
* Async wrapper around grep to avoid blocking the event loop. `ok: false`
|
|
865
|
+
* means grep could not be spawned or exited with status ≥ 2 (distinguishable
|
|
866
|
+
* from a clean "no matches", which is `ok: true, output: null`).
|
|
773
867
|
*/
|
|
774
868
|
spawnGrep(args) {
|
|
775
869
|
return new Promise((resolve) => {
|
|
@@ -786,19 +880,16 @@ class SearcherService {
|
|
|
786
880
|
proc.stderr.on('data', () => { });
|
|
787
881
|
proc.on('error', (err) => {
|
|
788
882
|
this.logger.error('SearcherService: grep spawn failed', { reason: err.message });
|
|
789
|
-
resolve(
|
|
883
|
+
resolve({ ok: false });
|
|
790
884
|
});
|
|
791
885
|
proc.on('close', (code) => {
|
|
792
|
-
// grep returns 1 for "no matches" — not an error
|
|
793
|
-
if (code !==
|
|
886
|
+
// grep returns 1 for "no matches" — not an error; 2+ is a real failure
|
|
887
|
+
if (code !== null && code >= 2) {
|
|
794
888
|
this.logger.warn('SearcherService: grep exited with code', { code });
|
|
889
|
+
resolve({ ok: false });
|
|
890
|
+
return;
|
|
795
891
|
}
|
|
796
|
-
|
|
797
|
-
resolve(null);
|
|
798
|
-
}
|
|
799
|
-
else {
|
|
800
|
-
resolve(Buffer.concat(chunks).toString('utf-8'));
|
|
801
|
-
}
|
|
892
|
+
resolve({ ok: true, output: chunks.length === 0 ? null : Buffer.concat(chunks).toString('utf-8') });
|
|
802
893
|
});
|
|
803
894
|
});
|
|
804
895
|
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export declare const BARE_NAME_RE: RegExp;
|
|
2
|
+
export declare const QUALIFIED_NAME_RE: RegExp;
|
|
3
|
+
export type FindSymbolQuery = {
|
|
4
|
+
form: 'bare';
|
|
5
|
+
name: string;
|
|
6
|
+
} | {
|
|
7
|
+
form: 'qualified';
|
|
8
|
+
container: string;
|
|
9
|
+
name: string;
|
|
10
|
+
} | {
|
|
11
|
+
form: 'quoted';
|
|
12
|
+
container: string | null;
|
|
13
|
+
name: string;
|
|
14
|
+
} | {
|
|
15
|
+
form: 'computed';
|
|
16
|
+
container: string | null;
|
|
17
|
+
name: string;
|
|
18
|
+
};
|
|
19
|
+
/** Split `P.n` into container `P` and last segment `n`. Requires a qualified name. */
|
|
20
|
+
export declare function splitQualified(input: string): {
|
|
21
|
+
container: string;
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
/**
|
|
25
|
+
* Parse a `find_symbol` name into one of the four accepted forms (bare,
|
|
26
|
+
* qualified, quoted, computed), or null when it matches none — the caller
|
|
27
|
+
* should reject with `Invalid symbol name` without querying anything.
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseFindSymbolName(input: string): FindSymbolQuery | null;
|
|
30
|
+
/**
|
|
31
|
+
* Parse a `get_callers` symbol into bare or qualified form only — the quoted
|
|
32
|
+
* and computed forms of `find_symbol` are meaningless here (calls through
|
|
33
|
+
* element access are never recorded in the call graph).
|
|
34
|
+
*/
|
|
35
|
+
export declare function parseCallerSymbol(input: string): {
|
|
36
|
+
form: 'bare';
|
|
37
|
+
name: string;
|
|
38
|
+
} | {
|
|
39
|
+
form: 'qualified';
|
|
40
|
+
container: string;
|
|
41
|
+
name: string;
|
|
42
|
+
} | null;
|
|
43
|
+
/** Escape `%`, `_` and `\` for a `LIKE ... ESCAPE '\'` pattern. */
|
|
44
|
+
export declare function escapeLike(s: string): string;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QUALIFIED_NAME_RE = exports.BARE_NAME_RE = void 0;
|
|
4
|
+
exports.splitQualified = splitQualified;
|
|
5
|
+
exports.parseFindSymbolName = parseFindSymbolName;
|
|
6
|
+
exports.parseCallerSymbol = parseCallerSymbol;
|
|
7
|
+
exports.escapeLike = escapeLike;
|
|
8
|
+
exports.BARE_NAME_RE = /^#?[\w$]+$/;
|
|
9
|
+
exports.QUALIFIED_NAME_RE = /^#?[\w$]+(\.#?[\w$]+)+$/;
|
|
10
|
+
const PFX_SRC = '(?:#?[\\w$]+(?:\\.#?[\\w$]+)*\\.)?';
|
|
11
|
+
const QUOTED_RE = new RegExp(`^(${PFX_SRC})('[^'\\r\\n]+'|"[^"\\r\\n]+")$`);
|
|
12
|
+
const COMPUTED_RE = new RegExp(`^(${PFX_SRC})(\\[[^\\]\\r\\n]+\\])$`);
|
|
13
|
+
const MAX_QUOTED_COMPUTED_LEN = 256;
|
|
14
|
+
/** Strip a qualified name's trailing `.` container prefix, if present. */
|
|
15
|
+
function stripTrailingDot(prefix) {
|
|
16
|
+
if (!prefix)
|
|
17
|
+
return null;
|
|
18
|
+
return prefix.slice(0, -1);
|
|
19
|
+
}
|
|
20
|
+
/** Split `P.n` into container `P` and last segment `n`. Requires a qualified name. */
|
|
21
|
+
function splitQualified(input) {
|
|
22
|
+
const idx = input.lastIndexOf('.');
|
|
23
|
+
return { container: input.slice(0, idx), name: input.slice(idx + 1) };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse a `find_symbol` name into one of the four accepted forms (bare,
|
|
27
|
+
* qualified, quoted, computed), or null when it matches none — the caller
|
|
28
|
+
* should reject with `Invalid symbol name` without querying anything.
|
|
29
|
+
*/
|
|
30
|
+
function parseFindSymbolName(input) {
|
|
31
|
+
if (input.length <= MAX_QUOTED_COMPUTED_LEN) {
|
|
32
|
+
const quoted = input.match(QUOTED_RE);
|
|
33
|
+
if (quoted) {
|
|
34
|
+
return { form: 'quoted', container: stripTrailingDot(quoted[1]), name: quoted[2].slice(1, -1) };
|
|
35
|
+
}
|
|
36
|
+
const computed = input.match(COMPUTED_RE);
|
|
37
|
+
if (computed) {
|
|
38
|
+
return { form: 'computed', container: stripTrailingDot(computed[1]), name: computed[2].replace(/\s+/g, '') };
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (exports.QUALIFIED_NAME_RE.test(input)) {
|
|
42
|
+
const { container, name } = splitQualified(input);
|
|
43
|
+
return { form: 'qualified', container, name };
|
|
44
|
+
}
|
|
45
|
+
if (exports.BARE_NAME_RE.test(input)) {
|
|
46
|
+
return { form: 'bare', name: input };
|
|
47
|
+
}
|
|
48
|
+
return null;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Parse a `get_callers` symbol into bare or qualified form only — the quoted
|
|
52
|
+
* and computed forms of `find_symbol` are meaningless here (calls through
|
|
53
|
+
* element access are never recorded in the call graph).
|
|
54
|
+
*/
|
|
55
|
+
function parseCallerSymbol(input) {
|
|
56
|
+
if (exports.QUALIFIED_NAME_RE.test(input)) {
|
|
57
|
+
const { container, name } = splitQualified(input);
|
|
58
|
+
return { form: 'qualified', container, name };
|
|
59
|
+
}
|
|
60
|
+
if (exports.BARE_NAME_RE.test(input)) {
|
|
61
|
+
return { form: 'bare', name: input };
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
65
|
+
/** Escape `%`, `_` and `\` for a `LIKE ... ESCAPE '\'` pattern. */
|
|
66
|
+
function escapeLike(s) {
|
|
67
|
+
return s.replace(/\\/g, '\\\\').replace(/%/g, '\\%').replace(/_/g, '\\_');
|
|
68
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { SymbolEntry } from './interfaces/IAstParser';
|
|
2
|
+
export type CodeSymbolRow = Pick<SymbolEntry, 'project' | 'filePath' | 'name' | 'kind' | 'signature' | 'line' | 'col' | 'container' | 'isStatic'>;
|
|
3
|
+
/**
|
|
4
|
+
* Combine top-level symbols and members into the rows written to `code_symbol`.
|
|
5
|
+
* Overload signatures of one declaration — same `(container, name, kind,
|
|
6
|
+
* isStatic)` — collapse to a single representative row: the one with a body
|
|
7
|
+
* (the implementation) when present, otherwise the earliest by line (the first
|
|
8
|
+
* signature in source order). Static and instance members sharing a name are
|
|
9
|
+
* never collapsed into each other (`isStatic` is part of the group key). A
|
|
10
|
+
* final dedup by `(container, name, kind, line)` matches the DB's unique index.
|
|
11
|
+
*/
|
|
12
|
+
export declare function buildSymbolRows(symbols: SymbolEntry[], members: SymbolEntry[]): CodeSymbolRow[];
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildSymbolRows = buildSymbolRows;
|
|
4
|
+
/**
|
|
5
|
+
* Combine top-level symbols and members into the rows written to `code_symbol`.
|
|
6
|
+
* Overload signatures of one declaration — same `(container, name, kind,
|
|
7
|
+
* isStatic)` — collapse to a single representative row: the one with a body
|
|
8
|
+
* (the implementation) when present, otherwise the earliest by line (the first
|
|
9
|
+
* signature in source order). Static and instance members sharing a name are
|
|
10
|
+
* never collapsed into each other (`isStatic` is part of the group key). A
|
|
11
|
+
* final dedup by `(container, name, kind, line)` matches the DB's unique index.
|
|
12
|
+
*/
|
|
13
|
+
function buildSymbolRows(symbols, members) {
|
|
14
|
+
const groups = new Map();
|
|
15
|
+
for (const entry of [...symbols, ...members]) {
|
|
16
|
+
const key = JSON.stringify([entry.container, entry.name, entry.kind, entry.isStatic]);
|
|
17
|
+
const group = groups.get(key);
|
|
18
|
+
if (group)
|
|
19
|
+
group.push(entry);
|
|
20
|
+
else
|
|
21
|
+
groups.set(key, [entry]);
|
|
22
|
+
}
|
|
23
|
+
const collapsed = [];
|
|
24
|
+
for (const group of groups.values()) {
|
|
25
|
+
if (group.length === 1) {
|
|
26
|
+
collapsed.push(group[0]);
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
const implementation = group.find(e => e.hasBody);
|
|
30
|
+
collapsed.push(implementation ?? group.reduce((a, b) => ((a.line ?? 0) <= (b.line ?? 0) ? a : b)));
|
|
31
|
+
}
|
|
32
|
+
const dedup = new Map();
|
|
33
|
+
for (const entry of collapsed) {
|
|
34
|
+
const key = JSON.stringify([entry.container, entry.name, entry.kind, entry.line]);
|
|
35
|
+
dedup.set(key, entry);
|
|
36
|
+
}
|
|
37
|
+
return [...dedup.values()].map(({ project, filePath, name, kind, signature, line, col, container, isStatic }) => ({ project, filePath, name, kind, signature, line, col, container, isStatic }));
|
|
38
|
+
}
|
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.FindSymbolTool = void 0;
|
|
4
4
|
const format_helpers_1 = require("./format-helpers");
|
|
5
|
-
const VALID_KINDS = [
|
|
5
|
+
const VALID_KINDS = [
|
|
6
|
+
'function', 'class', 'type', 'interface', 'variable',
|
|
7
|
+
'method', 'get_accessor', 'set_accessor', 'constructor', 'function_property', 'method_signature',
|
|
8
|
+
'all',
|
|
9
|
+
];
|
|
6
10
|
class FindSymbolTool {
|
|
7
11
|
searcher;
|
|
8
12
|
constructor(searcher) {
|
|
@@ -11,11 +15,16 @@ class FindSymbolTool {
|
|
|
11
15
|
definition() {
|
|
12
16
|
return {
|
|
13
17
|
name: 'find_symbol',
|
|
14
|
-
description: 'Find TypeScript/JavaScript symbol definitions
|
|
18
|
+
description: 'Find TypeScript/JavaScript symbol definitions by exact name: top-level declarations (functions, classes, types, ' +
|
|
19
|
+
'interfaces, variables), class/object-literal members (methods, get/set accessors, constructors, function ' +
|
|
20
|
+
'properties), interface and type-literal method signatures, and namespace-level declarations. Accepts a bare name ' +
|
|
21
|
+
'(any container), a qualified name (`Class.method`, `NS.Class.method`), a quoted name for string-literal members ' +
|
|
22
|
+
"(`Router.'kebab-name'`), or a computed name (`Stack.[Symbol.iterator]`). Use when you know the symbol name and " +
|
|
23
|
+
'want its definition location fast.',
|
|
15
24
|
inputSchema: {
|
|
16
25
|
type: 'object',
|
|
17
26
|
properties: {
|
|
18
|
-
name: { type: 'string', description: '
|
|
27
|
+
name: { type: 'string', description: 'Bare, qualified (Class.method), quoted, or computed symbol name (case-sensitive)' },
|
|
19
28
|
project: { type: 'string', description: 'Limit search to this project (optional). Use list_projects to find names.' },
|
|
20
29
|
kind: { type: 'string', enum: VALID_KINDS, description: 'Symbol kind filter. Default: all.' },
|
|
21
30
|
},
|
|
@@ -28,15 +37,29 @@ class FindSymbolTool {
|
|
|
28
37
|
const project = args.project ? String(args.project) : null;
|
|
29
38
|
const rawKind = args.kind;
|
|
30
39
|
const kind = VALID_KINDS.includes(rawKind) ? rawKind : 'all';
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
40
|
+
let parsed;
|
|
41
|
+
try {
|
|
42
|
+
parsed = await this.searcher.findSymbol(name, project, kind);
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
return (0, format_helpers_1.errorResponse)(err.message);
|
|
34
46
|
}
|
|
47
|
+
const { hits, truncated, grepFailed } = parsed;
|
|
48
|
+
const notice = await this.searcher.outdatedNotice(project);
|
|
35
49
|
const lines = [];
|
|
50
|
+
if (notice)
|
|
51
|
+
lines.push(notice + '\n');
|
|
52
|
+
if (hits.length === 0) {
|
|
53
|
+
const suffix = grepFailed ? ' (grep fallback failed — see log)' : '';
|
|
54
|
+
lines.push(`No definitions found for symbol: ${name}${suffix}`);
|
|
55
|
+
return (0, format_helpers_1.textResponse)(lines.join('\n'));
|
|
56
|
+
}
|
|
36
57
|
if (truncated)
|
|
37
58
|
lines.push(`*(truncated to ${hits.length} results)*\n`);
|
|
38
59
|
for (const h of hits) {
|
|
39
|
-
|
|
60
|
+
const staticTag = h.isStatic ? ' static' : '';
|
|
61
|
+
const grepTag = h.source === 'grep' ? ' (grep — not in index; may be stale or imprecise)' : '';
|
|
62
|
+
lines.push(`**${h.kind}** \`${h.qualified}\`${staticTag} \`${h.filePath}\` line ${h.line}, col ${h.column}${grepTag}`);
|
|
40
63
|
lines.push(`\`\`\`\n${h.signature}\n\`\`\``);
|
|
41
64
|
}
|
|
42
65
|
return (0, format_helpers_1.textResponse)(lines.join('\n'));
|
|
@@ -10,11 +10,15 @@ class GetCallersTool {
|
|
|
10
10
|
definition() {
|
|
11
11
|
return {
|
|
12
12
|
name: 'get_callers',
|
|
13
|
-
description: 'Find all call sites of a function or method by exact name.
|
|
13
|
+
description: 'Find all call sites of a function or method by exact name. Accepts a bare name (every call site with that ' +
|
|
14
|
+
'callee name) or a qualified name (`Class.method`, from find_symbol output) to disambiguate same-named methods ' +
|
|
15
|
+
'of different classes. Uses the call graph index — faster and more precise than grep. A call through an ' +
|
|
16
|
+
'interface-typed receiver is recorded under the interface member (e.g. `IEmbedder.embedBatch`), not the ' +
|
|
17
|
+
'implementing class. Use after find_symbol to trace where a symbol is actually called.',
|
|
14
18
|
inputSchema: {
|
|
15
19
|
type: 'object',
|
|
16
20
|
properties: {
|
|
17
|
-
symbol: { type: 'string', description: '
|
|
21
|
+
symbol: { type: 'string', description: 'Bare or qualified (Class.method) function/method name (case-sensitive)' },
|
|
18
22
|
project: { type: 'string', description: 'Project name (from list_projects)' },
|
|
19
23
|
callee_file: { type: 'string', description: 'Optional: filter by definition file path (as returned by find_symbol) to disambiguate overloaded names' },
|
|
20
24
|
},
|
|
@@ -26,11 +30,22 @@ class GetCallersTool {
|
|
|
26
30
|
const symbol = String(args.symbol ?? '');
|
|
27
31
|
const project = String(args.project ?? '');
|
|
28
32
|
const calleeFile = args.callee_file ? String(args.callee_file) : null;
|
|
29
|
-
|
|
33
|
+
let rows;
|
|
34
|
+
try {
|
|
35
|
+
rows = await this.searcher.getCallers(symbol, project, calleeFile);
|
|
36
|
+
}
|
|
37
|
+
catch (err) {
|
|
38
|
+
return (0, format_helpers_1.errorResponse)(err.message);
|
|
39
|
+
}
|
|
40
|
+
const notice = await this.searcher.outdatedNotice(project);
|
|
41
|
+
const lines = [];
|
|
42
|
+
if (notice)
|
|
43
|
+
lines.push(notice + '\n');
|
|
30
44
|
if (rows.length === 0) {
|
|
31
|
-
|
|
45
|
+
lines.push(`No call sites found for \`${symbol}\` in project \`${project}\`. Run \`rag-index --reindex\` if the call graph has not been populated yet.`);
|
|
46
|
+
return (0, format_helpers_1.textResponse)(lines.join('\n'));
|
|
32
47
|
}
|
|
33
|
-
|
|
48
|
+
lines.push(`**Call sites of \`${symbol}\`** (${rows.length} found)\n`);
|
|
34
49
|
for (const r of rows) {
|
|
35
50
|
const loc = r.line != null ? ` line ${r.line}` : '';
|
|
36
51
|
const ctx = r.fromSymbol ? `, in \`${r.fromSymbol}\`` : '';
|