@kodus/kodus-graph 0.1.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/package.json +62 -0
- package/src/analysis/blast-radius.ts +54 -0
- package/src/analysis/communities.ts +135 -0
- package/src/analysis/diff.ts +120 -0
- package/src/analysis/flows.ts +112 -0
- package/src/analysis/review-context.ts +141 -0
- package/src/analysis/risk-score.ts +62 -0
- package/src/analysis/search.ts +76 -0
- package/src/analysis/test-gaps.ts +21 -0
- package/src/cli.ts +192 -0
- package/src/commands/analyze.ts +66 -0
- package/src/commands/communities.ts +19 -0
- package/src/commands/context.ts +69 -0
- package/src/commands/diff.ts +96 -0
- package/src/commands/flows.ts +19 -0
- package/src/commands/parse.ts +100 -0
- package/src/commands/search.ts +41 -0
- package/src/commands/update.ts +166 -0
- package/src/graph/builder.ts +170 -0
- package/src/graph/edges.ts +101 -0
- package/src/graph/loader.ts +100 -0
- package/src/graph/merger.ts +25 -0
- package/src/graph/types.ts +218 -0
- package/src/parser/batch.ts +74 -0
- package/src/parser/discovery.ts +42 -0
- package/src/parser/extractor.ts +37 -0
- package/src/parser/extractors/generic.ts +87 -0
- package/src/parser/extractors/python.ts +127 -0
- package/src/parser/extractors/ruby.ts +142 -0
- package/src/parser/extractors/typescript.ts +329 -0
- package/src/parser/languages.ts +122 -0
- package/src/resolver/call-resolver.ts +179 -0
- package/src/resolver/import-map.ts +27 -0
- package/src/resolver/import-resolver.ts +72 -0
- package/src/resolver/languages/csharp.ts +7 -0
- package/src/resolver/languages/go.ts +7 -0
- package/src/resolver/languages/java.ts +7 -0
- package/src/resolver/languages/php.ts +7 -0
- package/src/resolver/languages/python.ts +35 -0
- package/src/resolver/languages/ruby.ts +21 -0
- package/src/resolver/languages/rust.ts +7 -0
- package/src/resolver/languages/typescript.ts +168 -0
- package/src/resolver/symbol-table.ts +53 -0
- package/src/shared/file-hash.ts +7 -0
- package/src/shared/filters.ts +243 -0
- package/src/shared/logger.ts +14 -0
- package/src/shared/qualified-name.ts +5 -0
- package/src/shared/safe-path.ts +31 -0
- package/src/shared/schemas.ts +31 -0
- package/src/shared/temp.ts +17 -0
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import type { SgNode, SgRoot } from '@ast-grep/napi';
|
|
2
|
+
import type { RawCallSite, RawGraph } from '../../graph/types';
|
|
3
|
+
import { NOISE } from '../../shared/filters';
|
|
4
|
+
import { log } from '../../shared/logger';
|
|
5
|
+
import { LANG_KINDS } from '../languages';
|
|
6
|
+
|
|
7
|
+
export function extractRuby(root: SgRoot, fp: string, seen: Set<string>, graph: RawGraph): void {
|
|
8
|
+
const kinds = LANG_KINDS.ruby;
|
|
9
|
+
const rootNode = root.root();
|
|
10
|
+
|
|
11
|
+
// ── Classes ──
|
|
12
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.class } })) {
|
|
13
|
+
const name = node.field('name')?.text();
|
|
14
|
+
if (!name || seen.has(`c:${fp}:${name}`)) continue;
|
|
15
|
+
seen.add(`c:${fp}:${name}`);
|
|
16
|
+
|
|
17
|
+
const superclass = node.field('superclass')?.text() || '';
|
|
18
|
+
graph.classes.push({
|
|
19
|
+
name,
|
|
20
|
+
file: fp,
|
|
21
|
+
line_start: node.range().start.line,
|
|
22
|
+
line_end: node.range().end.line,
|
|
23
|
+
extends: superclass,
|
|
24
|
+
implements: '',
|
|
25
|
+
qualified: `${fp}::${name}`,
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// ── Modules ──
|
|
30
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.module } })) {
|
|
31
|
+
const name = node.field('name')?.text();
|
|
32
|
+
if (!name || seen.has(`c:${fp}:${name}`)) continue;
|
|
33
|
+
seen.add(`c:${fp}:${name}`);
|
|
34
|
+
graph.classes.push({
|
|
35
|
+
name,
|
|
36
|
+
file: fp,
|
|
37
|
+
line_start: node.range().start.line,
|
|
38
|
+
line_end: node.range().end.line,
|
|
39
|
+
extends: '',
|
|
40
|
+
implements: '',
|
|
41
|
+
qualified: `${fp}::${name}`,
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ── Methods ──
|
|
46
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.method } })) {
|
|
47
|
+
const name = node.field('name')?.text();
|
|
48
|
+
if (!name) continue;
|
|
49
|
+
const line = node.range().start.line;
|
|
50
|
+
if (seen.has(`m:${fp}:${name}:${line}`)) continue;
|
|
51
|
+
seen.add(`m:${fp}:${name}:${line}`);
|
|
52
|
+
|
|
53
|
+
const classAncestor = node.ancestors().find((a: SgNode) => a.kind() === kinds.class || a.kind() === kinds.module);
|
|
54
|
+
const className = classAncestor?.field('name')?.text() || '';
|
|
55
|
+
|
|
56
|
+
graph.functions.push({
|
|
57
|
+
name,
|
|
58
|
+
file: fp,
|
|
59
|
+
line_start: line,
|
|
60
|
+
line_end: node.range().end.line,
|
|
61
|
+
params: node.field('parameters')?.text() || '()',
|
|
62
|
+
returnType: '',
|
|
63
|
+
kind: className ? 'Method' : 'Function',
|
|
64
|
+
className,
|
|
65
|
+
qualified: className ? `${fp}::${className}.${name}` : `${fp}::${name}`,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// ── Tests (RSpec: describe/it/context) ──
|
|
70
|
+
for (const p of [
|
|
71
|
+
"describe '$NAME' do $$$BODY end",
|
|
72
|
+
'describe "$NAME" do $$$BODY end',
|
|
73
|
+
"it '$NAME' do $$$BODY end",
|
|
74
|
+
'it "$NAME" do $$$BODY end',
|
|
75
|
+
"context '$NAME' do $$$BODY end",
|
|
76
|
+
'context "$NAME" do $$$BODY end',
|
|
77
|
+
]) {
|
|
78
|
+
try {
|
|
79
|
+
for (const m of rootNode.findAll(p)) {
|
|
80
|
+
const name = m.getMatch('NAME')?.text();
|
|
81
|
+
if (!name) continue;
|
|
82
|
+
const key = `t:${fp}:${name}:${m.range().start.line}`;
|
|
83
|
+
if (seen.has(key)) continue;
|
|
84
|
+
seen.add(key);
|
|
85
|
+
graph.tests.push({
|
|
86
|
+
name,
|
|
87
|
+
file: fp,
|
|
88
|
+
line_start: m.range().start.line,
|
|
89
|
+
line_end: m.range().end.line,
|
|
90
|
+
qualified: `${fp}::test:${name}`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
log.debug('Ruby pattern mismatch', { file: fp, pattern: p, error: String(err) });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Imports (require/require_relative) ──
|
|
99
|
+
for (const p of [
|
|
100
|
+
"require '$MODULE'",
|
|
101
|
+
'require "$MODULE"',
|
|
102
|
+
"require_relative '$MODULE'",
|
|
103
|
+
'require_relative "$MODULE"',
|
|
104
|
+
]) {
|
|
105
|
+
try {
|
|
106
|
+
for (const m of rootNode.findAll(p)) {
|
|
107
|
+
const mod = m.getMatch('MODULE')?.text();
|
|
108
|
+
if (mod) {
|
|
109
|
+
graph.imports.push({
|
|
110
|
+
module: mod,
|
|
111
|
+
file: fp,
|
|
112
|
+
line: m.range().start.line,
|
|
113
|
+
names: [],
|
|
114
|
+
lang: 'ruby',
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
} catch (err) {
|
|
119
|
+
log.debug('Ruby pattern mismatch', { file: fp, pattern: p, error: String(err) });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Extract raw call sites from a Ruby AST.
|
|
126
|
+
* Direct calls only — Ruby has no DI pattern.
|
|
127
|
+
*/
|
|
128
|
+
export function extractCallsFromRuby(root: SgRoot, fp: string, calls: RawCallSite[]): void {
|
|
129
|
+
const rootNode = root.root();
|
|
130
|
+
|
|
131
|
+
for (const m of rootNode.findAll('$CALLEE($$$ARGS)')) {
|
|
132
|
+
const callee = m.getMatch('CALLEE')?.text();
|
|
133
|
+
if (!callee) continue;
|
|
134
|
+
const callName = callee.includes('.') ? callee.split('.').pop()! : callee;
|
|
135
|
+
if (NOISE.has(callName)) continue;
|
|
136
|
+
calls.push({
|
|
137
|
+
source: fp,
|
|
138
|
+
callName,
|
|
139
|
+
line: m.range().start.line,
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import type { SgNode, SgRoot } from '@ast-grep/napi';
|
|
2
|
+
import { Lang } from '@ast-grep/napi';
|
|
3
|
+
import type { RawCallSite, RawGraph } from '../../graph/types';
|
|
4
|
+
import { NOISE } from '../../shared/filters';
|
|
5
|
+
import { LANG_KINDS } from '../languages';
|
|
6
|
+
|
|
7
|
+
export function extractTypeScript(
|
|
8
|
+
root: SgRoot,
|
|
9
|
+
fp: string,
|
|
10
|
+
seen: Set<string>,
|
|
11
|
+
graph: RawGraph,
|
|
12
|
+
lang: Lang | string = Lang.TypeScript,
|
|
13
|
+
): void {
|
|
14
|
+
const kinds = LANG_KINDS.typescript;
|
|
15
|
+
const rootNode = root.root();
|
|
16
|
+
const isTS = lang === Lang.TypeScript || lang === Lang.Tsx;
|
|
17
|
+
|
|
18
|
+
// ── Classes ──
|
|
19
|
+
const classKinds = isTS ? [kinds.class, kinds.abstractClass] : [kinds.class];
|
|
20
|
+
for (const kind of classKinds) {
|
|
21
|
+
for (const node of rootNode.findAll({ rule: { kind } })) {
|
|
22
|
+
const name = node.field('name')?.text();
|
|
23
|
+
if (!name || seen.has(`c:${fp}:${name}`)) continue;
|
|
24
|
+
seen.add(`c:${fp}:${name}`);
|
|
25
|
+
|
|
26
|
+
let extendsName = '';
|
|
27
|
+
let implementsName = '';
|
|
28
|
+
const heritage = node.children().find((c: SgNode) => c.kind() === 'class_heritage');
|
|
29
|
+
if (heritage) {
|
|
30
|
+
const ext = heritage.children().find((c: SgNode) => c.kind() === 'extends_clause');
|
|
31
|
+
extendsName =
|
|
32
|
+
ext
|
|
33
|
+
?.children()
|
|
34
|
+
.find(
|
|
35
|
+
(c: SgNode) =>
|
|
36
|
+
c.kind() === 'identifier' || c.kind() === 'type_identifier' || c.kind() === 'member_expression',
|
|
37
|
+
)
|
|
38
|
+
?.text() || '';
|
|
39
|
+
const impl = heritage.children().find((c: SgNode) => c.kind() === 'implements_clause');
|
|
40
|
+
implementsName =
|
|
41
|
+
impl
|
|
42
|
+
?.children()
|
|
43
|
+
.find((c: SgNode) => c.kind() === 'type_identifier' || c.kind() === 'identifier')
|
|
44
|
+
?.text() || '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
graph.classes.push({
|
|
48
|
+
name,
|
|
49
|
+
file: fp,
|
|
50
|
+
line_start: node.range().start.line,
|
|
51
|
+
line_end: node.range().end.line,
|
|
52
|
+
extends: extendsName,
|
|
53
|
+
implements: implementsName,
|
|
54
|
+
qualified: `${fp}::${name}`,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ── Methods (kind-based: catches constructor, async, getters/setters) ──
|
|
60
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.method } })) {
|
|
61
|
+
const name = node.field('name')?.text();
|
|
62
|
+
if (!name) continue;
|
|
63
|
+
const line = node.range().start.line;
|
|
64
|
+
if (seen.has(`m:${fp}:${name}:${line}`)) continue;
|
|
65
|
+
seen.add(`m:${fp}:${name}:${line}`);
|
|
66
|
+
|
|
67
|
+
const classAncestor = node
|
|
68
|
+
.ancestors()
|
|
69
|
+
.find((a: SgNode) => a.kind() === kinds.class || (isTS && a.kind() === kinds.abstractClass));
|
|
70
|
+
const className = classAncestor?.field('name')?.text() || '';
|
|
71
|
+
const params = node.field('parameters');
|
|
72
|
+
const retType = node.field('return_type')?.text()?.replace(/^:\s*/, '') || '';
|
|
73
|
+
|
|
74
|
+
if (name === 'constructor' && className) {
|
|
75
|
+
// Constructor DI extraction
|
|
76
|
+
const fieldTypeMap = new Map<string, string>();
|
|
77
|
+
if (params) {
|
|
78
|
+
for (const p of params.children()) {
|
|
79
|
+
if (p.kind() !== 'required_parameter') continue;
|
|
80
|
+
if (!p.children().some((c: SgNode) => c.kind() === 'accessibility_modifier')) continue;
|
|
81
|
+
const ident = p.children().find((c: SgNode) => c.kind() === 'identifier');
|
|
82
|
+
const typeAnn = p.children().find((c: SgNode) => c.kind() === 'type_annotation');
|
|
83
|
+
if (ident && typeAnn) {
|
|
84
|
+
const typeNode = typeAnn
|
|
85
|
+
.children()
|
|
86
|
+
.find(
|
|
87
|
+
(c: SgNode) =>
|
|
88
|
+
c.kind() === 'type_identifier' || c.kind() === 'identifier' || c.kind() === 'generic_type',
|
|
89
|
+
);
|
|
90
|
+
if (typeNode) {
|
|
91
|
+
const typeName =
|
|
92
|
+
typeNode.kind() === 'generic_type'
|
|
93
|
+
? typeNode
|
|
94
|
+
.children()
|
|
95
|
+
.find((c: SgNode) => c.kind() === 'type_identifier')
|
|
96
|
+
?.text() || typeNode.text()
|
|
97
|
+
: typeNode.text();
|
|
98
|
+
fieldTypeMap.set(ident.text(), typeName);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
if (fieldTypeMap.size > 0) graph.diMaps.set(fp, fieldTypeMap);
|
|
104
|
+
|
|
105
|
+
graph.functions.push({
|
|
106
|
+
name: `${className}.constructor`,
|
|
107
|
+
file: fp,
|
|
108
|
+
line_start: line,
|
|
109
|
+
line_end: node.range().end.line,
|
|
110
|
+
params: params?.text() || '()',
|
|
111
|
+
returnType: '',
|
|
112
|
+
kind: 'Constructor',
|
|
113
|
+
className,
|
|
114
|
+
qualified: `${fp}::${className}.constructor`,
|
|
115
|
+
});
|
|
116
|
+
} else {
|
|
117
|
+
graph.functions.push({
|
|
118
|
+
name,
|
|
119
|
+
file: fp,
|
|
120
|
+
line_start: line,
|
|
121
|
+
line_end: node.range().end.line,
|
|
122
|
+
params: params?.text() || '()',
|
|
123
|
+
returnType: retType,
|
|
124
|
+
kind: className ? 'Method' : 'Function',
|
|
125
|
+
className,
|
|
126
|
+
qualified: className ? `${fp}::${className}.${name}` : `${fp}::${name}`,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ── Standalone functions ──
|
|
132
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.function } })) {
|
|
133
|
+
const name = node.field('name')?.text();
|
|
134
|
+
if (!name) continue;
|
|
135
|
+
const line = node.range().start.line;
|
|
136
|
+
if (seen.has(`f:${fp}:${name}:${line}`)) continue;
|
|
137
|
+
if (node.ancestors().some((a: SgNode) => a.kind() === kinds.class || (isTS && a.kind() === kinds.abstractClass)))
|
|
138
|
+
continue;
|
|
139
|
+
seen.add(`f:${fp}:${name}:${line}`);
|
|
140
|
+
|
|
141
|
+
graph.functions.push({
|
|
142
|
+
name,
|
|
143
|
+
file: fp,
|
|
144
|
+
line_start: line,
|
|
145
|
+
line_end: node.range().end.line,
|
|
146
|
+
params: node.field('parameters')?.text() || '()',
|
|
147
|
+
returnType: node.field('return_type')?.text()?.replace(/^:\s*/, '') || '',
|
|
148
|
+
kind: 'Function',
|
|
149
|
+
className: '',
|
|
150
|
+
qualified: `${fp}::${name}`,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// ── Arrow functions ──
|
|
155
|
+
for (const node of rootNode.findAll({
|
|
156
|
+
rule: { kind: kinds.arrowContainer, has: { kind: kinds.arrowFunction } },
|
|
157
|
+
})) {
|
|
158
|
+
const name = node.field('name')?.text();
|
|
159
|
+
if (!name) continue;
|
|
160
|
+
const line = node.range().start.line;
|
|
161
|
+
if (seen.has(`f:${fp}:${name}:${line}`)) continue;
|
|
162
|
+
seen.add(`f:${fp}:${name}:${line}`);
|
|
163
|
+
|
|
164
|
+
const arrow = node.children().find((c: SgNode) => c.kind() === kinds.arrowFunction);
|
|
165
|
+
graph.functions.push({
|
|
166
|
+
name,
|
|
167
|
+
file: fp,
|
|
168
|
+
line_start: line,
|
|
169
|
+
line_end: node.range().end.line,
|
|
170
|
+
params: arrow?.field('parameters')?.text() || '()',
|
|
171
|
+
returnType: arrow?.field('return_type')?.text()?.replace(/^:\s*/, '') || '',
|
|
172
|
+
kind: 'Function',
|
|
173
|
+
className: '',
|
|
174
|
+
qualified: `${fp}::${name}`,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ── Interfaces (TS only — JS grammar has no interface_declaration) ──
|
|
179
|
+
if (isTS)
|
|
180
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.interface } })) {
|
|
181
|
+
const name = node.field('name')?.text();
|
|
182
|
+
if (!name || seen.has(`i:${fp}:${name}`)) continue;
|
|
183
|
+
seen.add(`i:${fp}:${name}`);
|
|
184
|
+
|
|
185
|
+
const methods: string[] = [];
|
|
186
|
+
const body = node.field('body');
|
|
187
|
+
if (body) {
|
|
188
|
+
for (const child of body.findAll({ rule: { kind: kinds.methodSignature } })) {
|
|
189
|
+
const mn = child.field('name')?.text();
|
|
190
|
+
if (mn) methods.push(mn);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
graph.interfaces.push({
|
|
195
|
+
name,
|
|
196
|
+
file: fp,
|
|
197
|
+
line_start: node.range().start.line,
|
|
198
|
+
line_end: node.range().end.line,
|
|
199
|
+
methods,
|
|
200
|
+
qualified: `${fp}::${name}`,
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ── Enums (TS only — JS grammar has no enum_declaration) ──
|
|
205
|
+
if (isTS)
|
|
206
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.enum } })) {
|
|
207
|
+
const name = node.field('name')?.text();
|
|
208
|
+
if (!name || seen.has(`e:${fp}:${name}`)) continue;
|
|
209
|
+
seen.add(`e:${fp}:${name}`);
|
|
210
|
+
graph.enums.push({
|
|
211
|
+
name,
|
|
212
|
+
file: fp,
|
|
213
|
+
line_start: node.range().start.line,
|
|
214
|
+
line_end: node.range().end.line,
|
|
215
|
+
qualified: `${fp}::${name}`,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// ── Imports ──
|
|
220
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.import } })) {
|
|
221
|
+
const sourceNode = node.children().find((c: SgNode) => c.kind() === 'string');
|
|
222
|
+
const frag = sourceNode?.children().find((c: SgNode) => c.kind() === 'string_fragment');
|
|
223
|
+
const modulePath = frag?.text() || sourceNode?.text()?.replace(/['"]/g, '') || '';
|
|
224
|
+
if (!modulePath) continue;
|
|
225
|
+
|
|
226
|
+
const names: string[] = [];
|
|
227
|
+
const importClause = node.children().find((c: SgNode) => c.kind() === 'import_clause');
|
|
228
|
+
if (importClause) {
|
|
229
|
+
for (const child of importClause.children()) {
|
|
230
|
+
if (child.kind() === 'identifier') {
|
|
231
|
+
names.push(child.text());
|
|
232
|
+
} else if (child.kind() === 'named_imports') {
|
|
233
|
+
for (const spec of child.findAll({ rule: { kind: 'import_specifier' } })) {
|
|
234
|
+
const n =
|
|
235
|
+
spec.field('name')?.text() ||
|
|
236
|
+
spec
|
|
237
|
+
.children()
|
|
238
|
+
.find((c: SgNode) => c.kind() === 'identifier')
|
|
239
|
+
?.text();
|
|
240
|
+
if (n) names.push(n);
|
|
241
|
+
}
|
|
242
|
+
} else if (child.kind() === 'namespace_import') {
|
|
243
|
+
const alias = child.children().find((c: SgNode) => c.kind() === 'identifier');
|
|
244
|
+
if (alias) names.push(alias.text());
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
graph.imports.push({
|
|
249
|
+
module: modulePath,
|
|
250
|
+
file: fp,
|
|
251
|
+
line: node.range().start.line,
|
|
252
|
+
names,
|
|
253
|
+
lang: 'ts',
|
|
254
|
+
});
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// ── Re-exports ──
|
|
258
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.export } })) {
|
|
259
|
+
const src = node.children().find((c: SgNode) => c.kind() === 'string');
|
|
260
|
+
if (src) {
|
|
261
|
+
const frag = src.children().find((c: SgNode) => c.kind() === 'string_fragment');
|
|
262
|
+
graph.reExports.push({
|
|
263
|
+
module: frag?.text() || src.text().replace(/['"]/g, ''),
|
|
264
|
+
file: fp,
|
|
265
|
+
line: node.range().start.line,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// ── Tests (pattern-based) ──
|
|
271
|
+
for (const p of [
|
|
272
|
+
'describe("$NAME", $$$BODY)',
|
|
273
|
+
"describe('$NAME', $$$BODY)",
|
|
274
|
+
'it("$NAME", $$$BODY)',
|
|
275
|
+
"it('$NAME', $$$BODY)",
|
|
276
|
+
'test("$NAME", $$$BODY)',
|
|
277
|
+
"test('$NAME', $$$BODY)",
|
|
278
|
+
]) {
|
|
279
|
+
for (const m of rootNode.findAll(p)) {
|
|
280
|
+
const name = m.getMatch('NAME')?.text();
|
|
281
|
+
if (!name) continue;
|
|
282
|
+
const key = `t:${fp}:${name}:${m.range().start.line}`;
|
|
283
|
+
if (seen.has(key)) continue;
|
|
284
|
+
seen.add(key);
|
|
285
|
+
graph.tests.push({
|
|
286
|
+
name,
|
|
287
|
+
file: fp,
|
|
288
|
+
line_start: m.range().start.line,
|
|
289
|
+
line_end: m.range().end.line,
|
|
290
|
+
qualified: `${fp}::test:${name}`,
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Extract raw call sites from a TypeScript/JavaScript AST.
|
|
298
|
+
* Finds DI calls (this.field.method) and direct calls ($CALLEE($$$ARGS)).
|
|
299
|
+
* Filters NOISE. Does NOT resolve — just collects raw sites.
|
|
300
|
+
*/
|
|
301
|
+
export function extractCallsFromTypeScript(root: SgRoot, fp: string, calls: RawCallSite[]): void {
|
|
302
|
+
const rootNode = root.root();
|
|
303
|
+
|
|
304
|
+
// DI pattern: this.$FIELD.$METHOD($$$ARGS)
|
|
305
|
+
for (const m of rootNode.findAll('this.$FIELD.$METHOD($$$ARGS)')) {
|
|
306
|
+
const field = m.getMatch('FIELD')?.text();
|
|
307
|
+
const method = m.getMatch('METHOD')?.text();
|
|
308
|
+
if (!method || NOISE.has(method)) continue;
|
|
309
|
+
calls.push({
|
|
310
|
+
source: fp,
|
|
311
|
+
callName: method,
|
|
312
|
+
line: m.range().start.line,
|
|
313
|
+
diField: field,
|
|
314
|
+
});
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
// Direct calls: $CALLEE($$$ARGS)
|
|
318
|
+
for (const m of rootNode.findAll('$CALLEE($$$ARGS)')) {
|
|
319
|
+
const callee = m.getMatch('CALLEE')?.text();
|
|
320
|
+
if (!callee || callee.startsWith('this.')) continue;
|
|
321
|
+
const callName = callee.includes('.') ? callee.split('.').pop()! : callee;
|
|
322
|
+
if (NOISE.has(callName)) continue;
|
|
323
|
+
calls.push({
|
|
324
|
+
source: fp,
|
|
325
|
+
callName,
|
|
326
|
+
line: m.range().start.line,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import csharp from '@ast-grep/lang-csharp';
|
|
2
|
+
import go from '@ast-grep/lang-go';
|
|
3
|
+
import java from '@ast-grep/lang-java';
|
|
4
|
+
import php from '@ast-grep/lang-php';
|
|
5
|
+
import python from '@ast-grep/lang-python';
|
|
6
|
+
import ruby from '@ast-grep/lang-ruby';
|
|
7
|
+
import rust from '@ast-grep/lang-rust';
|
|
8
|
+
import { Lang, registerDynamicLanguage } from '@ast-grep/napi';
|
|
9
|
+
|
|
10
|
+
// Register dynamic languages at import time (side effect).
|
|
11
|
+
// This must happen before parseAsync can parse these languages.
|
|
12
|
+
registerDynamicLanguage({ python, ruby, go, java, rust, php, csharp });
|
|
13
|
+
|
|
14
|
+
// Extension -> language identifier
|
|
15
|
+
// Built-in langs use Lang enum, dynamic langs use lowercase string
|
|
16
|
+
const EXT_TO_LANG: Record<string, Lang | string> = {
|
|
17
|
+
'.ts': Lang.TypeScript,
|
|
18
|
+
'.tsx': Lang.Tsx,
|
|
19
|
+
'.js': Lang.JavaScript,
|
|
20
|
+
'.jsx': Lang.JavaScript,
|
|
21
|
+
'.mjs': Lang.JavaScript,
|
|
22
|
+
'.cjs': Lang.JavaScript,
|
|
23
|
+
'.py': 'python',
|
|
24
|
+
'.rb': 'ruby',
|
|
25
|
+
'.go': 'go',
|
|
26
|
+
'.java': 'java',
|
|
27
|
+
'.rs': 'rust',
|
|
28
|
+
'.cs': 'csharp',
|
|
29
|
+
'.php': 'php',
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export function getLanguage(ext: string): Lang | string | null {
|
|
33
|
+
return EXT_TO_LANG[ext] ?? null;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function getSupportedExtensions(): string[] {
|
|
37
|
+
return Object.keys(EXT_TO_LANG);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function getLanguageName(lang: Lang | string): string {
|
|
41
|
+
if (typeof lang === 'string') return lang;
|
|
42
|
+
if (lang === Lang.TypeScript || lang === Lang.Tsx) return 'typescript';
|
|
43
|
+
if (lang === Lang.JavaScript) return 'javascript';
|
|
44
|
+
return 'unknown';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isTypeScriptLike(lang: Lang | string): boolean {
|
|
48
|
+
return lang === Lang.TypeScript || lang === Lang.Tsx || lang === Lang.JavaScript;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// AST node kinds per language for structural extraction
|
|
52
|
+
export const LANG_KINDS: Record<string, Record<string, string>> = {
|
|
53
|
+
typescript: {
|
|
54
|
+
class: 'class_declaration',
|
|
55
|
+
abstractClass: 'abstract_class_declaration',
|
|
56
|
+
method: 'method_definition',
|
|
57
|
+
function: 'function_declaration',
|
|
58
|
+
arrowContainer: 'variable_declarator',
|
|
59
|
+
arrowFunction: 'arrow_function',
|
|
60
|
+
interface: 'interface_declaration',
|
|
61
|
+
enum: 'enum_declaration',
|
|
62
|
+
import: 'import_statement',
|
|
63
|
+
export: 'export_statement',
|
|
64
|
+
methodSignature: 'method_signature',
|
|
65
|
+
},
|
|
66
|
+
python: {
|
|
67
|
+
class: 'class_definition',
|
|
68
|
+
method: 'function_definition',
|
|
69
|
+
function: 'function_definition',
|
|
70
|
+
import: 'import_from_statement',
|
|
71
|
+
importRegular: 'import_statement',
|
|
72
|
+
decorator: 'decorator',
|
|
73
|
+
},
|
|
74
|
+
ruby: {
|
|
75
|
+
class: 'class',
|
|
76
|
+
method: 'method',
|
|
77
|
+
singletonMethod: 'singleton_method',
|
|
78
|
+
module: 'module',
|
|
79
|
+
call: 'call',
|
|
80
|
+
},
|
|
81
|
+
go: {
|
|
82
|
+
function: 'function_declaration',
|
|
83
|
+
method: 'method_declaration',
|
|
84
|
+
struct: 'type_declaration',
|
|
85
|
+
interface: 'type_declaration',
|
|
86
|
+
import: 'import_declaration',
|
|
87
|
+
},
|
|
88
|
+
java: {
|
|
89
|
+
class: 'class_declaration',
|
|
90
|
+
interface: 'interface_declaration',
|
|
91
|
+
method: 'method_declaration',
|
|
92
|
+
constructor: 'constructor_declaration',
|
|
93
|
+
import: 'import_declaration',
|
|
94
|
+
enum: 'enum_declaration',
|
|
95
|
+
},
|
|
96
|
+
rust: {
|
|
97
|
+
function: 'function_item',
|
|
98
|
+
struct: 'struct_item',
|
|
99
|
+
impl: 'impl_item',
|
|
100
|
+
trait: 'trait_item',
|
|
101
|
+
enum: 'enum_item',
|
|
102
|
+
use: 'use_declaration',
|
|
103
|
+
},
|
|
104
|
+
csharp: {
|
|
105
|
+
class: 'class_declaration',
|
|
106
|
+
interface: 'interface_declaration',
|
|
107
|
+
method: 'method_declaration',
|
|
108
|
+
constructor: 'constructor_declaration',
|
|
109
|
+
using: 'using_directive',
|
|
110
|
+
enum: 'enum_declaration',
|
|
111
|
+
namespace: 'namespace_declaration',
|
|
112
|
+
},
|
|
113
|
+
php: {
|
|
114
|
+
class: 'class_declaration',
|
|
115
|
+
method: 'method_declaration',
|
|
116
|
+
function: 'function_definition',
|
|
117
|
+
namespace: 'namespace_definition',
|
|
118
|
+
use: 'namespace_use_declaration',
|
|
119
|
+
},
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
export { Lang };
|