@mr-jones123/toji 0.1.1
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 +158 -0
- package/package.json +47 -0
- package/packages/toji-comms/README.md +71 -0
- package/packages/toji-comms/src/cli/agents.ts +121 -0
- package/packages/toji-comms/src/cli/mmx.ts +65 -0
- package/packages/toji-comms/src/cli/subprocess.ts +47 -0
- package/packages/toji-comms/src/comms/orchestrator.ts +92 -0
- package/packages/toji-comms/src/comms/prompt.ts +84 -0
- package/packages/toji-comms/src/comms/store.ts +145 -0
- package/packages/toji-comms/src/comms/types.ts +94 -0
- package/packages/toji-comms/src/db/connection.ts +58 -0
- package/packages/toji-comms/src/db/migrations.ts +69 -0
- package/packages/toji-comms/src/index.ts +368 -0
- package/packages/toji-comms/src/mcp/client.ts +71 -0
- package/packages/toji-comms/src/mcp/server.ts +81 -0
- package/packages/toji-mem/README.md +52 -0
- package/packages/toji-mem/grammars/manifest.json +9 -0
- package/packages/toji-mem/grammars/tree-sitter-cpp.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-dart.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-java.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-javascript.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-python.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-tsx.wasm +0 -0
- package/packages/toji-mem/grammars/tree-sitter-typescript.wasm +0 -0
- package/packages/toji-mem/src/db/connection.ts +58 -0
- package/packages/toji-mem/src/db/migrations.ts +181 -0
- package/packages/toji-mem/src/index.ts +326 -0
- package/packages/toji-mem/src/indexer/file-walker.ts +45 -0
- package/packages/toji-mem/src/indexer/index-project.ts +277 -0
- package/packages/toji-mem/src/indexer/parsers/cpp.ts +81 -0
- package/packages/toji-mem/src/indexer/parsers/dart.ts +91 -0
- package/packages/toji-mem/src/indexer/parsers/java.ts +83 -0
- package/packages/toji-mem/src/indexer/parsers/python.ts +84 -0
- package/packages/toji-mem/src/indexer/parsers/registry.ts +28 -0
- package/packages/toji-mem/src/indexer/parsers/tree-sitter-loader.ts +39 -0
- package/packages/toji-mem/src/indexer/parsers/types.ts +48 -0
- package/packages/toji-mem/src/indexer/parsers/typescript.ts +105 -0
- package/packages/toji-mem/src/standards/store.ts +52 -0
- package/packages/toji-mem/src/tools/blast-radius.ts +98 -0
- package/packages/toji-mem/src/tools/graph-explore.ts +186 -0
- package/packages/toji-mem/src/tools/project-overview.ts +102 -0
- package/packages/toji-mem/src/tools/query-memory.ts +105 -0
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
import type { TojiDatabase } from "../db/connection";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import { readFile } from "node:fs/promises";
|
|
4
|
+
import { basename, dirname, join, normalize, parse, resolve } from "node:path";
|
|
5
|
+
|
|
6
|
+
import { walkProjectFiles } from "./file-walker";
|
|
7
|
+
import { getParserForPath, getSupportedExtensions } from "./parsers/registry";
|
|
8
|
+
|
|
9
|
+
export interface IndexProjectResult {
|
|
10
|
+
projectId: number;
|
|
11
|
+
rootPath: string;
|
|
12
|
+
indexedFiles: number;
|
|
13
|
+
symbols: number;
|
|
14
|
+
imports: number;
|
|
15
|
+
calls: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function indexProject(database: TojiDatabase, rootPath: string): Promise<IndexProjectResult> {
|
|
19
|
+
const normalizedRoot = resolve(rootPath);
|
|
20
|
+
const project = database
|
|
21
|
+
.query<{ id: number }, [string, string]>(
|
|
22
|
+
`INSERT INTO projects (root_path, name, indexed_at)
|
|
23
|
+
VALUES (?, ?, CURRENT_TIMESTAMP)
|
|
24
|
+
ON CONFLICT(root_path) DO UPDATE SET indexed_at = CURRENT_TIMESTAMP
|
|
25
|
+
RETURNING id`,
|
|
26
|
+
)
|
|
27
|
+
.get(normalizedRoot, basename(normalizedRoot));
|
|
28
|
+
|
|
29
|
+
if (!project) throw new Error("Failed to create or update project");
|
|
30
|
+
|
|
31
|
+
const files = await walkProjectFiles(normalizedRoot, getSupportedExtensions());
|
|
32
|
+
const freshHashes = await hashFiles(normalizedRoot, files);
|
|
33
|
+
const previousHashes = new Map(
|
|
34
|
+
database
|
|
35
|
+
.query<{ path: string; hash: string }, [number]>(`SELECT path, hash FROM files WHERE project_id = ?`)
|
|
36
|
+
.all(project.id)
|
|
37
|
+
.map((row) => [row.path, row.hash]),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
if (hashesMatch(freshHashes, previousHashes)) {
|
|
41
|
+
const counts = database
|
|
42
|
+
.query<{ symbols: number; imports: number; calls: number }, [number, number, number]>(
|
|
43
|
+
`SELECT
|
|
44
|
+
(SELECT COUNT(*) FROM symbols WHERE project_id = ?) AS symbols,
|
|
45
|
+
(SELECT COUNT(*) FROM imports WHERE project_id = ?) AS imports,
|
|
46
|
+
(SELECT COUNT(*) FROM calls WHERE project_id = ?) AS calls`,
|
|
47
|
+
)
|
|
48
|
+
.get(project.id, project.id, project.id);
|
|
49
|
+
return {
|
|
50
|
+
projectId: project.id,
|
|
51
|
+
rootPath: normalizedRoot,
|
|
52
|
+
indexedFiles: files.length,
|
|
53
|
+
symbols: counts?.symbols ?? 0,
|
|
54
|
+
imports: counts?.imports ?? 0,
|
|
55
|
+
calls: counts?.calls ?? 0,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
database.query("DELETE FROM files WHERE project_id = ?").run(project.id);
|
|
60
|
+
database.query("DELETE FROM edges WHERE project_id = ?").run(project.id);
|
|
61
|
+
database.query("DELETE FROM symbol_fts WHERE project_id = ?").run(project.id);
|
|
62
|
+
database.query("DELETE FROM file_fts WHERE project_id = ?").run(project.id);
|
|
63
|
+
|
|
64
|
+
let symbolCount = 0;
|
|
65
|
+
let importCount = 0;
|
|
66
|
+
let callCount = 0;
|
|
67
|
+
|
|
68
|
+
const insertFile = database.query<{ id: number }, [number, string, string, string, string]>(
|
|
69
|
+
`INSERT INTO files (project_id, path, name, language, hash)
|
|
70
|
+
VALUES (?, ?, ?, ?, ?)
|
|
71
|
+
RETURNING id`,
|
|
72
|
+
);
|
|
73
|
+
const insertSymbol = database.query<{ id: number }, [number, number, string, string, string, string, number, number, string | null, number]>(
|
|
74
|
+
`INSERT INTO symbols (project_id, file_id, name, canon_name, kind, language, start_line, end_line, signature, exported)
|
|
75
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
76
|
+
RETURNING id`,
|
|
77
|
+
);
|
|
78
|
+
const insertImport = database.query<unknown, [number, number, string | null, string, number]>(
|
|
79
|
+
`INSERT INTO imports (project_id, file_id, imported_name, source, start_line)
|
|
80
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
81
|
+
);
|
|
82
|
+
const insertCall = database.query<unknown, [number, number, string | null, string, number]>(
|
|
83
|
+
`INSERT INTO calls (project_id, file_id, caller_name, callee_name, start_line)
|
|
84
|
+
VALUES (?, ?, ?, ?, ?)`,
|
|
85
|
+
);
|
|
86
|
+
const insertSymbolFts = database.query<unknown, [number, number, number, string, string, string, string, string, string, string]>(
|
|
87
|
+
`INSERT INTO symbol_fts (symbol_id, project_id, file_id, name, canon_name, kind, language, path, signature, docstring)
|
|
88
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
89
|
+
);
|
|
90
|
+
const insertFileFts = database.query<unknown, [number, number, string, string, string, string]>(
|
|
91
|
+
`INSERT INTO file_fts (file_id, project_id, name, path, language, docstring) VALUES (?, ?, ?, ?, ?, ?)`,
|
|
92
|
+
);
|
|
93
|
+
|
|
94
|
+
const fileIds = new Map<string, number>();
|
|
95
|
+
const symbolsByFile = new Map<number, Array<{ id: number; name: string; startLine: number; endLine: number; exported: boolean }>>();
|
|
96
|
+
const importsByFile = new Map<number, Array<{ source: string; startLine: number }>>();
|
|
97
|
+
const callsByFile = new Map<number, Array<{ callerId?: number; calleeName: string; startLine: number }>>();
|
|
98
|
+
const relationsByFile = new Map<number, Array<{ fromName: string; toName: string; edgeType: "symbol_extends_symbol" | "symbol_implements_symbol" }>>();
|
|
99
|
+
|
|
100
|
+
for (const relativePath of files) {
|
|
101
|
+
const parser = getParserForPath(relativePath);
|
|
102
|
+
if (!parser) continue;
|
|
103
|
+
|
|
104
|
+
const absolutePath = join(normalizedRoot, relativePath);
|
|
105
|
+
const content = await readFile(absolutePath, "utf8");
|
|
106
|
+
const hash = freshHashes.get(relativePath) ?? createHash("sha256").update(content).digest("hex");
|
|
107
|
+
const fileName = basename(relativePath);
|
|
108
|
+
const file = insertFile.get(project.id, relativePath, fileName, parser.language, hash);
|
|
109
|
+
if (!file) continue;
|
|
110
|
+
|
|
111
|
+
insertFileFts.run(file.id, project.id, fileName, relativePath, parser.language, "");
|
|
112
|
+
fileIds.set(relativePath, file.id);
|
|
113
|
+
|
|
114
|
+
const parsed = await parser.parseFile({ projectId: project.id, filePath: relativePath, content });
|
|
115
|
+
const fileSymbols: Array<{ id: number; name: string; startLine: number; endLine: number; exported: boolean }> = [];
|
|
116
|
+
|
|
117
|
+
for (const symbol of parsed.symbols) {
|
|
118
|
+
const canon_name = symbolQname(relativePath, symbol.name);
|
|
119
|
+
const insertedSymbol = insertSymbol.get(
|
|
120
|
+
project.id,
|
|
121
|
+
file.id,
|
|
122
|
+
symbol.name,
|
|
123
|
+
canon_name,
|
|
124
|
+
symbol.kind,
|
|
125
|
+
parser.language,
|
|
126
|
+
symbol.startLine,
|
|
127
|
+
symbol.endLine,
|
|
128
|
+
symbol.signature ?? null,
|
|
129
|
+
symbol.exported ? 1 : 0,
|
|
130
|
+
);
|
|
131
|
+
if (!insertedSymbol) continue;
|
|
132
|
+
|
|
133
|
+
fileSymbols.push({
|
|
134
|
+
id: insertedSymbol.id,
|
|
135
|
+
name: symbol.name,
|
|
136
|
+
startLine: symbol.startLine,
|
|
137
|
+
endLine: symbol.endLine,
|
|
138
|
+
exported: symbol.exported,
|
|
139
|
+
});
|
|
140
|
+
insertSymbolFts.run(insertedSymbol.id, project.id, file.id, symbol.name, canon_name, symbol.kind, parser.language, relativePath, symbol.signature ?? "", "");
|
|
141
|
+
symbolCount += 1;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
symbolsByFile.set(file.id, fileSymbols);
|
|
145
|
+
|
|
146
|
+
const fileImports: Array<{ source: string; startLine: number }> = [];
|
|
147
|
+
for (const parsedImport of parsed.imports) {
|
|
148
|
+
insertImport.run(project.id, file.id, parsedImport.importedName ?? null, parsedImport.source, parsedImport.startLine);
|
|
149
|
+
fileImports.push({ source: parsedImport.source, startLine: parsedImport.startLine });
|
|
150
|
+
importCount += 1;
|
|
151
|
+
}
|
|
152
|
+
importsByFile.set(file.id, fileImports);
|
|
153
|
+
|
|
154
|
+
const fileCalls: Array<{ callerId?: number; calleeName: string; startLine: number }> = [];
|
|
155
|
+
for (const call of parsed.calls) {
|
|
156
|
+
const caller = findContainingSymbol(fileSymbols, call.startLine);
|
|
157
|
+
insertCall.run(project.id, file.id, caller?.name ?? call.callerName ?? null, call.calleeName, call.startLine);
|
|
158
|
+
fileCalls.push({ callerId: caller?.id, calleeName: call.calleeName, startLine: call.startLine });
|
|
159
|
+
callCount += 1;
|
|
160
|
+
}
|
|
161
|
+
callsByFile.set(file.id, fileCalls);
|
|
162
|
+
relationsByFile.set(file.id, parsed.relations.map((relation) => ({
|
|
163
|
+
fromName: relation.fromName,
|
|
164
|
+
toName: relation.toName,
|
|
165
|
+
edgeType: relation.edgeType,
|
|
166
|
+
})));
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
createGraphEdges(database, project.id, files, fileIds, symbolsByFile, importsByFile, callsByFile, relationsByFile);
|
|
170
|
+
|
|
171
|
+
return { projectId: project.id, rootPath: normalizedRoot, indexedFiles: files.length, symbols: symbolCount, imports: importCount, calls: callCount };
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
async function hashFiles(rootPath: string, files: string[]): Promise<Map<string, string>> {
|
|
175
|
+
const hashes = new Map<string, string>();
|
|
176
|
+
for (const relativePath of files) {
|
|
177
|
+
const content = await readFile(join(rootPath, relativePath), "utf8");
|
|
178
|
+
hashes.set(relativePath, createHash("sha256").update(content).digest("hex"));
|
|
179
|
+
}
|
|
180
|
+
return hashes;
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function hashesMatch(fresh: Map<string, string>, previous: Map<string, string>): boolean {
|
|
184
|
+
if (fresh.size === 0 || fresh.size !== previous.size) return false;
|
|
185
|
+
for (const [path, hash] of fresh) {
|
|
186
|
+
if (previous.get(path) !== hash) return false;
|
|
187
|
+
}
|
|
188
|
+
return true;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function symbolQname(filePath: string, symbolName: string): string {
|
|
192
|
+
const parsed = parse(filePath);
|
|
193
|
+
const moduleName = join(parsed.dir, parsed.name).replaceAll("/", ".").replaceAll("\\\\", ".");
|
|
194
|
+
return moduleName ? `${moduleName}.${symbolName}` : symbolName;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function findContainingSymbol(
|
|
198
|
+
symbols: Array<{ id: number; name: string; startLine: number; endLine: number }>,
|
|
199
|
+
line: number,
|
|
200
|
+
): { id: number; name: string; startLine: number; endLine: number } | undefined {
|
|
201
|
+
return symbols
|
|
202
|
+
.filter((symbol) => symbol.startLine <= line && symbol.endLine >= line)
|
|
203
|
+
.sort((left, right) => left.endLine - left.startLine - (right.endLine - right.startLine))[0];
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function createGraphEdges(
|
|
207
|
+
database: TojiDatabase,
|
|
208
|
+
projectId: number,
|
|
209
|
+
files: string[],
|
|
210
|
+
fileIds: Map<string, number>,
|
|
211
|
+
symbolsByFile: Map<number, Array<{ id: number; name: string; startLine: number; endLine: number; exported: boolean }>>,
|
|
212
|
+
importsByFile: Map<number, Array<{ source: string; startLine: number }>>,
|
|
213
|
+
callsByFile: Map<number, Array<{ callerId?: number; calleeName: string; startLine: number }>>,
|
|
214
|
+
relationsByFile: Map<number, Array<{ fromName: string; toName: string; edgeType: "symbol_extends_symbol" | "symbol_implements_symbol" }>>,
|
|
215
|
+
): void {
|
|
216
|
+
const insertEdge = database.query<unknown, [number, string, number, string, number | null, string | null, string, number]>(
|
|
217
|
+
`INSERT INTO edges (project_id, from_type, from_id, to_type, to_id, to_name, edge_type, confidence)
|
|
218
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
219
|
+
);
|
|
220
|
+
|
|
221
|
+
const symbolsByName = new Map<string, Array<{ id: number; fileId: number; exported: boolean }>>();
|
|
222
|
+
for (const [fileId, symbols] of symbolsByFile) {
|
|
223
|
+
for (const symbol of symbols) {
|
|
224
|
+
insertEdge.run(projectId, "file", fileId, "symbol", symbol.id, symbol.name, "file_contains_symbol", 1);
|
|
225
|
+
if (symbol.exported) insertEdge.run(projectId, "file", fileId, "symbol", symbol.id, symbol.name, "file_exports_symbol", 1);
|
|
226
|
+
|
|
227
|
+
const namedSymbols = symbolsByName.get(symbol.name) ?? [];
|
|
228
|
+
namedSymbols.push({ id: symbol.id, fileId, exported: symbol.exported });
|
|
229
|
+
symbolsByName.set(symbol.name, namedSymbols);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
for (const [relativePath, fileId] of fileIds) {
|
|
234
|
+
for (const parsedImport of importsByFile.get(fileId) ?? []) {
|
|
235
|
+
const importedFile = resolveImportPath(relativePath, parsedImport.source, files);
|
|
236
|
+
if (importedFile) {
|
|
237
|
+
insertEdge.run(projectId, "file", fileId, "file", fileIds.get(importedFile) ?? null, importedFile, "file_imports_file", 0.9);
|
|
238
|
+
} else {
|
|
239
|
+
insertEdge.run(projectId, "file", fileId, "package", null, parsedImport.source, "file_imports_package", 0.8);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
for (const relation of relationsByFile.get(fileId) ?? []) {
|
|
244
|
+
const fromSymbol = (symbolsByFile.get(fileId) ?? []).find((symbol) => symbol.name === relation.fromName);
|
|
245
|
+
if (!fromSymbol) continue;
|
|
246
|
+
|
|
247
|
+
const target = symbolsByName.get(relation.toName)?.find((symbol) => symbol.exported) ?? symbolsByName.get(relation.toName)?.[0];
|
|
248
|
+
insertEdge.run(projectId, "symbol", fromSymbol.id, "symbol", target?.id ?? null, relation.toName, relation.edgeType, target ? 0.8 : 0.4);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
for (const call of callsByFile.get(fileId) ?? []) {
|
|
252
|
+
if (!call.callerId) continue;
|
|
253
|
+
const sameFileTarget = (symbolsByFile.get(fileId) ?? []).find((symbol) => symbol.name === call.calleeName);
|
|
254
|
+
const globalTarget = symbolsByName.get(call.calleeName)?.find((symbol) => symbol.exported) ?? symbolsByName.get(call.calleeName)?.[0];
|
|
255
|
+
const target = sameFileTarget ? { id: sameFileTarget.id, confidence: 0.9 } : globalTarget ? { id: globalTarget.id, confidence: 0.55 } : undefined;
|
|
256
|
+
insertEdge.run(
|
|
257
|
+
projectId,
|
|
258
|
+
"symbol",
|
|
259
|
+
call.callerId,
|
|
260
|
+
"symbol",
|
|
261
|
+
target?.id ?? null,
|
|
262
|
+
call.calleeName,
|
|
263
|
+
"symbol_calls_symbol",
|
|
264
|
+
target?.confidence ?? 0.3,
|
|
265
|
+
);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function resolveImportPath(fromPath: string, source: string, files: string[]): string | undefined {
|
|
271
|
+
if (!source.startsWith(".")) return undefined;
|
|
272
|
+
|
|
273
|
+
const basePath = normalize(join(dirname(fromPath), source));
|
|
274
|
+
const candidates = [basePath, `${basePath}.ts`, `${basePath}.tsx`, `${basePath}.js`, `${basePath}.jsx`, `${basePath}.py`, join(basePath, "index.ts")];
|
|
275
|
+
return candidates.find((candidate) => files.includes(candidate));
|
|
276
|
+
}
|
|
277
|
+
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import { Query } from "web-tree-sitter";
|
|
2
|
+
|
|
3
|
+
import { loadTreeSitterLanguage, parseWithGrammar } from "./tree-sitter-loader";
|
|
4
|
+
import type { LanguageParser, ParsedCall, ParsedFile, ParsedImport, ParsedRelation, ParsedSymbol } from "./types";
|
|
5
|
+
|
|
6
|
+
const cppQuerySource = `
|
|
7
|
+
(function_definition declarator: (function_declarator declarator: (identifier) @function.name)) @function.definition
|
|
8
|
+
(function_definition declarator: (function_declarator declarator: (field_identifier) @method.name)) @function.definition
|
|
9
|
+
(class_specifier name: (type_identifier) @class.name) @class.definition
|
|
10
|
+
(struct_specifier name: (type_identifier) @class.name) @class.definition
|
|
11
|
+
(preproc_include path: [(string_literal) (system_lib_string)] @import.source) @import.statement
|
|
12
|
+
(call_expression function: (identifier) @call.name) @call.expression
|
|
13
|
+
(call_expression function: (field_expression field: (field_identifier) @call.member_name)) @call.expression
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
export const cppParser: LanguageParser = {
|
|
17
|
+
language: "cpp",
|
|
18
|
+
extensions: [".cpp", ".cc", ".cxx", ".c++", ".hpp", ".hh", ".hxx", ".h++", ".h"],
|
|
19
|
+
async parseFile(input): Promise<ParsedFile> {
|
|
20
|
+
const tree = await parseWithGrammar("tree-sitter-cpp.wasm", input.content);
|
|
21
|
+
const grammar = await loadTreeSitterLanguage("tree-sitter-cpp.wasm");
|
|
22
|
+
const query = new Query(grammar, cppQuerySource);
|
|
23
|
+
const captures = query.captures(tree.rootNode);
|
|
24
|
+
|
|
25
|
+
const symbols: ParsedSymbol[] = [];
|
|
26
|
+
const imports: ParsedImport[] = [];
|
|
27
|
+
const calls: ParsedCall[] = [];
|
|
28
|
+
const relations: ParsedRelation[] = [];
|
|
29
|
+
|
|
30
|
+
for (const capture of captures) {
|
|
31
|
+
if (capture.name === "function.name" || capture.name === "method.name") {
|
|
32
|
+
const definition = nearest(capture.node, "function_definition");
|
|
33
|
+
symbols.push({
|
|
34
|
+
name: capture.node.text,
|
|
35
|
+
kind: capture.name === "method.name" ? "method" : "function",
|
|
36
|
+
startLine: definition?.startPosition.row !== undefined ? definition.startPosition.row + 1 : capture.node.startPosition.row + 1,
|
|
37
|
+
endLine: definition?.endPosition.row !== undefined ? definition.endPosition.row + 1 : capture.node.endPosition.row + 1,
|
|
38
|
+
exported: true,
|
|
39
|
+
signature: definition?.text.split("\n")[0]?.trim(),
|
|
40
|
+
});
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (capture.name === "class.name") {
|
|
45
|
+
const definition = capture.node.parent;
|
|
46
|
+
symbols.push({
|
|
47
|
+
name: capture.node.text,
|
|
48
|
+
kind: "class",
|
|
49
|
+
startLine: definition?.startPosition.row !== undefined ? definition.startPosition.row + 1 : capture.node.startPosition.row + 1,
|
|
50
|
+
endLine: definition?.endPosition.row !== undefined ? definition.endPosition.row + 1 : capture.node.endPosition.row + 1,
|
|
51
|
+
exported: true,
|
|
52
|
+
signature: definition?.text.split("\n")[0]?.trim(),
|
|
53
|
+
});
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (capture.name === "import.source") {
|
|
58
|
+
imports.push({
|
|
59
|
+
source: capture.node.text.replace(/[<>"]/g, ""),
|
|
60
|
+
startLine: capture.node.startPosition.row + 1,
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
if (capture.name === "call.name" || capture.name === "call.member_name") {
|
|
66
|
+
calls.push({ calleeName: capture.node.text, startLine: capture.node.startPosition.row + 1 });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return { symbols, imports, calls, relations };
|
|
71
|
+
},
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
function nearest(node: { parent: unknown; type: string } | null, type: string): { type: string; text: string; startPosition: { row: number }; endPosition: { row: number } } | undefined {
|
|
75
|
+
let current = node;
|
|
76
|
+
while (current) {
|
|
77
|
+
if (current.type === type) return current as unknown as { type: string; text: string; startPosition: { row: number }; endPosition: { row: number } };
|
|
78
|
+
current = current.parent as typeof current;
|
|
79
|
+
}
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Query } from "web-tree-sitter";
|
|
2
|
+
|
|
3
|
+
import { loadTreeSitterLanguage, parseWithGrammar } from "./tree-sitter-loader";
|
|
4
|
+
import type { LanguageParser, ParsedCall, ParsedFile, ParsedImport, ParsedRelation, ParsedSymbol, SymbolKind } from "./types";
|
|
5
|
+
|
|
6
|
+
const querySource = `
|
|
7
|
+
(class_definition name: (identifier) @class.name) @class.definition
|
|
8
|
+
(class_definition name: (identifier) @heritage.from superclass: (superclass (type_identifier) @extends.to))
|
|
9
|
+
(class_definition name: (identifier) @heritage.from interfaces: (interfaces (type_identifier) @implements.to))
|
|
10
|
+
(function_signature name: (identifier) @function.name) @function.definition
|
|
11
|
+
(method_signature (function_signature name: (identifier) @method.name)) @method.definition
|
|
12
|
+
(library_import (import_specification (configurable_uri (uri (string_literal) @import.source)))) @import.statement
|
|
13
|
+
(expression_statement (identifier) @call.name (selector (argument_part))) @call.expression
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const symbolKinds = new Map<string, SymbolKind>([
|
|
17
|
+
["class.name", "class"],
|
|
18
|
+
["function.name", "function"],
|
|
19
|
+
["method.name", "method"],
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
export const dartParser: LanguageParser = {
|
|
23
|
+
language: "dart",
|
|
24
|
+
extensions: [".dart"],
|
|
25
|
+
async parseFile(input): Promise<ParsedFile> {
|
|
26
|
+
const grammarFile = "tree-sitter-dart.wasm";
|
|
27
|
+
const tree = await parseWithGrammar(grammarFile, input.content);
|
|
28
|
+
const grammar = await loadTreeSitterLanguage(grammarFile);
|
|
29
|
+
const query = new Query(grammar, querySource);
|
|
30
|
+
const captures = query.captures(tree.rootNode);
|
|
31
|
+
|
|
32
|
+
const symbols: ParsedSymbol[] = [];
|
|
33
|
+
const imports: ParsedImport[] = [];
|
|
34
|
+
const calls: ParsedCall[] = [];
|
|
35
|
+
const relations: ParsedRelation[] = [];
|
|
36
|
+
let heritageFrom: string | undefined;
|
|
37
|
+
|
|
38
|
+
for (const capture of captures) {
|
|
39
|
+
const kind = symbolKinds.get(capture.name);
|
|
40
|
+
if (kind) {
|
|
41
|
+
const parent = capture.node.parent;
|
|
42
|
+
if (kind === "function" && parent?.parent?.type === "method_signature") continue;
|
|
43
|
+
symbols.push({
|
|
44
|
+
name: capture.node.text,
|
|
45
|
+
kind,
|
|
46
|
+
startLine: capture.node.startPosition.row + 1,
|
|
47
|
+
endLine: parent ? parent.endPosition.row + 1 : capture.node.endPosition.row + 1,
|
|
48
|
+
exported: !capture.node.text.startsWith("_"),
|
|
49
|
+
signature: parent?.text.split("\n")[0]?.trim(),
|
|
50
|
+
});
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (capture.name === "import.source") {
|
|
55
|
+
imports.push({ source: capture.node.text.replace(/^['\"]|['\"]$/g, ""), startLine: capture.node.startPosition.row + 1 });
|
|
56
|
+
continue;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (capture.name === "call.name") {
|
|
60
|
+
calls.push({ calleeName: capture.node.text, startLine: capture.node.startPosition.row + 1 });
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (capture.name === "heritage.from") {
|
|
65
|
+
heritageFrom = capture.node.text;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (capture.name === "extends.to" && heritageFrom) {
|
|
70
|
+
relations.push({
|
|
71
|
+
fromName: heritageFrom,
|
|
72
|
+
toName: capture.node.text,
|
|
73
|
+
edgeType: "symbol_extends_symbol",
|
|
74
|
+
startLine: capture.node.startPosition.row + 1,
|
|
75
|
+
});
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (capture.name === "implements.to" && heritageFrom) {
|
|
80
|
+
relations.push({
|
|
81
|
+
fromName: heritageFrom,
|
|
82
|
+
toName: capture.node.text,
|
|
83
|
+
edgeType: "symbol_implements_symbol",
|
|
84
|
+
startLine: capture.node.startPosition.row + 1,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
return { symbols, imports, calls, relations };
|
|
90
|
+
},
|
|
91
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { Query } from "web-tree-sitter";
|
|
2
|
+
|
|
3
|
+
import { loadTreeSitterLanguage, parseWithGrammar } from "./tree-sitter-loader";
|
|
4
|
+
import type { LanguageParser, ParsedCall, ParsedFile, ParsedImport, ParsedRelation, ParsedSymbol, SymbolKind } from "./types";
|
|
5
|
+
|
|
6
|
+
const querySource = `
|
|
7
|
+
(class_declaration name: (identifier) @class.name) @class.definition
|
|
8
|
+
(interface_declaration name: (identifier) @interface.name) @interface.definition
|
|
9
|
+
(method_declaration name: (identifier) @method.name) @method.definition
|
|
10
|
+
(constructor_declaration name: (identifier) @method.name) @method.definition
|
|
11
|
+
(import_declaration (scoped_identifier) @import.source) @import.statement
|
|
12
|
+
(import_declaration (identifier) @import.source) @import.statement
|
|
13
|
+
(method_invocation name: (identifier) @call.name) @call.expression
|
|
14
|
+
(class_declaration name: (identifier) @heritage.from superclass: (superclass (type_identifier) @extends.to))
|
|
15
|
+
(class_declaration name: (identifier) @heritage.from interfaces: (super_interfaces (type_list (type_identifier) @implements.to)))
|
|
16
|
+
(interface_declaration name: (identifier) @heritage.from (extends_interfaces (type_list (type_identifier) @extends.to)))
|
|
17
|
+
`;
|
|
18
|
+
|
|
19
|
+
const symbolKinds = new Map<string, SymbolKind>([
|
|
20
|
+
["class.name", "class"],
|
|
21
|
+
["interface.name", "interface"],
|
|
22
|
+
["method.name", "method"],
|
|
23
|
+
]);
|
|
24
|
+
|
|
25
|
+
export const javaParser: LanguageParser = {
|
|
26
|
+
language: "java",
|
|
27
|
+
extensions: [".java"],
|
|
28
|
+
async parseFile(input): Promise<ParsedFile> {
|
|
29
|
+
const grammarFile = "tree-sitter-java.wasm";
|
|
30
|
+
const tree = await parseWithGrammar(grammarFile, input.content);
|
|
31
|
+
const grammar = await loadTreeSitterLanguage(grammarFile);
|
|
32
|
+
const query = new Query(grammar, querySource);
|
|
33
|
+
const captures = query.captures(tree.rootNode);
|
|
34
|
+
|
|
35
|
+
const symbols: ParsedSymbol[] = [];
|
|
36
|
+
const imports: ParsedImport[] = [];
|
|
37
|
+
const calls: ParsedCall[] = [];
|
|
38
|
+
const relations: ParsedRelation[] = [];
|
|
39
|
+
let heritageFrom: string | undefined;
|
|
40
|
+
|
|
41
|
+
for (const capture of captures) {
|
|
42
|
+
const kind = symbolKinds.get(capture.name);
|
|
43
|
+
if (kind) {
|
|
44
|
+
const parent = capture.node.parent;
|
|
45
|
+
symbols.push({
|
|
46
|
+
name: capture.node.text,
|
|
47
|
+
kind,
|
|
48
|
+
startLine: capture.node.startPosition.row + 1,
|
|
49
|
+
endLine: parent ? parent.endPosition.row + 1 : capture.node.endPosition.row + 1,
|
|
50
|
+
exported: parent?.text.startsWith("public ") ?? false,
|
|
51
|
+
signature: parent?.text.split("\n")[0]?.trim(),
|
|
52
|
+
});
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (capture.name === "import.source") {
|
|
57
|
+
imports.push({ source: capture.node.text, startLine: capture.node.startPosition.row + 1 });
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (capture.name === "call.name") {
|
|
62
|
+
calls.push({ calleeName: capture.node.text, startLine: capture.node.startPosition.row + 1 });
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (capture.name === "heritage.from") {
|
|
67
|
+
heritageFrom = capture.node.text;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if ((capture.name === "extends.to" || capture.name === "implements.to") && heritageFrom) {
|
|
72
|
+
relations.push({
|
|
73
|
+
fromName: heritageFrom,
|
|
74
|
+
toName: capture.node.text,
|
|
75
|
+
edgeType: capture.name === "extends.to" ? "symbol_extends_symbol" : "symbol_implements_symbol",
|
|
76
|
+
startLine: capture.node.startPosition.row + 1,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return { symbols, imports, calls, relations };
|
|
82
|
+
},
|
|
83
|
+
};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Query } from "web-tree-sitter";
|
|
2
|
+
|
|
3
|
+
import { loadTreeSitterLanguage, parseWithGrammar } from "./tree-sitter-loader";
|
|
4
|
+
import type { LanguageParser, ParsedCall, ParsedFile, ParsedImport, ParsedRelation, ParsedSymbol, SymbolKind } from "./types";
|
|
5
|
+
|
|
6
|
+
const querySource = `
|
|
7
|
+
(function_definition name: (identifier) @function.name) @function.definition
|
|
8
|
+
(class_definition name: (identifier) @class.name) @class.definition
|
|
9
|
+
(class_definition name: (identifier) @heritage.from superclasses: (argument_list (identifier) @extends.to))
|
|
10
|
+
(import_statement) @import.statement
|
|
11
|
+
(import_from_statement module_name: (dotted_name) @import.source) @import.statement
|
|
12
|
+
(call function: (identifier) @call.name) @call.expression
|
|
13
|
+
(call function: (attribute attribute: (identifier) @call.member_name)) @call.expression
|
|
14
|
+
`;
|
|
15
|
+
|
|
16
|
+
const symbolKinds = new Map<string, SymbolKind>([
|
|
17
|
+
["function.name", "function"],
|
|
18
|
+
["class.name", "class"],
|
|
19
|
+
]);
|
|
20
|
+
|
|
21
|
+
export const pythonParser: LanguageParser = {
|
|
22
|
+
language: "python",
|
|
23
|
+
extensions: [".py"],
|
|
24
|
+
async parseFile(input): Promise<ParsedFile> {
|
|
25
|
+
const grammarFile = "tree-sitter-python.wasm";
|
|
26
|
+
const tree = await parseWithGrammar(grammarFile, input.content);
|
|
27
|
+
const grammar = await loadTreeSitterLanguage(grammarFile);
|
|
28
|
+
const query = new Query(grammar, querySource);
|
|
29
|
+
const captures = query.captures(tree.rootNode);
|
|
30
|
+
|
|
31
|
+
const symbols: ParsedSymbol[] = [];
|
|
32
|
+
const imports: ParsedImport[] = [];
|
|
33
|
+
const calls: ParsedCall[] = [];
|
|
34
|
+
const relations: ParsedRelation[] = [];
|
|
35
|
+
let heritageFrom: string | undefined;
|
|
36
|
+
|
|
37
|
+
for (const capture of captures) {
|
|
38
|
+
const kind = symbolKinds.get(capture.name);
|
|
39
|
+
if (kind) {
|
|
40
|
+
const parent = capture.node.parent;
|
|
41
|
+
symbols.push({
|
|
42
|
+
name: capture.node.text,
|
|
43
|
+
kind,
|
|
44
|
+
startLine: capture.node.startPosition.row + 1,
|
|
45
|
+
endLine: parent ? parent.endPosition.row + 1 : capture.node.endPosition.row + 1,
|
|
46
|
+
exported: !capture.node.text.startsWith("_"),
|
|
47
|
+
signature: parent?.text.split("\n")[0]?.trim(),
|
|
48
|
+
});
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
if (capture.name === "import.source") {
|
|
53
|
+
imports.push({ source: capture.node.text, startLine: capture.node.startPosition.row + 1 });
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (capture.name === "import.statement") {
|
|
58
|
+
imports.push({ source: capture.node.text, startLine: capture.node.startPosition.row + 1 });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (capture.name === "call.name" || capture.name === "call.member_name") {
|
|
63
|
+
calls.push({ calleeName: capture.node.text, startLine: capture.node.startPosition.row + 1 });
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (capture.name === "heritage.from") {
|
|
68
|
+
heritageFrom = capture.node.text;
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (capture.name === "extends.to" && heritageFrom) {
|
|
73
|
+
relations.push({
|
|
74
|
+
fromName: heritageFrom,
|
|
75
|
+
toName: capture.node.text,
|
|
76
|
+
edgeType: "symbol_extends_symbol",
|
|
77
|
+
startLine: capture.node.startPosition.row + 1,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
return { symbols, imports, calls, relations };
|
|
83
|
+
},
|
|
84
|
+
};
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { extname } from "node:path";
|
|
2
|
+
|
|
3
|
+
import { cppParser } from "./cpp";
|
|
4
|
+
import { dartParser } from "./dart";
|
|
5
|
+
import { javaParser } from "./java";
|
|
6
|
+
import { pythonParser } from "./python";
|
|
7
|
+
import { createTypeScriptParser } from "./typescript";
|
|
8
|
+
import type { LanguageParser } from "./types";
|
|
9
|
+
|
|
10
|
+
const parsers: LanguageParser[] = [
|
|
11
|
+
createTypeScriptParser("typescript", "tree-sitter-typescript.wasm", [".ts", ".mts", ".cts"]),
|
|
12
|
+
createTypeScriptParser("tsx", "tree-sitter-tsx.wasm", [".tsx"]),
|
|
13
|
+
createTypeScriptParser("javascript", "tree-sitter-javascript.wasm", [".js", ".mjs", ".cjs"]),
|
|
14
|
+
createTypeScriptParser("jsx", "tree-sitter-javascript.wasm", [".jsx"]),
|
|
15
|
+
cppParser,
|
|
16
|
+
dartParser,
|
|
17
|
+
javaParser,
|
|
18
|
+
pythonParser,
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
export function getParserForPath(filePath: string): LanguageParser | undefined {
|
|
22
|
+
const extension = extname(filePath);
|
|
23
|
+
return parsers.find((parser) => parser.extensions.includes(extension));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function getSupportedExtensions(): string[] {
|
|
27
|
+
return parsers.flatMap((parser) => parser.extensions);
|
|
28
|
+
}
|