@kodus/kodus-graph 0.2.8 → 0.2.9
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 +57 -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 +83 -0
- package/dist/analysis/diff.d.ts +35 -0
- package/dist/analysis/diff.js +140 -0
- package/dist/analysis/enrich.d.ts +5 -0
- package/dist/analysis/enrich.js +98 -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 +166 -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 +208 -0
- package/dist/commands/analyze.d.ts +9 -0
- package/dist/commands/analyze.js +114 -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 +10 -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 +2 -0
- package/dist/graph/builder.js +216 -0
- package/dist/graph/edges.d.ts +19 -0
- package/dist/graph/edges.js +105 -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 +249 -0
- package/dist/graph/types.js +1 -0
- package/dist/parser/batch.d.ts +4 -0
- package/dist/parser/batch.js +78 -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 +303 -0
- package/dist/resolver/call-resolver.d.ts +36 -0
- package/dist/resolver/call-resolver.js +178 -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 +212 -0
- package/dist/resolver/languages/csharp.d.ts +1 -0
- package/dist/resolver/languages/csharp.js +31 -0
- package/dist/resolver/languages/go.d.ts +3 -0
- package/dist/resolver/languages/go.js +196 -0
- package/dist/resolver/languages/java.d.ts +1 -0
- package/dist/resolver/languages/java.js +108 -0
- package/dist/resolver/languages/php.d.ts +3 -0
- package/dist/resolver/languages/php.js +54 -0
- package/dist/resolver/languages/python.d.ts +11 -0
- package/dist/resolver/languages/python.js +51 -0
- package/dist/resolver/languages/ruby.d.ts +9 -0
- package/dist/resolver/languages/ruby.js +59 -0
- package/dist/resolver/languages/rust.d.ts +1 -0
- package/dist/resolver/languages/rust.js +196 -0
- package/dist/resolver/languages/typescript.d.ts +27 -0
- package/dist/resolver/languages/typescript.js +240 -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,61 @@
|
|
|
1
|
+
import { readdirSync } from 'fs';
|
|
2
|
+
import { extname, join, relative, resolve } from 'path';
|
|
3
|
+
import { isSkippableFile, SKIP_DIRS } from '../shared/filters';
|
|
4
|
+
import { log } from '../shared/logger';
|
|
5
|
+
import { ensureWithinRoot } from '../shared/safe-path';
|
|
6
|
+
import { getLanguage } from './languages';
|
|
7
|
+
/**
|
|
8
|
+
* Walk the filesystem and find all supported source files.
|
|
9
|
+
* If `filterFiles` is provided, only return those specific files (resolved to absolute paths).
|
|
10
|
+
* If `include` patterns are provided, keep only files matching at least one pattern.
|
|
11
|
+
* If `exclude` patterns are provided, remove files matching any pattern.
|
|
12
|
+
*/
|
|
13
|
+
export function discoverFiles(repoDir, filterFiles, include, exclude) {
|
|
14
|
+
const absRepoDir = resolve(repoDir);
|
|
15
|
+
if (filterFiles) {
|
|
16
|
+
return filterFiles
|
|
17
|
+
.map((f) => (f.startsWith('/') ? f : join(absRepoDir, f)))
|
|
18
|
+
.filter((f) => {
|
|
19
|
+
try {
|
|
20
|
+
ensureWithinRoot(f, absRepoDir);
|
|
21
|
+
return getLanguage(extname(f)) !== null;
|
|
22
|
+
}
|
|
23
|
+
catch (err) {
|
|
24
|
+
log.warn('Skipping file outside repository root', { file: f, error: String(err) });
|
|
25
|
+
return false;
|
|
26
|
+
}
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
let files = [];
|
|
30
|
+
walkFiles(absRepoDir, files);
|
|
31
|
+
// Apply include/exclude filters using Bun.Glob
|
|
32
|
+
const hasInclude = include && include.length > 0;
|
|
33
|
+
const hasExclude = exclude && exclude.length > 0;
|
|
34
|
+
if (hasInclude || hasExclude) {
|
|
35
|
+
const includeGlobs = hasInclude ? include.map((p) => new Bun.Glob(p)) : null;
|
|
36
|
+
const excludeGlobs = hasExclude ? exclude.map((p) => new Bun.Glob(p)) : null;
|
|
37
|
+
files = files.filter((absPath) => {
|
|
38
|
+
const rel = relative(absRepoDir, absPath);
|
|
39
|
+
// If include patterns exist, file must match at least one
|
|
40
|
+
if (includeGlobs && !includeGlobs.some((g) => g.match(rel))) {
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
// If exclude patterns exist, file must not match any
|
|
44
|
+
if (excludeGlobs?.some((g) => g.match(rel))) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
return true;
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
return files;
|
|
51
|
+
}
|
|
52
|
+
function walkFiles(dir, files) {
|
|
53
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
54
|
+
if (entry.isDirectory() && !SKIP_DIRS.has(entry.name)) {
|
|
55
|
+
walkFiles(join(dir, entry.name), files);
|
|
56
|
+
}
|
|
57
|
+
else if (entry.isFile() && getLanguage(extname(entry.name)) !== null && !isSkippableFile(entry.name)) {
|
|
58
|
+
files.push(join(dir, entry.name));
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import type { Lang, SgRoot } from '@ast-grep/napi';
|
|
2
|
+
import type { RawCallSite, RawGraph } from '../graph/types';
|
|
3
|
+
export declare function extractFromFile(root: SgRoot, filePath: string, lang: Lang | string, seen: Set<string>, graph: RawGraph): void;
|
|
4
|
+
export declare function extractCallsFromFile(root: SgRoot, filePath: string, lang: Lang | string, calls: RawCallSite[]): void;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { extractCallsFromGeneric, extractGeneric } from './extractors/generic';
|
|
2
|
+
import { extractCallsFromPython, extractPython } from './extractors/python';
|
|
3
|
+
import { extractCallsFromRuby, extractRuby } from './extractors/ruby';
|
|
4
|
+
import { extractCallsFromTypeScript, extractTypeScript } from './extractors/typescript';
|
|
5
|
+
import { isTypeScriptLike } from './languages';
|
|
6
|
+
export function extractFromFile(root, filePath, lang, seen, graph) {
|
|
7
|
+
if (isTypeScriptLike(lang)) {
|
|
8
|
+
extractTypeScript(root, filePath, seen, graph, lang);
|
|
9
|
+
}
|
|
10
|
+
else if (lang === 'python') {
|
|
11
|
+
extractPython(root, filePath, seen, graph);
|
|
12
|
+
}
|
|
13
|
+
else if (lang === 'ruby') {
|
|
14
|
+
extractRuby(root, filePath, seen, graph);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
extractGeneric(root, filePath, lang, seen, graph);
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
export function extractCallsFromFile(root, filePath, lang, calls) {
|
|
21
|
+
if (isTypeScriptLike(lang)) {
|
|
22
|
+
extractCallsFromTypeScript(root, filePath, calls);
|
|
23
|
+
}
|
|
24
|
+
else if (lang === 'python') {
|
|
25
|
+
extractCallsFromPython(root, filePath, calls);
|
|
26
|
+
}
|
|
27
|
+
else if (lang === 'ruby') {
|
|
28
|
+
extractCallsFromRuby(root, filePath, calls);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
extractCallsFromGeneric(root, filePath, lang, calls);
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { SgRoot } from '@ast-grep/napi';
|
|
2
|
+
import type { RawCallSite, RawGraph } from '../../graph/types';
|
|
3
|
+
export declare function extractGeneric(root: SgRoot, fp: string, lang: string, seen: Set<string>, graph: RawGraph): void;
|
|
4
|
+
/**
|
|
5
|
+
* Extract raw call sites from a generic language AST.
|
|
6
|
+
* Uses per-language config for self/super detection.
|
|
7
|
+
*/
|
|
8
|
+
export declare function extractCallsFromGeneric(root: SgRoot, fp: string, lang: string, calls: RawCallSite[]): void;
|
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import { extractCalls } from '../../shared/extract-calls';
|
|
2
|
+
import { computeContentHash } from '../../shared/file-hash';
|
|
3
|
+
import { log } from '../../shared/logger';
|
|
4
|
+
import { LANG_CONFIGS } from '../languages';
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
// Go disambiguation helpers
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
/** Determine whether a Go `type_declaration` node is a struct, interface, or unknown. */
|
|
9
|
+
function goTypeKind(node) {
|
|
10
|
+
const typeSpec = node.children().find((c) => c.kind() === 'type_spec');
|
|
11
|
+
if (!typeSpec) {
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
const hasStruct = typeSpec.children().some((c) => c.kind() === 'struct_type');
|
|
15
|
+
if (hasStruct) {
|
|
16
|
+
return 'struct';
|
|
17
|
+
}
|
|
18
|
+
const hasInterface = typeSpec.children().some((c) => c.kind() === 'interface_type');
|
|
19
|
+
if (hasInterface) {
|
|
20
|
+
return 'interface';
|
|
21
|
+
}
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
/** Get the name for a Go `type_declaration` node (name lives inside `type_spec`). */
|
|
25
|
+
function goTypeName(node) {
|
|
26
|
+
const typeSpec = node.children().find((c) => c.kind() === 'type_spec');
|
|
27
|
+
return typeSpec?.field('name')?.text();
|
|
28
|
+
}
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// Test detection helpers
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
/**
|
|
33
|
+
* Returns true if the function should be considered a test based on file-path
|
|
34
|
+
* and function-name conventions defined in config.tests.
|
|
35
|
+
*
|
|
36
|
+
* matchMode controls how filePatterns and funcPatterns interact:
|
|
37
|
+
* - 'and': BOTH must match (e.g., Go: file must be _test.go AND func must start with Test/Benchmark)
|
|
38
|
+
* - 'or' (default): EITHER matching is sufficient
|
|
39
|
+
*/
|
|
40
|
+
function isTestByConvention(fp, funcName, config) {
|
|
41
|
+
const tests = config.tests;
|
|
42
|
+
if (!tests) {
|
|
43
|
+
return false;
|
|
44
|
+
}
|
|
45
|
+
const fileMatch = tests.filePatterns ? tests.filePatterns.some((re) => re.test(fp)) : false;
|
|
46
|
+
const funcMatch = tests.funcPatterns ? tests.funcPatterns.some((re) => re.test(funcName)) : false;
|
|
47
|
+
if (tests.matchMode === 'and') {
|
|
48
|
+
// Both must match (or only the defined one must match)
|
|
49
|
+
const fileOk = !tests.filePatterns || fileMatch;
|
|
50
|
+
const funcOk = !tests.funcPatterns || funcMatch;
|
|
51
|
+
return fileOk && funcOk;
|
|
52
|
+
}
|
|
53
|
+
// Default: OR — either matching is sufficient
|
|
54
|
+
if (fileMatch) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (funcMatch) {
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Returns true if the function node has an annotation/attribute sibling or
|
|
64
|
+
* child that matches config.tests.annotationKind / annotationNames.
|
|
65
|
+
*/
|
|
66
|
+
function hasTestAnnotation(node, config) {
|
|
67
|
+
const tests = config.tests;
|
|
68
|
+
if (!tests?.annotationKind || !tests.annotationNames?.length) {
|
|
69
|
+
return false;
|
|
70
|
+
}
|
|
71
|
+
const annotationKind = tests.annotationKind;
|
|
72
|
+
const annotationNames = tests.annotationNames;
|
|
73
|
+
function textMatchesAnnotation(text) {
|
|
74
|
+
return annotationNames.some((name) => text.includes(name));
|
|
75
|
+
}
|
|
76
|
+
// Check previous siblings for annotation nodes
|
|
77
|
+
for (const sibling of node.prevAll()) {
|
|
78
|
+
if (sibling.kind() === annotationKind && textMatchesAnnotation(sibling.text())) {
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Check inside modifiers or attribute_list children of the function node
|
|
83
|
+
for (const child of node.children()) {
|
|
84
|
+
const ck = child.kind();
|
|
85
|
+
if (ck === 'modifiers' || ck === 'attribute_list' || ck === annotationKind) {
|
|
86
|
+
if (textMatchesAnnotation(child.text())) {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
// Import extraction helper
|
|
95
|
+
// ---------------------------------------------------------------------------
|
|
96
|
+
/**
|
|
97
|
+
* Extract the module name from an import node using multiple strategies.
|
|
98
|
+
*/
|
|
99
|
+
function extractImportModule(node) {
|
|
100
|
+
// Strategy 1: look for string literal children
|
|
101
|
+
for (const child of node.children()) {
|
|
102
|
+
const ck = child.kind();
|
|
103
|
+
if (ck === 'string' || ck === 'interpreted_string_literal' || ck === 'string_fragment') {
|
|
104
|
+
const raw = child.text();
|
|
105
|
+
// Strip surrounding quotes
|
|
106
|
+
return raw.replace(/^["'`]|["'`]$/g, '');
|
|
107
|
+
}
|
|
108
|
+
// Recurse into string children one level
|
|
109
|
+
for (const grandchild of child.children()) {
|
|
110
|
+
const gck = grandchild.kind();
|
|
111
|
+
if (gck === 'string_fragment' || gck === 'string_content') {
|
|
112
|
+
return grandchild.text();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Strategy 2: scoped identifiers / qualified names
|
|
117
|
+
for (const child of node.children()) {
|
|
118
|
+
const ck = child.kind();
|
|
119
|
+
if (ck === 'scoped_identifier' || ck === 'scoped_type_identifier' || ck === 'qualified_name') {
|
|
120
|
+
return child.text();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
// Strategy 3: namespace names / use_tree (Rust `use` paths)
|
|
124
|
+
for (const child of node.children()) {
|
|
125
|
+
const ck = child.kind();
|
|
126
|
+
if (ck === 'name' || ck === 'namespace_name' || ck === 'use_tree') {
|
|
127
|
+
return child.text();
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Strategy 4: identifier children as last resort
|
|
131
|
+
for (const child of node.children()) {
|
|
132
|
+
if (child.kind() === 'identifier' || child.kind() === 'type_identifier') {
|
|
133
|
+
return child.text();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
// Fallback: strip import/use/using/require prefix from full text
|
|
137
|
+
return node
|
|
138
|
+
.text()
|
|
139
|
+
.replace(/^\s*(import|use|using|require)\s+/i, '')
|
|
140
|
+
.replace(/[;{}]/g, '')
|
|
141
|
+
.trim();
|
|
142
|
+
}
|
|
143
|
+
/**
|
|
144
|
+
* Extract imported names (symbols) from an import node.
|
|
145
|
+
*/
|
|
146
|
+
function extractImportNames(node) {
|
|
147
|
+
const names = [];
|
|
148
|
+
// Look for namespace_use_group (PHP), use_tree_list (Rust), etc.
|
|
149
|
+
for (const child of node.children()) {
|
|
150
|
+
const ck = child.kind();
|
|
151
|
+
if (ck === 'identifier' || ck === 'type_identifier' || ck === 'name') {
|
|
152
|
+
names.push(child.text());
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
return names;
|
|
156
|
+
}
|
|
157
|
+
// ---------------------------------------------------------------------------
|
|
158
|
+
// Main extractor
|
|
159
|
+
// ---------------------------------------------------------------------------
|
|
160
|
+
export function extractGeneric(root, fp, lang, seen, graph) {
|
|
161
|
+
const config = LANG_CONFIGS[lang];
|
|
162
|
+
if (!config) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
const rootNode = root.root();
|
|
166
|
+
// ── Classes / Structs ─────────────────────────────────────────────────
|
|
167
|
+
if (config.class?.length) {
|
|
168
|
+
for (const classKind of config.class) {
|
|
169
|
+
try {
|
|
170
|
+
for (const node of rootNode.findAll({ rule: { kind: classKind } })) {
|
|
171
|
+
// Go disambiguation: type_declaration is shared between struct and interface
|
|
172
|
+
if (lang === 'go' && classKind === 'type_declaration') {
|
|
173
|
+
const kind = goTypeKind(node);
|
|
174
|
+
if (kind !== 'struct') {
|
|
175
|
+
continue; // interfaces handled separately below
|
|
176
|
+
}
|
|
177
|
+
const name = goTypeName(node);
|
|
178
|
+
if (!name || seen.has(`c:${fp}:${name}`)) {
|
|
179
|
+
continue;
|
|
180
|
+
}
|
|
181
|
+
seen.add(`c:${fp}:${name}`);
|
|
182
|
+
graph.classes.push({
|
|
183
|
+
name,
|
|
184
|
+
file: fp,
|
|
185
|
+
line_start: node.range().start.line,
|
|
186
|
+
line_end: node.range().end.line,
|
|
187
|
+
extends: '',
|
|
188
|
+
implements: [],
|
|
189
|
+
ast_kind: String(node.kind()),
|
|
190
|
+
qualified: `${fp}::${name}`,
|
|
191
|
+
content_hash: computeContentHash(node.text()),
|
|
192
|
+
});
|
|
193
|
+
continue;
|
|
194
|
+
}
|
|
195
|
+
const name = node.field('name')?.text();
|
|
196
|
+
if (!name || seen.has(`c:${fp}:${name}`)) {
|
|
197
|
+
continue;
|
|
198
|
+
}
|
|
199
|
+
seen.add(`c:${fp}:${name}`);
|
|
200
|
+
// Heritage extraction
|
|
201
|
+
let extendsVal = '';
|
|
202
|
+
let implementsVal = [];
|
|
203
|
+
if (config.heritage?.extends) {
|
|
204
|
+
const raw = config.heritage.extends(node);
|
|
205
|
+
if (typeof raw === 'string') {
|
|
206
|
+
extendsVal = raw;
|
|
207
|
+
}
|
|
208
|
+
else if (Array.isArray(raw) && raw.length > 0) {
|
|
209
|
+
extendsVal = raw[0];
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (config.heritage?.implements) {
|
|
213
|
+
const raw = config.heritage.implements(node);
|
|
214
|
+
if (typeof raw === 'string') {
|
|
215
|
+
implementsVal = [raw];
|
|
216
|
+
}
|
|
217
|
+
else if (Array.isArray(raw)) {
|
|
218
|
+
implementsVal = raw;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
graph.classes.push({
|
|
222
|
+
name,
|
|
223
|
+
file: fp,
|
|
224
|
+
line_start: node.range().start.line,
|
|
225
|
+
line_end: node.range().end.line,
|
|
226
|
+
extends: extendsVal,
|
|
227
|
+
implements: implementsVal,
|
|
228
|
+
ast_kind: String(node.kind()),
|
|
229
|
+
qualified: `${fp}::${name}`,
|
|
230
|
+
content_hash: computeContentHash(node.text()),
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch (err) {
|
|
235
|
+
log.debug('Generic class extraction failed', { file: fp, lang, error: String(err) });
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
// ── Interfaces / Traits ───────────────────────────────────────────────
|
|
240
|
+
if (config.interface?.length) {
|
|
241
|
+
for (const ifaceKind of config.interface) {
|
|
242
|
+
try {
|
|
243
|
+
for (const node of rootNode.findAll({ rule: { kind: ifaceKind } })) {
|
|
244
|
+
// Go disambiguation: type_declaration shared with class — only pick interface_type
|
|
245
|
+
if (lang === 'go' && ifaceKind === 'type_declaration') {
|
|
246
|
+
const kind = goTypeKind(node);
|
|
247
|
+
if (kind !== 'interface') {
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
const name = goTypeName(node);
|
|
251
|
+
if (!name || seen.has(`i:${fp}:${name}`)) {
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
seen.add(`i:${fp}:${name}`);
|
|
255
|
+
graph.interfaces.push({
|
|
256
|
+
name,
|
|
257
|
+
file: fp,
|
|
258
|
+
line_start: node.range().start.line,
|
|
259
|
+
line_end: node.range().end.line,
|
|
260
|
+
methods: [],
|
|
261
|
+
ast_kind: String(node.kind()),
|
|
262
|
+
qualified: `${fp}::${name}`,
|
|
263
|
+
content_hash: computeContentHash(node.text()),
|
|
264
|
+
});
|
|
265
|
+
continue;
|
|
266
|
+
}
|
|
267
|
+
const name = node.field('name')?.text();
|
|
268
|
+
if (!name || seen.has(`i:${fp}:${name}`)) {
|
|
269
|
+
continue;
|
|
270
|
+
}
|
|
271
|
+
seen.add(`i:${fp}:${name}`);
|
|
272
|
+
graph.interfaces.push({
|
|
273
|
+
name,
|
|
274
|
+
file: fp,
|
|
275
|
+
line_start: node.range().start.line,
|
|
276
|
+
line_end: node.range().end.line,
|
|
277
|
+
methods: [],
|
|
278
|
+
ast_kind: String(node.kind()),
|
|
279
|
+
qualified: `${fp}::${name}`,
|
|
280
|
+
content_hash: computeContentHash(node.text()),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
catch (err) {
|
|
285
|
+
log.debug('Generic interface extraction failed', { file: fp, lang, error: String(err) });
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
// ── Enums ─────────────────────────────────────────────────────────────
|
|
290
|
+
if (config.enum?.length) {
|
|
291
|
+
for (const enumKind of config.enum) {
|
|
292
|
+
try {
|
|
293
|
+
for (const node of rootNode.findAll({ rule: { kind: enumKind } })) {
|
|
294
|
+
const name = node.field('name')?.text();
|
|
295
|
+
if (!name || seen.has(`e:${fp}:${name}`)) {
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
seen.add(`e:${fp}:${name}`);
|
|
299
|
+
graph.enums.push({
|
|
300
|
+
name,
|
|
301
|
+
file: fp,
|
|
302
|
+
line_start: node.range().start.line,
|
|
303
|
+
line_end: node.range().end.line,
|
|
304
|
+
ast_kind: String(node.kind()),
|
|
305
|
+
qualified: `${fp}::${name}`,
|
|
306
|
+
content_hash: computeContentHash(node.text()),
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch (err) {
|
|
311
|
+
log.debug('Generic enum extraction failed', { file: fp, lang, error: String(err) });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
// ── Functions / Methods / Constructors ────────────────────────────────
|
|
316
|
+
const funcKinds = [...(config.function ?? []), ...(config.method ?? []), ...(config.constructorKinds ?? [])];
|
|
317
|
+
const constructorKindSet = new Set(config.constructorKinds ?? []);
|
|
318
|
+
const methodKindSet = new Set(config.method ?? []);
|
|
319
|
+
for (const funcKind of funcKinds) {
|
|
320
|
+
try {
|
|
321
|
+
for (const node of rootNode.findAll({ rule: { kind: funcKind } })) {
|
|
322
|
+
const name = node.field('name')?.text();
|
|
323
|
+
if (!name) {
|
|
324
|
+
continue;
|
|
325
|
+
}
|
|
326
|
+
const line = node.range().start.line;
|
|
327
|
+
if (seen.has(`f:${fp}:${name}:${line}`)) {
|
|
328
|
+
continue;
|
|
329
|
+
}
|
|
330
|
+
seen.add(`f:${fp}:${name}:${line}`);
|
|
331
|
+
const classAncestor = node.ancestors().find((a) => {
|
|
332
|
+
const k = String(a.kind());
|
|
333
|
+
return k.includes('class') || k.includes('struct') || k.includes('impl');
|
|
334
|
+
});
|
|
335
|
+
const className = classAncestor?.field('name')?.text() || '';
|
|
336
|
+
// Determine function kind
|
|
337
|
+
let kind;
|
|
338
|
+
if (constructorKindSet.has(funcKind)) {
|
|
339
|
+
kind = 'Constructor';
|
|
340
|
+
}
|
|
341
|
+
else if (methodKindSet.has(funcKind) || className) {
|
|
342
|
+
kind = 'Method';
|
|
343
|
+
}
|
|
344
|
+
else {
|
|
345
|
+
kind = 'Function';
|
|
346
|
+
}
|
|
347
|
+
// Test detection
|
|
348
|
+
const isTest = isTestByConvention(fp, name, config) || hasTestAnnotation(node, config);
|
|
349
|
+
if (isTest) {
|
|
350
|
+
// Push to graph.tests
|
|
351
|
+
const testKey = `t:${fp}:${name}:${line}`;
|
|
352
|
+
if (!seen.has(testKey)) {
|
|
353
|
+
seen.add(testKey);
|
|
354
|
+
graph.tests.push({
|
|
355
|
+
name,
|
|
356
|
+
file: fp,
|
|
357
|
+
line_start: line,
|
|
358
|
+
line_end: node.range().end.line,
|
|
359
|
+
ast_kind: String(node.kind()),
|
|
360
|
+
qualified: className ? `${fp}::${className}.${name}` : `${fp}::${name}`,
|
|
361
|
+
content_hash: computeContentHash(node.text()),
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
// Also push to functions so call resolution still works
|
|
365
|
+
}
|
|
366
|
+
graph.functions.push({
|
|
367
|
+
name,
|
|
368
|
+
file: fp,
|
|
369
|
+
line_start: line,
|
|
370
|
+
line_end: node.range().end.line,
|
|
371
|
+
params: node.field('parameters')?.text() || '()',
|
|
372
|
+
returnType: node.field('return_type')?.text() || '',
|
|
373
|
+
kind,
|
|
374
|
+
ast_kind: String(node.kind()),
|
|
375
|
+
className,
|
|
376
|
+
qualified: className ? `${fp}::${className}.${name}` : `${fp}::${name}`,
|
|
377
|
+
content_hash: computeContentHash(node.text()),
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
catch (err) {
|
|
382
|
+
log.debug('Generic function extraction failed', { file: fp, lang, error: String(err) });
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
// ── Imports ───────────────────────────────────────────────────────────
|
|
386
|
+
if (config.import?.length) {
|
|
387
|
+
for (const importKind of config.import) {
|
|
388
|
+
try {
|
|
389
|
+
for (const node of rootNode.findAll({ rule: { kind: importKind } })) {
|
|
390
|
+
const module = extractImportModule(node);
|
|
391
|
+
if (!module) {
|
|
392
|
+
continue;
|
|
393
|
+
}
|
|
394
|
+
graph.imports.push({
|
|
395
|
+
module,
|
|
396
|
+
file: fp,
|
|
397
|
+
line: node.range().start.line,
|
|
398
|
+
names: extractImportNames(node),
|
|
399
|
+
lang,
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
catch (err) {
|
|
404
|
+
log.debug('Generic import extraction failed', { file: fp, lang, error: String(err) });
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
/** Shared class-finder for languages using class/struct/impl AST kinds. */
|
|
410
|
+
function findEnclosingClassGeneric(node) {
|
|
411
|
+
return (node.ancestors().find((a) => {
|
|
412
|
+
const k = String(a.kind());
|
|
413
|
+
return k.includes('class') || k.includes('struct') || k.includes('impl');
|
|
414
|
+
}) ?? null);
|
|
415
|
+
}
|
|
416
|
+
/** Per-language call extraction configs for self/super detection. */
|
|
417
|
+
const GENERIC_CONFIGS = {
|
|
418
|
+
java: {
|
|
419
|
+
selfPrefixes: ['this.'],
|
|
420
|
+
superPrefixes: ['super.'],
|
|
421
|
+
findEnclosingClass: findEnclosingClassGeneric,
|
|
422
|
+
getParentClass: (classNode) => {
|
|
423
|
+
const sc = classNode.children().find((c) => c.kind() === 'superclass');
|
|
424
|
+
return sc
|
|
425
|
+
?.children()
|
|
426
|
+
.find((c) => c.kind() === 'type_identifier')
|
|
427
|
+
?.text();
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
csharp: {
|
|
431
|
+
selfPrefixes: ['this.'],
|
|
432
|
+
superPrefixes: ['base.'],
|
|
433
|
+
findEnclosingClass: findEnclosingClassGeneric,
|
|
434
|
+
getParentClass: (classNode) => {
|
|
435
|
+
const bl = classNode.children().find((c) => c.kind() === 'base_list');
|
|
436
|
+
return bl
|
|
437
|
+
?.children()
|
|
438
|
+
.find((c) => c.kind() === 'identifier' || c.kind() === 'type_identifier')
|
|
439
|
+
?.text();
|
|
440
|
+
},
|
|
441
|
+
},
|
|
442
|
+
rust: {
|
|
443
|
+
selfPrefixes: ['self.'],
|
|
444
|
+
superPrefixes: [],
|
|
445
|
+
findEnclosingClass: (node) => node.ancestors().find((a) => a.kind() === 'impl_item') ?? null,
|
|
446
|
+
},
|
|
447
|
+
go: {
|
|
448
|
+
selfPrefixes: [],
|
|
449
|
+
superPrefixes: [],
|
|
450
|
+
findEnclosingClass: findEnclosingClassGeneric,
|
|
451
|
+
},
|
|
452
|
+
php: {
|
|
453
|
+
selfPrefixes: ['$this->'],
|
|
454
|
+
superPrefixes: ['parent::'],
|
|
455
|
+
findEnclosingClass: findEnclosingClassGeneric,
|
|
456
|
+
},
|
|
457
|
+
};
|
|
458
|
+
/** Fallback config for unknown languages — no self/super detection. */
|
|
459
|
+
const FALLBACK_CONFIG = {
|
|
460
|
+
selfPrefixes: [],
|
|
461
|
+
superPrefixes: [],
|
|
462
|
+
findEnclosingClass: findEnclosingClassGeneric,
|
|
463
|
+
};
|
|
464
|
+
/**
|
|
465
|
+
* Extract raw call sites from a generic language AST.
|
|
466
|
+
* Uses per-language config for self/super detection.
|
|
467
|
+
*/
|
|
468
|
+
export function extractCallsFromGeneric(root, fp, lang, calls) {
|
|
469
|
+
const config = GENERIC_CONFIGS[lang] ?? FALLBACK_CONFIG;
|
|
470
|
+
extractCalls(root.root(), fp, config, calls);
|
|
471
|
+
}
|
|
@@ -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;
|