@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
@@ -0,0 +1,212 @@
1
+ /**
2
+ * Import resolver dispatcher.
3
+ *
4
+ * Routes import resolution to language-specific resolvers and
5
+ * falls back to tsconfig aliases for TypeScript/JavaScript.
6
+ */
7
+ import { existsSync, readdirSync, readFileSync } from 'fs';
8
+ import { join, resolve as resolvePath } from 'path';
9
+ import { log } from '../shared/logger';
10
+ import { ensureWithinRoot } from '../shared/safe-path';
11
+ import { resolve as resolveCsImport } from './languages/csharp';
12
+ import { resolve as resolveGoImport } from './languages/go';
13
+ import { resolve as resolveJavaImport } from './languages/java';
14
+ import { resolve as resolvePhpImport } from './languages/php';
15
+ import { resolve as resolvePyImport } from './languages/python';
16
+ import { resolve as resolveRbImport } from './languages/ruby';
17
+ import { resolve as resolveRustImport } from './languages/rust';
18
+ import { loadTsconfigAliases, resolve as resolveTsImport, resolveWithAliases } from './languages/typescript';
19
+ const RESOLVERS = {
20
+ ts: resolveTsImport,
21
+ javascript: resolveTsImport,
22
+ typescript: resolveTsImport,
23
+ python: resolvePyImport,
24
+ ruby: resolveRbImport,
25
+ go: resolveGoImport,
26
+ java: resolveJavaImport,
27
+ rust: resolveRustImport,
28
+ csharp: resolveCsImport,
29
+ php: resolvePhpImport,
30
+ };
31
+ /**
32
+ * Resolve package.json #imports (Node.js subpath imports).
33
+ * Handles both exact matches and wildcard patterns.
34
+ */
35
+ function resolveHashImport(modulePath, repoRoot) {
36
+ const pkgPath = join(repoRoot, 'package.json');
37
+ if (!existsSync(pkgPath)) {
38
+ return null;
39
+ }
40
+ try {
41
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
42
+ const imports = pkg?.imports;
43
+ if (!imports) {
44
+ return null;
45
+ }
46
+ for (const [pattern, target] of Object.entries(imports)) {
47
+ if (typeof target !== 'string') {
48
+ continue;
49
+ }
50
+ if (pattern === modulePath) {
51
+ // Exact match: "#utils" -> "./src/shared/utils.ts"
52
+ const resolved = resolvePath(repoRoot, target);
53
+ if (existsSync(resolved)) {
54
+ return resolved;
55
+ }
56
+ }
57
+ // Wildcard match: "#db/*" -> "./src/db/*.ts"
58
+ if (pattern.includes('*')) {
59
+ const prefix = pattern.split('*')[0]; // "#db/"
60
+ if (modulePath.startsWith(prefix)) {
61
+ const rest = modulePath.slice(prefix.length); // "connection"
62
+ const resolved = resolvePath(repoRoot, target.replace('*', rest));
63
+ if (existsSync(resolved)) {
64
+ return resolved;
65
+ }
66
+ }
67
+ }
68
+ }
69
+ }
70
+ catch {
71
+ // ignore parse errors
72
+ }
73
+ return null;
74
+ }
75
+ /**
76
+ * Resolve monorepo workspace package exports.
77
+ * Scans workspace directories to find packages matching the import specifier.
78
+ */
79
+ function resolveWorkspaceExport(modulePath, repoRoot) {
80
+ const rootPkgPath = join(repoRoot, 'package.json');
81
+ if (!existsSync(rootPkgPath)) {
82
+ return null;
83
+ }
84
+ try {
85
+ const rootPkg = JSON.parse(readFileSync(rootPkgPath, 'utf-8'));
86
+ const workspaces = rootPkg?.workspaces;
87
+ if (!Array.isArray(workspaces)) {
88
+ return null;
89
+ }
90
+ // Collect all workspace package directories
91
+ const pkgDirs = [];
92
+ for (const ws of workspaces) {
93
+ if (ws.endsWith('/*')) {
94
+ // Glob pattern like "packages/*"
95
+ const parentDir = join(repoRoot, ws.slice(0, -2));
96
+ if (existsSync(parentDir)) {
97
+ const entries = readdirSync(parentDir, { withFileTypes: true });
98
+ for (const entry of entries) {
99
+ if (entry.isDirectory()) {
100
+ pkgDirs.push(join(parentDir, entry.name));
101
+ }
102
+ }
103
+ }
104
+ }
105
+ else {
106
+ pkgDirs.push(join(repoRoot, ws));
107
+ }
108
+ }
109
+ // Search each workspace package for a matching name + exports
110
+ for (const pkgDir of pkgDirs) {
111
+ const pkgJsonPath = join(pkgDir, 'package.json');
112
+ if (!existsSync(pkgJsonPath)) {
113
+ continue;
114
+ }
115
+ const pkg = JSON.parse(readFileSync(pkgJsonPath, 'utf-8'));
116
+ const pkgName = pkg?.name;
117
+ if (!pkgName) {
118
+ continue;
119
+ }
120
+ const exports = pkg?.exports;
121
+ if (!exports || typeof exports !== 'object') {
122
+ continue;
123
+ }
124
+ // Check if modulePath matches this package (exact or subpath)
125
+ if (modulePath === pkgName) {
126
+ // Root export: "." entry
127
+ const target = exports['.'];
128
+ if (typeof target === 'string') {
129
+ const resolved = resolvePath(pkgDir, target);
130
+ if (existsSync(resolved)) {
131
+ return resolved;
132
+ }
133
+ }
134
+ }
135
+ else if (modulePath.startsWith(`${pkgName}/`)) {
136
+ // Subpath export: "./button" entry
137
+ const subpath = `./${modulePath.slice(pkgName.length + 1)}`;
138
+ const target = exports[subpath];
139
+ if (typeof target === 'string') {
140
+ const resolved = resolvePath(pkgDir, target);
141
+ if (existsSync(resolved)) {
142
+ return resolved;
143
+ }
144
+ }
145
+ }
146
+ }
147
+ }
148
+ catch {
149
+ // ignore parse errors
150
+ }
151
+ return null;
152
+ }
153
+ /**
154
+ * Resolve an import from one file to another.
155
+ *
156
+ * @param fromAbsFile - Absolute path of the importing file
157
+ * @param modulePath - The import specifier (e.g., './auth', 'express', '@/lib/db')
158
+ * @param lang - Language key (ts, javascript, typescript, python, ruby, etc.)
159
+ * @param repoRoot - Absolute path to the repository root
160
+ * @param tsconfigAliases - Optional pre-loaded tsconfig aliases for TS/JS
161
+ * @returns Absolute path to the resolved file, or null if unresolvable
162
+ */
163
+ export function resolveImport(fromAbsFile, modulePath, lang, repoRoot, tsconfigAliases) {
164
+ const resolver = RESOLVERS[lang];
165
+ if (!resolver) {
166
+ return null;
167
+ }
168
+ const isTs = lang === 'ts' || lang === 'javascript' || lang === 'typescript';
169
+ // Handle package.json #imports (TS/JS only)
170
+ if (isTs && modulePath.startsWith('#')) {
171
+ const result = resolveHashImport(modulePath, repoRoot);
172
+ if (result) {
173
+ try {
174
+ ensureWithinRoot(result, repoRoot);
175
+ return result;
176
+ }
177
+ catch {
178
+ log.warn('Import resolves outside repository root', {
179
+ from: fromAbsFile,
180
+ module: modulePath,
181
+ resolved: result,
182
+ });
183
+ return null;
184
+ }
185
+ }
186
+ }
187
+ let result = resolver(fromAbsFile, modulePath, repoRoot);
188
+ // Fallback: tsconfig aliases for TS/JS
189
+ if (!result && isTs && tsconfigAliases?.size) {
190
+ result = resolveWithAliases(modulePath, tsconfigAliases, repoRoot);
191
+ }
192
+ // Fallback: monorepo workspace exports for TS/JS bare specifiers
193
+ if (!result && isTs && !modulePath.startsWith('.')) {
194
+ result = resolveWorkspaceExport(modulePath, repoRoot);
195
+ }
196
+ // Validate resolved path is within repo root
197
+ if (result) {
198
+ try {
199
+ ensureWithinRoot(result, repoRoot);
200
+ }
201
+ catch {
202
+ log.warn('Import resolves outside repository root', {
203
+ from: fromAbsFile,
204
+ module: modulePath,
205
+ resolved: result,
206
+ });
207
+ return null;
208
+ }
209
+ }
210
+ return result;
211
+ }
212
+ export { loadTsconfigAliases };
@@ -0,0 +1 @@
1
+ export declare function resolve(_fromAbsFile: string, modulePath: string, repoRoot: string): string | null;
@@ -0,0 +1,31 @@
1
+ import { existsSync, statSync } from 'fs';
2
+ import { join, resolve as resolvePath } from 'path';
3
+ const STDLIB_PREFIXES = ['System.', 'System', 'Microsoft.', 'Newtonsoft.'];
4
+ export function resolve(_fromAbsFile, modulePath, repoRoot) {
5
+ if (STDLIB_PREFIXES.some((p) => modulePath.startsWith(p))) {
6
+ return null;
7
+ }
8
+ const segments = modulePath.split('.');
9
+ // Try resolving as a .cs file first
10
+ for (let i = segments.length - 1; i >= 0; i--) {
11
+ const pathPart = segments.slice(i).join('/');
12
+ const candidate = `${pathPart}.cs`;
13
+ for (const base of ['', 'src', 'lib', 'Source']) {
14
+ const full = join(repoRoot, base, candidate);
15
+ if (existsSync(full)) {
16
+ return resolvePath(full);
17
+ }
18
+ }
19
+ }
20
+ // Try resolving as a directory (namespace → folder mapping)
21
+ for (let i = segments.length - 1; i >= 0; i--) {
22
+ const pathPart = segments.slice(i).join('/');
23
+ for (const base of ['', 'src', 'lib', 'Source']) {
24
+ const full = join(repoRoot, base, pathPart);
25
+ if (existsSync(full) && statSync(full).isDirectory()) {
26
+ return resolvePath(full);
27
+ }
28
+ }
29
+ }
30
+ return null;
31
+ }
@@ -0,0 +1,3 @@
1
+ /** Clear cached go.mod data. Call between analysis runs or when switching repos. */
2
+ export declare function clearCache(): void;
3
+ export declare function resolve(_fromAbsFile: string, modulePath: string, repoRoot: string): string | null;
@@ -0,0 +1,196 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'fs';
2
+ import { join, resolve as resolvePath } from 'path';
3
+ const moduleCache = new Map();
4
+ const replaceCache = new Map();
5
+ const workspaceCache = new Map();
6
+ /** Clear cached go.mod data. Call between analysis runs or when switching repos. */
7
+ export function clearCache() {
8
+ moduleCache.clear();
9
+ replaceCache.clear();
10
+ workspaceCache.clear();
11
+ }
12
+ function getModuleName(repoRoot) {
13
+ const cached = moduleCache.get(repoRoot);
14
+ if (cached !== undefined) {
15
+ return cached || null;
16
+ }
17
+ const goModPath = join(repoRoot, 'go.mod');
18
+ if (!existsSync(goModPath)) {
19
+ moduleCache.set(repoRoot, '');
20
+ return null;
21
+ }
22
+ try {
23
+ const content = readFileSync(goModPath, 'utf-8');
24
+ const match = content.match(/^module\s+(\S+)/m);
25
+ if (match) {
26
+ moduleCache.set(repoRoot, match[1]);
27
+ return match[1];
28
+ }
29
+ }
30
+ catch {
31
+ /* ignore */
32
+ }
33
+ moduleCache.set(repoRoot, '');
34
+ return null;
35
+ }
36
+ /** Parse replace directives from go.mod. Returns map: module path → local directory (absolute). */
37
+ function getReplaceMap(repoRoot) {
38
+ const cached = replaceCache.get(repoRoot);
39
+ if (cached) {
40
+ return cached;
41
+ }
42
+ const result = new Map();
43
+ const goModPath = join(repoRoot, 'go.mod');
44
+ if (!existsSync(goModPath)) {
45
+ replaceCache.set(repoRoot, result);
46
+ return result;
47
+ }
48
+ try {
49
+ const content = readFileSync(goModPath, 'utf-8');
50
+ // Match single-line replace: replace mod => ./path or replace mod v1.2.3 => ./path
51
+ const replaceRe = /^replace\s+(\S+)(?:\s+\S+)?\s+=>\s+(\S+)/gm;
52
+ let m = replaceRe.exec(content);
53
+ while (m !== null) {
54
+ const modPath = m[1];
55
+ const replacement = m[2];
56
+ if (replacement.startsWith('./') || replacement.startsWith('../')) {
57
+ result.set(modPath, resolvePath(join(repoRoot, replacement)));
58
+ }
59
+ m = replaceRe.exec(content);
60
+ }
61
+ }
62
+ catch {
63
+ /* ignore */
64
+ }
65
+ replaceCache.set(repoRoot, result);
66
+ return result;
67
+ }
68
+ /** Parse go.work use directives. Returns map: module name → absolute directory of the module. */
69
+ function getWorkspaceModules(repoRoot) {
70
+ const cached = workspaceCache.get(repoRoot);
71
+ if (cached) {
72
+ return cached;
73
+ }
74
+ const result = new Map();
75
+ const goWorkPath = join(repoRoot, 'go.work');
76
+ if (!existsSync(goWorkPath)) {
77
+ workspaceCache.set(repoRoot, result);
78
+ return result;
79
+ }
80
+ try {
81
+ const content = readFileSync(goWorkPath, 'utf-8');
82
+ // Parse use directives — both single-line and block form
83
+ // Block: use ( ./a \n ./b )
84
+ const blockRe = /use\s*\(([\s\S]*?)\)/g;
85
+ let blockMatch = blockRe.exec(content);
86
+ const useDirs = [];
87
+ while (blockMatch !== null) {
88
+ const inner = blockMatch[1];
89
+ for (const line of inner.split('\n')) {
90
+ const trimmed = line.trim();
91
+ if (trimmed && !trimmed.startsWith('//')) {
92
+ useDirs.push(trimmed);
93
+ }
94
+ }
95
+ blockMatch = blockRe.exec(content);
96
+ }
97
+ // Single-line: use ./foo
98
+ const singleRe = /^use\s+(\S+)\s*$/gm;
99
+ let singleMatch = singleRe.exec(content);
100
+ while (singleMatch !== null) {
101
+ const dir = singleMatch[1];
102
+ if (dir !== '(') {
103
+ useDirs.push(dir);
104
+ }
105
+ singleMatch = singleRe.exec(content);
106
+ }
107
+ // For each use directory, read its go.mod to get the module name
108
+ for (const dir of useDirs) {
109
+ const absDir = resolvePath(join(repoRoot, dir));
110
+ const modName = getModuleName(absDir);
111
+ if (modName) {
112
+ result.set(modName, absDir);
113
+ }
114
+ }
115
+ }
116
+ catch {
117
+ /* ignore */
118
+ }
119
+ workspaceCache.set(repoRoot, result);
120
+ return result;
121
+ }
122
+ function isStdlib(modulePath) {
123
+ const first = modulePath.split('/')[0];
124
+ return !first.includes('.');
125
+ }
126
+ /** Find the first .go file (non-test) in a directory, or check for a .go file at the path. */
127
+ function findGoFile(absDir) {
128
+ if (existsSync(absDir)) {
129
+ try {
130
+ const files = readdirSync(absDir).sort();
131
+ const goFile = files.find((f) => f.endsWith('.go') && !f.endsWith('_test.go'));
132
+ if (goFile) {
133
+ return resolvePath(join(absDir, goFile));
134
+ }
135
+ }
136
+ catch {
137
+ /* not a directory */
138
+ }
139
+ }
140
+ if (existsSync(`${absDir}.go`)) {
141
+ return resolvePath(`${absDir}.go`);
142
+ }
143
+ return null;
144
+ }
145
+ export function resolve(_fromAbsFile, modulePath, repoRoot) {
146
+ if (isStdlib(modulePath)) {
147
+ return null;
148
+ }
149
+ // 1. Try resolving against the root module name
150
+ const moduleName = getModuleName(repoRoot);
151
+ if (moduleName && modulePath.startsWith(moduleName)) {
152
+ const relPath = modulePath.slice(moduleName.length + 1);
153
+ if (relPath) {
154
+ const result = findGoFile(join(repoRoot, relPath));
155
+ if (result) {
156
+ return result;
157
+ }
158
+ }
159
+ }
160
+ // 2. Try replace directives from go.mod
161
+ const replaces = getReplaceMap(repoRoot);
162
+ for (const [modPrefix, localDir] of replaces) {
163
+ if (modulePath.startsWith(modPrefix)) {
164
+ const suffix = modulePath.slice(modPrefix.length);
165
+ // suffix is either empty or starts with '/'
166
+ const relPath = suffix.startsWith('/') ? suffix.slice(1) : suffix;
167
+ if (relPath) {
168
+ const result = findGoFile(join(localDir, relPath));
169
+ if (result) {
170
+ return result;
171
+ }
172
+ }
173
+ }
174
+ }
175
+ // 3. Try go.work workspace modules
176
+ const workspaceModules = getWorkspaceModules(repoRoot);
177
+ for (const [wsModName, wsModDir] of workspaceModules) {
178
+ if (modulePath.startsWith(wsModName)) {
179
+ const suffix = modulePath.slice(wsModName.length);
180
+ const relPath = suffix.startsWith('/') ? suffix.slice(1) : suffix;
181
+ if (relPath) {
182
+ const result = findGoFile(join(wsModDir, relPath));
183
+ if (result) {
184
+ return result;
185
+ }
186
+ }
187
+ }
188
+ }
189
+ // 4. Try vendor directory
190
+ const vendorDir = join(repoRoot, 'vendor', modulePath);
191
+ const vendorResult = findGoFile(vendorDir);
192
+ if (vendorResult) {
193
+ return vendorResult;
194
+ }
195
+ return null;
196
+ }
@@ -0,0 +1 @@
1
+ export declare function resolve(_fromAbsFile: string, modulePath: string, repoRoot: string): string | null;
@@ -0,0 +1,108 @@
1
+ import { existsSync, readdirSync, readFileSync } from 'fs';
2
+ import { join, resolve as resolvePath } from 'path';
3
+ const STDLIB_PREFIXES = ['java.', 'javax.', 'sun.', 'com.sun.', 'jdk.'];
4
+ const SOURCE_ROOTS = ['src/main/java', 'src/main/kotlin', 'src', ''];
5
+ const EXTENSIONS = ['.java', '.kt'];
6
+ /**
7
+ * Collect all source roots, including those inside Gradle subproject directories.
8
+ */
9
+ function collectSourceRoots(repoRoot) {
10
+ const roots = [...SOURCE_ROOTS];
11
+ // Discover Gradle subprojects from settings.gradle / settings.gradle.kts
12
+ for (const settingsFile of ['settings.gradle', 'settings.gradle.kts']) {
13
+ const settingsPath = join(repoRoot, settingsFile);
14
+ if (!existsSync(settingsPath)) {
15
+ continue;
16
+ }
17
+ const content = readFileSync(settingsPath, 'utf-8');
18
+ // Match patterns like ':app', ':lib', ':core:domain'
19
+ const projectRegex = /['"]:([\w:/-]+)['"]/g;
20
+ let match = projectRegex.exec(content);
21
+ while (match !== null) {
22
+ const subDir = match[1].replace(/:/g, '/');
23
+ for (const srcRoot of SOURCE_ROOTS) {
24
+ if (srcRoot) {
25
+ roots.push(join(subDir, srcRoot));
26
+ }
27
+ }
28
+ match = projectRegex.exec(content);
29
+ }
30
+ break; // only read first settings file found
31
+ }
32
+ // Discover Maven subprojects from pom.xml
33
+ const pomPath = join(repoRoot, 'pom.xml');
34
+ if (existsSync(pomPath)) {
35
+ try {
36
+ const content = readFileSync(pomPath, 'utf-8');
37
+ const moduleRegex = /<module>([^<]+)<\/module>/g;
38
+ let mvnMatch = moduleRegex.exec(content);
39
+ while (mvnMatch !== null) {
40
+ const subDir = mvnMatch[1];
41
+ roots.push(join(subDir, 'src/main/java'));
42
+ roots.push(join(subDir, 'src/main/kotlin'));
43
+ mvnMatch = moduleRegex.exec(content);
44
+ }
45
+ }
46
+ catch {
47
+ // pom.xml read failed, continue
48
+ }
49
+ }
50
+ return roots;
51
+ }
52
+ /**
53
+ * Try to find a file at the given relative path (without extension) across all
54
+ * source roots, probing each supported extension.
55
+ */
56
+ function findFile(repoRoot, relPathNoExt, sourceRoots) {
57
+ for (const srcRoot of sourceRoots) {
58
+ for (const ext of EXTENSIONS) {
59
+ const candidate = join(repoRoot, srcRoot, relPathNoExt + ext);
60
+ if (existsSync(candidate)) {
61
+ return resolvePath(candidate);
62
+ }
63
+ }
64
+ }
65
+ return null;
66
+ }
67
+ export function resolve(_fromAbsFile, modulePath, repoRoot) {
68
+ if (STDLIB_PREFIXES.some((p) => modulePath.startsWith(p))) {
69
+ return null;
70
+ }
71
+ const sourceRoots = collectSourceRoots(repoRoot);
72
+ // --- Wildcard imports: com.example.models.* ---
73
+ if (modulePath.endsWith('.*')) {
74
+ const packagePath = modulePath.slice(0, -2).replace(/\./g, '/');
75
+ for (const srcRoot of sourceRoots) {
76
+ const dirPath = join(repoRoot, srcRoot, packagePath);
77
+ if (existsSync(dirPath)) {
78
+ try {
79
+ const files = readdirSync(dirPath).filter((f) => EXTENSIONS.some((ext) => f.endsWith(ext)));
80
+ if (files.length > 0) {
81
+ return resolvePath(join(dirPath, files[0]));
82
+ }
83
+ }
84
+ catch {
85
+ // directory read failed, try next root
86
+ }
87
+ }
88
+ }
89
+ return null;
90
+ }
91
+ // --- Standard resolution: try full path with all extensions ---
92
+ const relPathNoExt = modulePath.replace(/\./g, '/');
93
+ const direct = findFile(repoRoot, relPathNoExt, sourceRoots);
94
+ if (direct) {
95
+ return direct;
96
+ }
97
+ // --- Inner class fallback: progressively shorten the path ---
98
+ // com.example.Config.DatabaseSettings → try com/example/Config
99
+ const segments = modulePath.split('.');
100
+ for (let i = segments.length - 1; i >= 2; i--) {
101
+ const shorter = segments.slice(0, i).join('/');
102
+ const found = findFile(repoRoot, shorter, sourceRoots);
103
+ if (found) {
104
+ return found;
105
+ }
106
+ }
107
+ return null;
108
+ }
@@ -0,0 +1,3 @@
1
+ /** Clear cached composer.json PSR-4 data. Call between analysis runs or when switching repos. */
2
+ export declare function clearCache(): void;
3
+ export declare function resolve(_fromAbsFile: string, modulePath: string, repoRoot: string): string | null;
@@ -0,0 +1,54 @@
1
+ import { existsSync, readFileSync } from 'fs';
2
+ import { join, resolve as resolvePath } from 'path';
3
+ const psr4Cache = new Map();
4
+ /** Clear cached composer.json PSR-4 data. Call between analysis runs or when switching repos. */
5
+ export function clearCache() {
6
+ psr4Cache.clear();
7
+ }
8
+ function loadPsr4(repoRoot) {
9
+ const cached = psr4Cache.get(repoRoot);
10
+ if (cached) {
11
+ return cached;
12
+ }
13
+ const map = new Map();
14
+ const composerPath = join(repoRoot, 'composer.json');
15
+ if (existsSync(composerPath)) {
16
+ try {
17
+ const content = readFileSync(composerPath, 'utf-8');
18
+ const config = JSON.parse(content);
19
+ const psr4 = config?.autoload?.['psr-4'];
20
+ if (psr4) {
21
+ for (const [prefix, dir] of Object.entries(psr4)) {
22
+ const dirStr = Array.isArray(dir) ? dir[0] : dir;
23
+ map.set(prefix, dirStr);
24
+ }
25
+ }
26
+ }
27
+ catch {
28
+ /* ignore */
29
+ }
30
+ }
31
+ psr4Cache.set(repoRoot, map);
32
+ return map;
33
+ }
34
+ export function resolve(_fromAbsFile, modulePath, repoRoot) {
35
+ const psr4 = loadPsr4(repoRoot);
36
+ for (const [prefix, dir] of psr4) {
37
+ if (modulePath.startsWith(prefix)) {
38
+ const rest = modulePath.slice(prefix.length);
39
+ const relPath = `${rest.replace(/\\/g, '/')}.php`;
40
+ const candidate = join(repoRoot, dir, relPath);
41
+ if (existsSync(candidate)) {
42
+ return resolvePath(candidate);
43
+ }
44
+ }
45
+ }
46
+ const relPath = `${modulePath.replace(/\\/g, '/')}.php`;
47
+ for (const base of ['', 'src', 'lib', 'app']) {
48
+ const candidate = join(repoRoot, base, relPath);
49
+ if (existsSync(candidate)) {
50
+ return resolvePath(candidate);
51
+ }
52
+ }
53
+ return null;
54
+ }
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Python import resolver.
3
+ *
4
+ * Handles dotted module paths (e.g., "from x.y import z").
5
+ * Walks up directories to find packages.
6
+ */
7
+ /**
8
+ * Resolve a Python dotted import to a file path.
9
+ * Walks up from the importing file's directory to find the module.
10
+ */
11
+ export declare function resolve(fromAbsFile: string, modulePath: string, _repoRoot: string): string | null;
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Python import resolver.
3
+ *
4
+ * Handles dotted module paths (e.g., "from x.y import z").
5
+ * Walks up directories to find packages.
6
+ */
7
+ import { existsSync } from 'fs';
8
+ import { dirname, join, resolve as resolvePath } from 'path';
9
+ /**
10
+ * Resolve a Python dotted import to a file path.
11
+ * Walks up from the importing file's directory to find the module.
12
+ */
13
+ export function resolve(fromAbsFile, modulePath, _repoRoot) {
14
+ if (!modulePath) {
15
+ return null;
16
+ }
17
+ if (modulePath.startsWith('.')) {
18
+ // Relative import: count leading dots, walk up directories
19
+ const dotMatch = modulePath.match(/^(\.+)/);
20
+ const dots = dotMatch[1].length;
21
+ const rest = modulePath.slice(dots).replace(/\./g, '/');
22
+ let base = dirname(fromAbsFile);
23
+ for (let d = 1; d < dots; d++) {
24
+ base = dirname(base);
25
+ }
26
+ const candidates = rest ? [`${rest}.py`, `${rest}/__init__.py`] : [`__init__.py`];
27
+ for (const candidate of candidates) {
28
+ const full = join(base, candidate);
29
+ if (existsSync(full)) {
30
+ return resolvePath(full);
31
+ }
32
+ }
33
+ return null;
34
+ }
35
+ const parts = modulePath.replace(/\./g, '/');
36
+ let current = dirname(fromAbsFile);
37
+ for (let i = 0; i < 10; i++) {
38
+ for (const candidate of [`${parts}.py`, `${parts}/__init__.py`]) {
39
+ const full = join(current, candidate);
40
+ if (existsSync(full)) {
41
+ return resolvePath(full);
42
+ }
43
+ }
44
+ const parent = dirname(current);
45
+ if (parent === current) {
46
+ break;
47
+ }
48
+ current = parent;
49
+ }
50
+ return null;
51
+ }