@kodus/kodus-graph 0.2.8 → 0.2.10
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/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/analysis/blast-radius.d.ts +2 -0
- package/dist/analysis/blast-radius.js +55 -0
- package/dist/analysis/communities.d.ts +28 -0
- package/dist/analysis/communities.js +100 -0
- package/dist/analysis/context-builder.d.ts +34 -0
- package/dist/analysis/context-builder.js +92 -0
- package/dist/analysis/diff.d.ts +41 -0
- package/dist/analysis/diff.js +155 -0
- package/dist/analysis/enrich.d.ts +5 -0
- package/dist/analysis/enrich.js +126 -0
- package/dist/analysis/flows.d.ts +27 -0
- package/dist/analysis/flows.js +86 -0
- package/dist/analysis/inheritance.d.ts +3 -0
- package/dist/analysis/inheritance.js +31 -0
- package/dist/analysis/prompt-formatter.d.ts +2 -0
- package/dist/analysis/prompt-formatter.js +173 -0
- package/dist/analysis/risk-score.d.ts +4 -0
- package/dist/analysis/risk-score.js +51 -0
- package/dist/analysis/search.d.ts +11 -0
- package/dist/analysis/search.js +64 -0
- package/dist/analysis/test-gaps.d.ts +2 -0
- package/dist/analysis/test-gaps.js +14 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +210 -0
- package/dist/commands/analyze.d.ts +9 -0
- package/dist/commands/analyze.js +116 -0
- package/dist/commands/communities.d.ts +8 -0
- package/dist/commands/communities.js +9 -0
- package/dist/commands/context.d.ts +12 -0
- package/dist/commands/context.js +130 -0
- package/dist/commands/diff.d.ts +9 -0
- package/dist/commands/diff.js +89 -0
- package/dist/commands/flows.d.ts +8 -0
- package/dist/commands/flows.js +9 -0
- package/dist/commands/parse.d.ts +11 -0
- package/dist/commands/parse.js +101 -0
- package/dist/commands/search.d.ts +12 -0
- package/dist/commands/search.js +27 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.js +154 -0
- package/dist/graph/builder.d.ts +6 -0
- package/dist/graph/builder.js +248 -0
- package/dist/graph/edges.d.ts +23 -0
- package/dist/graph/edges.js +159 -0
- package/dist/graph/json-writer.d.ts +9 -0
- package/dist/graph/json-writer.js +38 -0
- package/dist/graph/loader.d.ts +13 -0
- package/dist/graph/loader.js +101 -0
- package/dist/graph/merger.d.ts +7 -0
- package/dist/graph/merger.js +18 -0
- package/dist/graph/types.d.ts +252 -0
- package/dist/graph/types.js +1 -0
- package/dist/parser/batch.d.ts +5 -0
- package/dist/parser/batch.js +93 -0
- package/dist/parser/discovery.d.ts +7 -0
- package/dist/parser/discovery.js +61 -0
- package/dist/parser/extractor.d.ts +4 -0
- package/dist/parser/extractor.js +33 -0
- package/dist/parser/extractors/generic.d.ts +8 -0
- package/dist/parser/extractors/generic.js +471 -0
- package/dist/parser/extractors/python.d.ts +8 -0
- package/dist/parser/extractors/python.js +133 -0
- package/dist/parser/extractors/ruby.d.ts +8 -0
- package/dist/parser/extractors/ruby.js +153 -0
- package/dist/parser/extractors/typescript.d.ts +10 -0
- package/dist/parser/extractors/typescript.js +365 -0
- package/dist/parser/languages.d.ts +32 -0
- package/dist/parser/languages.js +304 -0
- package/dist/resolver/call-resolver.d.ts +36 -0
- package/dist/resolver/call-resolver.js +178 -0
- package/dist/resolver/external-detector.d.ts +11 -0
- package/dist/resolver/external-detector.js +820 -0
- package/dist/resolver/fs-cache.d.ts +8 -0
- package/dist/resolver/fs-cache.js +36 -0
- package/dist/resolver/import-map.d.ts +12 -0
- package/dist/resolver/import-map.js +21 -0
- package/dist/resolver/import-resolver.d.ts +19 -0
- package/dist/resolver/import-resolver.js +310 -0
- package/dist/resolver/languages/csharp.d.ts +3 -0
- package/dist/resolver/languages/csharp.js +94 -0
- package/dist/resolver/languages/go.d.ts +3 -0
- package/dist/resolver/languages/go.js +197 -0
- package/dist/resolver/languages/java.d.ts +1 -0
- package/dist/resolver/languages/java.js +193 -0
- package/dist/resolver/languages/php.d.ts +3 -0
- package/dist/resolver/languages/php.js +75 -0
- package/dist/resolver/languages/python.d.ts +11 -0
- package/dist/resolver/languages/python.js +127 -0
- package/dist/resolver/languages/ruby.d.ts +24 -0
- package/dist/resolver/languages/ruby.js +110 -0
- package/dist/resolver/languages/rust.d.ts +1 -0
- package/dist/resolver/languages/rust.js +197 -0
- package/dist/resolver/languages/typescript.d.ts +35 -0
- package/dist/resolver/languages/typescript.js +416 -0
- package/dist/resolver/re-export-resolver.d.ts +24 -0
- package/dist/resolver/re-export-resolver.js +57 -0
- package/dist/resolver/symbol-table.d.ts +17 -0
- package/dist/resolver/symbol-table.js +60 -0
- package/dist/shared/extract-calls.d.ts +26 -0
- package/dist/shared/extract-calls.js +57 -0
- package/dist/shared/file-hash.d.ts +3 -0
- package/dist/shared/file-hash.js +10 -0
- package/dist/shared/filters.d.ts +3 -0
- package/dist/shared/filters.js +240 -0
- package/dist/shared/logger.d.ts +6 -0
- package/dist/shared/logger.js +17 -0
- package/dist/shared/qualified-name.d.ts +1 -0
- package/dist/shared/qualified-name.js +9 -0
- package/dist/shared/safe-path.d.ts +6 -0
- package/dist/shared/safe-path.js +29 -0
- package/dist/shared/schemas.d.ts +43 -0
- package/dist/shared/schemas.js +30 -0
- package/dist/shared/temp.d.ts +11 -0
- package/{src/shared/temp.ts → dist/shared/temp.js} +4 -5
- package/package.json +20 -6
- package/src/analysis/blast-radius.ts +0 -54
- package/src/analysis/communities.ts +0 -135
- package/src/analysis/context-builder.ts +0 -130
- package/src/analysis/diff.ts +0 -169
- package/src/analysis/enrich.ts +0 -110
- package/src/analysis/flows.ts +0 -112
- package/src/analysis/inheritance.ts +0 -34
- package/src/analysis/prompt-formatter.ts +0 -175
- package/src/analysis/risk-score.ts +0 -62
- package/src/analysis/search.ts +0 -76
- package/src/analysis/test-gaps.ts +0 -21
- package/src/cli.ts +0 -210
- package/src/commands/analyze.ts +0 -128
- package/src/commands/communities.ts +0 -19
- package/src/commands/context.ts +0 -182
- package/src/commands/diff.ts +0 -96
- package/src/commands/flows.ts +0 -19
- package/src/commands/parse.ts +0 -124
- package/src/commands/search.ts +0 -41
- package/src/commands/update.ts +0 -166
- package/src/graph/builder.ts +0 -209
- package/src/graph/edges.ts +0 -101
- package/src/graph/json-writer.ts +0 -43
- package/src/graph/loader.ts +0 -113
- package/src/graph/merger.ts +0 -25
- package/src/graph/types.ts +0 -283
- package/src/parser/batch.ts +0 -82
- package/src/parser/discovery.ts +0 -75
- package/src/parser/extractor.ts +0 -37
- package/src/parser/extractors/generic.ts +0 -132
- package/src/parser/extractors/python.ts +0 -133
- package/src/parser/extractors/ruby.ts +0 -147
- package/src/parser/extractors/typescript.ts +0 -350
- package/src/parser/languages.ts +0 -122
- package/src/resolver/call-resolver.ts +0 -244
- package/src/resolver/import-map.ts +0 -27
- package/src/resolver/import-resolver.ts +0 -72
- package/src/resolver/languages/csharp.ts +0 -7
- package/src/resolver/languages/go.ts +0 -7
- package/src/resolver/languages/java.ts +0 -7
- package/src/resolver/languages/php.ts +0 -7
- package/src/resolver/languages/python.ts +0 -35
- package/src/resolver/languages/ruby.ts +0 -21
- package/src/resolver/languages/rust.ts +0 -7
- package/src/resolver/languages/typescript.ts +0 -168
- package/src/resolver/re-export-resolver.ts +0 -66
- package/src/resolver/symbol-table.ts +0 -67
- package/src/shared/extract-calls.ts +0 -75
- package/src/shared/file-hash.ts +0 -12
- package/src/shared/filters.ts +0 -243
- package/src/shared/logger.ts +0 -17
- package/src/shared/qualified-name.ts +0 -5
- package/src/shared/safe-path.ts +0 -31
- package/src/shared/schemas.ts +0 -32
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SgRoot } from '@ast-grep/napi';
|
|
2
|
+
import type { RawCallSite, RawGraph } from '../../graph/types';
|
|
3
|
+
export declare function extractPython(root: SgRoot, fp: string, seen: Set<string>, graph: RawGraph): void;
|
|
4
|
+
/**
|
|
5
|
+
* Extract raw call sites from a Python AST.
|
|
6
|
+
* Detects self.X() and super().X() to preserve class resolution context.
|
|
7
|
+
*/
|
|
8
|
+
export declare function extractCallsFromPython(root: SgRoot, fp: string, calls: RawCallSite[]): void;
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { extractCalls } from '../../shared/extract-calls';
|
|
2
|
+
import { computeContentHash } from '../../shared/file-hash';
|
|
3
|
+
import { LANG_KINDS } from '../languages';
|
|
4
|
+
export function extractPython(root, fp, seen, graph) {
|
|
5
|
+
const kinds = LANG_KINDS.python;
|
|
6
|
+
const rootNode = root.root();
|
|
7
|
+
// ── Classes ──
|
|
8
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.class } })) {
|
|
9
|
+
const name = node.field('name')?.text();
|
|
10
|
+
if (!name || seen.has(`c:${fp}:${name}`)) {
|
|
11
|
+
continue;
|
|
12
|
+
}
|
|
13
|
+
seen.add(`c:${fp}:${name}`);
|
|
14
|
+
const argList = node.field('superclasses') || node.children().find((c) => c.kind() === 'argument_list');
|
|
15
|
+
const extendsName = argList
|
|
16
|
+
?.children()
|
|
17
|
+
.find((c) => c.kind() === 'identifier')
|
|
18
|
+
?.text() || '';
|
|
19
|
+
graph.classes.push({
|
|
20
|
+
name,
|
|
21
|
+
file: fp,
|
|
22
|
+
line_start: node.range().start.line,
|
|
23
|
+
line_end: node.range().end.line,
|
|
24
|
+
extends: extendsName,
|
|
25
|
+
implements: [],
|
|
26
|
+
ast_kind: String(node.kind()),
|
|
27
|
+
qualified: `${fp}::${name}`,
|
|
28
|
+
content_hash: computeContentHash(node.text()),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
// ── Functions / Methods ──
|
|
32
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.function } })) {
|
|
33
|
+
const name = node.field('name')?.text();
|
|
34
|
+
if (!name) {
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const line = node.range().start.line;
|
|
38
|
+
if (seen.has(`m:${fp}:${name}:${line}`)) {
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
seen.add(`m:${fp}:${name}:${line}`);
|
|
42
|
+
const classAncestor = node.ancestors().find((a) => a.kind() === kinds.class);
|
|
43
|
+
const className = classAncestor?.field('name')?.text() || '';
|
|
44
|
+
const retType = node
|
|
45
|
+
.field('return_type')
|
|
46
|
+
?.text()
|
|
47
|
+
?.replace(/^->\s*/, '') || '';
|
|
48
|
+
const isTest = name.startsWith('test_');
|
|
49
|
+
if (isTest) {
|
|
50
|
+
graph.tests.push({
|
|
51
|
+
name,
|
|
52
|
+
file: fp,
|
|
53
|
+
line_start: line,
|
|
54
|
+
line_end: node.range().end.line,
|
|
55
|
+
ast_kind: String(node.kind()),
|
|
56
|
+
qualified: `${fp}::test:${name}`,
|
|
57
|
+
content_hash: computeContentHash(node.text()),
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
graph.functions.push({
|
|
61
|
+
name,
|
|
62
|
+
file: fp,
|
|
63
|
+
line_start: line,
|
|
64
|
+
line_end: node.range().end.line,
|
|
65
|
+
params: node.field('parameters')?.text() || '()',
|
|
66
|
+
returnType: retType,
|
|
67
|
+
kind: name === '__init__' ? 'Constructor' : className ? 'Method' : 'Function',
|
|
68
|
+
ast_kind: String(node.kind()),
|
|
69
|
+
className,
|
|
70
|
+
qualified: className ? `${fp}::${className}.${name}` : `${fp}::${name}`,
|
|
71
|
+
content_hash: computeContentHash(node.text()),
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
// ── Imports (from X import Y) ──
|
|
75
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.import } })) {
|
|
76
|
+
const modNode = node
|
|
77
|
+
.children()
|
|
78
|
+
.find((c) => c.kind() === 'dotted_name' || c.kind() === 'relative_import');
|
|
79
|
+
const modulePath = modNode?.text() || '';
|
|
80
|
+
if (!modulePath) {
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
const names = [];
|
|
84
|
+
for (const child of node.children()) {
|
|
85
|
+
if (child.kind() === 'dotted_name' && child !== modNode) {
|
|
86
|
+
names.push(child.text());
|
|
87
|
+
}
|
|
88
|
+
if (child.kind() === 'identifier' && child !== modNode) {
|
|
89
|
+
names.push(child.text());
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
graph.imports.push({
|
|
93
|
+
module: modulePath,
|
|
94
|
+
file: fp,
|
|
95
|
+
line: node.range().start.line,
|
|
96
|
+
names,
|
|
97
|
+
lang: 'python',
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
// ── Regular imports (import X) ──
|
|
101
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.importRegular } })) {
|
|
102
|
+
const modNode = node.children().find((c) => c.kind() === 'dotted_name');
|
|
103
|
+
if (modNode) {
|
|
104
|
+
graph.imports.push({
|
|
105
|
+
module: modNode.text(),
|
|
106
|
+
file: fp,
|
|
107
|
+
line: node.range().start.line,
|
|
108
|
+
names: [modNode.text()],
|
|
109
|
+
lang: 'python',
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Python-specific call extraction config for shared extractCalls(). */
|
|
115
|
+
const PYTHON_CALL_CONFIG = {
|
|
116
|
+
selfPrefixes: ['self.'],
|
|
117
|
+
superPrefixes: ['super().'],
|
|
118
|
+
findEnclosingClass: (node) => node.ancestors().find((a) => a.kind() === 'class_definition') ?? null,
|
|
119
|
+
getParentClass: (classNode) => {
|
|
120
|
+
const argList = classNode.field('superclasses') || classNode.children().find((c) => c.kind() === 'argument_list');
|
|
121
|
+
return argList
|
|
122
|
+
?.children()
|
|
123
|
+
.find((c) => c.kind() === 'identifier')
|
|
124
|
+
?.text();
|
|
125
|
+
},
|
|
126
|
+
};
|
|
127
|
+
/**
|
|
128
|
+
* Extract raw call sites from a Python AST.
|
|
129
|
+
* Detects self.X() and super().X() to preserve class resolution context.
|
|
130
|
+
*/
|
|
131
|
+
export function extractCallsFromPython(root, fp, calls) {
|
|
132
|
+
extractCalls(root.root(), fp, PYTHON_CALL_CONFIG, calls);
|
|
133
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SgRoot } from '@ast-grep/napi';
|
|
2
|
+
import type { RawCallSite, RawGraph } from '../../graph/types';
|
|
3
|
+
export declare function extractRuby(root: SgRoot, fp: string, seen: Set<string>, graph: RawGraph): void;
|
|
4
|
+
/**
|
|
5
|
+
* Extract raw call sites from a Ruby AST.
|
|
6
|
+
* Detects self.X() and super() to preserve class resolution context.
|
|
7
|
+
*/
|
|
8
|
+
export declare function extractCallsFromRuby(root: SgRoot, fp: string, calls: RawCallSite[]): void;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { extractCalls } from '../../shared/extract-calls';
|
|
2
|
+
import { computeContentHash } from '../../shared/file-hash';
|
|
3
|
+
import { log } from '../../shared/logger';
|
|
4
|
+
import { LANG_KINDS } from '../languages';
|
|
5
|
+
export function extractRuby(root, fp, seen, graph) {
|
|
6
|
+
const kinds = LANG_KINDS.ruby;
|
|
7
|
+
const rootNode = root.root();
|
|
8
|
+
// ── Classes ──
|
|
9
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.class } })) {
|
|
10
|
+
const name = node.field('name')?.text();
|
|
11
|
+
if (!name || seen.has(`c:${fp}:${name}`)) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
seen.add(`c:${fp}:${name}`);
|
|
15
|
+
const superclass = node.field('superclass')?.text() || '';
|
|
16
|
+
graph.classes.push({
|
|
17
|
+
name,
|
|
18
|
+
file: fp,
|
|
19
|
+
line_start: node.range().start.line,
|
|
20
|
+
line_end: node.range().end.line,
|
|
21
|
+
extends: superclass,
|
|
22
|
+
implements: [],
|
|
23
|
+
ast_kind: String(node.kind()),
|
|
24
|
+
qualified: `${fp}::${name}`,
|
|
25
|
+
content_hash: computeContentHash(node.text()),
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
// ── Modules ──
|
|
29
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.module } })) {
|
|
30
|
+
const name = node.field('name')?.text();
|
|
31
|
+
if (!name || seen.has(`c:${fp}:${name}`)) {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
seen.add(`c:${fp}:${name}`);
|
|
35
|
+
graph.classes.push({
|
|
36
|
+
name,
|
|
37
|
+
file: fp,
|
|
38
|
+
line_start: node.range().start.line,
|
|
39
|
+
line_end: node.range().end.line,
|
|
40
|
+
extends: '',
|
|
41
|
+
implements: [],
|
|
42
|
+
ast_kind: String(node.kind()),
|
|
43
|
+
qualified: `${fp}::${name}`,
|
|
44
|
+
content_hash: computeContentHash(node.text()),
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
// ── Methods ──
|
|
48
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.method } })) {
|
|
49
|
+
const name = node.field('name')?.text();
|
|
50
|
+
if (!name) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
const line = node.range().start.line;
|
|
54
|
+
if (seen.has(`m:${fp}:${name}:${line}`)) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
seen.add(`m:${fp}:${name}:${line}`);
|
|
58
|
+
const classAncestor = node
|
|
59
|
+
.ancestors()
|
|
60
|
+
.find((a) => a.kind() === kinds.class || a.kind() === kinds.module);
|
|
61
|
+
const className = classAncestor?.field('name')?.text() || '';
|
|
62
|
+
graph.functions.push({
|
|
63
|
+
name,
|
|
64
|
+
file: fp,
|
|
65
|
+
line_start: line,
|
|
66
|
+
line_end: node.range().end.line,
|
|
67
|
+
params: node.field('parameters')?.text() || '()',
|
|
68
|
+
returnType: '',
|
|
69
|
+
kind: className ? 'Method' : 'Function',
|
|
70
|
+
ast_kind: String(node.kind()),
|
|
71
|
+
className,
|
|
72
|
+
qualified: className ? `${fp}::${className}.${name}` : `${fp}::${name}`,
|
|
73
|
+
content_hash: computeContentHash(node.text()),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// ── Tests (RSpec: describe/it/context) ──
|
|
77
|
+
for (const p of [
|
|
78
|
+
"describe '$NAME' do $$$BODY end",
|
|
79
|
+
'describe "$NAME" do $$$BODY end',
|
|
80
|
+
"it '$NAME' do $$$BODY end",
|
|
81
|
+
'it "$NAME" do $$$BODY end',
|
|
82
|
+
"context '$NAME' do $$$BODY end",
|
|
83
|
+
'context "$NAME" do $$$BODY end',
|
|
84
|
+
]) {
|
|
85
|
+
try {
|
|
86
|
+
for (const m of rootNode.findAll(p)) {
|
|
87
|
+
const name = m.getMatch('NAME')?.text();
|
|
88
|
+
if (!name) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const key = `t:${fp}:${name}:${m.range().start.line}`;
|
|
92
|
+
if (seen.has(key)) {
|
|
93
|
+
continue;
|
|
94
|
+
}
|
|
95
|
+
seen.add(key);
|
|
96
|
+
graph.tests.push({
|
|
97
|
+
name,
|
|
98
|
+
file: fp,
|
|
99
|
+
line_start: m.range().start.line,
|
|
100
|
+
line_end: m.range().end.line,
|
|
101
|
+
ast_kind: String(m.kind()),
|
|
102
|
+
qualified: `${fp}::test:${name}`,
|
|
103
|
+
content_hash: computeContentHash(m.text()),
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
catch (err) {
|
|
108
|
+
log.debug('Ruby pattern mismatch', { file: fp, pattern: p, error: String(err) });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// ── Imports (require/require_relative) ──
|
|
112
|
+
for (const p of [
|
|
113
|
+
"require '$MODULE'",
|
|
114
|
+
'require "$MODULE"',
|
|
115
|
+
"require_relative '$MODULE'",
|
|
116
|
+
'require_relative "$MODULE"',
|
|
117
|
+
]) {
|
|
118
|
+
try {
|
|
119
|
+
for (const m of rootNode.findAll(p)) {
|
|
120
|
+
const mod = m.getMatch('MODULE')?.text();
|
|
121
|
+
if (mod) {
|
|
122
|
+
graph.imports.push({
|
|
123
|
+
module: mod,
|
|
124
|
+
file: fp,
|
|
125
|
+
line: m.range().start.line,
|
|
126
|
+
names: [],
|
|
127
|
+
lang: 'ruby',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
catch (err) {
|
|
133
|
+
log.debug('Ruby pattern mismatch', { file: fp, pattern: p, error: String(err) });
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
/** Ruby-specific call extraction config for shared extractCalls(). */
|
|
138
|
+
function createRubyCallConfig() {
|
|
139
|
+
const kinds = LANG_KINDS.ruby;
|
|
140
|
+
return {
|
|
141
|
+
selfPrefixes: ['self.'],
|
|
142
|
+
superPrefixes: ['super'],
|
|
143
|
+
findEnclosingClass: (node) => node.ancestors().find((a) => a.kind() === kinds.class || a.kind() === kinds.module) ?? null,
|
|
144
|
+
getParentClass: (classNode) => classNode.field('superclass')?.text(),
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Extract raw call sites from a Ruby AST.
|
|
149
|
+
* Detects self.X() and super() to preserve class resolution context.
|
|
150
|
+
*/
|
|
151
|
+
export function extractCallsFromRuby(root, fp, calls) {
|
|
152
|
+
extractCalls(root.root(), fp, createRubyCallConfig(), calls);
|
|
153
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { SgRoot } from '@ast-grep/napi';
|
|
2
|
+
import { Lang } from '@ast-grep/napi';
|
|
3
|
+
import type { RawCallSite, RawGraph } from '../../graph/types';
|
|
4
|
+
export declare function extractTypeScript(root: SgRoot, fp: string, seen: Set<string>, graph: RawGraph, lang?: Lang | string): void;
|
|
5
|
+
/**
|
|
6
|
+
* Extract raw call sites from a TypeScript/JavaScript AST.
|
|
7
|
+
* Finds DI calls (this.field.method) and direct calls ($CALLEE($$$ARGS)).
|
|
8
|
+
* Filters NOISE. Does NOT resolve — just collects raw sites.
|
|
9
|
+
*/
|
|
10
|
+
export declare function extractCallsFromTypeScript(root: SgRoot, fp: string, calls: RawCallSite[]): void;
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
import { Lang } from '@ast-grep/napi';
|
|
2
|
+
import { extractCalls } from '../../shared/extract-calls';
|
|
3
|
+
import { computeContentHash } from '../../shared/file-hash';
|
|
4
|
+
import { NOISE } from '../../shared/filters';
|
|
5
|
+
import { LANG_KINDS } from '../languages';
|
|
6
|
+
export function extractTypeScript(root, fp, seen, graph, lang = Lang.TypeScript) {
|
|
7
|
+
const kinds = LANG_KINDS.typescript;
|
|
8
|
+
const rootNode = root.root();
|
|
9
|
+
const isTS = lang === Lang.TypeScript || lang === Lang.Tsx;
|
|
10
|
+
// ── Classes ──
|
|
11
|
+
const classKinds = isTS ? [kinds.class, kinds.abstractClass] : [kinds.class];
|
|
12
|
+
for (const kind of classKinds) {
|
|
13
|
+
for (const node of rootNode.findAll({ rule: { kind } })) {
|
|
14
|
+
const name = node.field('name')?.text();
|
|
15
|
+
if (!name || seen.has(`c:${fp}:${name}`)) {
|
|
16
|
+
continue;
|
|
17
|
+
}
|
|
18
|
+
seen.add(`c:${fp}:${name}`);
|
|
19
|
+
let extendsName = '';
|
|
20
|
+
let implementsNames = [];
|
|
21
|
+
const heritage = node.children().find((c) => c.kind() === 'class_heritage');
|
|
22
|
+
if (heritage) {
|
|
23
|
+
const ext = heritage.children().find((c) => c.kind() === 'extends_clause');
|
|
24
|
+
extendsName =
|
|
25
|
+
ext
|
|
26
|
+
?.children()
|
|
27
|
+
.find((c) => c.kind() === 'identifier' ||
|
|
28
|
+
c.kind() === 'type_identifier' ||
|
|
29
|
+
c.kind() === 'member_expression')
|
|
30
|
+
?.text() || '';
|
|
31
|
+
const impl = heritage.children().find((c) => c.kind() === 'implements_clause');
|
|
32
|
+
implementsNames =
|
|
33
|
+
impl
|
|
34
|
+
?.children()
|
|
35
|
+
.filter((c) => c.kind() === 'type_identifier' || c.kind() === 'identifier')
|
|
36
|
+
.map((c) => c.text()) ?? [];
|
|
37
|
+
}
|
|
38
|
+
graph.classes.push({
|
|
39
|
+
name,
|
|
40
|
+
file: fp,
|
|
41
|
+
line_start: node.range().start.line,
|
|
42
|
+
line_end: node.range().end.line,
|
|
43
|
+
extends: extendsName,
|
|
44
|
+
implements: implementsNames,
|
|
45
|
+
ast_kind: String(node.kind()),
|
|
46
|
+
qualified: `${fp}::${name}`,
|
|
47
|
+
content_hash: computeContentHash(node.text()),
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// ── Methods (kind-based: catches constructor, async, getters/setters) ──
|
|
52
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.method } })) {
|
|
53
|
+
const name = node.field('name')?.text();
|
|
54
|
+
if (!name) {
|
|
55
|
+
continue;
|
|
56
|
+
}
|
|
57
|
+
const line = node.range().start.line;
|
|
58
|
+
if (seen.has(`m:${fp}:${name}:${line}`)) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
seen.add(`m:${fp}:${name}:${line}`);
|
|
62
|
+
const classAncestor = node
|
|
63
|
+
.ancestors()
|
|
64
|
+
.find((a) => a.kind() === kinds.class || (isTS && a.kind() === kinds.abstractClass));
|
|
65
|
+
const className = classAncestor?.field('name')?.text() || '';
|
|
66
|
+
const params = node.field('parameters');
|
|
67
|
+
const retType = node.field('return_type')?.text()?.replace(/^:\s*/, '') || '';
|
|
68
|
+
if (name === 'constructor' && className) {
|
|
69
|
+
// Constructor DI extraction
|
|
70
|
+
const fieldTypeMap = new Map();
|
|
71
|
+
if (params) {
|
|
72
|
+
for (const p of params.children()) {
|
|
73
|
+
if (p.kind() !== 'required_parameter') {
|
|
74
|
+
continue;
|
|
75
|
+
}
|
|
76
|
+
if (!p.children().some((c) => c.kind() === 'accessibility_modifier')) {
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
const ident = p.children().find((c) => c.kind() === 'identifier');
|
|
80
|
+
const typeAnn = p.children().find((c) => c.kind() === 'type_annotation');
|
|
81
|
+
if (ident && typeAnn) {
|
|
82
|
+
const typeNode = typeAnn
|
|
83
|
+
.children()
|
|
84
|
+
.find((c) => c.kind() === 'type_identifier' ||
|
|
85
|
+
c.kind() === 'identifier' ||
|
|
86
|
+
c.kind() === 'generic_type');
|
|
87
|
+
if (typeNode) {
|
|
88
|
+
const typeName = typeNode.kind() === 'generic_type'
|
|
89
|
+
? typeNode
|
|
90
|
+
.children()
|
|
91
|
+
.find((c) => c.kind() === 'type_identifier')
|
|
92
|
+
?.text() || typeNode.text()
|
|
93
|
+
: typeNode.text();
|
|
94
|
+
fieldTypeMap.set(ident.text(), typeName);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (fieldTypeMap.size > 0) {
|
|
100
|
+
graph.diMaps.set(fp, fieldTypeMap);
|
|
101
|
+
}
|
|
102
|
+
graph.functions.push({
|
|
103
|
+
name: `${className}.constructor`,
|
|
104
|
+
file: fp,
|
|
105
|
+
line_start: line,
|
|
106
|
+
line_end: node.range().end.line,
|
|
107
|
+
params: params?.text() || '()',
|
|
108
|
+
returnType: '',
|
|
109
|
+
kind: 'Constructor',
|
|
110
|
+
ast_kind: String(node.kind()),
|
|
111
|
+
className,
|
|
112
|
+
qualified: `${fp}::${className}.constructor`,
|
|
113
|
+
content_hash: computeContentHash(node.text()),
|
|
114
|
+
});
|
|
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
|
+
ast_kind: String(node.kind()),
|
|
126
|
+
className,
|
|
127
|
+
qualified: className ? `${fp}::${className}.${name}` : `${fp}::${name}`,
|
|
128
|
+
content_hash: computeContentHash(node.text()),
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
// ── Standalone functions ──
|
|
133
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.function } })) {
|
|
134
|
+
const name = node.field('name')?.text();
|
|
135
|
+
if (!name) {
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const line = node.range().start.line;
|
|
139
|
+
if (seen.has(`f:${fp}:${name}:${line}`)) {
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (node.ancestors().some((a) => a.kind() === kinds.class || (isTS && a.kind() === kinds.abstractClass))) {
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
seen.add(`f:${fp}:${name}:${line}`);
|
|
146
|
+
graph.functions.push({
|
|
147
|
+
name,
|
|
148
|
+
file: fp,
|
|
149
|
+
line_start: line,
|
|
150
|
+
line_end: node.range().end.line,
|
|
151
|
+
params: node.field('parameters')?.text() || '()',
|
|
152
|
+
returnType: node.field('return_type')?.text()?.replace(/^:\s*/, '') || '',
|
|
153
|
+
kind: 'Function',
|
|
154
|
+
ast_kind: String(node.kind()),
|
|
155
|
+
className: '',
|
|
156
|
+
qualified: `${fp}::${name}`,
|
|
157
|
+
content_hash: computeContentHash(node.text()),
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
// ── Arrow functions ──
|
|
161
|
+
for (const node of rootNode.findAll({
|
|
162
|
+
rule: { kind: kinds.arrowContainer, has: { kind: kinds.arrowFunction } },
|
|
163
|
+
})) {
|
|
164
|
+
const name = node.field('name')?.text();
|
|
165
|
+
if (!name) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
const line = node.range().start.line;
|
|
169
|
+
if (seen.has(`f:${fp}:${name}:${line}`)) {
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
seen.add(`f:${fp}:${name}:${line}`);
|
|
173
|
+
const arrow = node.children().find((c) => c.kind() === kinds.arrowFunction);
|
|
174
|
+
graph.functions.push({
|
|
175
|
+
name,
|
|
176
|
+
file: fp,
|
|
177
|
+
line_start: line,
|
|
178
|
+
line_end: node.range().end.line,
|
|
179
|
+
params: arrow?.field('parameters')?.text() || '()',
|
|
180
|
+
returnType: arrow?.field('return_type')?.text()?.replace(/^:\s*/, '') || '',
|
|
181
|
+
kind: 'Function',
|
|
182
|
+
ast_kind: 'arrow_function',
|
|
183
|
+
className: '',
|
|
184
|
+
qualified: `${fp}::${name}`,
|
|
185
|
+
content_hash: computeContentHash(node.text()),
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
// ── Interfaces (TS only — JS grammar has no interface_declaration) ──
|
|
189
|
+
if (isTS) {
|
|
190
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.interface } })) {
|
|
191
|
+
const name = node.field('name')?.text();
|
|
192
|
+
if (!name || seen.has(`i:${fp}:${name}`)) {
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
seen.add(`i:${fp}:${name}`);
|
|
196
|
+
const methods = [];
|
|
197
|
+
const body = node.field('body');
|
|
198
|
+
if (body) {
|
|
199
|
+
for (const child of body.findAll({ rule: { kind: kinds.methodSignature } })) {
|
|
200
|
+
const mn = child.field('name')?.text();
|
|
201
|
+
if (mn) {
|
|
202
|
+
methods.push(mn);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
graph.interfaces.push({
|
|
207
|
+
name,
|
|
208
|
+
file: fp,
|
|
209
|
+
line_start: node.range().start.line,
|
|
210
|
+
line_end: node.range().end.line,
|
|
211
|
+
methods,
|
|
212
|
+
ast_kind: String(node.kind()),
|
|
213
|
+
qualified: `${fp}::${name}`,
|
|
214
|
+
content_hash: computeContentHash(node.text()),
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
// ── Enums (TS only — JS grammar has no enum_declaration) ──
|
|
219
|
+
if (isTS) {
|
|
220
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.enum } })) {
|
|
221
|
+
const name = node.field('name')?.text();
|
|
222
|
+
if (!name || seen.has(`e:${fp}:${name}`)) {
|
|
223
|
+
continue;
|
|
224
|
+
}
|
|
225
|
+
seen.add(`e:${fp}:${name}`);
|
|
226
|
+
graph.enums.push({
|
|
227
|
+
name,
|
|
228
|
+
file: fp,
|
|
229
|
+
line_start: node.range().start.line,
|
|
230
|
+
line_end: node.range().end.line,
|
|
231
|
+
ast_kind: String(node.kind()),
|
|
232
|
+
qualified: `${fp}::${name}`,
|
|
233
|
+
content_hash: computeContentHash(node.text()),
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// ── Imports ──
|
|
238
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.import } })) {
|
|
239
|
+
const sourceNode = node.children().find((c) => c.kind() === 'string');
|
|
240
|
+
const frag = sourceNode?.children().find((c) => c.kind() === 'string_fragment');
|
|
241
|
+
const modulePath = frag?.text() || sourceNode?.text()?.replace(/['"]/g, '') || '';
|
|
242
|
+
if (!modulePath) {
|
|
243
|
+
continue;
|
|
244
|
+
}
|
|
245
|
+
const names = [];
|
|
246
|
+
const importClause = node.children().find((c) => c.kind() === 'import_clause');
|
|
247
|
+
if (importClause) {
|
|
248
|
+
for (const child of importClause.children()) {
|
|
249
|
+
if (child.kind() === 'identifier') {
|
|
250
|
+
names.push(child.text());
|
|
251
|
+
}
|
|
252
|
+
else if (child.kind() === 'named_imports') {
|
|
253
|
+
for (const spec of child.findAll({ rule: { kind: 'import_specifier' } })) {
|
|
254
|
+
const n = spec.field('name')?.text() ||
|
|
255
|
+
spec
|
|
256
|
+
.children()
|
|
257
|
+
.find((c) => c.kind() === 'identifier')
|
|
258
|
+
?.text();
|
|
259
|
+
if (n) {
|
|
260
|
+
names.push(n);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
else if (child.kind() === 'namespace_import') {
|
|
265
|
+
const alias = child.children().find((c) => c.kind() === 'identifier');
|
|
266
|
+
if (alias) {
|
|
267
|
+
names.push(alias.text());
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
graph.imports.push({
|
|
273
|
+
module: modulePath,
|
|
274
|
+
file: fp,
|
|
275
|
+
line: node.range().start.line,
|
|
276
|
+
names,
|
|
277
|
+
lang: 'ts',
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
// ── Re-exports ──
|
|
281
|
+
for (const node of rootNode.findAll({ rule: { kind: kinds.export } })) {
|
|
282
|
+
const src = node.children().find((c) => c.kind() === 'string');
|
|
283
|
+
if (src) {
|
|
284
|
+
const frag = src.children().find((c) => c.kind() === 'string_fragment');
|
|
285
|
+
graph.reExports.push({
|
|
286
|
+
module: frag?.text() || src.text().replace(/['"]/g, ''),
|
|
287
|
+
file: fp,
|
|
288
|
+
line: node.range().start.line,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
// ── Tests (pattern-based) ──
|
|
293
|
+
for (const p of [
|
|
294
|
+
'describe("$NAME", $$$BODY)',
|
|
295
|
+
"describe('$NAME', $$$BODY)",
|
|
296
|
+
'it("$NAME", $$$BODY)',
|
|
297
|
+
"it('$NAME', $$$BODY)",
|
|
298
|
+
'test("$NAME", $$$BODY)',
|
|
299
|
+
"test('$NAME', $$$BODY)",
|
|
300
|
+
]) {
|
|
301
|
+
for (const m of rootNode.findAll(p)) {
|
|
302
|
+
const name = m.getMatch('NAME')?.text();
|
|
303
|
+
if (!name) {
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const key = `t:${fp}:${name}:${m.range().start.line}`;
|
|
307
|
+
if (seen.has(key)) {
|
|
308
|
+
continue;
|
|
309
|
+
}
|
|
310
|
+
seen.add(key);
|
|
311
|
+
graph.tests.push({
|
|
312
|
+
name,
|
|
313
|
+
file: fp,
|
|
314
|
+
line_start: m.range().start.line,
|
|
315
|
+
line_end: m.range().end.line,
|
|
316
|
+
ast_kind: String(m.kind()),
|
|
317
|
+
qualified: `${fp}::test:${name}`,
|
|
318
|
+
content_hash: computeContentHash(m.text()),
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
/** TypeScript-specific call extraction config for shared extractCalls(). */
|
|
324
|
+
const TS_CALL_CONFIG = {
|
|
325
|
+
selfPrefixes: ['this.'],
|
|
326
|
+
superPrefixes: ['super.'],
|
|
327
|
+
findEnclosingClass: (node) => {
|
|
328
|
+
const kinds = LANG_KINDS.typescript;
|
|
329
|
+
return (node.ancestors().find((a) => a.kind() === kinds.class || a.kind() === kinds.abstractClass) ?? null);
|
|
330
|
+
},
|
|
331
|
+
getParentClass: (classNode) => {
|
|
332
|
+
const heritage = classNode.children().find((c) => c.kind() === 'class_heritage');
|
|
333
|
+
const ext = heritage?.children().find((c) => c.kind() === 'extends_clause');
|
|
334
|
+
return ext
|
|
335
|
+
?.children()
|
|
336
|
+
.find((c) => c.kind() === 'identifier' || c.kind() === 'type_identifier' || c.kind() === 'member_expression')
|
|
337
|
+
?.text();
|
|
338
|
+
},
|
|
339
|
+
// Skip this.field.method — already handled by the DI pattern above
|
|
340
|
+
skipCallee: (callee) => callee.startsWith('this.') && callee.substring(5).includes('.'),
|
|
341
|
+
};
|
|
342
|
+
/**
|
|
343
|
+
* Extract raw call sites from a TypeScript/JavaScript AST.
|
|
344
|
+
* Finds DI calls (this.field.method) and direct calls ($CALLEE($$$ARGS)).
|
|
345
|
+
* Filters NOISE. Does NOT resolve — just collects raw sites.
|
|
346
|
+
*/
|
|
347
|
+
export function extractCallsFromTypeScript(root, fp, calls) {
|
|
348
|
+
const rootNode = root.root();
|
|
349
|
+
// DI pattern: this.$FIELD.$METHOD($$$ARGS)
|
|
350
|
+
for (const m of rootNode.findAll('this.$FIELD.$METHOD($$$ARGS)')) {
|
|
351
|
+
const field = m.getMatch('FIELD')?.text();
|
|
352
|
+
const method = m.getMatch('METHOD')?.text();
|
|
353
|
+
if (!method || NOISE.has(method)) {
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
calls.push({
|
|
357
|
+
source: fp,
|
|
358
|
+
callName: method,
|
|
359
|
+
line: m.range().start.line,
|
|
360
|
+
diField: field,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
// Direct calls + self/super detection via shared function
|
|
364
|
+
extractCalls(rootNode, fp, TS_CALL_CONFIG, calls);
|
|
365
|
+
}
|