autosnippet 3.2.7 → 3.2.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/bin/cli.js +7 -0
  2. package/dashboard/dist/assets/index-D5jiDBQG.css +1 -0
  3. package/dashboard/dist/assets/{index-DfHY_3ln.js → index-e5OKj-Ni.js} +38 -38
  4. package/dashboard/dist/index.html +2 -2
  5. package/lib/cli/AiScanService.js +3 -3
  6. package/lib/core/AstAnalyzer.js +26 -4
  7. package/lib/core/analysis/CallEdgeResolver.js +402 -0
  8. package/lib/core/analysis/CallGraphAnalyzer.js +367 -0
  9. package/lib/core/analysis/CallSiteExtractor.js +629 -0
  10. package/lib/core/analysis/DataFlowInferrer.js +57 -0
  11. package/lib/core/analysis/ImportPathResolver.js +189 -0
  12. package/lib/core/analysis/ImportRecord.js +105 -0
  13. package/lib/core/analysis/SymbolTableBuilder.js +211 -0
  14. package/lib/core/ast/ProjectGraph.js +8 -0
  15. package/lib/core/ast/lang-dart.js +352 -5
  16. package/lib/core/ast/lang-go.js +212 -10
  17. package/lib/core/ast/lang-java.js +205 -1
  18. package/lib/core/ast/lang-kotlin.js +330 -1
  19. package/lib/core/ast/lang-python.js +31 -2
  20. package/lib/core/ast/lang-rust.js +284 -3
  21. package/lib/core/ast/lang-swift.js +180 -1
  22. package/lib/core/ast/lang-typescript.js +290 -1
  23. package/lib/external/mcp/McpServer.js +1 -0
  24. package/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +21 -0
  25. package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +5 -4
  26. package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +2 -1
  27. package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +70 -4
  28. package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +95 -1
  29. package/lib/external/mcp/handlers/bootstrap-external.js +9 -2
  30. package/lib/external/mcp/handlers/bootstrap-internal.js +17 -6
  31. package/lib/external/mcp/handlers/consolidated.js +9 -0
  32. package/lib/external/mcp/handlers/guard.js +3 -3
  33. package/lib/external/mcp/handlers/structure.js +62 -0
  34. package/lib/external/mcp/handlers/wiki-external.js +66 -3
  35. package/lib/external/mcp/tools.js +36 -1
  36. package/lib/http/routes/remote.js +15 -15
  37. package/lib/injection/ServiceContainer.js +6 -11
  38. package/lib/platform/ios/index.js +2 -2
  39. package/lib/platform/ios/spm/PackageSwiftParser.js +14 -3
  40. package/lib/platform/ios/spm/SpmDiscoverer.js +123 -17
  41. package/lib/platform/ios/spm/{SpmService.js → SpmHelper.js} +43 -675
  42. package/lib/platform/ios/xcode/XcodeWriteUtils.js +1 -1
  43. package/lib/service/chat/ChatAgent.js +1 -1
  44. package/lib/service/chat/ChatAgentPrompts.js +13 -1
  45. package/lib/service/chat/ExplorationTracker.js +52 -8
  46. package/lib/service/chat/HandoffProtocol.js +19 -1
  47. package/lib/service/chat/WorkingMemory.js +3 -1
  48. package/lib/service/chat/memory/ActiveContext.js +3 -1
  49. package/lib/service/chat/memory/SessionStore.js +4 -3
  50. package/lib/service/chat/tools/ast-graph.js +229 -32
  51. package/lib/service/chat/tools/index.js +6 -1
  52. package/lib/service/chat/tools/infrastructure.js +5 -0
  53. package/lib/service/cursor/CursorDeliveryPipeline.js +167 -1
  54. package/lib/service/knowledge/CodeEntityGraph.js +327 -2
  55. package/lib/service/knowledge/KnowledgeService.js +5 -1
  56. package/lib/service/module/ModuleService.js +9 -0
  57. package/lib/service/wiki/WikiGenerator.js +1 -1
  58. package/lib/shared/PathGuard.js +1 -1
  59. package/package.json +1 -1
  60. package/dashboard/dist/assets/index-BaGY7kJI.css +0 -1
