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