dsh-codegraph-visualizer 0.1.0 → 0.3.0
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/README.md +391 -223
- package/assets/icon-180.png +0 -0
- package/assets/icon.svg +30 -0
- package/cordis.patch.yml +22 -17
- package/dist/CodeGraphAdapter-C8JvcS7m.js +10 -0
- package/dist/CodeGraphAdapter-eyrhs0K0.js +422 -0
- package/dist/CodeGraphAdapter-eyrhs0K0.js.map +1 -0
- package/dist/FileSystemAdapter-CgasSGbB.js +524 -0
- package/dist/FileSystemAdapter-CgasSGbB.js.map +1 -0
- package/dist/LensAdapter-C9Saf_i_.js +89 -0
- package/dist/LensAdapter-C9Saf_i_.js.map +1 -0
- package/dist/Logger-DUIJSeNu.js +80 -0
- package/dist/Logger-DUIJSeNu.js.map +1 -0
- package/dist/TreeSitterAdapter-C-EWy8br.js +631 -0
- package/dist/TreeSitterAdapter-C-EWy8br.js.map +1 -0
- package/dist/TreeSitterAdapter-C6FcO2_s.js +8 -0
- package/dist/client/index.css +8900 -1777
- package/dist/client/index.css.map +1 -0
- package/dist/client/index.d.ts +634 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.esm.js +57219 -20978
- package/dist/client/index.js.map +1 -0
- package/dist/client/{index.js → worker/layoutWorker.js} +7657 -8787
- package/dist/client.js +90067 -0
- package/dist/codeGraphDb-BT9nZCEO.js +357 -0
- package/dist/codeGraphDb-BT9nZCEO.js.map +1 -0
- package/dist/codeGraphDb-Bg7bWud2.js +9 -0
- package/dist/codeGraphRows-BhZX34uB.js +6 -0
- package/dist/codeGraphRows-C7hqu35E.js +246 -0
- package/dist/codeGraphRows-C7hqu35E.js.map +1 -0
- package/dist/constants-CQq93l3e.js +399 -0
- package/dist/constants-CQq93l3e.js.map +1 -0
- package/dist/graphFingerprint-CeARo7jG.js +4 -0
- package/dist/graphFingerprint-PuxORZUF.js +277 -0
- package/dist/graphFingerprint-PuxORZUF.js.map +1 -0
- package/dist/graphMetrics-C0F30730.js +555 -0
- package/dist/graphMetrics-C0F30730.js.map +1 -0
- package/dist/index.d.ts +572 -9
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +14734 -2003
- package/dist/index.js.map +1 -1
- package/dist/inheritanceEnhancer-Bi3UoYJS.js +291 -0
- package/dist/inheritanceEnhancer-Bi3UoYJS.js.map +1 -0
- package/dist/layerUtils-DDm-EsaD.js +72 -0
- package/dist/layerUtils-DDm-EsaD.js.map +1 -0
- package/dist/nativeIndex-Cx7RLXUp.js +5 -0
- package/dist/nativeIndex-DlSpD611.js +347 -0
- package/dist/nativeIndex-DlSpD611.js.map +1 -0
- package/dist/treeSitterExtractors-B1ybKNrC.js +394 -0
- package/dist/treeSitterExtractors-B1ybKNrC.js.map +1 -0
- package/dist/types-BUDxI_00.js +23 -0
- package/dist/types-BUDxI_00.js.map +1 -0
- package/locale/en.json +6 -0
- package/locale/zh-CN.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +101 -21
- package/dist/client-cjs/index.cjs +0 -44283
- package/dist/client-cjs/index.css +0 -2404
|
@@ -0,0 +1,631 @@
|
|
|
1
|
+
import { EdgeId, NodeId } from "./types-BUDxI_00.js";
|
|
2
|
+
import { TREE_SITTER } from "./constants-CQq93l3e.js";
|
|
3
|
+
import { scoped } from "./Logger-DUIJSeNu.js";
|
|
4
|
+
import { inferLayerFromPath } from "./layerUtils-DDm-EsaD.js";
|
|
5
|
+
import { IMPORT_STATEMENT_TYPES, applyTodoMarkers, collectHeritageTargets, extractImportSources, extractSymbolName, maybeCollectCallSite } from "./treeSitterExtractors-B1ybKNrC.js";
|
|
6
|
+
import { createRequire } from "node:module";
|
|
7
|
+
|
|
8
|
+
//#region src/adapters/TreeSitterAdapter.ts
|
|
9
|
+
const log = scoped("tree-sitter-adapter");
|
|
10
|
+
/**
|
|
11
|
+
* 同步探测「本宿主上是否**存在** AST 分析通道」——给能力位 `astSource` 用
|
|
12
|
+
* (`src/host/prerequisites.ts` 登记探针,`src/host/routes.ts` 接线)。
|
|
13
|
+
*
|
|
14
|
+
* ## 为什么不能直接用 `adapter.isAvailable()`
|
|
15
|
+
*
|
|
16
|
+
* `isAvailable()` 的语义是「解析器**已经**初始化」(`parserCtor !== null`),而初始化发生在
|
|
17
|
+
* 第一次 `parseFile()`/`parseToAst()` 里的 `await this.init()`。插件 `apply()` 时还没有任何
|
|
18
|
+
* 文件被解析过,于是它在**启动后的一整段时间里恒为 false** —— 用它当能力位会让 `/status`
|
|
19
|
+
* 在一个完全正常的宿主上报告「AST 通道不可用」,那是与 `builtin: true` 相反方向但同样虚假的
|
|
20
|
+
* 叙事。能力位回答的是「这条通道存在吗」,不是「它此刻已热了吗」。
|
|
21
|
+
*
|
|
22
|
+
* ## 判据是两条真实探针的**析取**,没有第三条字面量
|
|
23
|
+
*
|
|
24
|
+
* 1. `isAvailable()` —— 已初始化(最强证据,进程生命周期后段基本恒真);
|
|
25
|
+
* 2. 运行时模块**可解析** —— `web-tree-sitter`(解析器)与 `tree-sitter-wasm`(语法 wasm)
|
|
26
|
+
* 都能被 Node 解析到。缺依赖、装坏、profile 里漏装都会让它变 false。
|
|
27
|
+
*
|
|
28
|
+
* ## 诚实边界
|
|
29
|
+
*
|
|
30
|
+
* 本探针**不能**证明语法 wasm 真的能加载(那是异步 `Language.load()` 的事)。它证明的是
|
|
31
|
+
* 「这两个包在这台宿主上存在」。这个窄化的失败面是刻意接受的:它比 `true` 严格更强
|
|
32
|
+
* (字面量 `true` 在任何安装状态下都为真),而要做到「真的能加载」就必须把
|
|
33
|
+
* `checkPrerequisites` 改成 async,代价是 20 余处同步调用点。
|
|
34
|
+
* 非 Node 环境(理论上不该发生:本适配器只在宿主进程运行)直接返回 `false`。
|
|
35
|
+
*/
|
|
36
|
+
function isTreeSitterRuntimePresent() {
|
|
37
|
+
if (typeof process === "undefined" || !process.versions?.node) return false;
|
|
38
|
+
try {
|
|
39
|
+
const req = createRequire(import.meta.url);
|
|
40
|
+
req.resolve("web-tree-sitter");
|
|
41
|
+
req.resolve("tree-sitter-wasm");
|
|
42
|
+
return true;
|
|
43
|
+
} catch (e) {
|
|
44
|
+
log.debug("tree-sitter runtime not resolvable", { error: e });
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const SUPPORTED_LANGUAGES = [
|
|
49
|
+
"typescript",
|
|
50
|
+
"javascript",
|
|
51
|
+
"python",
|
|
52
|
+
"java",
|
|
53
|
+
"rust",
|
|
54
|
+
"go"
|
|
55
|
+
];
|
|
56
|
+
/**
|
|
57
|
+
* Grammar a file is parsed with. `.tsx` needs the JSX-aware `tsx` build: with the plain
|
|
58
|
+
* `typescript` grammar a component containing `<div>…</div>` produces ERROR nodes and the JSX
|
|
59
|
+
* arrow component is swallowed by the error node, so its symbol never reaches the graph
|
|
60
|
+
* (measured: `hasError: true`, 2 ERROR nodes, 0 arrow functions, vs clean parse with `tsx`).
|
|
61
|
+
* `tree-sitter-wasm` ships both grammars.
|
|
62
|
+
*/
|
|
63
|
+
const GRAMMARS = [...SUPPORTED_LANGUAGES, "tsx"];
|
|
64
|
+
/** Logical language a grammar belongs to — what `detectLanguage` reports (`.tsx` is TypeScript). */
|
|
65
|
+
const LANGUAGE_BY_GRAMMAR = {
|
|
66
|
+
typescript: "typescript",
|
|
67
|
+
javascript: "javascript",
|
|
68
|
+
python: "python",
|
|
69
|
+
java: "java",
|
|
70
|
+
rust: "rust",
|
|
71
|
+
go: "go",
|
|
72
|
+
tsx: "typescript"
|
|
73
|
+
};
|
|
74
|
+
/** Grammar per file extension — `.tsx` must not fall back to the plain `typescript` grammar. */
|
|
75
|
+
const GRAMMAR_BY_EXTENSION = {
|
|
76
|
+
".ts": "typescript",
|
|
77
|
+
".tsx": "tsx",
|
|
78
|
+
".js": "javascript",
|
|
79
|
+
".jsx": "javascript",
|
|
80
|
+
".mjs": "javascript",
|
|
81
|
+
".cjs": "javascript",
|
|
82
|
+
".py": "python",
|
|
83
|
+
".java": "java",
|
|
84
|
+
".rs": "rust",
|
|
85
|
+
".go": "go"
|
|
86
|
+
};
|
|
87
|
+
/**
|
|
88
|
+
* Node type → graph symbol kind, **per grammar**. Previously this was one flat set shared by all
|
|
89
|
+
* grammars, which was guesswork: it listed types no supported grammar emits (`struct_declaration`,
|
|
90
|
+
* `const_declaration`) while missing real ones, so Rust functions never reached the graph
|
|
91
|
+
* (`function_item` was absent) and TypeScript variables were labelled `<variable_declaration>`
|
|
92
|
+
* (the *wrapper* was listed instead of `variable_declarator`, the node that actually carries the
|
|
93
|
+
* name).
|
|
94
|
+
*
|
|
95
|
+
* Every entry below was measured against the shipped grammars by
|
|
96
|
+
* `.cycles/probe-grammar-node-types.mjs` (all samples parse with `hasError: false`). A type is
|
|
97
|
+
* listed only when the grammar emits it **and** `extractName` yields a real label for it — a
|
|
98
|
+
* `name` field, a `type` field (`impl_item`), or an identifier child (`assignment`,
|
|
99
|
+
* `let_declaration`). Declaration wrappers are deliberately absent because they carry no name of
|
|
100
|
+
* their own and would enter the graph as `<lexical_declaration>`-style noise:
|
|
101
|
+
* `lexical_declaration`, `variable_declaration`, `var_declaration`, `short_var_declaration`,
|
|
102
|
+
* `type_declaration`, `field_declaration`, plus the `*_body` / `*_type` container nodes.
|
|
103
|
+
*/
|
|
104
|
+
const SYMBOL_NODE_TYPES = (() => {
|
|
105
|
+
const js = {
|
|
106
|
+
function_declaration: "function",
|
|
107
|
+
method_definition: "function",
|
|
108
|
+
arrow_function: "function",
|
|
109
|
+
class_declaration: "class",
|
|
110
|
+
variable_declarator: "variable"
|
|
111
|
+
};
|
|
112
|
+
return {
|
|
113
|
+
typescript: {
|
|
114
|
+
...js,
|
|
115
|
+
interface_declaration: "class",
|
|
116
|
+
enum_declaration: "class",
|
|
117
|
+
type_alias_declaration: "class"
|
|
118
|
+
},
|
|
119
|
+
tsx: {
|
|
120
|
+
...js,
|
|
121
|
+
interface_declaration: "class",
|
|
122
|
+
enum_declaration: "class",
|
|
123
|
+
type_alias_declaration: "class"
|
|
124
|
+
},
|
|
125
|
+
javascript: js,
|
|
126
|
+
python: {
|
|
127
|
+
function_definition: "function",
|
|
128
|
+
class_definition: "class",
|
|
129
|
+
assignment: "variable"
|
|
130
|
+
},
|
|
131
|
+
java: {
|
|
132
|
+
method_declaration: "function",
|
|
133
|
+
constructor_declaration: "function",
|
|
134
|
+
class_declaration: "class",
|
|
135
|
+
interface_declaration: "class",
|
|
136
|
+
enum_declaration: "class",
|
|
137
|
+
variable_declarator: "variable"
|
|
138
|
+
},
|
|
139
|
+
rust: {
|
|
140
|
+
function_item: "function",
|
|
141
|
+
function_signature_item: "function",
|
|
142
|
+
closure_expression: "function",
|
|
143
|
+
struct_item: "class",
|
|
144
|
+
enum_item: "class",
|
|
145
|
+
trait_item: "class",
|
|
146
|
+
impl_item: "class",
|
|
147
|
+
let_declaration: "variable",
|
|
148
|
+
const_item: "variable",
|
|
149
|
+
static_item: "variable"
|
|
150
|
+
},
|
|
151
|
+
go: {
|
|
152
|
+
function_declaration: "function",
|
|
153
|
+
method_declaration: "function",
|
|
154
|
+
method_elem: "function",
|
|
155
|
+
type_spec: "class",
|
|
156
|
+
type_alias: "class",
|
|
157
|
+
var_spec: "variable",
|
|
158
|
+
const_spec: "variable"
|
|
159
|
+
}
|
|
160
|
+
};
|
|
161
|
+
})();
|
|
162
|
+
function symbolKindFor(grammar, nodeType) {
|
|
163
|
+
return SYMBOL_NODE_TYPES[grammar][nodeType] ?? null;
|
|
164
|
+
}
|
|
165
|
+
const COMMENT_NODE_TYPES = new Set([
|
|
166
|
+
"comment",
|
|
167
|
+
"line_comment",
|
|
168
|
+
"block_comment"
|
|
169
|
+
]);
|
|
170
|
+
const TODO_PATTERNS = /\b(TODO|FIXME|HACK|XXX|BUG)\b/g;
|
|
171
|
+
var TreeSitterAdapter = class TreeSitterAdapter {
|
|
172
|
+
source = "tree-sitter";
|
|
173
|
+
parserCtor = null;
|
|
174
|
+
/**
|
|
175
|
+
* One parser per grammar. A single shared parser with `setLanguage` swapped per file is not safe:
|
|
176
|
+
* `ensureLanguageLoaded` awaits, so a mixed-language batch (`Promise.all` in `batchScan`) could
|
|
177
|
+
* apply another file's grammar between the await and the parse. Separate instances make the
|
|
178
|
+
* language a property of the parser instead of mutable shared state.
|
|
179
|
+
*/
|
|
180
|
+
parsers = new Map();
|
|
181
|
+
languages = new Map();
|
|
182
|
+
pendingGrammarLoads = new Map();
|
|
183
|
+
initAttempts = 0;
|
|
184
|
+
static MAX_INIT_ATTEMPTS = 3;
|
|
185
|
+
async init() {
|
|
186
|
+
if (this.parserCtor) return;
|
|
187
|
+
if (this.initAttempts >= TreeSitterAdapter.MAX_INIT_ATTEMPTS) {
|
|
188
|
+
log.warn(`TreeSitter init skipped after ${this.initAttempts} failed attempts`);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
this.initAttempts++;
|
|
192
|
+
try {
|
|
193
|
+
const { Parser } = await import("web-tree-sitter");
|
|
194
|
+
await Parser.init();
|
|
195
|
+
this.parserCtor = Parser;
|
|
196
|
+
this.initAttempts = 0;
|
|
197
|
+
log.info("TreeSitter parser initialized");
|
|
198
|
+
} catch (e) {
|
|
199
|
+
log.warn(`Failed to init TreeSitter (attempt ${this.initAttempts}/${TreeSitterAdapter.MAX_INIT_ATTEMPTS})`, e);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
isAvailable() {
|
|
203
|
+
return this.parserCtor !== null;
|
|
204
|
+
}
|
|
205
|
+
getSupportedLanguages() {
|
|
206
|
+
return SUPPORTED_LANGUAGES;
|
|
207
|
+
}
|
|
208
|
+
/** Logical language for a file, or null when the extension is not supported. */
|
|
209
|
+
detectLanguage(filePath) {
|
|
210
|
+
const grammar = this.detectGrammar(filePath);
|
|
211
|
+
return grammar ? LANGUAGE_BY_GRAMMAR[grammar] : null;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Grammar to parse a file with. Longest matching extension wins, so the lookup cannot depend on
|
|
215
|
+
* the declaration order of `GRAMMAR_BY_EXTENSION`.
|
|
216
|
+
*/
|
|
217
|
+
detectGrammar(filePath) {
|
|
218
|
+
let best = null;
|
|
219
|
+
let bestLength = 0;
|
|
220
|
+
for (const [ext, grammar] of Object.entries(GRAMMAR_BY_EXTENSION)) if (ext.length > bestLength && filePath.endsWith(ext)) {
|
|
221
|
+
best = grammar;
|
|
222
|
+
bestLength = ext.length;
|
|
223
|
+
}
|
|
224
|
+
return best;
|
|
225
|
+
}
|
|
226
|
+
/**
|
|
227
|
+
* Parser for a grammar, loading its wasm once. Concurrent requests for the same grammar share one
|
|
228
|
+
* in-flight load, so a batch never downloads/compiles the same grammar twice.
|
|
229
|
+
*/
|
|
230
|
+
async ensureParser(grammar) {
|
|
231
|
+
const ready = this.parsers.get(grammar);
|
|
232
|
+
if (ready) return ready;
|
|
233
|
+
const inFlight = this.pendingGrammarLoads.get(grammar);
|
|
234
|
+
if (inFlight) return inFlight;
|
|
235
|
+
const pending = (async () => {
|
|
236
|
+
const ctor = this.parserCtor;
|
|
237
|
+
if (!ctor) return null;
|
|
238
|
+
try {
|
|
239
|
+
const language = await this.loadLanguageWasm(grammar);
|
|
240
|
+
const parser = new ctor();
|
|
241
|
+
parser.setLanguage(language);
|
|
242
|
+
this.languages.set(grammar, language);
|
|
243
|
+
this.parsers.set(grammar, parser);
|
|
244
|
+
return parser;
|
|
245
|
+
} catch (e) {
|
|
246
|
+
log.warn(`Failed to load grammar: ${grammar}`, e);
|
|
247
|
+
return null;
|
|
248
|
+
} finally {
|
|
249
|
+
this.pendingGrammarLoads.delete(grammar);
|
|
250
|
+
}
|
|
251
|
+
})();
|
|
252
|
+
this.pendingGrammarLoads.set(grammar, pending);
|
|
253
|
+
return pending;
|
|
254
|
+
}
|
|
255
|
+
async parseFile(filePath, content) {
|
|
256
|
+
if (!this.parserCtor) {
|
|
257
|
+
await this.init();
|
|
258
|
+
if (!this.parserCtor) return {
|
|
259
|
+
nodes: [],
|
|
260
|
+
edges: [],
|
|
261
|
+
source: "tree-sitter",
|
|
262
|
+
timestamp: Date.now(),
|
|
263
|
+
error: "Parser not initialized"
|
|
264
|
+
};
|
|
265
|
+
}
|
|
266
|
+
const grammar = this.detectGrammar(filePath);
|
|
267
|
+
if (!grammar) return {
|
|
268
|
+
nodes: [],
|
|
269
|
+
edges: [],
|
|
270
|
+
source: "tree-sitter",
|
|
271
|
+
timestamp: Date.now(),
|
|
272
|
+
error: `Unsupported file: ${filePath}`
|
|
273
|
+
};
|
|
274
|
+
const parser = await this.ensureParser(grammar);
|
|
275
|
+
if (!parser) return {
|
|
276
|
+
nodes: [],
|
|
277
|
+
edges: [],
|
|
278
|
+
source: "tree-sitter",
|
|
279
|
+
timestamp: Date.now(),
|
|
280
|
+
error: `Failed to load ${grammar}`
|
|
281
|
+
};
|
|
282
|
+
try {
|
|
283
|
+
const tree = parser.parse(content);
|
|
284
|
+
const { nodes, edges, importSources, callSites } = this.extractSymbols(tree.rootNode, filePath, grammar);
|
|
285
|
+
tree.delete();
|
|
286
|
+
return {
|
|
287
|
+
nodes,
|
|
288
|
+
edges,
|
|
289
|
+
source: "tree-sitter",
|
|
290
|
+
timestamp: Date.now(),
|
|
291
|
+
importsByFile: importSources.length > 0 ? new Map([[filePath, importSources]]) : void 0,
|
|
292
|
+
callSitesByFile: callSites.length > 0 ? new Map([[filePath, callSites]]) : void 0
|
|
293
|
+
};
|
|
294
|
+
} catch (e) {
|
|
295
|
+
log.warn("parseFile failed", {
|
|
296
|
+
filePath,
|
|
297
|
+
error: e
|
|
298
|
+
});
|
|
299
|
+
return {
|
|
300
|
+
nodes: [],
|
|
301
|
+
edges: [],
|
|
302
|
+
source: "tree-sitter",
|
|
303
|
+
timestamp: Date.now(),
|
|
304
|
+
error: String(e)
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
async batchScan(files) {
|
|
309
|
+
if (!this.parserCtor) {
|
|
310
|
+
await this.init();
|
|
311
|
+
if (!this.parserCtor) return {
|
|
312
|
+
nodes: [],
|
|
313
|
+
edges: [],
|
|
314
|
+
source: "tree-sitter",
|
|
315
|
+
timestamp: Date.now(),
|
|
316
|
+
error: "Parser not initialized"
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
const allNodes = [];
|
|
320
|
+
const allEdges = [];
|
|
321
|
+
const errors = [];
|
|
322
|
+
/** 文件 → 该文件 import 的模块名(cycle-129 · T1;消费方 FileSystemAdapter 解析成边)。 */
|
|
323
|
+
const importsByFile = new Map();
|
|
324
|
+
/** 文件 → 该文件的调用点(T2;消费方 FileSystemAdapter 解析成 `call` 边)。 */
|
|
325
|
+
const callSitesByFile = new Map();
|
|
326
|
+
for (let i = 0; i < files.length; i += TREE_SITTER.BATCH_SIZE) {
|
|
327
|
+
const batch = files.slice(i, i + TREE_SITTER.BATCH_SIZE);
|
|
328
|
+
const results = await Promise.all(batch.map(async (file) => {
|
|
329
|
+
const grammar = this.detectGrammar(file.filePath);
|
|
330
|
+
if (!grammar) return null;
|
|
331
|
+
const parser = await this.ensureParser(grammar);
|
|
332
|
+
if (!parser) return null;
|
|
333
|
+
try {
|
|
334
|
+
const tree = parser.parse(file.content);
|
|
335
|
+
const { nodes, edges, importSources, callSites } = this.extractSymbols(tree.rootNode, file.filePath, grammar);
|
|
336
|
+
tree.delete();
|
|
337
|
+
return {
|
|
338
|
+
nodes,
|
|
339
|
+
edges,
|
|
340
|
+
filePath: file.filePath,
|
|
341
|
+
importSources,
|
|
342
|
+
callSites
|
|
343
|
+
};
|
|
344
|
+
} catch (e) {
|
|
345
|
+
errors.push(`${file.filePath}: ${String(e)}`);
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}));
|
|
349
|
+
for (const result of results) if (result) {
|
|
350
|
+
allNodes.push(...result.nodes);
|
|
351
|
+
allEdges.push(...result.edges);
|
|
352
|
+
if (result.importSources.length > 0) importsByFile.set(result.filePath, result.importSources);
|
|
353
|
+
callSitesByFile.set(result.filePath, result.callSites);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
log.info(`batchScan: ${files.length} files, ${allNodes.length} nodes, ${allEdges.length} edges, ${errors.length} errors, import-modules ${importsByFile.size} files, call-sites ${callSitesByFile.size} files`);
|
|
357
|
+
return {
|
|
358
|
+
nodes: allNodes,
|
|
359
|
+
edges: allEdges,
|
|
360
|
+
source: "tree-sitter",
|
|
361
|
+
timestamp: Date.now(),
|
|
362
|
+
importsByFile,
|
|
363
|
+
callSitesByFile,
|
|
364
|
+
...errors.length > 0 ? { error: errors.join("; ") } : {}
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
async loadLanguageWasm(grammar) {
|
|
368
|
+
const wasmFile = `tree-sitter-${grammar}.wasm`;
|
|
369
|
+
const { Language } = await import("web-tree-sitter");
|
|
370
|
+
const candidates = [];
|
|
371
|
+
if (typeof process !== "undefined" && process.versions?.node) try {
|
|
372
|
+
const { getWasmPath } = await import("tree-sitter-wasm");
|
|
373
|
+
candidates.push(getWasmPath(grammar));
|
|
374
|
+
} catch (e) {
|
|
375
|
+
log.debug("tree-sitter-wasm not available", { error: e });
|
|
376
|
+
}
|
|
377
|
+
candidates.push(wasmFile);
|
|
378
|
+
for (const path of candidates) try {
|
|
379
|
+
return await Language.load(path);
|
|
380
|
+
} catch (e) {
|
|
381
|
+
log.debug("candidate path failed", {
|
|
382
|
+
path,
|
|
383
|
+
error: e
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
throw new Error(`Cannot load ${wasmFile} from any candidate path`);
|
|
387
|
+
}
|
|
388
|
+
extractSymbols(rootNode, filePath, grammar) {
|
|
389
|
+
const nodes = [];
|
|
390
|
+
const edges = [];
|
|
391
|
+
const visited = new Set();
|
|
392
|
+
const nodeMap = new Map();
|
|
393
|
+
const importSources = [];
|
|
394
|
+
const callSites = [];
|
|
395
|
+
const extractName = extractSymbolName;
|
|
396
|
+
const makeId = (node, label) => `${filePath}:${label}:${node.startPosition.row}`;
|
|
397
|
+
/**
|
|
398
|
+
* 提取一个 class/interface 声明节点的全部继承目标(cycle-123)。
|
|
399
|
+
*
|
|
400
|
+
* 两种语法形态(tree-sitter-typescript 实测,见测试 fixture):
|
|
401
|
+
* - `extends_type_clause`:interface 的 extends(直接子节点,`type_identifier` 列表),
|
|
402
|
+
* 子节点序列为 [extends, ident, ',', ident, ...];
|
|
403
|
+
* - `class_heritage`:class 的 extends/implements 容器,内含 `extends_clause`
|
|
404
|
+
* (`extends` + type 列表)与 `implements_clause`(`implements` + type 列表)。
|
|
405
|
+
* - Rust 的 `impl_item` 无继承语义(trait impl 不是继承),不提取。
|
|
406
|
+
* - Python 的 `class_definition` 用 `argument_list` 表达基类(tree-sitter-python
|
|
407
|
+
* 的 `superclasses` 字段),一并覆盖(kind 判定走 clause 名单)。
|
|
408
|
+
*
|
|
409
|
+
* 返回 `clauseKind`(extends/implements)用于边类型映射:`extends → 'extend'`、
|
|
410
|
+
* `implements → 'implement'`(与上游 codegraph 的 kind 语义一致)。
|
|
411
|
+
* 目标统一返回占位 id 策略(消费方造 `${filePath}:<type>:<name>`),由
|
|
412
|
+
* merger 按 label 归并——避免遍历时序依赖(子符号可能尚未入 nodeMap)。
|
|
413
|
+
*/
|
|
414
|
+
/**
|
|
415
|
+
* import 模块源提取(cycle-129 · T1:原生依赖图的缺口)。
|
|
416
|
+
*
|
|
417
|
+
* import_statement(TS/JS)与 import_declaration(Python/Java/Rust use)的
|
|
418
|
+
* `source` 字段持有模块名字符串。此前 walk 只把 import 节点收成 module 节点
|
|
419
|
+
* (symbolKindFor 命中 `import_statement: 'module'`),模块字符串被丢弃——
|
|
420
|
+
* 原生图永远没有 `import` 边,依赖视图(dependencies view)在原生通道上是
|
|
421
|
+
* 空的。本函数返回去重后的模块名列表(同一模块多次 import 只记一条的输入)。
|
|
422
|
+
*
|
|
423
|
+
* 语法形态(各 grammar 实测):
|
|
424
|
+
* - TS/JS `import_statement`:`source` 字段是 string 节点(含引号,剥掉);
|
|
425
|
+
* - Python `import_statement` / `import_from_statement`:module_name /
|
|
426
|
+
* `dotted_name`(无引号);`relative_import`(from . import x)跳过——
|
|
427
|
+
* 相对层级语义留待后续(登记为缺口);
|
|
428
|
+
* - Rust `use_declaration`:`argument` 字段(scoped_identifier 树),取
|
|
429
|
+
* 整段文本按 `::` 保留(Rust 路径语义与文件路径不直接对应,消费方
|
|
430
|
+
* 解析不到就跳过——与「解析不到就跳过」的总策略一致)。
|
|
431
|
+
* - Java `import_declaration`:`scoped_identifier`/`package` 文本(点分名)。
|
|
432
|
+
*
|
|
433
|
+
* 刻意**不在这里解析成文件路径**:模块名 → 文件的映射需要 workspace 上下文
|
|
434
|
+
* (扩展名歧义、index 解析、tsconfig paths),由 FileSystemAdapter(T2)
|
|
435
|
+
* 统一裁决——适配器保持「语法事实收集者」,不做语义猜测。
|
|
436
|
+
*/
|
|
437
|
+
const walk = (rootNode$1) => {
|
|
438
|
+
const stack = [{
|
|
439
|
+
node: rootNode$1,
|
|
440
|
+
parentId: void 0
|
|
441
|
+
}];
|
|
442
|
+
while (stack.length > 0) {
|
|
443
|
+
const { node, parentId } = stack.pop();
|
|
444
|
+
if (!node) continue;
|
|
445
|
+
const nodeType = node.type;
|
|
446
|
+
if (IMPORT_STATEMENT_TYPES.has(nodeType)) {
|
|
447
|
+
const sources = extractImportSources(node);
|
|
448
|
+
if (sources.length > 0) importSources.push(...sources);
|
|
449
|
+
}
|
|
450
|
+
const callSite = maybeCollectCallSite(node, nodeType, parentId, parentId ? nodeMap.get(parentId)?.type : void 0);
|
|
451
|
+
if (callSite) callSites.push(callSite);
|
|
452
|
+
let graphType = null;
|
|
453
|
+
const symbolKind = symbolKindFor(grammar, nodeType);
|
|
454
|
+
if (symbolKind) graphType = symbolKind;
|
|
455
|
+
let currentParentId = parentId;
|
|
456
|
+
if (graphType) {
|
|
457
|
+
const label = extractName(node);
|
|
458
|
+
const id = makeId(node, label);
|
|
459
|
+
if (!visited.has(id)) {
|
|
460
|
+
visited.add(id);
|
|
461
|
+
const graphNode = {
|
|
462
|
+
id: NodeId(id),
|
|
463
|
+
label,
|
|
464
|
+
type: graphType,
|
|
465
|
+
filePath,
|
|
466
|
+
lineNumber: node.startPosition.row + 1,
|
|
467
|
+
properties: {
|
|
468
|
+
source: "tree-sitter",
|
|
469
|
+
nodeType
|
|
470
|
+
},
|
|
471
|
+
...parentId ? { parentId: NodeId(parentId) } : {},
|
|
472
|
+
layer: inferLayerFromPath(filePath)
|
|
473
|
+
};
|
|
474
|
+
nodes.push(graphNode);
|
|
475
|
+
nodeMap.set(id, graphNode);
|
|
476
|
+
const heritageTargets = collectHeritageTargets(node);
|
|
477
|
+
if (heritageTargets.length > 0) for (const { clauseKind, targetName } of heritageTargets) {
|
|
478
|
+
const edgeType = clauseKind === "extends" ? "extend" : "implement";
|
|
479
|
+
const targetId = NodeId(`${filePath}:<type>:${targetName}`);
|
|
480
|
+
const selfId = NodeId(id);
|
|
481
|
+
if (targetName === label) continue;
|
|
482
|
+
edges.push({
|
|
483
|
+
id: EdgeId(`tsedge:${id}->${targetId}`),
|
|
484
|
+
source: selfId,
|
|
485
|
+
target: targetId,
|
|
486
|
+
type: edgeType,
|
|
487
|
+
properties: {
|
|
488
|
+
edgeOrigin: "ast",
|
|
489
|
+
edgeLocatable: false,
|
|
490
|
+
targetName,
|
|
491
|
+
line: node.startPosition.row + 1
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
}
|
|
495
|
+
if (parentId) {
|
|
496
|
+
const line = node.startPosition.row + 1;
|
|
497
|
+
edges.push({
|
|
498
|
+
id: EdgeId(`tsedge:${parentId}->${id}`),
|
|
499
|
+
source: NodeId(parentId),
|
|
500
|
+
target: NodeId(id),
|
|
501
|
+
type: "contains",
|
|
502
|
+
properties: {
|
|
503
|
+
edgeOrigin: "ast",
|
|
504
|
+
edgeLocatable: true,
|
|
505
|
+
line
|
|
506
|
+
}
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
currentParentId = id;
|
|
511
|
+
}
|
|
512
|
+
if (COMMENT_NODE_TYPES.has(nodeType)) {
|
|
513
|
+
const parentNode = parentId ? nodeMap.get(parentId) : void 0;
|
|
514
|
+
const matches = node.text.match(TODO_PATTERNS);
|
|
515
|
+
if (parentNode && matches) applyTodoMarkers(parentNode, matches);
|
|
516
|
+
}
|
|
517
|
+
for (let i = (node.childCount || 0) - 1; i >= 0; i--) {
|
|
518
|
+
const child = node.child(i);
|
|
519
|
+
if (child) stack.push({
|
|
520
|
+
node: child,
|
|
521
|
+
parentId: currentParentId
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
walk(rootNode);
|
|
527
|
+
return {
|
|
528
|
+
nodes,
|
|
529
|
+
edges,
|
|
530
|
+
importSources: [...new Set(importSources)],
|
|
531
|
+
callSites
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
async parseToAst(filePath, content) {
|
|
535
|
+
if (!this.parserCtor) {
|
|
536
|
+
await this.init();
|
|
537
|
+
if (!this.parserCtor) {
|
|
538
|
+
log.warn("parseToAst: parser not initialized after init()");
|
|
539
|
+
return null;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const grammar = this.detectGrammar(filePath);
|
|
543
|
+
if (!grammar) {
|
|
544
|
+
log.warn(`parseToAst: unsupported file extension: ${filePath}`);
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
const parser = await this.ensureParser(grammar);
|
|
548
|
+
if (!parser) {
|
|
549
|
+
log.warn(`parseToAst: failed to load grammar ${grammar}`);
|
|
550
|
+
return null;
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
const tree = parser.parse(content);
|
|
554
|
+
const astRoot = this.convertToAstNode(tree.rootNode);
|
|
555
|
+
tree.delete();
|
|
556
|
+
return astRoot;
|
|
557
|
+
} catch (e) {
|
|
558
|
+
log.warn(`parseToAst: parse failed for ${filePath}:`, e);
|
|
559
|
+
return null;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
convertToAstNode(rootNode) {
|
|
563
|
+
const result = {
|
|
564
|
+
type: rootNode.type,
|
|
565
|
+
text: rootNode.text,
|
|
566
|
+
startPosition: {
|
|
567
|
+
row: rootNode.startPosition.row,
|
|
568
|
+
column: rootNode.startPosition.column
|
|
569
|
+
},
|
|
570
|
+
endPosition: {
|
|
571
|
+
row: rootNode.endPosition.row,
|
|
572
|
+
column: rootNode.endPosition.column
|
|
573
|
+
},
|
|
574
|
+
children: []
|
|
575
|
+
};
|
|
576
|
+
const stack = [];
|
|
577
|
+
for (let i = (rootNode.childCount || 0) - 1; i >= 0; i--) {
|
|
578
|
+
const child = rootNode.child(i);
|
|
579
|
+
if (child) stack.push({
|
|
580
|
+
tsNode: child,
|
|
581
|
+
astParent: result
|
|
582
|
+
});
|
|
583
|
+
}
|
|
584
|
+
while (stack.length > 0) {
|
|
585
|
+
const { tsNode, astParent } = stack.pop();
|
|
586
|
+
const astNode = {
|
|
587
|
+
type: tsNode.type,
|
|
588
|
+
text: tsNode.text,
|
|
589
|
+
startPosition: {
|
|
590
|
+
row: tsNode.startPosition.row,
|
|
591
|
+
column: tsNode.startPosition.column
|
|
592
|
+
},
|
|
593
|
+
endPosition: {
|
|
594
|
+
row: tsNode.endPosition.row,
|
|
595
|
+
column: tsNode.endPosition.column
|
|
596
|
+
},
|
|
597
|
+
children: []
|
|
598
|
+
};
|
|
599
|
+
astParent.children.push(astNode);
|
|
600
|
+
for (let i = (tsNode.childCount || 0) - 1; i >= 0; i--) {
|
|
601
|
+
const child = tsNode.child(i);
|
|
602
|
+
if (child) stack.push({
|
|
603
|
+
tsNode: child,
|
|
604
|
+
astParent: astNode
|
|
605
|
+
});
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return result;
|
|
609
|
+
}
|
|
610
|
+
destroy() {
|
|
611
|
+
for (const parser of this.parsers.values()) try {
|
|
612
|
+
parser.delete();
|
|
613
|
+
} catch (e) {
|
|
614
|
+
log.debug("parser cleanup failed", { error: e });
|
|
615
|
+
}
|
|
616
|
+
for (const language of this.languages.values()) try {
|
|
617
|
+
language.delete?.();
|
|
618
|
+
} catch (e) {
|
|
619
|
+
log.debug("language cleanup failed", { error: e });
|
|
620
|
+
}
|
|
621
|
+
this.parsers.clear();
|
|
622
|
+
this.languages.clear();
|
|
623
|
+
this.pendingGrammarLoads.clear();
|
|
624
|
+
this.parserCtor = null;
|
|
625
|
+
this.initAttempts = 0;
|
|
626
|
+
}
|
|
627
|
+
};
|
|
628
|
+
|
|
629
|
+
//#endregion
|
|
630
|
+
export { TreeSitterAdapter, isTreeSitterRuntimePresent };
|
|
631
|
+
//# sourceMappingURL=TreeSitterAdapter-C-EWy8br.js.map
|