@@ -5,13 +5,13 @@
5
5
  <link rel="icon" type="image/svg+xml" href="/vite.svg" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>AutoSnippet Dashboard</title>
8
- <script type="module" crossorigin src="/assets/index-DfHY_3ln.js"></script>
8
+ <script type="module" crossorigin src="/assets/index-e5OKj-Ni.js"></script>
9
9
  <link rel="modulepreload" crossorigin href="/assets/yaml-qRaU8Ldn.js">
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-Ck-HBmg5.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/axios-C0Zqfgkc.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/icons-pSac4wYO.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/framer-motion-DOATyqla.js">
14
- <link rel="stylesheet" crossorigin href="/assets/index-BaGY7kJI.css">
14
+ <link rel="stylesheet" crossorigin href="/assets/index-D5jiDBQG.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -145,14 +145,14 @@ export class AiScanService {
145
145
  const files = [];
146
146
 
147
147
  try {
148
- // 优先使用 ModuleService(多语言统一入口),回退到 SpmService
148
+ // 优先使用 ModuleService(多语言统一入口),回退到 SpmHelper
149
149
  let service;
150
150
  try {
151
151
  const { ModuleService } = await import('../service/module/ModuleService.js');
152
152
  service = new ModuleService(this.projectRoot);
153
153
  } catch {
154
- const { SpmService } = await import('../platform/ios/spm/SpmService.js');
155
- service = new SpmService(this.projectRoot);
154
+ const { SpmHelper } = await import('../platform/ios/spm/SpmHelper.js');
155
+ service = new SpmHelper(this.projectRoot);
156
156
  }
157
157
  await service.load();
158
158
 
@@ -14,6 +14,7 @@
14
14
  */
15
15
 
16
16
  import { getParserClass, isParserReady } from './ast/parser-init.js';
17
+ import { getCallSiteExtractor, defaultExtractCallSites } from './analysis/CallSiteExtractor.js';
17
18
 
18
19
  // ──────────────────────────────────────────────────────────────────
19
20
  // 插件注册表
@@ -49,9 +50,11 @@ export function registerLanguage(langId, plugin) {
49
50
  * 分析单个源文件,返回结构化 AST 摘要
50
51
  * @param {string} source 源代码文本
51
52
  * @param {string} lang 语言标识 'objectivec' | 'swift' | 'typescript' | 'javascript' | 'python' | 'java' | 'kotlin' | 'go' | 'dart' | 'rust' | 'tsx'
53
+ * @param {object} [options]
54
+ * @param {boolean} [options.extractCallSites=true] — 是否提取调用点 (Phase 5)
52
55
  * @returns {AstSummary | null}
53
56
  */
54
- function analyzeFile(source, lang) {
57
+ function analyzeFile(source, lang, options = {}) {
55
58
  const plugin = _langPlugins.get(lang);
56
59
  if (!plugin) {
57
60
  return null; // 无插件 → 优雅降级
@@ -74,10 +77,23 @@ function analyzeFile(source, lang) {
74
77
  patterns: [],
75
78
  imports: [],
76
79
  exports: [],
80
+ // ─── Phase 5 新增 ───
81
+ callSites: [],
82
+ references: [],
77
83
  };
78
84
 
79
85
  plugin.walk(root, ctx);
80
86
 
87
+ // Phase 5: 可选的 call site 提取 pass (post-walk extraction)
88
+ if (options.extractCallSites !== false) {
89
+ const extractor = plugin.extractCallSites || getCallSiteExtractor(lang) || defaultExtractCallSites;
90
+ try {
91
+ extractor(root, ctx, lang);
92
+ } catch (_e) {
93
+ // Call site extraction failure is non-fatal — degrade gracefully
94
+ }
95
+ }
96
+
81
97
  // 构建继承图谱
82
98
  const inheritanceGraph = _buildInheritanceGraph(ctx.classes, ctx.protocols, ctx.categories);
83
99
 
@@ -100,6 +116,8 @@ function analyzeFile(source, lang) {
100
116
  patterns: ctx.patterns,
101
117
  imports: ctx.imports,
102
118
  exports: ctx.exports,
119
+ callSites: ctx.callSites,
120
+ references: ctx.references,
103
121
  inheritanceGraph,
104
122
  metrics,
105
123
  };
@@ -433,14 +451,18 @@ function _buildInheritanceGraph(classes, protocols, categories) {
433
451
  }
434
452
 
435
453
  for (const cat of categories) {
454
+ // 兼容 ObjC category (className/categoryName) 和 Dart extension (name/targetClass)
455
+ const catClassName = cat.className || cat.targetClass;
456
+ const catCategoryName = cat.categoryName || cat.name;
457
+ if (!catClassName) continue; // 跳过无法确定目标类的 category
436
458
  edges.push({
437
- from: `${cat.className}(${cat.categoryName})`,
438
- to: cat.className,
459
+ from: `${catClassName}(${catCategoryName})`,
460
+ to: catClassName,
439
461
  type: 'extends',
440
462
  });
441
463
  if (cat.protocols) {
442
464
  for (const proto of cat.protocols) {
443
- edges.push({ from: cat.className, to: proto, type: 'conforms' });
465
+ edges.push({ from: catClassName, to: proto, type: 'conforms' });
444
466
  }
445
467
  }
446
468
  }
@@ -0,0 +1,402 @@
1
+ /**
2
+ * @module CallEdgeResolver
3
+ * @description Phase 5: 将调用点 (CallSite) 解析为调用边 (ResolvedEdge)
4
+ *
5
+ * 解析优先级 (4-priority system):
6
+ * 1. this.xxx() — 同类方法调用
7
+ * 2. ImportedType.method() / importedFunc() — import-based 解析
8
+ * 3. localFunc() — 同文件内函数调用
9
+ * 4. globalSearch(name) — 全局唯一匹配 (fallback, 低置信度)
10
+ *
11
+ * 数据流:
12
+ * SymbolTable + ImportPathResolver + CallSite[] → ResolvedEdge[]
13
+ */
14
+
15
+ /**
16
+ * @typedef {object} ResolvedEdge
17
+ * @property {string} caller — 调用者 FQN e.g. "src/service/UserService.ts::UserService.getUser"
18
+ * @property {string} callee — 被调用者 FQN e.g. "src/repository/UserRepo.ts::UserRepo.findById"
19
+ * @property {string} callType — 'function'|'method'|'constructor'|'super'|'static'
20
+ * @property {string} resolveMethod — 'direct'|'inferred'|'cha'
21
+ * @property {number} line — 调用点行号
22
+ * @property {string} file — 调用者文件
23
+ * @property {boolean} isAwait
24
+ */
25
+
26
+ export class CallEdgeResolver {
27
+ /**
28
+ * @param {import('./SymbolTableBuilder.js').SymbolTable} symbolTable
29
+ * @param {import('./ImportPathResolver.js').ImportPathResolver} importResolver
30
+ * @param {Array<{from: string, to: string, type: string}>} [inheritanceGraph=[]] — 继承图边
31
+ */
32
+ constructor(symbolTable, importResolver, inheritanceGraph = []) {
33
+ this.symbolTable = symbolTable;
34
+ this.importResolver = importResolver;
35
+ this.inheritanceGraph = inheritanceGraph;
36
+
37
+ // Phase 5.3: RTA — set of classes that are actually instantiated in the program
38
+ this.instantiatedClasses = symbolTable.instantiatedClasses || new Set();
39
+ // Phase 5.3: DI — property type annotations: className → (fieldName → typeName)
40
+ this.propertyTypes = symbolTable.propertyTypes || new Map();
41
+
42
+ // 构建反向索引: symbolName → [fqn1, fqn2, ...]
43
+ /** @type {Map<string, string[]>} */
44
+ this.nameIndex = new Map();
45
+ // 构建文件级索引: file → [{ name, qualifiedName, fqn }] (Issue #14 性能优化)
46
+ /** @type {Map<string, Array<{name: string, qualifiedName: string, fqn: string}>>} */
47
+ this.fileIndex = new Map();
48
+
49
+ // Phase 5.3: 类名集合索引 (用于 _inferFieldType 优化,避免全表扫描)
50
+ /** @type {Set<string>} */
51
+ this.classNames = new Set();
52
+
53
+ for (const [fqn, decl] of symbolTable.declarations) {
54
+ const names = [decl.name];
55
+ const qualifiedName = decl.className ? `${decl.className}.${decl.name}` : decl.name;
56
+ if (decl.className) {
57
+ names.push(qualifiedName);
58
+ }
59
+ for (const name of names) {
60
+ if (!this.nameIndex.has(name)) this.nameIndex.set(name, []);
61
+ this.nameIndex.get(name).push(fqn);
62
+ }
63
+ // 文件级索引
64
+ if (!this.fileIndex.has(decl.file)) this.fileIndex.set(decl.file, []);
65
+ this.fileIndex.get(decl.file).push({ name: decl.name, qualifiedName, fqn });
66
+
67
+ // Phase 5.3: 收集类名用于快速 DI 推断
68
+ if (decl.kind === 'class') {
69
+ this.classNames.add(decl.name);
70
+ }
71
+ }
72
+ }
73
+
74
+ /**
75
+ * 解析一个文件中的所有调用点为边
76
+ *
77
+ * @param {import('./CallSiteExtractor.js').CallSite[]} callSites — 来自某个文件的所有调用点
78
+ * @param {string} callerFile — 调用者文件路径 (相对)
79
+ * @returns {ResolvedEdge[]}
80
+ */
81
+ resolveFile(callSites, callerFile) {
82
+ const edges = [];
83
+ const fileImports = this.symbolTable.fileImports.get(callerFile) || [];
84
+
85
+ // 构建局部 import 映射: symbolName → { file, namespace }
86
+ const importedSymbols = this._buildImportMap(fileImports, callerFile);
87
+
88
+ // 去重集合: "caller→callee@line" 防止同一调用点产生重复边
89
+ const seen = new Set();
90
+
91
+ for (const cs of callSites) {
92
+ const resolved = this._resolveCallSite(cs, callerFile, importedSymbols);
93
+ if (resolved) {
94
+ const key = `${resolved.caller}→${resolved.callee}@${resolved.line}`;
95
+ if (!seen.has(key)) {
96
+ seen.add(key);
97
+ edges.push(resolved);
98
+ }
99
+ }
100
+ }
101
+
102
+ return edges;
103
+ }
104
+
105
+ /**
106
+ * @private 构建局部 import 映射
107
+ */
108
+ _buildImportMap(fileImports, callerFile) {
109
+ /** @type {Map<string, { file: string, namespace: boolean }>} */
110
+ const importedSymbols = new Map();
111
+
112
+ for (const imp of fileImports) {
113
+ const targetFile = this.importResolver.resolve(imp.path || String(imp), callerFile);
114
+ if (!targetFile) continue; // 外部依赖, 跳过
115
+
116
+ if (imp.symbols && imp.symbols.length > 0) {
117
+ for (const sym of imp.symbols) {
118
+ if (sym === '*' && imp.alias) {
119
+ importedSymbols.set(imp.alias, { file: targetFile, namespace: true });
120
+ } else if (sym !== '*') {
121
+ // named/default: symbols 已包含本地名 (alias baked-in), 直接使用
122
+ importedSymbols.set(sym, { file: targetFile, namespace: false });
123
+ }
124
+ }
125
+ } else {
126
+ // 无结构化信息时,使用路径最后一段作为 namespace hint
127
+ const pathParts = String(imp).split('/');
128
+ const lastPart = pathParts[pathParts.length - 1]?.replace(/\.\w+$/, '');
129
+ if (lastPart) {
130
+ importedSymbols.set(lastPart, { file: targetFile, namespace: true });
131
+ }
132
+ }
133
+ }
134
+
135
+ return importedSymbols;
136
+ }
137
+
138
+ /**
139
+ * @private 解析单个调用点
140
+ */
141
+ _resolveCallSite(cs, callerFile, importedSymbols) {
142
+ const callerFqn = `${callerFile}::${cs.callerClass ? `${cs.callerClass}.` : ''}${cs.callerMethod}`;
143
+
144
+ // Priority 0: super.xxx() — 父类方法调用 (CHA 解析,禁止 fallthrough 防止自引用边)
145
+ if (cs.callType === 'super' || cs.receiver === 'super' || cs.receiver === 'super()') {
146
+ if (cs.callerClass && cs.callee && cs.callee !== 'super') {
147
+ const chaResult = this._resolveByCHA(cs.callee, cs.callerClass);
148
+ if (chaResult) {
149
+ return this._makeEdge(callerFqn, chaResult, 'cha', cs, callerFile);
150
+ }
151
+ }
152
+ // CHA 无法解析时不 fallthrough (避免 local search 匹配到自己产生 self-edge)
153
+ return null;
154
+ }
155
+
156
+ // Priority 1: this.xxx() / self.xxx() — 同类方法调用
157
+ if (cs.receiver === 'this' || cs.receiver === 'self') {
158
+ if (cs.callerClass) {
159
+ const candidates = this._findInFile(`${cs.callerClass}.${cs.callee}`, callerFile);
160
+ if (candidates.length > 0) {
161
+ return this._makeEdge(callerFqn, candidates[0], 'direct', cs, callerFile);
162
+ }
163
+ // CHA fallback: 在继承链上查找方法
164
+ const chaResult = this._resolveByCHA(cs.callee, cs.callerClass);
165
+ if (chaResult) {
166
+ return this._makeEdge(callerFqn, chaResult, 'cha', cs, callerFile);
167
+ }
168
+ }
169
+ }
170
+
171
+ // Priority 1.5: this.field.method() — DI 注入字段方法调用
172
+ if (cs.receiver && (cs.receiver.startsWith('this.') || cs.receiver.startsWith('self.'))) {
173
+ const fieldName = cs.receiver.split('.').slice(1).join('.');
174
+
175
+ // Phase 5.3: First try explicit type annotation from property declarations (DI-aware)
176
+ if (cs.callerClass) {
177
+ const classProps = this.propertyTypes.get(cs.callerClass);
178
+ if (classProps) {
179
+ const fieldType = classProps.get(fieldName);
180
+ if (fieldType) {
181
+ const typeCandidates = this.nameIndex.get(`${fieldType}.${cs.callee}`) || [];
182
+ if (typeCandidates.length > 0) {
183
+ return this._makeEdge(callerFqn, typeCandidates[0], 'direct', cs, callerFile);
184
+ }
185
+ }
186
+ }
187
+ }
188
+
189
+ // 尝试从 receiverType 解析 (可能 extractCallSites 已推断)
190
+ if (cs.receiverType) {
191
+ const typeCandidates = this.nameIndex.get(`${cs.receiverType}.${cs.callee}`) || [];
192
+ if (typeCandidates.length > 0) {
193
+ return this._makeEdge(callerFqn, typeCandidates[0], 'direct', cs, callerFile);
194
+ }
195
+ }
196
+ // 尝试通过命名约定推断: userRepo → UserRepo, userService → UserService
197
+ const inferredType = this._inferFieldType(fieldName);
198
+ if (inferredType) {
199
+ const typeCandidates = this.nameIndex.get(`${inferredType}.${cs.callee}`) || [];
200
+ if (typeCandidates.length > 0) {
201
+ return this._makeEdge(callerFqn, typeCandidates[0], 'inferred', cs, callerFile);
202
+ }
203
+ }
204
+ }
205
+
206
+ // Priority 2: Import-based 解析
207
+ const importInfo = importedSymbols.get(cs.receiver || cs.callee);
208
+ if (importInfo) {
209
+ const targetFile = importInfo.file;
210
+
211
+ if (importInfo.namespace && cs.receiver) {
212
+ // namespace import: M.foo() → 在 targetFile 中查找 foo
213
+ const candidates = this._findInFile(cs.callee, targetFile);
214
+ if (candidates.length > 0) {
215
+ return this._makeEdge(callerFqn, candidates[0], 'direct', cs, callerFile);
216
+ }
217
+ } else {
218
+ // named import: 查找 import 的符号
219
+ const lookupName = cs.receiver
220
+ ? `${cs.receiver}.${cs.callee}`
221
+ : cs.callee;
222
+ let candidates = this._findInFile(lookupName, targetFile);
223
+ if (candidates.length === 0 && cs.receiver) {
224
+ // 可能 import 的是类名,方法是类的方法
225
+ candidates = this._findInFile(`${cs.receiver}.${cs.callee}`, targetFile);
226
+ }
227
+ if (candidates.length === 0 && !cs.receiver) {
228
+ // 可能是函数名
229
+ candidates = this._findInFile(cs.callee, targetFile);
230
+ }
231
+ if (candidates.length > 0) {
232
+ return this._makeEdge(callerFqn, candidates[0], 'direct', cs, callerFile);
233
+ }
234
+ }
235
+ }
236
+
237
+ // Priority 2.5: Implicit this — OOP 语言中 bare method() 即 this.method()
238
+ // 在 Dart/Java/Kotlin/Swift 等语言中, 类内调用 method() 等价于 this.method()
239
+ // 先查同类方法, 再 CHA 查父类方法
240
+ if (!cs.receiver && cs.callerClass && cs.callType !== 'constructor') {
241
+ // 2.5a: 同类方法 (精确匹配 Class.method)
242
+ const implicitThisCandidates = this._findInFile(
243
+ `${cs.callerClass}.${cs.callee}`, callerFile,
244
+ ).filter(fqn => fqn !== callerFqn);
245
+ if (implicitThisCandidates.length > 0) {
246
+ return this._makeEdge(callerFqn, implicitThisCandidates[0], 'direct', cs, callerFile);
247
+ }
248
+ // 2.5b: CHA 查父类 (继承链上的方法)
249
+ const chaImplicit = this._resolveByCHA(cs.callee, cs.callerClass);
250
+ if (chaImplicit) {
251
+ return this._makeEdge(callerFqn, chaImplicit, 'cha', cs, callerFile);
252
+ }
253
+ }
254
+
255
+ // Priority 3: 同文件内的函数调用
256
+ // 过滤 callerFqn 防止同名方法重载(overload)产生假自引用边
257
+ const localCandidates = this._findInFile(cs.callee, callerFile)
258
+ .filter(fqn => fqn !== callerFqn);
259
+ if (localCandidates.length > 0) {
260
+ return this._makeEdge(callerFqn, localCandidates[0], 'direct', cs, callerFile);
261
+ }
262
+ // 也尝试 Class.method 格式
263
+ if (cs.receiver && !importedSymbols.has(cs.receiver)) {
264
+ const qualifiedLocal = this._findInFile(`${cs.receiver}.${cs.callee}`, callerFile)
265
+ .filter(fqn => fqn !== callerFqn);
266
+ if (qualifiedLocal.length > 0) {
267
+ return this._makeEdge(callerFqn, qualifiedLocal[0], 'direct', cs, callerFile);
268
+ }
269
+ }
270
+
271
+ // Priority 4: 全局搜索 (唯一匹配才采用)
272
+ // 过滤 callerFqn 防止全局唯一命名碰撞自己
273
+ const globalCandidates = (this.nameIndex.get(cs.callee) || [])
274
+ .filter(fqn => fqn !== callerFqn);
275
+ if (globalCandidates.length === 1) {
276
+ return this._makeEdge(callerFqn, globalCandidates[0], 'inferred', cs, callerFile);
277
+ }
278
+
279
+ // Phase 5.3 RTA: 多个全局候选 → 用实例化集合过滤
280
+ if (globalCandidates.length > 1 && this.instantiatedClasses.size > 0) {
281
+ const rtaFiltered = globalCandidates.filter((fqn) => {
282
+ if (fqn === callerFqn) return false; // 排除自己
283
+ const decl = this.symbolTable.declarations.get(fqn);
284
+ if (!decl) return false;
285
+ // 非类方法 (顶层函数) 不做 RTA 过滤
286
+ if (!decl.className) return true;
287
+ // 类方法 → 仅保留实际实例化的类
288
+ return this.instantiatedClasses.has(decl.className);
289
+ });
290
+ if (rtaFiltered.length === 1) {
291
+ return this._makeEdge(callerFqn, rtaFiltered[0], 'rta', cs, callerFile);
292
+ }
293
+ }
294
+
295
+ // 无法解析 → 不创建边 (宁缺勿滥)
296
+ return null;
297
+ }
298
+
299
+ /**
300
+ * @private CHA (Class Hierarchy Analysis): 沿继承链向上搜索方法
301
+ *
302
+ * 使用 BFS 遍历 inheritanceGraph,从 className 向上搜索直到找到
303
+ * 定义了 methodName 的祖先类。只跟踪 'inherits' 类型的边。
304
+ *
305
+ * @param {string} methodName — 被调用的方法名
306
+ * @param {string} className — 起始类名
307
+ * @returns {string|null} — 找到的 FQN 或 null
308
+ */
309
+ _resolveByCHA(methodName, className) {
310
+ if (!this.inheritanceGraph || this.inheritanceGraph.length === 0) return null;
311
+
312
+ // BFS 向上遍历继承链 (最多 10 层防止循环)
313
+ const visited = new Set([className]);
314
+ const queue = [className];
315
+ const MAX_DEPTH = 10;
316
+ let depth = 0;
317
+
318
+ while (queue.length > 0 && depth < MAX_DEPTH) {
319
+ depth++;
320
+ const nextQueue = [];
321
+ for (const current of queue) {
322
+ // 查找 current 的所有父类 (inherits 和 conforms 类型的边)
323
+ for (const edge of this.inheritanceGraph) {
324
+ if (edge.from === current && !visited.has(edge.to)) {
325
+ visited.add(edge.to);
326
+
327
+ // 在全局符号表中查找 ParentClass.methodName
328
+ const qualifiedName = `${edge.to}.${methodName}`;
329
+ const candidates = this.nameIndex.get(qualifiedName) || [];
330
+ if (candidates.length > 0) {
331
+ return candidates[0];
332
+ }
333
+
334
+ nextQueue.push(edge.to);
335
+ }
336
+ }
337
+ }
338
+ queue.length = 0;
339
+ queue.push(...nextQueue);
340
+ }
341
+
342
+ return null;
343
+ }
344
+
345
+ /**
346
+ * @private 从字段名推断类型(DI/IoC 命名约定推断)
347
+ *
348
+ * 常见模式:
349
+ * - userRepo → UserRepo
350
+ * - userRepository → UserRepository
351
+ * - userService → UserService
352
+ * - _userRepo → UserRepo (Java/Kotlin private field)
353
+ *
354
+ * 只在符号表中存在匹配类时返回
355
+ *
356
+ * @param {string} fieldName
357
+ * @returns {string|null}
358
+ */
359
+ _inferFieldType(fieldName) {
360
+ // 去除前导下划线
361
+ const cleaned = fieldName.replace(/^_+/, '');
362
+ if (!cleaned) return null;
363
+
364
+ // camelCase → PascalCase
365
+ const pascalCase = cleaned.charAt(0).toUpperCase() + cleaned.slice(1);
366
+
367
+ // Phase 5.3: 使用 classNames Set 快速查找 (O(1) 替代 O(n) 全表扫描)
368
+ return this.classNames.has(pascalCase) ? pascalCase : null;
369
+ }
370
+
371
+ /**
372
+ * @private 在指定文件中查找声明 (使用 fileIndex 优化,避免全表扫描)
373
+ * @param {string} name — 符号名 (可以是 "ClassName.methodName" 或 "functionName")
374
+ * @param {string} file
375
+ * @returns {string[]} 匹配的 FQN 列表
376
+ */
377
+ _findInFile(name, file) {
378
+ const fileDecls = this.fileIndex.get(file);
379
+ if (!fileDecls) return [];
380
+ return fileDecls
381
+ .filter((d) => d.name === name || d.qualifiedName === name)
382
+ .map((d) => d.fqn);
383
+ }
384
+
385
+ /**
386
+ * @private 构建 ResolvedEdge
387
+ */
388
+ _makeEdge(callerFqn, calleeFqn, resolveMethod, cs, callerFile) {
389
+ return {
390
+ caller: callerFqn,
391
+ callee: calleeFqn,
392
+ callType: cs.callType,
393
+ resolveMethod,
394
+ line: cs.line,
395
+ file: callerFile,
396
+ isAwait: cs.isAwait,
397
+ argCount: cs.argCount || 0,
398
+ };
399
+ }
400
+ }
401
+
402
+ export default CallEdgeResolver;