@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.
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 -169
  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 -210
  126. package/src/commands/analyze.ts +0 -128
  127. package/src/commands/communities.ts +0 -19
  128. package/src/commands/context.ts +0 -182
  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 -17
  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
@@ -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
- }