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.
- package/bin/cli.js +7 -0
- package/dashboard/dist/assets/index-D5jiDBQG.css +1 -0
- package/dashboard/dist/assets/{index-DfHY_3ln.js → index-e5OKj-Ni.js} +38 -38
- package/dashboard/dist/index.html +2 -2
- package/lib/cli/AiScanService.js +3 -3
- package/lib/core/AstAnalyzer.js +26 -4
- package/lib/core/analysis/CallEdgeResolver.js +402 -0
- package/lib/core/analysis/CallGraphAnalyzer.js +367 -0
- package/lib/core/analysis/CallSiteExtractor.js +629 -0
- package/lib/core/analysis/DataFlowInferrer.js +57 -0
- package/lib/core/analysis/ImportPathResolver.js +189 -0
- package/lib/core/analysis/ImportRecord.js +105 -0
- package/lib/core/analysis/SymbolTableBuilder.js +211 -0
- package/lib/core/ast/ProjectGraph.js +8 -0
- package/lib/core/ast/lang-dart.js +352 -5
- package/lib/core/ast/lang-go.js +212 -10
- package/lib/core/ast/lang-java.js +205 -1
- package/lib/core/ast/lang-kotlin.js +330 -1
- package/lib/core/ast/lang-python.js +31 -2
- package/lib/core/ast/lang-rust.js +284 -3
- package/lib/core/ast/lang-swift.js +180 -1
- package/lib/core/ast/lang-typescript.js +290 -1
- package/lib/external/mcp/McpServer.js +1 -0
- package/lib/external/mcp/handlers/bootstrap/MissionBriefingBuilder.js +21 -0
- package/lib/external/mcp/handlers/bootstrap/pipeline/EpisodicMemory.js +5 -4
- package/lib/external/mcp/handlers/bootstrap/pipeline/dimension-configs.js +2 -1
- package/lib/external/mcp/handlers/bootstrap/pipeline/orchestrator.js +70 -4
- package/lib/external/mcp/handlers/bootstrap/shared/bootstrap-phases.js +95 -1
- package/lib/external/mcp/handlers/bootstrap-external.js +9 -2
- package/lib/external/mcp/handlers/bootstrap-internal.js +17 -6
- package/lib/external/mcp/handlers/consolidated.js +9 -0
- package/lib/external/mcp/handlers/guard.js +3 -3
- package/lib/external/mcp/handlers/structure.js +62 -0
- package/lib/external/mcp/handlers/wiki-external.js +66 -3
- package/lib/external/mcp/tools.js +36 -1
- package/lib/http/routes/remote.js +15 -15
- package/lib/injection/ServiceContainer.js +6 -11
- package/lib/platform/ios/index.js +2 -2
- package/lib/platform/ios/spm/PackageSwiftParser.js +14 -3
- package/lib/platform/ios/spm/SpmDiscoverer.js +123 -17
- package/lib/platform/ios/spm/{SpmService.js → SpmHelper.js} +43 -675
- package/lib/platform/ios/xcode/XcodeWriteUtils.js +1 -1
- package/lib/service/chat/ChatAgent.js +1 -1
- package/lib/service/chat/ChatAgentPrompts.js +13 -1
- package/lib/service/chat/ExplorationTracker.js +52 -8
- package/lib/service/chat/HandoffProtocol.js +19 -1
- package/lib/service/chat/WorkingMemory.js +3 -1
- package/lib/service/chat/memory/ActiveContext.js +3 -1
- package/lib/service/chat/memory/SessionStore.js +4 -3
- package/lib/service/chat/tools/ast-graph.js +229 -32
- package/lib/service/chat/tools/index.js +6 -1
- package/lib/service/chat/tools/infrastructure.js +5 -0
- package/lib/service/cursor/CursorDeliveryPipeline.js +167 -1
- package/lib/service/knowledge/CodeEntityGraph.js +327 -2
- package/lib/service/knowledge/KnowledgeService.js +5 -1
- package/lib/service/module/ModuleService.js +9 -0
- package/lib/service/wiki/WikiGenerator.js +1 -1
- package/lib/shared/PathGuard.js +1 -1
- package/package.json +1 -1
- 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-
|
|
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-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D5jiDBQG.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
package/lib/cli/AiScanService.js
CHANGED
|
@@ -145,14 +145,14 @@ export class AiScanService {
|
|
|
145
145
|
const files = [];
|
|
146
146
|
|
|
147
147
|
try {
|
|
148
|
-
// 优先使用 ModuleService(多语言统一入口),回退到
|
|
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 {
|
|
155
|
-
service = new
|
|
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
|
|
package/lib/core/AstAnalyzer.js
CHANGED
|
@@ -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: `${
|
|
438
|
-
to:
|
|
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:
|
|
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;
|