codesearch-mcp 0.2.2-alpha → 0.3.0-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 +65 -1
- package/README.md +60 -33
- package/dist/app.js +46 -22
- package/dist/src/chunking/AstChunker.d.ts +12 -0
- package/dist/src/chunking/AstChunker.js +30 -0
- package/dist/src/chunking/CharacterChunkStrategy.d.ts +8 -0
- package/dist/src/chunking/CharacterChunkStrategy.js +15 -0
- package/dist/src/chunking/DeclarationChunkStrategy.d.ts +15 -0
- package/dist/src/chunking/DeclarationChunkStrategy.js +87 -0
- package/dist/src/chunking/FragmentMerger.d.ts +11 -0
- package/dist/src/chunking/FragmentMerger.js +61 -0
- package/dist/src/chunking/ImportOnlyFilter.d.ts +5 -0
- package/dist/src/chunking/ImportOnlyFilter.js +14 -0
- package/dist/src/chunking/LangchainTextSplitter.d.ts +21 -0
- package/dist/src/chunking/LangchainTextSplitter.js +133 -0
- package/dist/src/chunking/MinSizeSplitPolicy.d.ts +9 -0
- package/dist/src/chunking/MinSizeSplitPolicy.js +18 -0
- package/dist/src/chunking/RunPacker.d.ts +12 -0
- package/dist/src/chunking/RunPacker.js +84 -0
- package/dist/src/chunking/create-chunker.d.ts +3 -0
- package/dist/src/chunking/create-chunker.js +15 -0
- package/dist/src/chunking/interfaces.d.ts +27 -0
- package/dist/src/chunking/interfaces.js +2 -0
- package/dist/src/commands/ConfigCommand.js +1 -9
- package/dist/src/commands/EvalCommand.d.ts +11 -0
- package/dist/src/commands/EvalCommand.js +264 -0
- package/dist/src/commands/IndexCommand.js +70 -66
- package/dist/src/commands/McpCommand.js +2 -8
- package/dist/src/commands/WebCommand.js +2 -5
- package/dist/src/config-error.d.ts +0 -4
- package/dist/src/config-error.js +0 -4
- package/dist/src/config-store.d.ts +1 -54
- package/dist/src/config-store.js +8 -69
- package/dist/src/config.d.ts +8 -17
- package/dist/src/config.js +35 -34
- package/dist/src/constants.d.ts +2 -28
- package/dist/src/constants.js +6 -28
- package/dist/src/container.d.ts +6 -0
- package/dist/src/container.js +12 -3
- package/dist/src/db/index.d.ts +1 -1
- package/dist/src/db/index.js +1 -2
- package/dist/src/db/schema.d.ts +22 -29
- package/dist/src/db/schema.js +3 -14
- package/dist/src/declarations/DeclarationTree.d.ts +19 -0
- package/dist/src/declarations/DeclarationTree.js +66 -0
- package/dist/src/declarations/DeclarationTreeBuilder.d.ts +22 -0
- package/dist/src/declarations/DeclarationTreeBuilder.js +179 -0
- package/dist/src/declarations/SourceLines.d.ts +37 -0
- package/dist/src/declarations/SourceLines.js +415 -0
- package/dist/src/dependency-traversal.d.ts +10 -0
- package/dist/src/dependency-traversal.js +39 -0
- package/dist/src/enriched-prompt.d.ts +0 -5
- package/dist/src/enriched-prompt.js +0 -5
- package/dist/src/error-record.d.ts +12 -0
- package/dist/src/error-record.js +248 -0
- package/dist/src/eval-corpus.d.ts +16 -0
- package/dist/src/eval-corpus.js +169 -0
- package/dist/src/eval-results.d.ts +15 -0
- package/dist/src/eval-results.js +397 -0
- package/dist/src/file-utils.d.ts +10 -0
- package/dist/src/file-utils.js +19 -1
- package/dist/src/graph.d.ts +0 -25
- package/dist/src/graph.js +0 -28
- package/dist/src/help.js +4 -1
- package/dist/src/hnsw-settings.d.ts +3 -0
- package/dist/src/hnsw-settings.js +24 -0
- package/dist/src/interfaces/IAstParser.d.ts +5 -30
- package/dist/src/interfaces/IChunker.d.ts +3 -12
- package/dist/src/interfaces/ICorpusInspector.d.ts +7 -0
- package/dist/src/interfaces/ICorpusInspector.js +2 -0
- package/dist/src/interfaces/IDatabase.d.ts +2 -36
- package/dist/src/interfaces/IEmbedder.d.ts +0 -9
- package/dist/src/interfaces/ILogger.d.ts +1 -11
- package/dist/src/interfaces/IMcpToolHandler.d.ts +0 -5
- package/dist/src/interfaces/IMonorepoResolver.d.ts +0 -22
- package/dist/src/interfaces/IProjectDiscovery.d.ts +0 -17
- package/dist/src/interfaces/IRetrievalEvaluator.d.ts +216 -0
- package/dist/src/interfaces/IRetrievalEvaluator.js +2 -0
- package/dist/src/interfaces/ISearcher.d.ts +26 -20
- package/dist/src/interfaces/index.d.ts +1 -0
- package/dist/src/interrupt.js +0 -2
- package/dist/src/owner-path.d.ts +9 -10
- package/dist/src/owner-path.js +11 -23
- package/dist/src/project-scope.d.ts +7 -0
- package/dist/src/project-scope.js +37 -0
- package/dist/src/project-selector.d.ts +1 -0
- package/dist/src/project-selector.js +13 -6
- package/dist/src/retrieval-eval.d.ts +24 -0
- package/dist/src/retrieval-eval.js +500 -0
- package/dist/src/search/result-shaping.d.ts +4 -0
- package/dist/src/search/result-shaping.js +19 -0
- package/dist/src/services/AstParserService.d.ts +28 -23
- package/dist/src/services/AstParserService.js +391 -98
- package/dist/src/services/DatabaseService.d.ts +2 -19
- package/dist/src/services/DatabaseService.js +44 -81
- package/dist/src/services/EmbedderFactory.d.ts +0 -4
- package/dist/src/services/EmbedderFactory.js +0 -5
- package/dist/src/services/IndexerService.d.ts +2 -10
- package/dist/src/services/IndexerService.js +29 -45
- package/dist/src/services/InfinityEmbedder.d.ts +0 -21
- package/dist/src/services/InfinityEmbedder.js +0 -15
- package/dist/src/services/LoggerService.d.ts +7 -5
- package/dist/src/services/LoggerService.js +37 -41
- package/dist/src/services/McpServer.d.ts +25 -10
- package/dist/src/services/McpServer.js +101 -64
- package/dist/src/services/MonorepoAwareDiscoveryService.d.ts +1 -45
- package/dist/src/services/MonorepoAwareDiscoveryService.js +1 -51
- package/dist/src/services/NxMonorepoResolver.d.ts +0 -29
- package/dist/src/services/NxMonorepoResolver.js +5 -56
- package/dist/src/services/OpenAICompatibleEmbedder.d.ts +0 -6
- package/dist/src/services/OpenAICompatibleEmbedder.js +1 -14
- package/dist/src/services/ProjectDiscoveryService.d.ts +0 -4
- package/dist/src/services/ProjectDiscoveryService.js +0 -4
- package/dist/src/services/SearcherService.d.ts +127 -70
- package/dist/src/services/SearcherService.js +526 -305
- package/dist/src/services/VoyageEmbedder.d.ts +0 -6
- package/dist/src/services/VoyageEmbedder.js +0 -6
- package/dist/src/services/WebServerService.d.ts +1 -53
- package/dist/src/services/WebServerService.js +11 -57
- package/dist/src/services/http-retry.d.ts +0 -7
- package/dist/src/services/http-retry.js +1 -10
- package/dist/src/services/infinity-provisioning.d.ts +0 -74
- package/dist/src/services/infinity-provisioning.js +5 -88
- package/dist/src/services/legacy-ollama-index.d.ts +0 -16
- package/dist/src/services/legacy-ollama-index.js +0 -23
- package/dist/src/services/reranker.d.ts +0 -9
- package/dist/src/services/reranker.js +46 -12
- package/dist/src/setup.d.ts +11 -23
- package/dist/src/setup.js +98 -71
- package/dist/src/symbol-name.d.ts +0 -12
- package/dist/src/symbol-name.js +0 -13
- package/dist/src/symbol-rows.d.ts +2 -11
- package/dist/src/symbol-rows.js +5 -13
- package/dist/src/tool-call-log.d.ts +2 -0
- package/dist/src/tool-call-log.js +24 -0
- 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 +7 -2
- package/dist/src/tools/format-helpers.d.ts +10 -1
- package/dist/src/tools/format-helpers.js +145 -8
- package/dist/src/web/dashboard.d.ts +0 -8
- package/dist/src/web/dashboard.js +7 -10
- package/package.json +10 -9
- package/dist/src/services/AstChunker.d.ts +0 -42
- package/dist/src/services/AstChunker.js +0 -158
- package/docs/web-dashboard.md +0 -206
|
@@ -37,7 +37,15 @@ exports.AstParserService = void 0;
|
|
|
37
37
|
const path = __importStar(require("path"));
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const ts_morph_1 = require("ts-morph");
|
|
40
|
+
const DeclarationTreeBuilder_1 = require("../declarations/DeclarationTreeBuilder");
|
|
41
|
+
const DeclarationTree_1 = require("../declarations/DeclarationTree");
|
|
40
42
|
const LoggerService_1 = require("./LoggerService");
|
|
43
|
+
const GRAPHQL_MODULE = '@nestjs/graphql';
|
|
44
|
+
const GRAPHQL_TYPE_DECORATORS = new Set(['ObjectType', 'InputType', 'InterfaceType']);
|
|
45
|
+
const GRAPHQL_OPERATION_KIND = {
|
|
46
|
+
Query: 'graphql_query', Mutation: 'graphql_mutation', Subscription: 'graphql_subscription',
|
|
47
|
+
};
|
|
48
|
+
const GRAPHQL_NAME_RE = /^[_A-Za-z][_0-9A-Za-z]*$/;
|
|
41
49
|
const FALLBACK_OPTS = {
|
|
42
50
|
skipAddingFilesFromTsConfig: true,
|
|
43
51
|
skipFileDependencyResolution: false,
|
|
@@ -47,11 +55,13 @@ const MORPH_CACHE_MAX = 10;
|
|
|
47
55
|
const VALID_SEGMENT = /^#?[\w$]+$/;
|
|
48
56
|
class AstParserService {
|
|
49
57
|
logger;
|
|
58
|
+
deps;
|
|
50
59
|
morphProjects = new Map();
|
|
51
|
-
|
|
60
|
+
declarationBuilder = new DeclarationTreeBuilder_1.DeclarationTreeBuilder();
|
|
61
|
+
constructor(logger = LoggerService_1.LoggerService.silent(), deps = {}) {
|
|
52
62
|
this.logger = logger;
|
|
63
|
+
this.deps = deps;
|
|
53
64
|
}
|
|
54
|
-
// ── public interface ────────────────────────────────────────────────────────
|
|
55
65
|
async analyzeProject(filePaths, serviceName, projectRoot = process.cwd(), opts = {}) {
|
|
56
66
|
const t0 = Date.now();
|
|
57
67
|
const skipDependencyResolution = opts.skipDependencyResolution ?? false;
|
|
@@ -61,22 +71,21 @@ class AstParserService {
|
|
|
61
71
|
const morphProject = this.createMorphProject(projectRoot, skipDependencyResolution, rootTsConfigPath);
|
|
62
72
|
for (const filePath of filePaths) {
|
|
63
73
|
if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
|
|
64
|
-
analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [] });
|
|
74
|
+
analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] });
|
|
65
75
|
continue;
|
|
66
76
|
}
|
|
67
77
|
try {
|
|
68
78
|
morphProject.addSourceFileAtPath(filePath);
|
|
69
79
|
}
|
|
70
80
|
catch (err) {
|
|
71
|
-
this.logger.warn('AstParser: failed to add source file', { filePath,
|
|
72
|
-
analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [] });
|
|
81
|
+
this.logger.warn('AstParser: failed to add source file', { filePath, err });
|
|
82
|
+
analysisMap.set(filePath, { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] });
|
|
73
83
|
}
|
|
74
84
|
}
|
|
75
85
|
for (const sourceFile of morphProject.getSourceFiles()) {
|
|
76
86
|
const filePath = sourceFile.getFilePath();
|
|
77
87
|
analysisMap.set(filePath, this.analyzeSourceFile(sourceFile, filePath, serviceName));
|
|
78
88
|
}
|
|
79
|
-
// Release source file memory before returning
|
|
80
89
|
for (const sourceFile of morphProject.getSourceFiles()) {
|
|
81
90
|
morphProject.removeSourceFile(sourceFile);
|
|
82
91
|
}
|
|
@@ -101,7 +110,7 @@ class AstParserService {
|
|
|
101
110
|
this.logger.debug('AstParser: analyzeFile start', { filePath, serviceName, projectRoot });
|
|
102
111
|
if (!/\.(ts|tsx|js|jsx)$/.test(filePath) || /\.d\.ts$/.test(filePath)) {
|
|
103
112
|
this.logger.debug('AstParser: analyzeFile skip (non-TS)', { filePath, ms: Date.now() - t0 });
|
|
104
|
-
return { edges: [], symbols: [], calls: [], members: [] };
|
|
113
|
+
return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
|
|
105
114
|
}
|
|
106
115
|
const morphProject = this.getMorphProject(projectRoot);
|
|
107
116
|
const existing = morphProject.getSourceFile(filePath);
|
|
@@ -112,8 +121,8 @@ class AstParserService {
|
|
|
112
121
|
sourceFile = morphProject.addSourceFileAtPath(filePath);
|
|
113
122
|
}
|
|
114
123
|
catch (err) {
|
|
115
|
-
this.logger.warn('AstParser: analyzeFile failed to add source file', { filePath,
|
|
116
|
-
return { edges: [], symbols: [], calls: [], members: [] };
|
|
124
|
+
this.logger.warn('AstParser: analyzeFile failed to add source file', { filePath, err, ms: Date.now() - t0 });
|
|
125
|
+
return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
|
|
117
126
|
}
|
|
118
127
|
const result = this.analyzeSourceFile(sourceFile, filePath, serviceName);
|
|
119
128
|
morphProject.removeSourceFile(sourceFile);
|
|
@@ -122,7 +131,6 @@ class AstParserService {
|
|
|
122
131
|
});
|
|
123
132
|
return result;
|
|
124
133
|
}
|
|
125
|
-
// Kept for test compatibility (extract-import-edges.test.ts).
|
|
126
134
|
async extractImportEdges(filePath, serviceName, projectRoot) {
|
|
127
135
|
const { edges } = await this.analyzeFile(filePath, serviceName, projectRoot);
|
|
128
136
|
return edges;
|
|
@@ -132,41 +140,37 @@ class AstParserService {
|
|
|
132
140
|
const edges = this.extractEdges(sourceFile, filePath, serviceName);
|
|
133
141
|
const symbolCandidates = this.extractSymbols(sourceFile, filePath, serviceName);
|
|
134
142
|
const symbols = symbolCandidates.map(c => c.entry);
|
|
135
|
-
// Member extraction (classes anywhere, class/interface/object-literal
|
|
136
|
-
// members, namespace-level declarations) is isolated in its own try: a
|
|
137
|
-
// bug there must never change a file's top-level symbols, chunks or
|
|
138
|
-
// embedding prompt (see the `symbol-index-members` retrieval-invariance
|
|
139
|
-
// requirement).
|
|
140
143
|
let memberCandidates = [];
|
|
141
144
|
try {
|
|
142
145
|
memberCandidates = this.extractMembers(sourceFile, filePath, serviceName);
|
|
143
146
|
}
|
|
144
147
|
catch (err) {
|
|
145
|
-
this.logger.warn('AstParser: member extraction failed', { filePath,
|
|
148
|
+
this.logger.warn('AstParser: member extraction failed', { filePath, err });
|
|
146
149
|
memberCandidates = [];
|
|
147
150
|
}
|
|
148
151
|
const members = memberCandidates.map(c => c.entry);
|
|
149
152
|
const calls = this.extractCalls(sourceFile, filePath, serviceName);
|
|
150
|
-
const
|
|
151
|
-
|
|
153
|
+
const declarations = this.buildDeclarations(sourceFile, filePath, symbolCandidates, memberCandidates);
|
|
154
|
+
let graphqlNames = [];
|
|
155
|
+
try {
|
|
156
|
+
graphqlNames = this.extractGraphqlNames(sourceFile, filePath, serviceName, [...symbolCandidates, ...memberCandidates]);
|
|
157
|
+
}
|
|
158
|
+
catch (err) {
|
|
159
|
+
this.logger.warn('AstParser: GraphQL extraction failed', { filePath, err });
|
|
160
|
+
graphqlNames = [];
|
|
161
|
+
}
|
|
162
|
+
return { edges, symbols, members, calls, declarations, graphqlNames };
|
|
152
163
|
}
|
|
153
164
|
catch (err) {
|
|
154
|
-
this.logger.warn('AstParser: failed to analyze file', { filePath,
|
|
155
|
-
return { edges: [], symbols: [], calls: [], members: [] };
|
|
165
|
+
this.logger.warn('AstParser: failed to analyze file', { filePath, err });
|
|
166
|
+
return { edges: [], symbols: [], calls: [], members: [], graphqlNames: [] };
|
|
156
167
|
}
|
|
157
168
|
}
|
|
158
|
-
// ── private: MorphProject cache (LRU, max MORPH_CACHE_MAX entries) ──────────
|
|
159
169
|
createMorphProject(projectRoot, skipFileDependencyResolution = false, rootTsConfigPath) {
|
|
160
170
|
const t0 = Date.now();
|
|
161
|
-
// Determine which tsconfig to use:
|
|
162
|
-
// 1. Project-local tsconfig.json
|
|
163
|
-
// 2. Root tsconfig (e.g. tsconfig.base.json for Nx monorepos)
|
|
164
|
-
// 3. Fallback options
|
|
165
171
|
const localTsconfigPath = path.join(projectRoot, 'tsconfig.json');
|
|
166
172
|
const hasLocalTsconfig = fs.existsSync(localTsconfigPath);
|
|
167
173
|
const hasRootTsconfig = rootTsConfigPath ? fs.existsSync(rootTsConfigPath) : false;
|
|
168
|
-
// For monorepos: prefer the root tsconfig.base.json which contains
|
|
169
|
-
// workspace-wide path aliases (e.g. @scope/* → libs/*/src)
|
|
170
174
|
const effectiveTsconfig = hasRootTsconfig ? rootTsConfigPath : (hasLocalTsconfig ? localTsconfigPath : null);
|
|
171
175
|
this.logger.debug('AstParser: tsconfig lookup', {
|
|
172
176
|
projectRoot,
|
|
@@ -198,7 +202,7 @@ class AstParserService {
|
|
|
198
202
|
catch (err) {
|
|
199
203
|
this.logger.warn('AstParser: tsconfig load failed, using fallback', {
|
|
200
204
|
tsconfigPath: effectiveTsconfig ?? localTsconfigPath,
|
|
201
|
-
|
|
205
|
+
err,
|
|
202
206
|
ms: Date.now() - t0,
|
|
203
207
|
});
|
|
204
208
|
project = new ts_morph_1.Project(FALLBACK_OPTS);
|
|
@@ -207,7 +211,6 @@ class AstParserService {
|
|
|
207
211
|
}
|
|
208
212
|
getMorphProject(projectRoot) {
|
|
209
213
|
if (this.morphProjects.has(projectRoot)) {
|
|
210
|
-
// Move to end = most recently used
|
|
211
214
|
const cached = this.morphProjects.get(projectRoot);
|
|
212
215
|
this.morphProjects.delete(projectRoot);
|
|
213
216
|
this.morphProjects.set(projectRoot, cached);
|
|
@@ -215,7 +218,6 @@ class AstParserService {
|
|
|
215
218
|
return cached;
|
|
216
219
|
}
|
|
217
220
|
if (this.morphProjects.size >= MORPH_CACHE_MAX) {
|
|
218
|
-
// Evict least recently used (first entry in insertion-order Map)
|
|
219
221
|
const lruKey = this.morphProjects.keys().next().value;
|
|
220
222
|
this.morphProjects.delete(lruKey);
|
|
221
223
|
this.logger.debug('AstParser: evicted LRU project', { lruKey });
|
|
@@ -225,13 +227,20 @@ class AstParserService {
|
|
|
225
227
|
this.morphProjects.set(projectRoot, project);
|
|
226
228
|
return project;
|
|
227
229
|
}
|
|
228
|
-
// ── private: edge extraction (imports + re-exports) ────────────────────────
|
|
229
230
|
extractEdges(sourceFile, filePath, serviceName) {
|
|
230
231
|
const edges = [];
|
|
231
232
|
const usageMap = this.buildUsageMap(sourceFile);
|
|
232
233
|
for (const decl of sourceFile.getImportDeclarations()) {
|
|
234
|
+
const namedImports = decl.getNamedImports();
|
|
235
|
+
const defaultImport = decl.getDefaultImport();
|
|
236
|
+
const namespaceImport = decl.getNamespaceImport();
|
|
237
|
+
const hasClause = Boolean(defaultImport || namespaceImport || namedImports.length > 0);
|
|
233
238
|
const resolved = decl.getModuleSpecifierSourceFile();
|
|
234
|
-
|
|
239
|
+
let toFile = resolved ? resolved.getFilePath() : null;
|
|
240
|
+
if (!toFile && !hasClause) {
|
|
241
|
+
toFile = this.resolveSideEffectModuleSpecifier(decl, sourceFile, filePath);
|
|
242
|
+
}
|
|
243
|
+
if (!toFile) {
|
|
235
244
|
const specifier = decl.getModuleSpecifierValue();
|
|
236
245
|
const isCodeImport = !/\.(json|css|scss|less|svg|png|jpg|gif|ico|woff2?|ttf|eot)(\?.*)?$/.test(specifier);
|
|
237
246
|
if (specifier.startsWith('.') && isCodeImport) {
|
|
@@ -239,34 +248,37 @@ class AstParserService {
|
|
|
239
248
|
}
|
|
240
249
|
continue;
|
|
241
250
|
}
|
|
242
|
-
const toFile = resolved.getFilePath();
|
|
243
251
|
if (toFile.includes('/node_modules/'))
|
|
244
252
|
continue;
|
|
245
|
-
|
|
246
|
-
const defaultImport = decl.getDefaultImport();
|
|
247
|
-
const namespaceImport = decl.getNamespaceImport();
|
|
248
|
-
if (namedImports.length > 0) {
|
|
249
|
-
for (const named of namedImports) {
|
|
250
|
-
const localName = named.getAliasNode()?.getText() ?? named.getName();
|
|
251
|
-
const fromSymbol = this.resolveFromSymbol(localName, usageMap);
|
|
252
|
-
edges.push({
|
|
253
|
-
project: serviceName, fromFile: filePath, fromSymbol,
|
|
254
|
-
toFile, toSymbol: named.getName(), depType: 'import',
|
|
255
|
-
});
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
else if (defaultImport) {
|
|
253
|
+
if (!hasClause) {
|
|
259
254
|
edges.push({
|
|
260
255
|
project: serviceName, fromFile: filePath, fromSymbol: null,
|
|
256
|
+
toFile, toSymbol: null, depType: 'import-side-effect',
|
|
257
|
+
});
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (defaultImport) {
|
|
261
|
+
const fromSymbol = this.resolveFromSymbol(defaultImport.getText(), usageMap);
|
|
262
|
+
edges.push({
|
|
263
|
+
project: serviceName, fromFile: filePath, fromSymbol,
|
|
261
264
|
toFile, toSymbol: null, depType: 'import-default',
|
|
262
265
|
});
|
|
263
266
|
}
|
|
264
|
-
|
|
267
|
+
if (namespaceImport) {
|
|
268
|
+
const fromSymbol = this.resolveFromSymbol(namespaceImport.getText(), usageMap);
|
|
265
269
|
edges.push({
|
|
266
|
-
project: serviceName, fromFile: filePath, fromSymbol
|
|
270
|
+
project: serviceName, fromFile: filePath, fromSymbol,
|
|
267
271
|
toFile, toSymbol: null, depType: 'import-namespace',
|
|
268
272
|
});
|
|
269
273
|
}
|
|
274
|
+
for (const named of namedImports) {
|
|
275
|
+
const localName = named.getAliasNode()?.getText() ?? named.getName();
|
|
276
|
+
const fromSymbol = this.resolveFromSymbol(localName, usageMap);
|
|
277
|
+
edges.push({
|
|
278
|
+
project: serviceName, fromFile: filePath, fromSymbol,
|
|
279
|
+
toFile, toSymbol: named.getName(), depType: 'import',
|
|
280
|
+
});
|
|
281
|
+
}
|
|
270
282
|
}
|
|
271
283
|
for (const decl of sourceFile.getExportDeclarations()) {
|
|
272
284
|
const resolved = decl.getModuleSpecifierSourceFile();
|
|
@@ -293,6 +305,15 @@ class AstParserService {
|
|
|
293
305
|
}
|
|
294
306
|
return edges;
|
|
295
307
|
}
|
|
308
|
+
resolveSideEffectModuleSpecifier(decl, sourceFile, filePath) {
|
|
309
|
+
const specifier = decl.getModuleSpecifierValue();
|
|
310
|
+
const compilerOptions = sourceFile.getProject().getCompilerOptions();
|
|
311
|
+
const result = ts_morph_1.ts.resolveModuleName(specifier, filePath, compilerOptions, ts_morph_1.ts.sys);
|
|
312
|
+
const resolvedFileName = result.resolvedModule?.resolvedFileName;
|
|
313
|
+
if (!resolvedFileName)
|
|
314
|
+
return null;
|
|
315
|
+
return sourceFile.getProject().getSourceFile(resolvedFileName)?.getFilePath() ?? null;
|
|
316
|
+
}
|
|
296
317
|
resolveFromSymbol(localName, usageMap) {
|
|
297
318
|
const users = usageMap.get(localName);
|
|
298
319
|
return users?.length === 1 ? users[0] : null;
|
|
@@ -302,7 +323,6 @@ class AstParserService {
|
|
|
302
323
|
const containers = [
|
|
303
324
|
...sourceFile.getClasses().flatMap(n => n.getName() ? [{ name: n.getName(), node: n }] : []),
|
|
304
325
|
...sourceFile.getFunctions().flatMap(n => n.getName() ? [{ name: n.getName(), node: n }] : []),
|
|
305
|
-
// include top-level variable declarations so arrow function exports are tracked for fromSymbol
|
|
306
326
|
...sourceFile.getVariableStatements()
|
|
307
327
|
.flatMap(stmt => stmt.getDeclarations())
|
|
308
328
|
.flatMap(n => {
|
|
@@ -325,20 +345,15 @@ class AstParserService {
|
|
|
325
345
|
}
|
|
326
346
|
return map;
|
|
327
347
|
}
|
|
328
|
-
// ── private: position / signature helpers ───────────────────────────────────
|
|
329
348
|
getPos(sourceFile, node) {
|
|
330
349
|
const { line, character } = sourceFile.compilerNode
|
|
331
350
|
.getLineAndCharacterOfPosition(node.getStart());
|
|
332
351
|
return { line: line + 1, col: character + 1 };
|
|
333
352
|
}
|
|
334
|
-
/** Trimmed text of the source line containing `node`'s start — the signature
|
|
335
|
-
* rule for every kind (name node for members, the position node for
|
|
336
|
-
* constructors/anonymous default classes). */
|
|
337
353
|
lineTextAt(sourceLines, sourceFile, node) {
|
|
338
354
|
const { line } = this.getPos(sourceFile, node);
|
|
339
355
|
return (sourceLines[line - 1] ?? node.getText().split('\n')[0]).trim();
|
|
340
356
|
}
|
|
341
|
-
// ── private: top-level symbol extraction (feeds the chunker + prompt) ──────
|
|
342
357
|
extractSymbols(sourceFile, filePath, serviceName) {
|
|
343
358
|
const candidates = [];
|
|
344
359
|
const sourceLines = sourceFile.getFullText().split('\n');
|
|
@@ -406,8 +421,6 @@ class AstParserService {
|
|
|
406
421
|
const nameNode = decl.getNameNode();
|
|
407
422
|
const { line, col } = this.getPos(sourceFile, nameNode);
|
|
408
423
|
candidates.push({
|
|
409
|
-
// Owner ranges use the declarator's own span, not the statement's —
|
|
410
|
-
// each declarator of `const a = 1, b = () => {…}` owns only its lines.
|
|
411
424
|
declNode: decl,
|
|
412
425
|
entry: {
|
|
413
426
|
project: serviceName, filePath, name: decl.getName(), kind: 'variable',
|
|
@@ -420,7 +433,6 @@ class AstParserService {
|
|
|
420
433
|
}
|
|
421
434
|
return candidates;
|
|
422
435
|
}
|
|
423
|
-
// ── private: member extraction (classes anywhere, members, namespace body) ─
|
|
424
436
|
extractMembers(sourceFile, filePath, serviceName) {
|
|
425
437
|
const candidates = [];
|
|
426
438
|
const sourceLines = sourceFile.getFullText().split('\n');
|
|
@@ -438,14 +450,10 @@ class AstParserService {
|
|
|
438
450
|
},
|
|
439
451
|
});
|
|
440
452
|
};
|
|
441
|
-
// Classes anywhere in the file — nested, expression, anonymous default.
|
|
442
453
|
for (const cls of [
|
|
443
454
|
...sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassDeclaration),
|
|
444
455
|
...sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassExpression),
|
|
445
456
|
]) {
|
|
446
|
-
// Named top-level classes are already handled by extractSymbols; an
|
|
447
|
-
// anonymous top-level class (`export default class {}`) is not — it
|
|
448
|
-
// must still be collected here.
|
|
449
457
|
if (ts_morph_1.Node.isClassDeclaration(cls) && cls.getName() && ts_morph_1.Node.isSourceFile(cls.getParent()))
|
|
450
458
|
continue;
|
|
451
459
|
const described = this.describeIndexedNode(cls);
|
|
@@ -454,7 +462,6 @@ class AstParserService {
|
|
|
454
462
|
const posNode = (ts_morph_1.Node.isClassDeclaration(cls) ? cls.getNameNode() : undefined) ?? cls;
|
|
455
463
|
push(cls, posNode, described.name, 'class', described.container, false, true);
|
|
456
464
|
}
|
|
457
|
-
// Class / object-literal methods, accessors, constructors, function properties.
|
|
458
465
|
for (const m of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.MethodDeclaration)) {
|
|
459
466
|
const described = this.describeIndexedNode(m);
|
|
460
467
|
if (!described)
|
|
@@ -492,7 +499,6 @@ class AstParserService {
|
|
|
492
499
|
continue;
|
|
493
500
|
push(p, p.getNameNode(), described.name, 'function_property', described.container, false, true);
|
|
494
501
|
}
|
|
495
|
-
// Interface / type-literal method and function-typed property signatures.
|
|
496
502
|
for (const s of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.MethodSignature)) {
|
|
497
503
|
const described = this.describeIndexedNode(s);
|
|
498
504
|
if (!described)
|
|
@@ -505,7 +511,6 @@ class AstParserService {
|
|
|
505
511
|
continue;
|
|
506
512
|
push(s, s.getNameNode(), described.name, 'method_signature', described.container, false, false);
|
|
507
513
|
}
|
|
508
|
-
// Declarations directly in a namespace body.
|
|
509
514
|
for (const fn of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.FunctionDeclaration)) {
|
|
510
515
|
if (!ts_morph_1.Node.isModuleBlock(fn.getParent()))
|
|
511
516
|
continue;
|
|
@@ -545,21 +550,327 @@ class AstParserService {
|
|
|
545
550
|
}
|
|
546
551
|
return candidates;
|
|
547
552
|
}
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
553
|
+
buildDeclarations(sourceFile, filePath, symbolCandidates, memberCandidates) {
|
|
554
|
+
try {
|
|
555
|
+
return this.declarationBuilder.build(sourceFile, [...symbolCandidates, ...memberCandidates]);
|
|
556
|
+
}
|
|
557
|
+
catch (err) {
|
|
558
|
+
this.logger.warn('AstParser: declaration tree build failed', { filePath, err });
|
|
559
|
+
try {
|
|
560
|
+
return this.declarationBuilder.flat(symbolCandidates);
|
|
561
|
+
}
|
|
562
|
+
catch {
|
|
563
|
+
return DeclarationTree_1.DeclarationTree.EMPTY;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
buildGraphqlImportMap(sourceFile) {
|
|
568
|
+
const map = new Map();
|
|
569
|
+
for (const decl of sourceFile.getImportDeclarations()) {
|
|
570
|
+
if (decl.getModuleSpecifierValue() !== GRAPHQL_MODULE)
|
|
571
|
+
continue;
|
|
572
|
+
for (const named of decl.getNamedImports()) {
|
|
573
|
+
const exportedName = named.getName();
|
|
574
|
+
const localName = named.getAliasNode()?.getText() ?? exportedName;
|
|
575
|
+
map.set(localName, exportedName);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return map;
|
|
579
|
+
}
|
|
580
|
+
static decoratorIdentifier(dec) {
|
|
581
|
+
const expr = dec.getExpression();
|
|
582
|
+
if (ts_morph_1.Node.isCallExpression(expr)) {
|
|
583
|
+
const callee = expr.getExpression();
|
|
584
|
+
return ts_morph_1.Node.isIdentifier(callee) ? callee : undefined;
|
|
585
|
+
}
|
|
586
|
+
return ts_morph_1.Node.isIdentifier(expr) ? expr : undefined;
|
|
587
|
+
}
|
|
588
|
+
recognizedDecorator(dec, importMap) {
|
|
589
|
+
const idNode = AstParserService.decoratorIdentifier(dec);
|
|
590
|
+
if (!idNode)
|
|
591
|
+
return null;
|
|
592
|
+
const exported = importMap.get(idNode.getText());
|
|
593
|
+
if (!exported)
|
|
594
|
+
return null;
|
|
595
|
+
const expr = dec.getExpression();
|
|
596
|
+
const args = ts_morph_1.Node.isCallExpression(expr) ? expr.getArguments() : [];
|
|
597
|
+
return { name: exported, args };
|
|
598
|
+
}
|
|
599
|
+
static literalStringValue(node) {
|
|
600
|
+
if (ts_morph_1.Node.isStringLiteral(node) || ts_morph_1.Node.isNoSubstitutionTemplateLiteral(node))
|
|
601
|
+
return node.getLiteralValue();
|
|
602
|
+
return null;
|
|
603
|
+
}
|
|
604
|
+
static checkGraphqlGrammar(name) {
|
|
605
|
+
return GRAPHQL_NAME_RE.test(name) ? { ok: true, name } : { ok: false, reason: 'invalid-name', value: name };
|
|
606
|
+
}
|
|
607
|
+
static isAbstractGraphqlType(args) {
|
|
608
|
+
return args.some((arg) => {
|
|
609
|
+
if (!ts_morph_1.Node.isObjectLiteralExpression(arg))
|
|
610
|
+
return false;
|
|
611
|
+
const prop = arg.getProperties().find((p) => {
|
|
612
|
+
if (!ts_morph_1.Node.isPropertyAssignment(p))
|
|
613
|
+
return false;
|
|
614
|
+
const nameNode = p.getNameNode();
|
|
615
|
+
if (ts_morph_1.Node.isIdentifier(nameNode))
|
|
616
|
+
return nameNode.getText() === 'isAbstract';
|
|
617
|
+
if (ts_morph_1.Node.isStringLiteral(nameNode))
|
|
618
|
+
return nameNode.getLiteralValue() === 'isAbstract';
|
|
619
|
+
return false;
|
|
620
|
+
});
|
|
621
|
+
const init = prop?.getInitializer();
|
|
622
|
+
return init !== undefined && ts_morph_1.Node.isTrueLiteral(init);
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
static findNameProperty(obj) {
|
|
626
|
+
return obj.getProperties().find((p) => {
|
|
627
|
+
if (!ts_morph_1.Node.isPropertyAssignment(p) && !ts_morph_1.Node.isShorthandPropertyAssignment(p))
|
|
628
|
+
return false;
|
|
629
|
+
const nameNode = p.getNameNode();
|
|
630
|
+
if (ts_morph_1.Node.isIdentifier(nameNode))
|
|
631
|
+
return nameNode.getText() === 'name';
|
|
632
|
+
if (ts_morph_1.Node.isStringLiteral(nameNode))
|
|
633
|
+
return nameNode.getLiteralValue() === 'name';
|
|
634
|
+
return false;
|
|
635
|
+
});
|
|
636
|
+
}
|
|
637
|
+
static peekThroughWrapper(node) {
|
|
638
|
+
let current = node;
|
|
639
|
+
let wrapped = false;
|
|
640
|
+
while (true) {
|
|
641
|
+
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)) {
|
|
642
|
+
current = current.getExpression();
|
|
643
|
+
wrapped = true;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
break;
|
|
647
|
+
}
|
|
648
|
+
return { inner: current, wrapped };
|
|
649
|
+
}
|
|
650
|
+
deriveGraphqlName(args, allowOptionsName, fallbackName) {
|
|
651
|
+
if (args.length > 0) {
|
|
652
|
+
const first = args[0];
|
|
653
|
+
const { inner, wrapped } = AstParserService.peekThroughWrapper(first);
|
|
654
|
+
const literal = AstParserService.literalStringValue(inner);
|
|
655
|
+
if (literal !== null) {
|
|
656
|
+
if (wrapped)
|
|
657
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
658
|
+
return AstParserService.checkGraphqlGrammar(literal);
|
|
659
|
+
}
|
|
660
|
+
const isTypeFnOrOptions = ts_morph_1.Node.isArrowFunction(inner) || ts_morph_1.Node.isFunctionExpression(inner) || ts_morph_1.Node.isObjectLiteralExpression(inner);
|
|
661
|
+
if (!isTypeFnOrOptions)
|
|
662
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
663
|
+
}
|
|
664
|
+
if (allowOptionsName) {
|
|
665
|
+
for (const arg of args) {
|
|
666
|
+
const { inner, wrapped } = AstParserService.peekThroughWrapper(arg);
|
|
667
|
+
if (!ts_morph_1.Node.isObjectLiteralExpression(inner))
|
|
668
|
+
continue;
|
|
669
|
+
if (wrapped)
|
|
670
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
671
|
+
const nameProp = AstParserService.findNameProperty(inner);
|
|
672
|
+
if (!nameProp)
|
|
673
|
+
continue;
|
|
674
|
+
if (ts_morph_1.Node.isShorthandPropertyAssignment(nameProp))
|
|
675
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
676
|
+
const init = nameProp.getInitializer();
|
|
677
|
+
const value = init ? AstParserService.literalStringValue(init) : null;
|
|
678
|
+
if (value !== null)
|
|
679
|
+
return AstParserService.checkGraphqlGrammar(value);
|
|
680
|
+
return { ok: false, reason: 'non-literal-name' };
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
return AstParserService.checkGraphqlGrammar(fallbackName);
|
|
684
|
+
}
|
|
685
|
+
logGraphqlSkip(filePath, declaration, decorator, reason, value) {
|
|
686
|
+
const fields = { filePath, declaration, decorator, reason };
|
|
687
|
+
if (value !== undefined)
|
|
688
|
+
fields.value = value;
|
|
689
|
+
this.logger.debug('AstParser: graphql row skipped', fields);
|
|
690
|
+
}
|
|
691
|
+
declarationNameFor(node, fallback) {
|
|
692
|
+
const described = this.describeIndexedNode(node);
|
|
693
|
+
return described ? this.qualifiedName(described) : fallback;
|
|
694
|
+
}
|
|
695
|
+
static extractResolverIdentifier(argNode) {
|
|
696
|
+
let target = argNode;
|
|
697
|
+
if (ts_morph_1.Node.isArrowFunction(target)) {
|
|
698
|
+
const body = target.getBody();
|
|
699
|
+
if (ts_morph_1.Node.isBlock(body))
|
|
700
|
+
return null;
|
|
701
|
+
target = body;
|
|
702
|
+
}
|
|
703
|
+
if (ts_morph_1.Node.isIdentifier(target))
|
|
704
|
+
return target;
|
|
705
|
+
if (ts_morph_1.Node.isArrayLiteralExpression(target)) {
|
|
706
|
+
const elements = target.getElements();
|
|
707
|
+
if (elements.length === 1 && ts_morph_1.Node.isIdentifier(elements[0]))
|
|
708
|
+
return elements[0];
|
|
709
|
+
}
|
|
710
|
+
return null;
|
|
711
|
+
}
|
|
712
|
+
defaultResolveDeclaration(identifier) {
|
|
713
|
+
const sym = identifier.getSymbol();
|
|
714
|
+
if (!sym)
|
|
715
|
+
return undefined;
|
|
716
|
+
const firstDecl = sym.getDeclarations()?.[0];
|
|
717
|
+
const resolved = firstDecl && ts_morph_1.Node.isImportSpecifier(firstDecl) ? (sym.getAliasedSymbol() ?? sym) : sym;
|
|
718
|
+
return resolved.getDeclarations()?.[0];
|
|
719
|
+
}
|
|
720
|
+
classGraphqlTypeName(declClass) {
|
|
721
|
+
const declImportMap = this.buildGraphqlImportMap(declClass.getSourceFile());
|
|
722
|
+
const decorators = declClass.getDecorators();
|
|
723
|
+
const hasAnyTypeSpelledDecorator = decorators.some((d) => {
|
|
724
|
+
const idNode = AstParserService.decoratorIdentifier(d);
|
|
725
|
+
const text = idNode?.getText();
|
|
726
|
+
return text === 'ObjectType' || text === 'InterfaceType';
|
|
727
|
+
});
|
|
728
|
+
const recognized = decorators
|
|
729
|
+
.map(d => this.recognizedDecorator(d, declImportMap))
|
|
730
|
+
.find(r => r?.name === 'ObjectType')
|
|
731
|
+
?? decorators.map(d => this.recognizedDecorator(d, declImportMap)).find(r => r?.name === 'InterfaceType');
|
|
732
|
+
if (recognized) {
|
|
733
|
+
const fallbackName = declClass.getName() ?? (declClass.isDefaultExport() ? 'default' : '');
|
|
734
|
+
const nameResult = this.deriveGraphqlName(recognized.args, false, fallbackName);
|
|
735
|
+
return nameResult.ok ? { ok: true, name: nameResult.name } : { ok: false };
|
|
736
|
+
}
|
|
737
|
+
if (hasAnyTypeSpelledDecorator)
|
|
738
|
+
return { ok: false };
|
|
739
|
+
const name = declClass.getName();
|
|
740
|
+
if (!name)
|
|
741
|
+
return { ok: false };
|
|
742
|
+
return GRAPHQL_NAME_RE.test(name) ? { ok: true, name } : { ok: false, value: name };
|
|
743
|
+
}
|
|
744
|
+
resolveResolverParentType(cls, importMap) {
|
|
745
|
+
const resolverDec = cls.getDecorators()
|
|
746
|
+
.map(d => this.recognizedDecorator(d, importMap))
|
|
747
|
+
.find(r => r?.name === 'Resolver');
|
|
748
|
+
if (!resolverDec || resolverDec.args.length === 0)
|
|
749
|
+
return { ok: false };
|
|
750
|
+
const argNode = resolverDec.args[0];
|
|
751
|
+
const literal = AstParserService.literalStringValue(argNode);
|
|
752
|
+
if (literal !== null) {
|
|
753
|
+
return GRAPHQL_NAME_RE.test(literal) ? { ok: true, name: literal } : { ok: false, value: literal };
|
|
754
|
+
}
|
|
755
|
+
const identifier = AstParserService.extractResolverIdentifier(argNode);
|
|
756
|
+
if (!identifier)
|
|
757
|
+
return { ok: false };
|
|
758
|
+
const resolve = this.deps.resolveDeclaration ?? this.defaultResolveDeclaration.bind(this);
|
|
759
|
+
const declaration = resolve(identifier);
|
|
760
|
+
if (!declaration || !ts_morph_1.Node.isClassDeclaration(declaration))
|
|
761
|
+
return { ok: false };
|
|
762
|
+
return this.classGraphqlTypeName(declaration);
|
|
763
|
+
}
|
|
764
|
+
extractClassTypeRows(cls, importMap, ownByDeclNode, filePath, serviceName, entries) {
|
|
765
|
+
const own = ownByDeclNode.get(cls);
|
|
766
|
+
const declaration = this.declarationNameFor(cls, own?.entry.name ?? cls.getName() ?? 'default');
|
|
767
|
+
const seenNames = new Set();
|
|
768
|
+
for (const dec of cls.getDecorators()) {
|
|
769
|
+
const rec = this.recognizedDecorator(dec, importMap);
|
|
770
|
+
if (!rec || !GRAPHQL_TYPE_DECORATORS.has(rec.name))
|
|
771
|
+
continue;
|
|
772
|
+
if (AstParserService.isAbstractGraphqlType(rec.args)) {
|
|
773
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, 'abstract-type');
|
|
774
|
+
continue;
|
|
775
|
+
}
|
|
776
|
+
if (!own) {
|
|
777
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, 'no-symbol-row');
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
const nameResult = this.deriveGraphqlName(rec.args, false, own.entry.name);
|
|
781
|
+
if (!nameResult.ok) {
|
|
782
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
|
|
783
|
+
continue;
|
|
784
|
+
}
|
|
785
|
+
if (seenNames.has(nameResult.name)) {
|
|
786
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, 'duplicate', own.entry.name);
|
|
787
|
+
continue;
|
|
788
|
+
}
|
|
789
|
+
seenNames.add(nameResult.name);
|
|
790
|
+
entries.push({
|
|
791
|
+
project: serviceName, filePath, name: nameResult.name, kind: 'graphql_type',
|
|
792
|
+
signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
|
|
793
|
+
container: null, isStatic: false, target: declaration,
|
|
794
|
+
});
|
|
795
|
+
}
|
|
796
|
+
}
|
|
797
|
+
extractResolveFieldRow(method, rec, own, declaration, importMap, filePath, serviceName, entries) {
|
|
798
|
+
if (!own) {
|
|
799
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'no-symbol-row');
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
const cls = method.getParent();
|
|
803
|
+
if (!cls || !ts_morph_1.Node.isClassDeclaration(cls)) {
|
|
804
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent');
|
|
805
|
+
return;
|
|
806
|
+
}
|
|
807
|
+
const parent = this.resolveResolverParentType(cls, importMap);
|
|
808
|
+
if (!parent.ok) {
|
|
809
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent', parent.value);
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const nameResult = this.deriveGraphqlName(rec.args, true, own.entry.name);
|
|
813
|
+
if (!nameResult.ok) {
|
|
814
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
entries.push({
|
|
818
|
+
project: serviceName, filePath, name: nameResult.name, kind: 'graphql_field',
|
|
819
|
+
signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
|
|
820
|
+
container: parent.name, isStatic: own.entry.isStatic, target: declaration,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
extractMethodOperationRows(method, importMap, ownByDeclNode, filePath, serviceName, entries) {
|
|
824
|
+
const own = ownByDeclNode.get(method);
|
|
825
|
+
const declaration = this.declarationNameFor(method, own?.entry.name ?? AstParserService.memberName(method.getNameNode()) ?? '<anonymous>');
|
|
826
|
+
for (const dec of method.getDecorators()) {
|
|
827
|
+
const rec = this.recognizedDecorator(dec, importMap);
|
|
828
|
+
if (!rec)
|
|
829
|
+
continue;
|
|
830
|
+
if (rec.name === 'ResolveField') {
|
|
831
|
+
try {
|
|
832
|
+
this.extractResolveFieldRow(method, rec, own, declaration, importMap, filePath, serviceName, entries);
|
|
833
|
+
}
|
|
834
|
+
catch (err) {
|
|
835
|
+
this.logGraphqlSkip(filePath, declaration, 'ResolveField', 'unresolvable-parent');
|
|
836
|
+
}
|
|
837
|
+
continue;
|
|
838
|
+
}
|
|
839
|
+
const kind = GRAPHQL_OPERATION_KIND[rec.name];
|
|
840
|
+
if (!kind)
|
|
841
|
+
continue;
|
|
842
|
+
if (!own) {
|
|
843
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, 'no-symbol-row');
|
|
844
|
+
continue;
|
|
845
|
+
}
|
|
846
|
+
const nameResult = this.deriveGraphqlName(rec.args, true, own.entry.name);
|
|
847
|
+
if (!nameResult.ok) {
|
|
848
|
+
this.logGraphqlSkip(filePath, declaration, rec.name, nameResult.reason, nameResult.reason === 'invalid-name' ? nameResult.value : undefined);
|
|
849
|
+
continue;
|
|
850
|
+
}
|
|
851
|
+
entries.push({
|
|
852
|
+
project: serviceName, filePath, name: nameResult.name, kind,
|
|
853
|
+
signature: own.entry.signature, line: own.entry.line, endLine: own.entry.endLine, col: own.entry.col,
|
|
854
|
+
container: rec.name, isStatic: own.entry.isStatic, target: declaration,
|
|
855
|
+
});
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
extractGraphqlNames(sourceFile, filePath, serviceName, ownRows) {
|
|
859
|
+
const importMap = this.buildGraphqlImportMap(sourceFile);
|
|
860
|
+
if (importMap.size === 0)
|
|
861
|
+
return [];
|
|
862
|
+
const ownByDeclNode = new Map();
|
|
863
|
+
for (const c of ownRows)
|
|
864
|
+
ownByDeclNode.set(c.declNode, c);
|
|
865
|
+
const entries = [];
|
|
866
|
+
for (const cls of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.ClassDeclaration)) {
|
|
867
|
+
this.extractClassTypeRows(cls, importMap, ownByDeclNode, filePath, serviceName, entries);
|
|
868
|
+
}
|
|
869
|
+
for (const method of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.MethodDeclaration)) {
|
|
870
|
+
this.extractMethodOperationRows(method, importMap, ownByDeclNode, filePath, serviceName, entries);
|
|
871
|
+
}
|
|
872
|
+
return entries;
|
|
873
|
+
}
|
|
563
874
|
static isValidSegment(s) {
|
|
564
875
|
return VALID_SEGMENT.test(s);
|
|
565
876
|
}
|
|
@@ -593,7 +904,6 @@ class AstParserService {
|
|
|
593
904
|
return current;
|
|
594
905
|
}
|
|
595
906
|
isModuleOrNamespaceLevelVariable(varDecl) {
|
|
596
|
-
// VariableDeclaration -> VariableDeclarationList -> VariableStatement -> (SourceFile | ModuleBlock)
|
|
597
907
|
const stmt = varDecl.getParent()?.getParent();
|
|
598
908
|
const stmtParent = stmt?.getParent();
|
|
599
909
|
return ts_morph_1.Node.isSourceFile(stmtParent) || ts_morph_1.Node.isModuleBlock(stmtParent);
|
|
@@ -626,9 +936,6 @@ class AstParserService {
|
|
|
626
936
|
isTopLevelOrNamespaceDirect(node) {
|
|
627
937
|
return ts_morph_1.Node.isSourceFile(node) || ts_morph_1.Node.isModuleBlock(node);
|
|
628
938
|
}
|
|
629
|
-
/** One step of the ancestor walk used by `containerSegments`: does `node`
|
|
630
|
-
* contribute a named segment, pass through transparently, or disqualify
|
|
631
|
-
* (exclude) everything nested inside it? */
|
|
632
939
|
walkStep(node) {
|
|
633
940
|
if (ts_morph_1.Node.isModuleDeclaration(node)) {
|
|
634
941
|
if (node.compilerNode.flags & ts_morph_1.NodeFlags.GlobalAugmentation)
|
|
@@ -681,10 +988,6 @@ class AstParserService {
|
|
|
681
988
|
}
|
|
682
989
|
return { kind: 'transparent' };
|
|
683
990
|
}
|
|
684
|
-
/** The dotted container path of `node`'s enclosing named scopes, outermost
|
|
685
|
-
* first, or null when any contributing ancestor is invalid (a computed /
|
|
686
|
-
* string-literal segment, a string-named module, `declare global`, or an
|
|
687
|
-
* unrooted class expression / object literal). */
|
|
688
991
|
containerSegments(node) {
|
|
689
992
|
const segments = [];
|
|
690
993
|
let current = node.getParent();
|
|
@@ -703,10 +1006,6 @@ class AstParserService {
|
|
|
703
1006
|
}
|
|
704
1007
|
return segments;
|
|
705
1008
|
}
|
|
706
|
-
/** If `node` is a declaration this service indexes (top-level symbol, member,
|
|
707
|
-
* or collected class), its own name + container. Otherwise null. Shared by
|
|
708
|
-
* member extraction, ownership ranges and the call graph so all three name a
|
|
709
|
-
* declaration identically. */
|
|
710
1009
|
describeIndexedNode(node) {
|
|
711
1010
|
if (ts_morph_1.Node.isClassDeclaration(node)) {
|
|
712
1011
|
const name = node.getName() ?? (node.isDefaultExport() ? 'default' : null);
|
|
@@ -799,9 +1098,6 @@ class AstParserService {
|
|
|
799
1098
|
qualifiedName(described) {
|
|
800
1099
|
return described.container.length ? `${described.container.join('.')}.${described.name}` : described.name;
|
|
801
1100
|
}
|
|
802
|
-
/** The qualified name of the nearest ancestor of `node` that is itself an
|
|
803
|
-
* indexed declaration (method, accessor, constructor, function property,
|
|
804
|
-
* namespace/top-level function, or module-/namespace-level variable). */
|
|
805
1101
|
findFromSymbol(node) {
|
|
806
1102
|
let current = node.getParent();
|
|
807
1103
|
while (current) {
|
|
@@ -812,7 +1108,6 @@ class AstParserService {
|
|
|
812
1108
|
}
|
|
813
1109
|
return null;
|
|
814
1110
|
}
|
|
815
|
-
// ── private: call graph extraction ────────────────────────────────────────
|
|
816
1111
|
extractCalls(sourceFile, filePath, serviceName) {
|
|
817
1112
|
const calls = [];
|
|
818
1113
|
for (const callExpr of sourceFile.getDescendantsOfKind(ts_morph_1.SyntaxKind.CallExpression)) {
|
|
@@ -835,7 +1130,6 @@ class AstParserService {
|
|
|
835
1130
|
calleeName = expr.getText();
|
|
836
1131
|
const sym = expr.getSymbol();
|
|
837
1132
|
if (sym) {
|
|
838
|
-
// Imported identifiers resolve to ImportSpecifier (same file); follow the alias.
|
|
839
1133
|
const firstDecl = sym.getDeclarations()?.[0];
|
|
840
1134
|
const resolved = firstDecl && ts_morph_1.Node.isImportSpecifier(firstDecl)
|
|
841
1135
|
? (sym.getAliasedSymbol() ?? sym)
|
|
@@ -850,7 +1144,6 @@ class AstParserService {
|
|
|
850
1144
|
}
|
|
851
1145
|
}
|
|
852
1146
|
catch {
|
|
853
|
-
// type resolution failures are normal — skip this call
|
|
854
1147
|
}
|
|
855
1148
|
if (!calleeName || !calleeFile)
|
|
856
1149
|
continue;
|