@monoes/graph 1.0.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/dist/src/analyze.d.ts +23 -0
- package/dist/src/analyze.d.ts.map +1 -0
- package/dist/src/analyze.js +105 -0
- package/dist/src/analyze.js.map +1 -0
- package/dist/src/build.d.ts +8 -0
- package/dist/src/build.d.ts.map +1 -0
- package/dist/src/build.js +59 -0
- package/dist/src/build.js.map +1 -0
- package/dist/src/cache.d.ts +10 -0
- package/dist/src/cache.d.ts.map +1 -0
- package/dist/src/cache.js +34 -0
- package/dist/src/cache.js.map +1 -0
- package/dist/src/cluster.d.ts +8 -0
- package/dist/src/cluster.d.ts.map +1 -0
- package/dist/src/cluster.js +50 -0
- package/dist/src/cluster.js.map +1 -0
- package/dist/src/detect.d.ts +8 -0
- package/dist/src/detect.d.ts.map +1 -0
- package/dist/src/detect.js +108 -0
- package/dist/src/detect.js.map +1 -0
- package/dist/src/export.d.ts +21 -0
- package/dist/src/export.d.ts.map +1 -0
- package/dist/src/export.js +68 -0
- package/dist/src/export.js.map +1 -0
- package/dist/src/extract/index.d.ts +20 -0
- package/dist/src/extract/index.d.ts.map +1 -0
- package/dist/src/extract/index.js +158 -0
- package/dist/src/extract/index.js.map +1 -0
- package/dist/src/extract/languages/go.d.ts +3 -0
- package/dist/src/extract/languages/go.d.ts.map +1 -0
- package/dist/src/extract/languages/go.js +181 -0
- package/dist/src/extract/languages/go.js.map +1 -0
- package/dist/src/extract/languages/python.d.ts +3 -0
- package/dist/src/extract/languages/python.d.ts.map +1 -0
- package/dist/src/extract/languages/python.js +230 -0
- package/dist/src/extract/languages/python.js.map +1 -0
- package/dist/src/extract/languages/rust.d.ts +3 -0
- package/dist/src/extract/languages/rust.d.ts.map +1 -0
- package/dist/src/extract/languages/rust.js +195 -0
- package/dist/src/extract/languages/rust.js.map +1 -0
- package/dist/src/extract/languages/typescript.d.ts +3 -0
- package/dist/src/extract/languages/typescript.d.ts.map +1 -0
- package/dist/src/extract/languages/typescript.js +295 -0
- package/dist/src/extract/languages/typescript.js.map +1 -0
- package/dist/src/extract/tree-sitter-runner.d.ts +48 -0
- package/dist/src/extract/tree-sitter-runner.d.ts.map +1 -0
- package/dist/src/extract/tree-sitter-runner.js +128 -0
- package/dist/src/extract/tree-sitter-runner.js.map +1 -0
- package/dist/src/extract/types.d.ts +7 -0
- package/dist/src/extract/types.d.ts.map +1 -0
- package/dist/src/extract/types.js +2 -0
- package/dist/src/extract/types.js.map +1 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +9 -0
- package/dist/src/index.js.map +1 -0
- package/dist/src/pipeline.d.ts +16 -0
- package/dist/src/pipeline.d.ts.map +1 -0
- package/dist/src/pipeline.js +143 -0
- package/dist/src/pipeline.js.map +1 -0
- package/dist/src/types.d.ts +99 -0
- package/dist/src/types.d.ts.map +1 -0
- package/dist/src/types.js +2 -0
- package/dist/src/types.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +44 -0
- package/src/analyze.ts +122 -0
- package/src/build.ts +62 -0
- package/src/cache.ts +38 -0
- package/src/cluster.ts +54 -0
- package/src/detect.ts +123 -0
- package/src/export.ts +78 -0
- package/src/extract/index.ts +190 -0
- package/src/extract/languages/go.ts +206 -0
- package/src/extract/languages/python.ts +270 -0
- package/src/extract/languages/rust.ts +230 -0
- package/src/extract/languages/typescript.ts +344 -0
- package/src/extract/tree-sitter-runner.ts +165 -0
- package/src/extract/types.ts +7 -0
- package/src/index.ts +10 -0
- package/src/pipeline.ts +166 -0
- package/src/types.ts +116 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
import { basename } from 'path';
|
|
2
|
+
import type { GraphNode, GraphEdge, ExtractionResult } from '../../types.js';
|
|
3
|
+
import type { LanguageExtractor } from '../types.js';
|
|
4
|
+
import {
|
|
5
|
+
tryLoadParser,
|
|
6
|
+
walk,
|
|
7
|
+
type SyntaxNodeLike,
|
|
8
|
+
} from '../tree-sitter-runner.js';
|
|
9
|
+
|
|
10
|
+
// ---- helpers ----
|
|
11
|
+
|
|
12
|
+
function nodeName(node: SyntaxNodeLike): string {
|
|
13
|
+
const nameNode = node.childForFieldName('name');
|
|
14
|
+
return nameNode?.text ?? '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function loc(node: SyntaxNodeLike): string {
|
|
18
|
+
return `L${node.startPosition.row + 1}`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// ---- tree-sitter extraction ----
|
|
22
|
+
|
|
23
|
+
function extractWithTreeSitter(filePath: string, content: string): ExtractionResult {
|
|
24
|
+
const nodes: GraphNode[] = [];
|
|
25
|
+
const edges: GraphEdge[] = [];
|
|
26
|
+
const errors: string[] = [];
|
|
27
|
+
|
|
28
|
+
const parser = tryLoadParser('rust');
|
|
29
|
+
if (!parser) {
|
|
30
|
+
return { nodes, edges, filesProcessed: 1, fromCache: 0, errors };
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let tree: { rootNode: SyntaxNodeLike };
|
|
34
|
+
try {
|
|
35
|
+
tree = parser.parse(content);
|
|
36
|
+
} catch (err) {
|
|
37
|
+
errors.push(`tree-sitter parse error in ${filePath}: ${String(err)}`);
|
|
38
|
+
return { nodes, edges, filesProcessed: 1, fromCache: 0, errors };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
walk(tree.rootNode, (n) => {
|
|
42
|
+
// ---- functions ----
|
|
43
|
+
if (n.type === 'function_item') {
|
|
44
|
+
const name = nodeName(n);
|
|
45
|
+
if (name) {
|
|
46
|
+
nodes.push({
|
|
47
|
+
id: name,
|
|
48
|
+
label: name,
|
|
49
|
+
fileType: 'code',
|
|
50
|
+
sourceFile: filePath,
|
|
51
|
+
sourceLocation: loc(n),
|
|
52
|
+
nodeKind: 'function',
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// ---- structs ----
|
|
59
|
+
if (n.type === 'struct_item') {
|
|
60
|
+
const name = nodeName(n);
|
|
61
|
+
if (name) {
|
|
62
|
+
nodes.push({
|
|
63
|
+
id: name,
|
|
64
|
+
label: name,
|
|
65
|
+
fileType: 'code',
|
|
66
|
+
sourceFile: filePath,
|
|
67
|
+
sourceLocation: loc(n),
|
|
68
|
+
nodeKind: 'struct',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ---- traits ----
|
|
75
|
+
if (n.type === 'trait_item') {
|
|
76
|
+
const name = nodeName(n);
|
|
77
|
+
if (name) {
|
|
78
|
+
nodes.push({
|
|
79
|
+
id: name,
|
|
80
|
+
label: name,
|
|
81
|
+
fileType: 'code',
|
|
82
|
+
sourceFile: filePath,
|
|
83
|
+
sourceLocation: loc(n),
|
|
84
|
+
nodeKind: 'trait',
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// ---- impl blocks ----
|
|
91
|
+
if (n.type === 'impl_item') {
|
|
92
|
+
const typeNode = n.childForFieldName('type');
|
|
93
|
+
const traitNode = n.childForFieldName('trait');
|
|
94
|
+
const typeName = typeNode?.text ?? '';
|
|
95
|
+
|
|
96
|
+
if (typeName) {
|
|
97
|
+
nodes.push({
|
|
98
|
+
id: typeName,
|
|
99
|
+
label: typeName,
|
|
100
|
+
fileType: 'code',
|
|
101
|
+
sourceFile: filePath,
|
|
102
|
+
sourceLocation: loc(n),
|
|
103
|
+
nodeKind: 'impl',
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
// impl Trait for Type
|
|
107
|
+
if (traitNode) {
|
|
108
|
+
edges.push({
|
|
109
|
+
source: typeName,
|
|
110
|
+
target: traitNode.text,
|
|
111
|
+
relation: 'implements',
|
|
112
|
+
confidence: 'EXTRACTED',
|
|
113
|
+
sourceFile: filePath,
|
|
114
|
+
sourceLocation: loc(n),
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ---- modules ----
|
|
122
|
+
if (n.type === 'mod_item') {
|
|
123
|
+
const name = nodeName(n);
|
|
124
|
+
if (name) {
|
|
125
|
+
nodes.push({
|
|
126
|
+
id: name,
|
|
127
|
+
label: name,
|
|
128
|
+
fileType: 'code',
|
|
129
|
+
sourceFile: filePath,
|
|
130
|
+
sourceLocation: loc(n),
|
|
131
|
+
nodeKind: 'module',
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// ---- use declarations ----
|
|
138
|
+
if (n.type === 'use_declaration') {
|
|
139
|
+
const argNode = n.childForFieldName('argument');
|
|
140
|
+
if (argNode) {
|
|
141
|
+
// Flatten the use path to a string (handles use a::b::c and use a::b::{c, d})
|
|
142
|
+
const usePath = argNode.text.replace(/\s+/g, '');
|
|
143
|
+
edges.push({
|
|
144
|
+
source: basename(filePath),
|
|
145
|
+
target: usePath,
|
|
146
|
+
relation: 'imports',
|
|
147
|
+
confidence: 'EXTRACTED',
|
|
148
|
+
sourceFile: filePath,
|
|
149
|
+
sourceLocation: loc(n),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return { nodes, edges, filesProcessed: 1, fromCache: 0, errors };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ---- regex fallback ----
|
|
159
|
+
|
|
160
|
+
function extractWithRegex(filePath: string, content: string): ExtractionResult {
|
|
161
|
+
const nodes: GraphNode[] = [];
|
|
162
|
+
const edges: GraphEdge[] = [];
|
|
163
|
+
|
|
164
|
+
const lines = content.split('\n');
|
|
165
|
+
|
|
166
|
+
lines.forEach((line, idx) => {
|
|
167
|
+
const location = `L${idx + 1}`;
|
|
168
|
+
const trimmed = line.trim();
|
|
169
|
+
|
|
170
|
+
// pub fn / fn
|
|
171
|
+
const fnMatch = trimmed.match(/^(?:pub\s+)?(?:async\s+)?fn\s+(\w+)/);
|
|
172
|
+
if (fnMatch) {
|
|
173
|
+
nodes.push({ id: fnMatch[1], label: fnMatch[1], fileType: 'code', sourceFile: filePath, sourceLocation: location, nodeKind: 'function' });
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// pub struct / struct
|
|
177
|
+
const structMatch = trimmed.match(/^(?:pub\s+)?struct\s+(\w+)/);
|
|
178
|
+
if (structMatch) {
|
|
179
|
+
nodes.push({ id: structMatch[1], label: structMatch[1], fileType: 'code', sourceFile: filePath, sourceLocation: location, nodeKind: 'struct' });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// pub trait / trait
|
|
183
|
+
const traitMatch = trimmed.match(/^(?:pub\s+)?trait\s+(\w+)/);
|
|
184
|
+
if (traitMatch) {
|
|
185
|
+
nodes.push({ id: traitMatch[1], label: traitMatch[1], fileType: 'code', sourceFile: filePath, sourceLocation: location, nodeKind: 'trait' });
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// impl Trait for Type
|
|
189
|
+
const implForMatch = trimmed.match(/^impl(?:<[^>]+>)?\s+(\w+)\s+for\s+(\w+)/);
|
|
190
|
+
if (implForMatch) {
|
|
191
|
+
edges.push({ source: implForMatch[2], target: implForMatch[1], relation: 'implements', confidence: 'EXTRACTED', sourceFile: filePath, sourceLocation: location });
|
|
192
|
+
nodes.push({ id: implForMatch[2], label: implForMatch[2], fileType: 'code', sourceFile: filePath, sourceLocation: location, nodeKind: 'impl' });
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// plain impl Type
|
|
196
|
+
const implMatch = trimmed.match(/^impl(?:<[^>]+>)?\s+(\w+)\s*\{/);
|
|
197
|
+
if (implMatch && !implForMatch) {
|
|
198
|
+
nodes.push({ id: implMatch[1], label: implMatch[1], fileType: 'code', sourceFile: filePath, sourceLocation: location, nodeKind: 'impl' });
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// use statements
|
|
202
|
+
const useMatch = trimmed.match(/^use\s+([^;]+)/);
|
|
203
|
+
if (useMatch) {
|
|
204
|
+
edges.push({ source: basename(filePath), target: useMatch[1].trim(), relation: 'imports', confidence: 'EXTRACTED', sourceFile: filePath, sourceLocation: location });
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// mod declarations
|
|
208
|
+
const modMatch = trimmed.match(/^(?:pub\s+)?mod\s+(\w+)/);
|
|
209
|
+
if (modMatch) {
|
|
210
|
+
nodes.push({ id: modMatch[1], label: modMatch[1], fileType: 'code', sourceFile: filePath, sourceLocation: location, nodeKind: 'module' });
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
return { nodes, edges, filesProcessed: 1, fromCache: 0, errors: [] };
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// ---- extractor implementation ----
|
|
218
|
+
|
|
219
|
+
export const rustExtractor: LanguageExtractor = {
|
|
220
|
+
language: 'rust',
|
|
221
|
+
extensions: ['.rs'],
|
|
222
|
+
|
|
223
|
+
extract(filePath: string, content: string): ExtractionResult {
|
|
224
|
+
const tsResult = extractWithTreeSitter(filePath, content);
|
|
225
|
+
if (tsResult.nodes.length > 0 || tsResult.edges.length > 0 || tsResult.errors.length > 0) {
|
|
226
|
+
return tsResult;
|
|
227
|
+
}
|
|
228
|
+
return extractWithRegex(filePath, content);
|
|
229
|
+
},
|
|
230
|
+
};
|
|
@@ -0,0 +1,344 @@
|
|
|
1
|
+
import { basename } from 'path';
|
|
2
|
+
import type { GraphNode, GraphEdge, ExtractionResult } from '../../types.js';
|
|
3
|
+
import type { LanguageExtractor } from '../types.js';
|
|
4
|
+
import {
|
|
5
|
+
tryLoadParser,
|
|
6
|
+
walk,
|
|
7
|
+
type SyntaxNodeLike,
|
|
8
|
+
} from '../tree-sitter-runner.js';
|
|
9
|
+
|
|
10
|
+
// ---- helpers ----
|
|
11
|
+
|
|
12
|
+
function nodeName(node: SyntaxNodeLike): string {
|
|
13
|
+
// Most declaration nodes expose "name" as a named field
|
|
14
|
+
const nameNode = node.childForFieldName('name');
|
|
15
|
+
return nameNode?.text ?? '';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function loc(node: SyntaxNodeLike): string {
|
|
19
|
+
return `L${node.startPosition.row + 1}`;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// ---- tree-sitter extraction ----
|
|
23
|
+
|
|
24
|
+
const CLASS_TYPES = new Set([
|
|
25
|
+
'class_declaration',
|
|
26
|
+
'abstract_class_declaration',
|
|
27
|
+
]);
|
|
28
|
+
|
|
29
|
+
const FUNCTION_TYPES = new Set([
|
|
30
|
+
'function_declaration',
|
|
31
|
+
'generator_function_declaration',
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
function extractWithTreeSitter(
|
|
35
|
+
filePath: string,
|
|
36
|
+
content: string,
|
|
37
|
+
language: string,
|
|
38
|
+
): ExtractionResult {
|
|
39
|
+
const nodes: GraphNode[] = [];
|
|
40
|
+
const edges: GraphEdge[] = [];
|
|
41
|
+
const errors: string[] = [];
|
|
42
|
+
|
|
43
|
+
const parser = tryLoadParser(language);
|
|
44
|
+
if (!parser) {
|
|
45
|
+
return { nodes, edges, filesProcessed: 1, fromCache: 0, errors };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
let tree: { rootNode: SyntaxNodeLike };
|
|
49
|
+
try {
|
|
50
|
+
tree = parser.parse(content);
|
|
51
|
+
} catch (err) {
|
|
52
|
+
errors.push(`tree-sitter parse error in ${filePath}: ${String(err)}`);
|
|
53
|
+
return { nodes, edges, filesProcessed: 1, fromCache: 0, errors };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Track current class/method context for call-edge attribution
|
|
57
|
+
const classStack: string[] = [];
|
|
58
|
+
const methodStack: string[] = [];
|
|
59
|
+
|
|
60
|
+
const addNode = (name: string, n: SyntaxNodeLike): void => {
|
|
61
|
+
if (!name) return;
|
|
62
|
+
nodes.push({
|
|
63
|
+
id: name,
|
|
64
|
+
label: name,
|
|
65
|
+
fileType: 'code',
|
|
66
|
+
sourceFile: filePath,
|
|
67
|
+
sourceLocation: loc(n),
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
walk(tree.rootNode, (n) => {
|
|
72
|
+
// ---- classes ----
|
|
73
|
+
if (CLASS_TYPES.has(n.type)) {
|
|
74
|
+
const name = nodeName(n);
|
|
75
|
+
addNode(name, n);
|
|
76
|
+
if (name) classStack.push(name);
|
|
77
|
+
|
|
78
|
+
// heritage: extends / implements
|
|
79
|
+
for (const child of n.children) {
|
|
80
|
+
if (child.type === 'class_heritage') {
|
|
81
|
+
for (const clause of child.children) {
|
|
82
|
+
if (clause.type !== 'extends_clause' && clause.type !== 'implements_clause') continue;
|
|
83
|
+
const relation = clause.type === 'extends_clause' ? 'extends' : 'implements';
|
|
84
|
+
// Each type_identifier inside the clause is a target
|
|
85
|
+
for (const target of clause.children) {
|
|
86
|
+
if (
|
|
87
|
+
target.type === 'type_identifier' ||
|
|
88
|
+
target.type === 'identifier'
|
|
89
|
+
) {
|
|
90
|
+
edges.push({
|
|
91
|
+
source: name,
|
|
92
|
+
target: target.text,
|
|
93
|
+
relation,
|
|
94
|
+
confidence: 'EXTRACTED',
|
|
95
|
+
sourceFile: filePath,
|
|
96
|
+
sourceLocation: loc(clause),
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ---- functions (top-level) ----
|
|
107
|
+
if (FUNCTION_TYPES.has(n.type)) {
|
|
108
|
+
const name = nodeName(n);
|
|
109
|
+
addNode(name, n);
|
|
110
|
+
if (name) methodStack.push(name);
|
|
111
|
+
return;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ---- method definitions (inside class body) ----
|
|
115
|
+
if (n.type === 'method_definition' || n.type === 'public_field_definition') {
|
|
116
|
+
const name = nodeName(n);
|
|
117
|
+
// Qualify name with class if available
|
|
118
|
+
const qualifiedName = classStack.length > 0 ? `${classStack[classStack.length - 1]}.${name}` : name;
|
|
119
|
+
addNode(qualifiedName, n);
|
|
120
|
+
if (qualifiedName) methodStack.push(qualifiedName);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ---- interfaces ----
|
|
125
|
+
if (n.type === 'interface_declaration') {
|
|
126
|
+
const name = nodeName(n);
|
|
127
|
+
addNode(name, n);
|
|
128
|
+
|
|
129
|
+
// interface X extends Y
|
|
130
|
+
for (const child of n.children) {
|
|
131
|
+
if (child.type === 'extends_type_clause' || child.type === 'extends_clause') {
|
|
132
|
+
for (const target of child.children) {
|
|
133
|
+
if (target.type === 'type_identifier' || target.type === 'identifier') {
|
|
134
|
+
edges.push({
|
|
135
|
+
source: name,
|
|
136
|
+
target: target.text,
|
|
137
|
+
relation: 'extends',
|
|
138
|
+
confidence: 'EXTRACTED',
|
|
139
|
+
sourceFile: filePath,
|
|
140
|
+
sourceLocation: loc(child),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// ---- type aliases ----
|
|
150
|
+
if (n.type === 'type_alias_declaration') {
|
|
151
|
+
const name = nodeName(n);
|
|
152
|
+
addNode(name, n);
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---- import statements ----
|
|
157
|
+
if (n.type === 'import_statement') {
|
|
158
|
+
// Extract the module specifier
|
|
159
|
+
const moduleSpecifier = n.childForFieldName('source');
|
|
160
|
+
const moduleText = moduleSpecifier
|
|
161
|
+
? moduleSpecifier.text.replace(/^['"]|['"]$/g, '')
|
|
162
|
+
: '';
|
|
163
|
+
|
|
164
|
+
// Extract named specifiers: import { A, B } from '...'
|
|
165
|
+
for (const child of n.children) {
|
|
166
|
+
if (child.type === 'named_imports' || child.type === 'import_clause') {
|
|
167
|
+
for (const spec of child.children) {
|
|
168
|
+
if (spec.type === 'import_specifier') {
|
|
169
|
+
const importedName = spec.childForFieldName('name')?.text ?? spec.text;
|
|
170
|
+
if (importedName) {
|
|
171
|
+
edges.push({
|
|
172
|
+
source: filePath,
|
|
173
|
+
target: importedName,
|
|
174
|
+
relation: 'imports',
|
|
175
|
+
confidence: 'EXTRACTED',
|
|
176
|
+
sourceFile: filePath,
|
|
177
|
+
sourceLocation: loc(n),
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
// Default import: import X from '...'
|
|
184
|
+
if (child.type === 'import_clause') {
|
|
185
|
+
const defaultId = child.childForFieldName('name') ?? (
|
|
186
|
+
child.children.find(c => c.type === 'identifier') ?? null
|
|
187
|
+
);
|
|
188
|
+
if (defaultId) {
|
|
189
|
+
edges.push({
|
|
190
|
+
source: filePath,
|
|
191
|
+
target: defaultId.text,
|
|
192
|
+
relation: 'imports',
|
|
193
|
+
confidence: 'EXTRACTED',
|
|
194
|
+
sourceFile: filePath,
|
|
195
|
+
sourceLocation: loc(n),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// Always emit a file-level import edge to the module path
|
|
202
|
+
if (moduleText) {
|
|
203
|
+
edges.push({
|
|
204
|
+
source: basename(filePath),
|
|
205
|
+
target: moduleText,
|
|
206
|
+
relation: 'imports',
|
|
207
|
+
confidence: 'EXTRACTED',
|
|
208
|
+
sourceFile: filePath,
|
|
209
|
+
sourceLocation: loc(n),
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ---- call expressions ----
|
|
216
|
+
if (n.type === 'call_expression') {
|
|
217
|
+
const fnNode = n.childForFieldName('function');
|
|
218
|
+
if (!fnNode) return;
|
|
219
|
+
|
|
220
|
+
// Resolve callee name (may be `a.b()` or plain `foo()`)
|
|
221
|
+
let calleeName: string;
|
|
222
|
+
if (fnNode.type === 'member_expression') {
|
|
223
|
+
calleeName = fnNode.text;
|
|
224
|
+
} else {
|
|
225
|
+
calleeName = fnNode.text;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// Attribute to deepest known method context
|
|
229
|
+
const caller =
|
|
230
|
+
methodStack[methodStack.length - 1] ??
|
|
231
|
+
classStack[classStack.length - 1] ??
|
|
232
|
+
basename(filePath);
|
|
233
|
+
|
|
234
|
+
if (calleeName && caller && calleeName !== caller) {
|
|
235
|
+
edges.push({
|
|
236
|
+
source: caller,
|
|
237
|
+
target: calleeName,
|
|
238
|
+
relation: 'calls',
|
|
239
|
+
confidence: 'INFERRED',
|
|
240
|
+
confidenceScore: 0.8,
|
|
241
|
+
sourceFile: filePath,
|
|
242
|
+
sourceLocation: loc(n),
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
return { nodes, edges, filesProcessed: 1, fromCache: 0, errors };
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
// ---- regex fallback ----
|
|
252
|
+
|
|
253
|
+
function extractWithRegex(filePath: string, content: string): ExtractionResult {
|
|
254
|
+
const nodes: GraphNode[] = [];
|
|
255
|
+
const edges: GraphEdge[] = [];
|
|
256
|
+
|
|
257
|
+
const lines = content.split('\n');
|
|
258
|
+
|
|
259
|
+
lines.forEach((line, idx) => {
|
|
260
|
+
const location = `L${idx + 1}`;
|
|
261
|
+
|
|
262
|
+
// class
|
|
263
|
+
const classMatch = line.match(/^(export\s+)?(abstract\s+)?class\s+(\w+)/);
|
|
264
|
+
if (classMatch) {
|
|
265
|
+
const name = classMatch[3];
|
|
266
|
+
nodes.push({ id: name, label: name, fileType: 'code', sourceFile: filePath, sourceLocation: location });
|
|
267
|
+
|
|
268
|
+
// extends
|
|
269
|
+
const extendsMatch = line.match(/\bextends\s+(\w+)/);
|
|
270
|
+
if (extendsMatch) {
|
|
271
|
+
edges.push({ source: name, target: extendsMatch[1], relation: 'extends', confidence: 'EXTRACTED', sourceFile: filePath, sourceLocation: location });
|
|
272
|
+
}
|
|
273
|
+
// implements
|
|
274
|
+
const implementsMatch = line.match(/\bimplements\s+([\w,\s]+)/);
|
|
275
|
+
if (implementsMatch) {
|
|
276
|
+
for (const impl of implementsMatch[1].split(',')) {
|
|
277
|
+
const implName = impl.trim().split(/\s/)[0];
|
|
278
|
+
if (implName) {
|
|
279
|
+
edges.push({ source: name, target: implName, relation: 'implements', confidence: 'EXTRACTED', sourceFile: filePath, sourceLocation: location });
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// interface
|
|
286
|
+
const ifaceMatch = line.match(/^(export\s+)?interface\s+(\w+)/);
|
|
287
|
+
if (ifaceMatch) {
|
|
288
|
+
const name = ifaceMatch[2];
|
|
289
|
+
nodes.push({ id: name, label: name, fileType: 'code', sourceFile: filePath, sourceLocation: location });
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// type alias
|
|
293
|
+
const typeMatch = line.match(/^(export\s+)?type\s+(\w+)\s*=/);
|
|
294
|
+
if (typeMatch) {
|
|
295
|
+
const name = typeMatch[2];
|
|
296
|
+
nodes.push({ id: name, label: name, fileType: 'code', sourceFile: filePath, sourceLocation: location });
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// function
|
|
300
|
+
const funcMatch = line.match(/^(export\s+)?(default\s+)?(async\s+)?function\s+(\w+)/);
|
|
301
|
+
if (funcMatch) {
|
|
302
|
+
const name = funcMatch[4];
|
|
303
|
+
nodes.push({ id: name, label: name, fileType: 'code', sourceFile: filePath, sourceLocation: location });
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
// import
|
|
307
|
+
const importMatch = line.match(/^import\s+.*from\s+['"]([^'"]+)['"]/);
|
|
308
|
+
if (importMatch) {
|
|
309
|
+
edges.push({ source: basename(filePath), target: importMatch[1], relation: 'imports', confidence: 'EXTRACTED', sourceFile: filePath, sourceLocation: location });
|
|
310
|
+
|
|
311
|
+
// Named specifiers
|
|
312
|
+
const namedMatch = line.match(/\{([^}]+)\}/);
|
|
313
|
+
if (namedMatch) {
|
|
314
|
+
for (const spec of namedMatch[1].split(',')) {
|
|
315
|
+
const specName = spec.trim().split(/\s+as\s+/)[0].trim();
|
|
316
|
+
if (specName) {
|
|
317
|
+
edges.push({ source: filePath, target: specName, relation: 'imports', confidence: 'EXTRACTED', sourceFile: filePath, sourceLocation: location });
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return { nodes, edges, filesProcessed: 1, fromCache: 0, errors: [] };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// ---- extractor implementation ----
|
|
328
|
+
|
|
329
|
+
export const typescriptExtractor: LanguageExtractor = {
|
|
330
|
+
language: 'typescript',
|
|
331
|
+
extensions: ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'],
|
|
332
|
+
|
|
333
|
+
extract(filePath: string, content: string): ExtractionResult {
|
|
334
|
+
const lang = filePath.endsWith('.tsx') || filePath.endsWith('.jsx') ? 'tsx' : 'typescript';
|
|
335
|
+
|
|
336
|
+
const tsResult = extractWithTreeSitter(filePath, content, lang);
|
|
337
|
+
if (tsResult.nodes.length > 0 || tsResult.edges.length > 0 || tsResult.errors.length > 0) {
|
|
338
|
+
return tsResult;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
// tree-sitter not available or produced nothing — fall back to regex
|
|
342
|
+
return extractWithRegex(filePath, content);
|
|
343
|
+
},
|
|
344
|
+
};
|