codesearch-mcp 0.2.1-alpha → 0.2.3-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 +40 -0
- package/README.md +15 -8
- package/dist/app.js +45 -7
- package/dist/src/commands/EvalCommand.d.ts +10 -0
- package/dist/src/commands/EvalCommand.js +231 -0
- package/dist/src/commands/IndexCommand.js +79 -53
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +1 -2
- package/dist/src/db/schema.d.ts +5 -29
- package/dist/src/db/schema.js +2 -8
- package/dist/src/dependency-traversal.d.ts +16 -0
- package/dist/src/dependency-traversal.js +45 -0
- package/dist/src/enriched-prompt.d.ts +3 -2
- package/dist/src/enriched-prompt.js +3 -2
- package/dist/src/eval-results.d.ts +12 -0
- package/dist/src/eval-results.js +114 -0
- package/dist/src/file-utils.d.ts +15 -0
- package/dist/src/file-utils.js +24 -1
- package/dist/src/help.js +4 -1
- package/dist/src/interfaces/IAstParser.d.ts +13 -1
- package/dist/src/interfaces/IDatabase.d.ts +0 -14
- package/dist/src/interfaces/IRetrievalEvaluator.d.ts +94 -0
- package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
- package/dist/src/interfaces/ISearcher.d.ts +11 -16
- package/dist/src/interfaces/index.d.ts +1 -0
- package/dist/src/project-scope.d.ts +19 -0
- package/dist/src/project-scope.js +49 -0
- package/dist/src/project-selector.d.ts +6 -0
- package/dist/src/project-selector.js +18 -0
- package/dist/src/retrieval-eval.d.ts +16 -0
- package/dist/src/retrieval-eval.js +214 -0
- package/dist/src/services/AstParserService.d.ts +66 -1
- package/dist/src/services/AstParserService.js +432 -28
- package/dist/src/services/DatabaseService.d.ts +0 -12
- package/dist/src/services/DatabaseService.js +2 -52
- package/dist/src/services/IndexerService.d.ts +8 -0
- package/dist/src/services/IndexerService.js +24 -11
- package/dist/src/services/McpServer.d.ts +7 -0
- package/dist/src/services/McpServer.js +42 -38
- package/dist/src/services/OpenAICompatibleEmbedder.js +2 -1
- package/dist/src/services/SearcherService.d.ts +189 -25
- package/dist/src/services/SearcherService.js +505 -203
- package/dist/src/symbol-rows.d.ts +14 -9
- package/dist/src/symbol-rows.js +17 -11
- package/dist/src/tools/FindSymbolTool.js +14 -5
- package/dist/src/tools/GetCallersTool.d.ts +3 -1
- package/dist/src/tools/GetCallersTool.js +16 -6
- package/dist/src/tools/GetDependenciesTool.d.ts +3 -1
- package/dist/src/tools/GetDependenciesTool.js +15 -8
- package/dist/src/tools/GetDependentsTool.d.ts +3 -1
- package/dist/src/tools/GetDependentsTool.js +31 -16
- package/dist/src/tools/ListFilesTool.d.ts +3 -1
- package/dist/src/tools/ListFilesTool.js +14 -12
- package/dist/src/tools/ListProjectsTool.js +5 -1
- package/dist/src/tools/ReadFileRangeTool.d.ts +3 -1
- package/dist/src/tools/ReadFileRangeTool.js +12 -11
- package/dist/src/tools/ReadFileTool.d.ts +3 -1
- package/dist/src/tools/ReadFileTool.js +12 -10
- package/dist/src/tools/SearchCodebaseTool.js +3 -1
- package/dist/src/tools/format-helpers.d.ts +16 -1
- package/dist/src/tools/format-helpers.js +64 -0
- package/package.json +9 -8
|
@@ -36,9 +36,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.AstParserService = void 0;
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
|
-
const typescript_1 = require("typescript");
|
|
40
39
|
const ts_morph_1 = require("ts-morph");
|
|
41
40
|
const LoggerService_1 = require("./LoggerService");
|
|
41
|
+
const GRAPHQL_MODULE = '@nestjs/graphql';
|
|
42
|
+
const GRAPHQL_TYPE_DECORATORS = new Set(['ObjectType', 'InputType', 'InterfaceType']);
|
|
43
|
+
const GRAPHQL_OPERATION_KIND = {
|
|
44
|
+
Query: 'graphql_query', Mutation: 'graphql_mutation', Subscription: 'graphql_subscription',
|
|
45
|
+
};
|
|
46
|
+
const GRAPHQL_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
42
47
|
const FALLBACK_OPTS = {
|
|
43
48
|
skipAddingFilesFromTsConfig: true,
|
|
44
49
|
skipFileDependencyResolution: false,
|
|
@@ -48,9 +53,11 @@ const MORPH_CACHE_MAX = 10;
|
|
|
48
53
|
const VALID_SEGMENT = /^#?[\w$]+$/;
|
|
49
54
|
class AstParserService {
|
|
50
55
|
logger;
|
|
56
|
+
deps;
|
|
51
57
|
morphProjects = new Map();
|
|
52
|
-
constructor(logger = LoggerService_1.LoggerService.silent()) {
|
|
58
|
+
constructor(logger = LoggerService_1.LoggerService.silent(), deps = {}) {
|
|
53
59
|
this.logger = logger;
|
|
60
|
+
this.deps = deps;
|
|
54
61
|
}
|
|
55
62
|
// ── public interface ────────────────────────────────────────────────────────
|
|
56
63
|
async analyzeProject(filePaths, serviceName, projectRoot = process.cwd(), opts = {}) {
|
|
@@ -62,7 +69,7 @@ class AstParserService {
|
|
|
62
69
|
const morphProject = this.createMorphProject(projectRoot, skipDependencyResolution, rootTsConfigPath);
|
|
63
70
|
for (const filePath of filePaths) {
|
|
64
71
|
if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
|
|
65
|
-
analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [] });
|
|
72
|
+
analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] });
|
|
66
73
|
continue;
|
|
67
74
|
}
|
|
68
75
|
try {
|
|
@@ -70,7 +77,7 @@ class AstParserService {
|
|
|
70
77
|
}
|
|
71
78
|
catch (err) {
|
|
72
79
|
this.logger.warn('AstParser: failed to add source file', { filePath, reason: err.message });
|
|
73
|
-
analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [] });
|
|
80
|
+
analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] });
|
|
74
81
|
}
|
|
75
82
|
}
|
|
76
83
|
for (const sourceFile of morphProject.getSourceFiles()) {
|
|
@@ -102,7 +109,7 @@ class AstParserService {
|
|
|
102
109
|
this.logger.debug('AstParser: analyzeFile start', { filePath, serviceName, projectRoot });
|
|
103
110
|
if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
|
|
104
111
|
this.logger.debug('AstParser: analyzeFile skip (non-TS)', { filePath, ms: Date.now() - t0 });
|
|
105
|
-
return { edges: [], symbols: [], calls: [], members: [] };
|
|
112
|
+
return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
|
|
106
113
|
}
|
|
107
114
|
const morphProject = this.getMorphProject(projectRoot);
|
|
108
115
|
const existing = morphProject.getSourceFile(filePath);
|
|
@@ -114,7 +121,7 @@ class AstParserService {
|
|
|
114
121
|
}
|
|
115
122
|
catch (err) {
|
|
116
123
|
this.logger.warn('AstParser: analyzeFile failed to add source file', { filePath, reason: err.message, ms: Date.now() - t0 });
|
|
117
|
-
return { edges: [], symbols: [], calls: [], members: [] };
|
|
124
|
+
return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
|
|
118
125
|
}
|
|
119
126
|
const result = this.analyzeSourceFile(sourceFile, filePath, serviceName);
|
|
120
127
|
morphProject.removeSourceFile(sourceFile);
|
|
@@ -149,11 +156,24 @@ class AstParserService {
|
|
|
149
156
|
const members = memberCandidates.map(c => c.entry);
|
|
150
157
|
const calls = this.extractCalls(sourceFile, filePath, serviceName);
|
|
151
158
|
const ownerRanges = this.buildOwnerRanges([...symbolCandidates, ...memberCandidates]);
|
|
152
|
-
|
|
159
|
+
// GraphQL-named rows are isolated like members: a bug here must never
|
|
160
|
+
// change a file's symbols, members, chunks, ownerPath or call graph.
|
|
161
|
+
// Runs after member extraction (decision 6 of `graphql-symbols`) — a
|
|
162
|
+
// GraphQL row is written only when the decorated declaration's own row
|
|
163
|
+
// was written to code_symbol.
|
|
164
|
+
let graphqlNames = [];
|
|
165
|
+
try {
|
|
166
|
+
graphqlNames = this.extractGraphqlNames(sourceFile, filePath, serviceName, [...symbolCandidates, ...memberCandidates]);
|
|
167
|
+
}
|
|
168
|
+
catch (err) {
|
|
169
|
+
this.logger.warn('AstParser: GraphQL extraction failed', { filePath, reason: err.message });
|
|
170
|
+
graphqlNames = [];
|
|
171
|
+
}
|
|
172
|
+
return { edges, symbols, members, calls, ownerRanges, graphqlNames };
|
|
153
173
|
}
|
|
154
174
|
catch (err) {
|
|
155
175
|
this.logger.warn('AstParser: failed to analyze file', { filePath, reason: err.message });
|
|
156
|
-
return { edges: [], symbols: [], calls: [], members: [] };
|
|
176
|
+
return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
|
|
157
177
|
}
|
|
158
178
|
}
|
|
159
179
|
// ── private: MorphProject cache (LRU, max MORPH_CACHE_MAX entries) ──────────
|
|
@@ -231,8 +251,19 @@ class AstParserService {
|
|
|
231
251
|
const edges = [];
|
|
232
252
|
const usageMap = this.buildUsageMap(sourceFile);
|
|
233
253
|
for (const decl of sourceFile.getImportDeclarations()) {
|
|
254
|
+
const namedImports = decl.getNamedImports();
|
|
255
|
+
const defaultImport = decl.getDefaultImport();
|
|
256
|
+
const namespaceImport = decl.getNamespaceImport();
|
|
257
|
+
const hasClause = Boolean(defaultImport || namespaceImport || namedImports.length > 0);
|
|
234
258
|
const resolved = decl.getModuleSpecifierSourceFile();
|
|
235
|
-
|
|
259
|
+
let toFile = resolved ? resolved.getFilePath() : null;
|
|
260
|
+
// A bindingless specifier never resolves through ts-morph's bound symbol
|
|
261
|
+
// (nothing is bound), even for a real project file — fall back to
|
|
262
|
+
// TypeScript's own resolver and map the result back onto the project.
|
|
263
|
+
if (!toFile && !hasClause) {
|
|
264
|
+
toFile = this.resolveSideEffectModuleSpecifier(decl, sourceFile, filePath);
|
|
265
|
+
}
|
|
266
|
+
if (!toFile) {
|
|
236
267
|
const specifier = decl.getModuleSpecifierValue();
|
|
237
268
|
const isCodeImport = !/\.(json|css|scss|less|svg|png|jpg|gif|ico|woff2?|ttf|eot)(\?.*)?$/.test(specifier);
|
|
238
269
|
if (specifier.startsWith('.') && isCodeImport) {
|
|
@@ -240,34 +271,37 @@ class AstParserService {
|
|
|
240
271
|
}
|
|
241
272
|
continue;
|
|
242
273
|
}
|
|
243
|
-
const toFile = resolved.getFilePath();
|
|
244
274
|
if (toFile.includes('/node_modules/'))
|
|
245
275
|
continue;
|
|
246
|
-
|
|
247
|
-
const defaultImport = decl.getDefaultImport();
|
|
248
|
-
const namespaceImport = decl.getNamespaceImport();
|
|
249
|
-
if (namedImports.length > 0) {
|
|
250
|
-
for (const named of namedImports) {
|
|
251
|
-
const localName = named.getAliasNode()?.getText() ?? named.getName();
|
|
252
|
-
const fromSymbol = this.resolveFromSymbol(localName, usageMap);
|
|
253
|
-
edges.push({
|
|
254
|
-
project: serviceName, fromFile: filePath, fromSymbol,
|
|
255
|
-
toFile, toSymbol: named.getName(), depType: 'import',
|
|
256
|
-
});
|
|
257
|
-
}
|
|
258
|
-
}
|
|
259
|
-
else if (defaultImport) {
|
|
276
|
+
if (!hasClause) {
|
|
260
277
|
edges.push({
|
|
261
278
|
project: serviceName, fromFile: filePath, fromSymbol: null,
|
|
279
|
+
toFile, toSymbol: null, depType: 'import-side-effect',
|
|
280
|
+
});
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
if (defaultImport) {
|
|
284
|
+
const fromSymbol = this.resolveFromSymbol(defaultImport.getText(), usageMap);
|
|
285
|
+
edges.push({
|
|
286
|
+
project: serviceName, fromFile: filePath, fromSymbol,
|
|
262
287
|
toFile, toSymbol: null, depType: 'import-default',
|
|
263
288
|
});
|
|
264
289
|
}
|
|
265
|
-
|
|
290
|
+
if (namespaceImport) {
|
|
291
|
+
const fromSymbol = this.resolveFromSymbol(namespaceImport.getText(), usageMap);
|
|
266
292
|
edges.push({
|
|
267
|
-
project: serviceName, fromFile: filePath, fromSymbol
|
|
293
|
+
project: serviceName, fromFile: filePath, fromSymbol,
|
|
268
294
|
toFile, toSymbol: null, depType: 'import-namespace',
|
|
269
295
|
});
|
|
270
296
|
}
|
|
297
|
+
for (const named of namedImports) {
|
|
298
|
+
const localName = named.getAliasNode()?.getText() ?? named.getName();
|
|
299
|
+
const fromSymbol = this.resolveFromSymbol(localName, usageMap);
|
|
300
|
+
edges.push({
|
|
301
|
+
project: serviceName, fromFile: filePath, fromSymbol,
|
|
302
|
+
toFile, toSymbol: named.getName(), depType: 'import',
|
|
303
|
+
});
|
|
304
|
+
}
|
|
271
305
|
}
|
|
272
306
|
for (const decl of sourceFile.getExportDeclarations()) {
|
|
273
307
|
const resolved = decl.getModuleSpecifierSourceFile();
|
|
@@ -294,6 +328,18 @@ class AstParserService {
|
|
|
294
328
|
}
|
|
295
329
|
return edges;
|
|
296
330
|
}
|
|
331
|
+
/** Manual fallback for a bindingless import specifier (design.md decision 2) —
|
|
332
|
+
* ts-morph's `getModuleSpecifierSourceFile()` is `null` for these even when
|
|
333
|
+
* the target is a real project file, since nothing is bound to resolve through. */
|
|
334
|
+
resolveSideEffectModuleSpecifier(decl, sourceFile, filePath) {
|
|
335
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
336
|
+
const compilerOptions = sourceFile.getProject().getCompilerOptions();
|
|
337
|
+
const result = ts_morph_1.ts.resolveModuleName(specifier, filePath, compilerOptions, ts_morph_1.ts.sys);
|
|
338
|
+
const resolvedFileName = result.resolvedModule?.resolvedFileName;
|
|
339
|
+
if (!resolvedFileName)
|
|
340
|
+
return null;
|
|
341
|
+
return sourceFile.getProject().getSourceFile(resolvedFileName)?.getFilePath() ?? null;
|
|
342
|
+
}
|
|
297
343
|
resolveFromSymbol(localName, usageMap) {
|
|
298
344
|
const users = usageMap.get(localName);
|
|
299
345
|
return users?.length === 1 ? users[0] : null;
|
|
@@ -478,7 +524,7 @@ class AstParserService {
|
|
|
478
524
|
const described = this.describeIndexedNode(c);
|
|
479
525
|
if (!described)
|
|
480
526
|
continue;
|
|
481
|
-
const kw = c.getFirstChildByKind(
|
|
527
|
+
const kw = c.getFirstChildByKind(ts_morph_1.SyntaxKind.ConstructorKeyword) ?? c;
|
|
482
528
|
push(c, kw, described.name, 'constructor', described.container, false, c.getBody() !== undefined);
|
|
483
529
|
}
|
|
484
530
|
for (const p of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.PropertyDeclaration)) {
|
|
@@ -560,6 +606,364 @@ class AstParserService {
|
|
|
560
606
|
endLine: c.declNode.getEndLineNumber(),
|
|
561
607
|
}));
|
|
562
608
|
}
|
|
609
|
+
// ── private: GraphQL-named rows (graphql-symbols) ───────────────────────────
|
|
610
|
+
/** Local name → the name exported by `@nestjs/graphql`, for every named import
|
|
611
|
+
* of that module specifier (aliases followed). Recognition is import-based,
|
|
612
|
+
* not type-checker-based, so `skipFileDependencyResolution` batches agree. */
|
|
613
|
+
buildGraphqlImportMap(sourceFile) {
|
|
614
|
+
const map = new Map();
|
|
615
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
616
|
+
if (decl.getModuleSpecifierValue() !== GRAPHQL_MODULE)
|
|
617
|
+
continue;
|
|
618
|
+
for (const named of decl.getNamedImports()) {
|
|
619
|
+
const exportedName = named.getName();
|
|
620
|
+
const localName = named.getAliasNode()?.getText() ?? exportedName;
|
|
621
|
+
map.set(localName, exportedName);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
return map;
|
|
625
|
+
}
|
|
626
|
+
/** The identifier naming a decorator's expression (`@Foo` or the callee of `@Foo(...)`). */
|
|
627
|
+
static decoratorIdentifier(dec) {
|
|
628
|
+
const expr = dec.getExpression();
|
|
629
|
+
if (ts_morph_1.Node.isCallExpression(expr)) {
|
|
630
|
+
const callee = expr.getExpression();
|
|
631
|
+
return ts_morph_1.Node.isIdentifier(callee) ? callee : undefined;
|
|
632
|
+
}
|
|
633
|
+
return ts_morph_1.Node.isIdentifier(expr) ? expr : undefined;
|
|
634
|
+
}
|
|
635
|
+
/** `dec` recognised against `importMap` — its exported `@nestjs/graphql` name and its call arguments (`[]` for a bare `@Foo`). */
|
|
636
|
+
recognizedDecorator(dec, importMap) {
|
|
637
|
+
const idNode = AstParserService.decoratorIdentifier(dec);
|
|
638
|
+
if (!idNode)
|
|
639
|
+
return null;
|
|
640
|
+
const exported = importMap.get(idNode.getText());
|
|
641
|
+
if (!exported)
|
|
642
|
+
return null;
|
|
643
|
+
const expr = dec.getExpression();
|
|
644
|
+
const args = ts_morph_1.Node.isCallExpression(expr) ? expr.getArguments() : [];
|
|
645
|
+
return { name: exported, args };
|
|
646
|
+
}
|
|
647
|
+
static literalStringValue(node) {
|
|
648
|
+
if (ts_morph_1.Node.isStringLiteral(node) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(node))
|
|
649
|
+
return node.getLiteralValue();
|
|
650
|
+
return null;
|
|
651
|
+
}
|
|
652
|
+
static checkGraphqlGrammar(name) {
|
|
653
|
+
return GRAPHQL_NAME_RE.test(name) ? { ok: true, name } : { ok: false, reason: 'invalid-name', value: name };
|
|
654
|
+
}
|
|
655
|
+
/** `true` when any decorator argument is an (unwrapped) options object
|
|
656
|
+
* literal with `isAbstract: true` — an abstract base type (`PaginatedType`
|
|
657
|
+
* pattern) that NestJS never registers in the actual GraphQL schema. */
|
|
658
|
+
static isAbstractGraphqlType(args) {
|
|
659
|
+
return args.some((arg) => {
|
|
660
|
+
if (!ts_morph_1.Node.isObjectLiteralExpression(arg))
|
|
661
|
+
return false;
|
|
662
|
+
const prop = arg.getProperties().find((p) => {
|
|
663
|
+
if (!ts_morph_1.Node.isPropertyAssignment(p))
|
|
664
|
+
return false;
|
|
665
|
+
const nameNode = p.getNameNode();
|
|
666
|
+
if (ts_morph_1.Node.isIdentifier(nameNode))
|
|
667
|
+
return nameNode.getText() === 'isAbstract';
|
|
668
|
+
if (ts_morph_1.Node.isStringLiteral(nameNode))
|
|
669
|
+
return nameNode.getLiteralValue() === 'isAbstract';
|
|
670
|
+
return false;
|
|
671
|
+
});
|
|
672
|
+
const init = prop?.getInitializer();
|
|
673
|
+
return init !== undefined && ts_morph_1.Node.isTrueLiteral(init);
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
/** `obj`'s own `name` property (`PropertyAssignment` or `ShorthandPropertyAssignment`), if any. */
|
|
677
|
+
static findNameProperty(obj) {
|
|
678
|
+
return obj.getProperties().find((p) => {
|
|
679
|
+
if (!ts_morph_1.Node.isPropertyAssignment(p) && !ts_morph_1.Node.isShorthandPropertyAssignment(p))
|
|
680
|
+
return false;
|
|
681
|
+
const nameNode = p.getNameNode();
|
|
682
|
+
if (ts_morph_1.Node.isIdentifier(nameNode))
|
|
683
|
+
return nameNode.getText() === 'name';
|
|
684
|
+
if (ts_morph_1.Node.isStringLiteral(nameNode))
|
|
685
|
+
return nameNode.getLiteralValue() === 'name';
|
|
686
|
+
return false;
|
|
687
|
+
});
|
|
688
|
+
}
|
|
689
|
+
/** Strips parens/`as`/`satisfies`/non-null wrappers (any nesting) to find the
|
|
690
|
+
* underlying node — used only to classify an argument's *shape* (type
|
|
691
|
+
* function vs. options object vs. literal vs. other), never to read a name
|
|
692
|
+
* value through the wrapper (decision 4 of `graphql-symbols` forbids that). */
|
|
693
|
+
static peekThroughWrapper(node) {
|
|
694
|
+
let current = node;
|
|
695
|
+
let wrapped = false;
|
|
696
|
+
while (true) {
|
|
697
|
+
if (ts_morph_1.Node.isParenthesizedExpression(current) || ts_morph_1.Node.isAsExpression(current) || ts_morph_1.Node.isSatisfiesExpression(current) || ts_morph_1.Node.isNonNullExpression(current)) {
|
|
698
|
+
current = current.getExpression();
|
|
699
|
+
wrapped = true;
|
|
700
|
+
continue;
|
|
701
|
+
}
|
|
702
|
+
break;
|
|
703
|
+
}
|
|
704
|
+
return { inner: current, wrapped };
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* GraphQL name from a decorator's arguments (design decision 4 of
|
|
708
|
+
* `graphql-symbols`) — syntactic, no wrapper looked through at any step.
|
|
709
|
+
* `allowOptionsName` gates step 2 (the `name` option — `Query`/`Mutation`/
|
|
710
|
+
* `Subscription`/`ResolveField` only); `fallbackName` is the TS name used at
|
|
711
|
+
* step 3.
|
|
712
|
+
*/
|
|
713
|
+
deriveGraphqlName(args, allowOptionsName, fallbackName) {
|
|
714
|
+
if (args.length > 0) {
|
|
715
|
+
const first = args[0];
|
|
716
|
+
const { inner, wrapped } = AstParserService.peekThroughWrapper(first);
|
|
717
|
+
const literal = AstParserService.literalStringValue(inner);
|
|
718
|
+
if (literal !== null) {
|
|
719
|
+
// A wrapped literal is still "a wrapped ... argument", an anything-else
|
|
720
|
+
// node (decision 4) — reading its value through the wrapper is exactly
|
|
721
|
+
// what "no wrapper is looked through" forbids.
|
|
722
|
+
if (wrapped)
|
|
723
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
724
|
+
return AstParserService.checkGraphqlGrammar(literal);
|
|
725
|
+
}
|
|
726
|
+
const isTypeFnOrOptions = ts_morph_1.Node.isArrowFunction(inner) || ts_morph_1.Node.isFunctionExpression(inner) || ts_morph_1.Node.isObjectLiteralExpression(inner);
|
|
727
|
+
if (!isTypeFnOrOptions)
|
|
728
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
729
|
+
// Type function / options object, wrapped or not — this is a routing
|
|
730
|
+
// decision only (go to step 2/3); nothing is read from it here, so its
|
|
731
|
+
// wrapper doesn't matter yet.
|
|
732
|
+
}
|
|
733
|
+
if (allowOptionsName) {
|
|
734
|
+
for (const arg of args) {
|
|
735
|
+
const { inner, wrapped } = AstParserService.peekThroughWrapper(arg);
|
|
736
|
+
if (!ts_morph_1.Node.isObjectLiteralExpression(inner))
|
|
737
|
+
continue;
|
|
738
|
+
// A wrapped options object cannot be trusted without looking through
|
|
739
|
+
// the wrapper, which decision 4 forbids — treat it as non-literal
|
|
740
|
+
// rather than silently skipping to the next argument or to step 3.
|
|
741
|
+
if (wrapped)
|
|
742
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
743
|
+
const nameProp = AstParserService.findNameProperty(inner);
|
|
744
|
+
if (!nameProp)
|
|
745
|
+
continue;
|
|
746
|
+
if (ts_morph_1.Node.isShorthandPropertyAssignment(nameProp))
|
|
747
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
748
|
+
const init = nameProp.getInitializer();
|
|
749
|
+
const value = init ? AstParserService.literalStringValue(init) : null;
|
|
750
|
+
if (value !== null)
|
|
751
|
+
return AstParserService.checkGraphqlGrammar(value);
|
|
752
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
753
|
+
}
|
|
754
|
+
}
|
|
755
|
+
return AstParserService.checkGraphqlGrammar(fallbackName);
|
|
756
|
+
}
|
|
757
|
+
logGraphqlSkip(filePath, declaration, decorator, reason, value) {
|
|
758
|
+
const fields = { filePath, declaration, decorator, reason };
|
|
759
|
+
if (value !== undefined)
|
|
760
|
+
fields.value = value;
|
|
761
|
+
this.logger.debug('AstParser: graphql row skipped', fields);
|
|
762
|
+
}
|
|
763
|
+
/** `declNode`'s qualified TypeScript name for diagnostics, independent of
|
|
764
|
+
* whether its own `code_symbol` row was actually written. */
|
|
765
|
+
declarationNameFor(node, fallback) {
|
|
766
|
+
const described = this.describeIndexedNode(node);
|
|
767
|
+
return described ? this.qualifiedName(described) : fallback;
|
|
768
|
+
}
|
|
769
|
+
/** The identifier named by a `@Resolver` argument: directly, or as an arrow
|
|
770
|
+
* function's expression body; or the sole identifier of a single-element
|
|
771
|
+
* array literal in either position (list-returning resolver). */
|
|
772
|
+
static extractResolverIdentifier(argNode) {
|
|
773
|
+
let target = argNode;
|
|
774
|
+
if (ts_morph_1.Node.isArrowFunction(target)) {
|
|
775
|
+
const body = target.getBody();
|
|
776
|
+
if (ts_morph_1.Node.isBlock(body))
|
|
777
|
+
return null;
|
|
778
|
+
target = body;
|
|
779
|
+
}
|
|
780
|
+
if (ts_morph_1.Node.isIdentifier(target))
|
|
781
|
+
return target;
|
|
782
|
+
if (ts_morph_1.Node.isArrayLiteralExpression(target)) {
|
|
783
|
+
const elements = target.getElements();
|
|
784
|
+
if (elements.length === 1 && ts_morph_1.Node.isIdentifier(elements[0]))
|
|
785
|
+
return elements[0];
|
|
786
|
+
}
|
|
787
|
+
return null;
|
|
788
|
+
}
|
|
789
|
+
defaultResolveDeclaration(identifier) {
|
|
790
|
+
const sym = identifier.getSymbol();
|
|
791
|
+
if (!sym)
|
|
792
|
+
return undefined;
|
|
793
|
+
const firstDecl = sym.getDeclarations()?.[0];
|
|
794
|
+
const resolved = firstDecl && ts_morph_1.Node.isImportSpecifier(firstDecl) ? (sym.getAliasedSymbol() ?? sym) : sym;
|
|
795
|
+
return resolved.getDeclarations()?.[0];
|
|
796
|
+
}
|
|
797
|
+
/** The GraphQL type name of a `@ResolveField`'s resolved parent class
|
|
798
|
+
* (design decision 5, second half): its recognised `@ObjectType` (else
|
|
799
|
+
* `@InterfaceType`) decorator, or its class name when it carries no
|
|
800
|
+
* decorator spelled `ObjectType`/`InterfaceType` at all, recognised or not. */
|
|
801
|
+
classGraphqlTypeName(declClass) {
|
|
802
|
+
const declImportMap = this.buildGraphqlImportMap(declClass.getSourceFile());
|
|
803
|
+
const decorators = declClass.getDecorators();
|
|
804
|
+
const hasAnyTypeSpelledDecorator = decorators.some((d) => {
|
|
805
|
+
const idNode = AstParserService.decoratorIdentifier(d);
|
|
806
|
+
const text = idNode?.getText();
|
|
807
|
+
return text === 'ObjectType' || text === 'InterfaceType';
|
|
808
|
+
});
|
|
809
|
+
const recognized = decorators
|
|
810
|
+
.map(d => this.recognizedDecorator(d, declImportMap))
|
|
811
|
+
.find(r => r?.name === 'ObjectType')
|
|
812
|
+
?? decorators.map(d => this.recognizedDecorator(d, declImportMap)).find(r => r?.name === 'InterfaceType');
|
|
813
|
+
if (recognized) {
|
|
814
|
+
const fallbackName = declClass.getName() ?? (declClass.isDefaultExport() ? 'default' : '');
|
|
815
|
+
const nameResult = this.deriveGraphqlName(recognized.args, false, fallbackName);
|
|
816
|
+
return nameResult.ok ? { ok: true, name: nameResult.name } : { ok: false };
|
|
817
|
+
}
|
|
818
|
+
// Carries a type-spelled decorator that isn't recognised (e.g. imported
|
|
819
|
+
// through a barrel) — its GraphQL name is unreadable; the class-name
|
|
820
|
+
// fallback never applies to it (it may differ from the unreadable name).
|
|
821
|
+
if (hasAnyTypeSpelledDecorator)
|
|
822
|
+
return { ok: false };
|
|
823
|
+
const name = declClass.getName();
|
|
824
|
+
if (!name)
|
|
825
|
+
return { ok: false };
|
|
826
|
+
return GRAPHQL_NAME_RE.test(name) ? { ok: true, name } : { ok: false, value: name };
|
|
827
|
+
}
|
|
828
|
+
/** The parent type of a `@ResolveField` — the enclosing class's recognised
|
|
829
|
+
* `@Resolver` argument (design decision 5, first half). */
|
|
830
|
+
resolveResolverParentType(cls, importMap) {
|
|
831
|
+
const resolverDec = cls.getDecorators()
|
|
832
|
+
.map(d => this.recognizedDecorator(d, importMap))
|
|
833
|
+
.find(r => r?.name === 'Resolver');
|
|
834
|
+
if (!resolverDec || resolverDec.args.length === 0)
|
|
835
|
+
return { ok: false };
|
|
836
|
+
const argNode = resolverDec.args[0];
|
|
837
|
+
const literal = AstParserService.literalStringValue(argNode);
|
|
838
|
+
if (literal !== null) {
|
|
839
|
+
return GRAPHQL_NAME_RE.test(literal) ? { ok: true, name: literal } : { ok: false, value: literal };
|
|
840
|
+
}
|
|
841
|
+
const identifier = AstParserService.extractResolverIdentifier(argNode);
|
|
842
|
+
if (!identifier)
|
|
843
|
+
return { ok: false };
|
|
844
|
+
const resolve = this.deps.resolveDeclaration ?? this.defaultResolveDeclaration.bind(this);
|
|
845
|
+
const declaration = resolve(identifier);
|
|
846
|
+
if (!declaration || !ts_morph_1.Node.isClassDeclaration(declaration))
|
|
847
|
+
return { ok: false };
|
|
848
|
+
return this.classGraphqlTypeName(declaration);
|
|
849
|
+
}
|
|
850
|
+
extractClassTypeRows(cls, importMap, ownByDeclNode, filePath, serviceName, entries) {
|
|
851
|
+
const own = ownByDeclNode.get(cls);
|
|
852
|
+
const declaration = this.declarationNameFor(cls, own?.entry.name ?? cls.getName() ?? 'default');
|
|
853
|
+
const seenNames = new Set();
|
|
854
|
+
for (const dec of cls.getDecorators()) {
|
|
855
|
+
const rec = this.recognizedDecorator(dec, importMap);
|
|
856
|
+
if (!rec || !GRAPHQL_TYPE_DECORATORS.has(rec.name))
|
|
857
|
+
continue;
|
|
858
|
+
if (AstParserService.isAbstractGraphqlType(rec.args)) {
|
|
859
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, 'abstract-type');
|
|
860
|
+
continue;
|
|
861
|
+
}
|
|
862
|
+
if (!own) {
|
|
863
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, 'no-symbol-row');
|
|
864
|
+
continue;
|
|
865
|
+
}
|
|
866
|
+
const nameResult = this.deriveGraphqlName(rec.args, false, own.entry.name);
|
|
867
|
+
if (!nameResult.ok) {
|
|
868
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
|
|
869
|
+
continue;
|
|
870
|
+
}
|
|
871
|
+
if (seenNames.has(nameResult.name)) {
|
|
872
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, 'duplicate', own.entry.name);
|
|
873
|
+
continue;
|
|
874
|
+
}
|
|
875
|
+
seenNames.add(nameResult.name);
|
|
876
|
+
entries.push({
|
|
877
|
+
project: serviceName, filePath, name: nameResult.name, kind: 'graphql_type',
|
|
878
|
+
signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
|
|
879
|
+
container: null, isStatic: false, target: declaration,
|
|
880
|
+
});
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
extractResolveFieldRow(method, rec, own, declaration, importMap, filePath, serviceName, entries) {
|
|
884
|
+
if (!own) {
|
|
885
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'no-symbol-row');
|
|
886
|
+
return;
|
|
887
|
+
}
|
|
888
|
+
const cls = method.getParent();
|
|
889
|
+
if (!cls || !ts_morph_1.Node.isClassDeclaration(cls)) {
|
|
890
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent');
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
const parent = this.resolveResolverParentType(cls, importMap);
|
|
894
|
+
if (!parent.ok) {
|
|
895
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent', parent.value);
|
|
896
|
+
return;
|
|
897
|
+
}
|
|
898
|
+
const nameResult = this.deriveGraphqlName(rec.args, true, own.entry.name);
|
|
899
|
+
if (!nameResult.ok) {
|
|
900
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
entries.push({
|
|
904
|
+
project: serviceName, filePath, name: nameResult.name, kind: 'graphql_field',
|
|
905
|
+
signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
|
|
906
|
+
container: parent.name, isStatic: own.entry.isStatic, target: declaration,
|
|
907
|
+
});
|
|
908
|
+
}
|
|
909
|
+
extractMethodOperationRows(method, importMap, ownByDeclNode, filePath, serviceName, entries) {
|
|
910
|
+
const own = ownByDeclNode.get(method);
|
|
911
|
+
const declaration = this.declarationNameFor(method, own?.entry.name ?? AstParserService.memberName(method.getNameNode()) ?? '<anonymous>');
|
|
912
|
+
for (const dec of method.getDecorators()) {
|
|
913
|
+
const rec = this.recognizedDecorator(dec, importMap);
|
|
914
|
+
if (!rec)
|
|
915
|
+
continue;
|
|
916
|
+
if (rec.name === 'ResolveField') {
|
|
917
|
+
// Isolated per field: a throw resolving this field's parent (e.g. the
|
|
918
|
+
// injected test seam) degrades to "no row for that field" and never
|
|
919
|
+
// trips the file-level catch in `analyzeSourceFile`.
|
|
920
|
+
try {
|
|
921
|
+
this.extractResolveFieldRow(method, rec, own, declaration, importMap, filePath, serviceName, entries);
|
|
922
|
+
}
|
|
923
|
+
catch (err) {
|
|
924
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent');
|
|
925
|
+
}
|
|
926
|
+
continue;
|
|
927
|
+
}
|
|
928
|
+
const kind = GRAPHQL_OPERATION_KIND[rec.name];
|
|
929
|
+
if (!kind)
|
|
930
|
+
continue; // recognised but not an operation decorator (wrong declaration kind) — ignored, no log
|
|
931
|
+
if (!own) {
|
|
932
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, 'no-symbol-row');
|
|
933
|
+
continue;
|
|
934
|
+
}
|
|
935
|
+
const nameResult = this.deriveGraphqlName(rec.args, true, own.entry.name);
|
|
936
|
+
if (!nameResult.ok) {
|
|
937
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
entries.push({
|
|
941
|
+
project: serviceName, filePath, name: nameResult.name, kind,
|
|
942
|
+
signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
|
|
943
|
+
container: rec.name, isStatic: own.entry.isStatic, target: declaration,
|
|
944
|
+
});
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
/** GraphQL-named rows for every recognised `@nestjs/graphql` decorator in
|
|
948
|
+
* the file (`graphql-symbols`). `ownRows` is `symbols ∪ members` — the
|
|
949
|
+
* declarations' own `code_symbol` candidates, checked so a GraphQL row is
|
|
950
|
+
* never written without its decorated declaration's own row. */
|
|
951
|
+
extractGraphqlNames(sourceFile, filePath, serviceName, ownRows) {
|
|
952
|
+
const importMap = this.buildGraphqlImportMap(sourceFile);
|
|
953
|
+
if (importMap.size === 0)
|
|
954
|
+
return [];
|
|
955
|
+
const ownByDeclNode = new Map();
|
|
956
|
+
for (const c of ownRows)
|
|
957
|
+
ownByDeclNode.set(c.declNode, c);
|
|
958
|
+
const entries = [];
|
|
959
|
+
for (const cls of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassDeclaration)) {
|
|
960
|
+
this.extractClassTypeRows(cls, importMap, ownByDeclNode, filePath, serviceName, entries);
|
|
961
|
+
}
|
|
962
|
+
for (const method of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.MethodDeclaration)) {
|
|
963
|
+
this.extractMethodOperationRows(method, importMap, ownByDeclNode, filePath, serviceName, entries);
|
|
964
|
+
}
|
|
965
|
+
return entries;
|
|
966
|
+
}
|
|
563
967
|
// ── private: shared container resolution ────────────────────────────────────
|
|
564
968
|
static isValidSegment(s) {
|
|
565
969
|
return VALID_SEGMENT.test(s);
|
|
@@ -632,7 +1036,7 @@ class AstParserService {
|
|
|
632
1036
|
* (exclude) everything nested inside it? */
|
|
633
1037
|
walkStep(node) {
|
|
634
1038
|
if (ts_morph_1.Node.isModuleDeclaration(node)) {
|
|
635
|
-
if (node.compilerNode.flags &
|
|
1039
|
+
if (node.compilerNode.flags & ts_morph_1.NodeFlags.GlobalAugmentation)
|
|
636
1040
|
return { kind: 'exclude' };
|
|
637
1041
|
const nameNode = node.getNameNode();
|
|
638
1042
|
if (ts_morph_1.Node.isStringLiteral(nameNode))
|
|
@@ -6,7 +6,6 @@ import * as schema from '../db/schema';
|
|
|
6
6
|
type Schema = typeof schema;
|
|
7
7
|
export type DrizzleDb = NodePgDatabase<Schema>;
|
|
8
8
|
export type DrizzleTx = NodePgTransaction<Schema, ExtractTablesWithRelations<Schema>>;
|
|
9
|
-
export declare const CURRENT_SYMBOL_INDEX_VERSION = 2;
|
|
10
9
|
export declare class DatabaseService implements IDatabase {
|
|
11
10
|
private config;
|
|
12
11
|
private pool;
|
|
@@ -32,17 +31,6 @@ export declare class DatabaseService implements IDatabase {
|
|
|
32
31
|
deleteFiles(project: string, filePaths: string[]): Promise<void>;
|
|
33
32
|
listIndexedProjects(): Promise<string[]>;
|
|
34
33
|
deleteProject(name: string): Promise<void>;
|
|
35
|
-
listProjectsWithIndexRows(): Promise<string[]>;
|
|
36
|
-
/**
|
|
37
|
-
* Among `projectNames` (or every project with rows when null), those with at
|
|
38
|
-
* least one row in `code_chunk`/`code_symbol` and a `symbol_index_version`
|
|
39
|
-
* below `CURRENT_SYMBOL_INDEX_VERSION` (a missing row counts as version 1).
|
|
40
|
-
* A missing `symbol_index_version` table (un-migrated DB, `rag-mcp`/`rag-web`
|
|
41
|
-
* bootstrapped with `initDb`) counts as "no marker rows" — every project with
|
|
42
|
-
* rows is then outdated. Any other database error propagates.
|
|
43
|
-
*/
|
|
44
|
-
listOutdatedProjects(projectNames: string[] | null): Promise<string[]>;
|
|
45
|
-
upsertSymbolIndexVersion(project: string): Promise<void>;
|
|
46
34
|
/** Guard against SQL injection — the dimension is interpolated into DDL. */
|
|
47
35
|
private assertDim;
|
|
48
36
|
private assertMinPostgresVersion;
|