@kodus/kodus-graph 0.2.7 → 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.
Files changed (167) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +252 -0
  3. package/dist/analysis/blast-radius.d.ts +2 -0
  4. package/dist/analysis/blast-radius.js +57 -0
  5. package/dist/analysis/communities.d.ts +28 -0
  6. package/dist/analysis/communities.js +100 -0
  7. package/dist/analysis/context-builder.d.ts +34 -0
  8. package/dist/analysis/context-builder.js +83 -0
  9. package/dist/analysis/diff.d.ts +35 -0
  10. package/dist/analysis/diff.js +140 -0
  11. package/dist/analysis/enrich.d.ts +5 -0
  12. package/dist/analysis/enrich.js +98 -0
  13. package/dist/analysis/flows.d.ts +27 -0
  14. package/dist/analysis/flows.js +86 -0
  15. package/dist/analysis/inheritance.d.ts +3 -0
  16. package/dist/analysis/inheritance.js +31 -0
  17. package/dist/analysis/prompt-formatter.d.ts +2 -0
  18. package/dist/analysis/prompt-formatter.js +166 -0
  19. package/dist/analysis/risk-score.d.ts +4 -0
  20. package/dist/analysis/risk-score.js +51 -0
  21. package/dist/analysis/search.d.ts +11 -0
  22. package/dist/analysis/search.js +64 -0
  23. package/dist/analysis/test-gaps.d.ts +2 -0
  24. package/dist/analysis/test-gaps.js +14 -0
  25. package/dist/cli.d.ts +2 -0
  26. package/dist/cli.js +208 -0
  27. package/dist/commands/analyze.d.ts +9 -0
  28. package/dist/commands/analyze.js +114 -0
  29. package/dist/commands/communities.d.ts +8 -0
  30. package/dist/commands/communities.js +9 -0
  31. package/dist/commands/context.d.ts +12 -0
  32. package/dist/commands/context.js +130 -0
  33. package/dist/commands/diff.d.ts +9 -0
  34. package/dist/commands/diff.js +89 -0
  35. package/dist/commands/flows.d.ts +8 -0
  36. package/dist/commands/flows.js +9 -0
  37. package/dist/commands/parse.d.ts +10 -0
  38. package/dist/commands/parse.js +101 -0
  39. package/dist/commands/search.d.ts +12 -0
  40. package/dist/commands/search.js +27 -0
  41. package/dist/commands/update.d.ts +7 -0
  42. package/dist/commands/update.js +154 -0
  43. package/dist/graph/builder.d.ts +2 -0
  44. package/dist/graph/builder.js +216 -0
  45. package/dist/graph/edges.d.ts +19 -0
  46. package/dist/graph/edges.js +105 -0
  47. package/dist/graph/json-writer.d.ts +9 -0
  48. package/dist/graph/json-writer.js +38 -0
  49. package/dist/graph/loader.d.ts +13 -0
  50. package/dist/graph/loader.js +101 -0
  51. package/dist/graph/merger.d.ts +7 -0
  52. package/dist/graph/merger.js +18 -0
  53. package/dist/graph/types.d.ts +249 -0
  54. package/dist/graph/types.js +1 -0
  55. package/dist/parser/batch.d.ts +4 -0
  56. package/dist/parser/batch.js +78 -0
  57. package/dist/parser/discovery.d.ts +7 -0
  58. package/dist/parser/discovery.js +61 -0
  59. package/dist/parser/extractor.d.ts +4 -0
  60. package/dist/parser/extractor.js +33 -0
  61. package/dist/parser/extractors/generic.d.ts +8 -0
  62. package/dist/parser/extractors/generic.js +471 -0
  63. package/dist/parser/extractors/python.d.ts +8 -0
  64. package/dist/parser/extractors/python.js +133 -0
  65. package/dist/parser/extractors/ruby.d.ts +8 -0
  66. package/dist/parser/extractors/ruby.js +153 -0
  67. package/dist/parser/extractors/typescript.d.ts +10 -0
  68. package/dist/parser/extractors/typescript.js +365 -0
  69. package/dist/parser/languages.d.ts +32 -0
  70. package/dist/parser/languages.js +303 -0
  71. package/dist/resolver/call-resolver.d.ts +36 -0
  72. package/dist/resolver/call-resolver.js +178 -0
  73. package/dist/resolver/import-map.d.ts +12 -0
  74. package/dist/resolver/import-map.js +21 -0
  75. package/dist/resolver/import-resolver.d.ts +19 -0
  76. package/dist/resolver/import-resolver.js +212 -0
  77. package/dist/resolver/languages/csharp.d.ts +1 -0
  78. package/dist/resolver/languages/csharp.js +31 -0
  79. package/dist/resolver/languages/go.d.ts +3 -0
  80. package/dist/resolver/languages/go.js +196 -0
  81. package/dist/resolver/languages/java.d.ts +1 -0
  82. package/dist/resolver/languages/java.js +108 -0
  83. package/dist/resolver/languages/php.d.ts +3 -0
  84. package/dist/resolver/languages/php.js +54 -0
  85. package/dist/resolver/languages/python.d.ts +11 -0
  86. package/dist/resolver/languages/python.js +51 -0
  87. package/dist/resolver/languages/ruby.d.ts +9 -0
  88. package/dist/resolver/languages/ruby.js +59 -0
  89. package/dist/resolver/languages/rust.d.ts +1 -0
  90. package/dist/resolver/languages/rust.js +196 -0
  91. package/dist/resolver/languages/typescript.d.ts +27 -0
  92. package/dist/resolver/languages/typescript.js +240 -0
  93. package/dist/resolver/re-export-resolver.d.ts +24 -0
  94. package/dist/resolver/re-export-resolver.js +57 -0
  95. package/dist/resolver/symbol-table.d.ts +17 -0
  96. package/dist/resolver/symbol-table.js +60 -0
  97. package/dist/shared/extract-calls.d.ts +26 -0
  98. package/dist/shared/extract-calls.js +57 -0
  99. package/dist/shared/file-hash.d.ts +3 -0
  100. package/dist/shared/file-hash.js +10 -0
  101. package/dist/shared/filters.d.ts +3 -0
  102. package/dist/shared/filters.js +240 -0
  103. package/dist/shared/logger.d.ts +6 -0
  104. package/dist/shared/logger.js +17 -0
  105. package/dist/shared/qualified-name.d.ts +1 -0
  106. package/dist/shared/qualified-name.js +9 -0
  107. package/dist/shared/safe-path.d.ts +6 -0
  108. package/dist/shared/safe-path.js +29 -0
  109. package/dist/shared/schemas.d.ts +43 -0
  110. package/dist/shared/schemas.js +30 -0
  111. package/dist/shared/temp.d.ts +11 -0
  112. package/{src/shared/temp.ts → dist/shared/temp.js} +4 -5
  113. package/package.json +20 -6
  114. package/src/analysis/blast-radius.ts +0 -54
  115. package/src/analysis/communities.ts +0 -135
  116. package/src/analysis/context-builder.ts +0 -130
  117. package/src/analysis/diff.ts +0 -131
  118. package/src/analysis/enrich.ts +0 -110
  119. package/src/analysis/flows.ts +0 -112
  120. package/src/analysis/inheritance.ts +0 -34
  121. package/src/analysis/prompt-formatter.ts +0 -175
  122. package/src/analysis/risk-score.ts +0 -62
  123. package/src/analysis/search.ts +0 -76
  124. package/src/analysis/test-gaps.ts +0 -21
  125. package/src/cli.ts +0 -207
  126. package/src/commands/analyze.ts +0 -128
  127. package/src/commands/communities.ts +0 -19
  128. package/src/commands/context.ts +0 -139
  129. package/src/commands/diff.ts +0 -96
  130. package/src/commands/flows.ts +0 -19
  131. package/src/commands/parse.ts +0 -124
  132. package/src/commands/search.ts +0 -41
  133. package/src/commands/update.ts +0 -166
  134. package/src/graph/builder.ts +0 -209
  135. package/src/graph/edges.ts +0 -101
  136. package/src/graph/json-writer.ts +0 -43
  137. package/src/graph/loader.ts +0 -113
  138. package/src/graph/merger.ts +0 -25
  139. package/src/graph/types.ts +0 -283
  140. package/src/parser/batch.ts +0 -82
  141. package/src/parser/discovery.ts +0 -75
  142. package/src/parser/extractor.ts +0 -37
  143. package/src/parser/extractors/generic.ts +0 -132
  144. package/src/parser/extractors/python.ts +0 -133
  145. package/src/parser/extractors/ruby.ts +0 -147
  146. package/src/parser/extractors/typescript.ts +0 -350
  147. package/src/parser/languages.ts +0 -122
  148. package/src/resolver/call-resolver.ts +0 -244
  149. package/src/resolver/import-map.ts +0 -27
  150. package/src/resolver/import-resolver.ts +0 -72
  151. package/src/resolver/languages/csharp.ts +0 -7
  152. package/src/resolver/languages/go.ts +0 -7
  153. package/src/resolver/languages/java.ts +0 -7
  154. package/src/resolver/languages/php.ts +0 -7
  155. package/src/resolver/languages/python.ts +0 -35
  156. package/src/resolver/languages/ruby.ts +0 -21
  157. package/src/resolver/languages/rust.ts +0 -7
  158. package/src/resolver/languages/typescript.ts +0 -168
  159. package/src/resolver/re-export-resolver.ts +0 -66
  160. package/src/resolver/symbol-table.ts +0 -67
  161. package/src/shared/extract-calls.ts +0 -75
  162. package/src/shared/file-hash.ts +0 -12
  163. package/src/shared/filters.ts +0 -243
  164. package/src/shared/logger.ts +0 -14
  165. package/src/shared/qualified-name.ts +0 -5
  166. package/src/shared/safe-path.ts +0 -31
  167. 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;