@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
@@ -0,0 +1,197 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join, resolve as resolvePath } from 'path';
3
+ import { cachedExists, cachedReaddir } from '../fs-cache';
4
+ const moduleCache = new Map();
5
+ const replaceCache = new Map();
6
+ const workspaceCache = new Map();
7
+ /** Clear cached go.mod data. Call between analysis runs or when switching repos. */
8
+ export function clearCache() {
9
+ moduleCache.clear();
10
+ replaceCache.clear();
11
+ workspaceCache.clear();
12
+ }
13
+ function getModuleName(repoRoot) {
14
+ const cached = moduleCache.get(repoRoot);
15
+ if (cached !== undefined) {
16
+ return cached || null;
17
+ }
18
+ const goModPath = join(repoRoot, 'go.mod');
19
+ if (!cachedExists(goModPath)) {
20
+ moduleCache.set(repoRoot, '');
21
+ return null;
22
+ }
23
+ try {
24
+ const content = readFileSync(goModPath, 'utf-8');
25
+ const match = content.match(/^module\s+(\S+)/m);
26
+ if (match) {
27
+ moduleCache.set(repoRoot, match[1]);
28
+ return match[1];
29
+ }
30
+ }
31
+ catch {
32
+ /* ignore */
33
+ }
34
+ moduleCache.set(repoRoot, '');
35
+ return null;
36
+ }
37
+ /** Parse replace directives from go.mod. Returns map: module path → local directory (absolute). */
38
+ function getReplaceMap(repoRoot) {
39
+ const cached = replaceCache.get(repoRoot);
40
+ if (cached) {
41
+ return cached;
42
+ }
43
+ const result = new Map();
44
+ const goModPath = join(repoRoot, 'go.mod');
45
+ if (!cachedExists(goModPath)) {
46
+ replaceCache.set(repoRoot, result);
47
+ return result;
48
+ }
49
+ try {
50
+ const content = readFileSync(goModPath, 'utf-8');
51
+ // Match single-line replace: replace mod => ./path or replace mod v1.2.3 => ./path
52
+ const replaceRe = /^replace\s+(\S+)(?:\s+\S+)?\s+=>\s+(\S+)/gm;
53
+ let m = replaceRe.exec(content);
54
+ while (m !== null) {
55
+ const modPath = m[1];
56
+ const replacement = m[2];
57
+ if (replacement.startsWith('./') || replacement.startsWith('../')) {
58
+ result.set(modPath, resolvePath(join(repoRoot, replacement)));
59
+ }
60
+ m = replaceRe.exec(content);
61
+ }
62
+ }
63
+ catch {
64
+ /* ignore */
65
+ }
66
+ replaceCache.set(repoRoot, result);
67
+ return result;
68
+ }
69
+ /** Parse go.work use directives. Returns map: module name → absolute directory of the module. */
70
+ function getWorkspaceModules(repoRoot) {
71
+ const cached = workspaceCache.get(repoRoot);
72
+ if (cached) {
73
+ return cached;
74
+ }
75
+ const result = new Map();
76
+ const goWorkPath = join(repoRoot, 'go.work');
77
+ if (!cachedExists(goWorkPath)) {
78
+ workspaceCache.set(repoRoot, result);
79
+ return result;
80
+ }
81
+ try {
82
+ const content = readFileSync(goWorkPath, 'utf-8');
83
+ // Parse use directives — both single-line and block form
84
+ // Block: use ( ./a \n ./b )
85
+ const blockRe = /use\s*\(([\s\S]*?)\)/g;
86
+ let blockMatch = blockRe.exec(content);
87
+ const useDirs = [];
88
+ while (blockMatch !== null) {
89
+ const inner = blockMatch[1];
90
+ for (const line of inner.split('\n')) {
91
+ const trimmed = line.trim();
92
+ if (trimmed && !trimmed.startsWith('//')) {
93
+ useDirs.push(trimmed);
94
+ }
95
+ }
96
+ blockMatch = blockRe.exec(content);
97
+ }
98
+ // Single-line: use ./foo
99
+ const singleRe = /^use\s+(\S+)\s*$/gm;
100
+ let singleMatch = singleRe.exec(content);
101
+ while (singleMatch !== null) {
102
+ const dir = singleMatch[1];
103
+ if (dir !== '(') {
104
+ useDirs.push(dir);
105
+ }
106
+ singleMatch = singleRe.exec(content);
107
+ }
108
+ // For each use directory, read its go.mod to get the module name
109
+ for (const dir of useDirs) {
110
+ const absDir = resolvePath(join(repoRoot, dir));
111
+ const modName = getModuleName(absDir);
112
+ if (modName) {
113
+ result.set(modName, absDir);
114
+ }
115
+ }
116
+ }
117
+ catch {
118
+ /* ignore */
119
+ }
120
+ workspaceCache.set(repoRoot, result);
121
+ return result;
122
+ }
123
+ function isStdlib(modulePath) {
124
+ const first = modulePath.split('/')[0];
125
+ return !first.includes('.');
126
+ }
127
+ /** Find the first .go file (non-test) in a directory, or check for a .go file at the path. */
128
+ function findGoFile(absDir) {
129
+ if (cachedExists(absDir)) {
130
+ try {
131
+ const files = cachedReaddir(absDir).sort();
132
+ const goFile = files.find((f) => f.endsWith('.go') && !f.endsWith('_test.go'));
133
+ if (goFile) {
134
+ return resolvePath(join(absDir, goFile));
135
+ }
136
+ }
137
+ catch {
138
+ /* not a directory */
139
+ }
140
+ }
141
+ if (cachedExists(`${absDir}.go`)) {
142
+ return resolvePath(`${absDir}.go`);
143
+ }
144
+ return null;
145
+ }
146
+ export function resolve(_fromAbsFile, modulePath, repoRoot) {
147
+ if (isStdlib(modulePath)) {
148
+ return null;
149
+ }
150
+ // 1. Try resolving against the root module name
151
+ const moduleName = getModuleName(repoRoot);
152
+ if (moduleName && modulePath.startsWith(moduleName)) {
153
+ const relPath = modulePath.slice(moduleName.length + 1);
154
+ if (relPath) {
155
+ const result = findGoFile(join(repoRoot, relPath));
156
+ if (result) {
157
+ return result;
158
+ }
159
+ }
160
+ }
161
+ // 2. Try replace directives from go.mod
162
+ const replaces = getReplaceMap(repoRoot);
163
+ for (const [modPrefix, localDir] of replaces) {
164
+ if (modulePath.startsWith(modPrefix)) {
165
+ const suffix = modulePath.slice(modPrefix.length);
166
+ // suffix is either empty or starts with '/'
167
+ const relPath = suffix.startsWith('/') ? suffix.slice(1) : suffix;
168
+ if (relPath) {
169
+ const result = findGoFile(join(localDir, relPath));
170
+ if (result) {
171
+ return result;
172
+ }
173
+ }
174
+ }
175
+ }
176
+ // 3. Try go.work workspace modules
177
+ const workspaceModules = getWorkspaceModules(repoRoot);
178
+ for (const [wsModName, wsModDir] of workspaceModules) {
179
+ if (modulePath.startsWith(wsModName)) {
180
+ const suffix = modulePath.slice(wsModName.length);
181
+ const relPath = suffix.startsWith('/') ? suffix.slice(1) : suffix;
182
+ if (relPath) {
183
+ const result = findGoFile(join(wsModDir, relPath));
184
+ if (result) {
185
+ return result;
186
+ }
187
+ }
188
+ }
189
+ }
190
+ // 4. Try vendor directory
191
+ const vendorDir = join(repoRoot, 'vendor', modulePath);
192
+ const vendorResult = findGoFile(vendorDir);
193
+ if (vendorResult) {
194
+ return vendorResult;
195
+ }
196
+ return null;
197
+ }
@@ -0,0 +1 @@
1
+ export declare function resolve(_fromAbsFile: string, modulePath: string, repoRoot: string): string | null;
@@ -0,0 +1,193 @@
1
+ import { readFileSync } from 'fs';
2
+ import { join, resolve as resolvePath } from 'path';
3
+ import { cachedExists, cachedReaddir } from '../fs-cache';
4
+ const STDLIB_PREFIXES = ['java.', 'javax.', 'sun.', 'com.sun.', 'jdk.'];
5
+ const SOURCE_ROOTS = ['src/main/java', 'src/main/kotlin', 'src', ''];
6
+ const EXTENSIONS = ['.java', '.kt'];
7
+ /**
8
+ * Collect all source roots, including those inside Gradle subproject directories.
9
+ */
10
+ function collectSourceRoots(repoRoot) {
11
+ const roots = [...SOURCE_ROOTS];
12
+ // Discover Gradle subprojects from settings.gradle / settings.gradle.kts
13
+ for (const settingsFile of ['settings.gradle', 'settings.gradle.kts']) {
14
+ const settingsPath = join(repoRoot, settingsFile);
15
+ if (!cachedExists(settingsPath)) {
16
+ continue;
17
+ }
18
+ const content = readFileSync(settingsPath, 'utf-8');
19
+ // Match patterns like ':app', ':lib', ':core:domain'
20
+ const projectRegex = /['"]:([\w:/-]+)['"]/g;
21
+ let match = projectRegex.exec(content);
22
+ while (match !== null) {
23
+ const subDir = match[1].replace(/:/g, '/');
24
+ for (const srcRoot of SOURCE_ROOTS) {
25
+ if (srcRoot) {
26
+ roots.push(join(subDir, srcRoot));
27
+ }
28
+ }
29
+ match = projectRegex.exec(content);
30
+ }
31
+ break; // only read first settings file found
32
+ }
33
+ // Discover custom sourceSets from build.gradle / build.gradle.kts in subprojects
34
+ const gradleFiles = [];
35
+ // Collect build.gradle files: root + discovered subproject dirs
36
+ for (const buildFile of ['build.gradle', 'build.gradle.kts']) {
37
+ if (cachedExists(join(repoRoot, buildFile))) {
38
+ gradleFiles.push({ dir: '', file: buildFile });
39
+ }
40
+ }
41
+ // Also check subproject directories already discovered above
42
+ const subDirs = new Set();
43
+ for (const r of roots) {
44
+ // Extract the subproject directory (everything before src/...)
45
+ const srcIdx = r.indexOf('/src');
46
+ if (srcIdx > 0) {
47
+ subDirs.add(r.slice(0, srcIdx));
48
+ }
49
+ }
50
+ for (const sub of subDirs) {
51
+ for (const buildFile of ['build.gradle', 'build.gradle.kts']) {
52
+ const buildPath = join(repoRoot, sub, buildFile);
53
+ if (cachedExists(buildPath)) {
54
+ gradleFiles.push({ dir: sub, file: join(sub, buildFile) });
55
+ }
56
+ }
57
+ }
58
+ for (const { dir, file } of gradleFiles) {
59
+ try {
60
+ const content = readFileSync(join(repoRoot, file), 'utf-8');
61
+ // Match: srcDirs = ['path1', 'path2']
62
+ const srcDirsArrayRegex = /srcDirs\s*=\s*\[([^\]]+)\]/g;
63
+ let sdMatch = srcDirsArrayRegex.exec(content);
64
+ while (sdMatch !== null) {
65
+ const entries = sdMatch[1];
66
+ const pathRegex = /['"]([^'"]+)['"]/g;
67
+ let pathMatch = pathRegex.exec(entries);
68
+ while (pathMatch !== null) {
69
+ const srcDir = pathMatch[1];
70
+ const root = dir ? join(dir, srcDir) : srcDir;
71
+ if (!roots.includes(root)) {
72
+ roots.push(root);
73
+ }
74
+ pathMatch = pathRegex.exec(entries);
75
+ }
76
+ sdMatch = srcDirsArrayRegex.exec(content);
77
+ }
78
+ // Match: srcDir 'path' or srcDir "path"
79
+ const srcDirSingleRegex = /srcDir\s+['"]([^'"]+)['"]/g;
80
+ let singleMatch = srcDirSingleRegex.exec(content);
81
+ while (singleMatch !== null) {
82
+ const srcDir = singleMatch[1];
83
+ const root = dir ? join(dir, srcDir) : srcDir;
84
+ if (!roots.includes(root)) {
85
+ roots.push(root);
86
+ }
87
+ singleMatch = srcDirSingleRegex.exec(content);
88
+ }
89
+ }
90
+ catch {
91
+ // build.gradle read failed, continue
92
+ }
93
+ }
94
+ // Discover Maven subprojects from pom.xml (recursive)
95
+ discoverMavenModules(repoRoot, '', roots, 0);
96
+ return roots;
97
+ }
98
+ /**
99
+ * Try to find a file at the given relative path (without extension) across all
100
+ * source roots, probing each supported extension.
101
+ */
102
+ function findFile(repoRoot, relPathNoExt, sourceRoots) {
103
+ for (const srcRoot of sourceRoots) {
104
+ for (const ext of EXTENSIONS) {
105
+ const candidate = join(repoRoot, srcRoot, relPathNoExt + ext);
106
+ if (cachedExists(candidate)) {
107
+ return resolvePath(candidate);
108
+ }
109
+ }
110
+ }
111
+ return null;
112
+ }
113
+ const MAX_MAVEN_DEPTH = 5;
114
+ /**
115
+ * Recursively discover Maven modules from pom.xml files.
116
+ * Each module's pom.xml may declare its own <module> elements,
117
+ * forming a tree (e.g. Keycloak: root → services → sub-service).
118
+ */
119
+ function discoverMavenModules(repoRoot, relDir, roots, depth) {
120
+ if (depth > MAX_MAVEN_DEPTH) {
121
+ return;
122
+ }
123
+ const pomPath = join(repoRoot, relDir, 'pom.xml');
124
+ if (!cachedExists(pomPath)) {
125
+ return;
126
+ }
127
+ try {
128
+ const content = readFileSync(pomPath, 'utf-8');
129
+ const moduleRegex = /<module>([^<]+)<\/module>/g;
130
+ let mvnMatch = moduleRegex.exec(content);
131
+ while (mvnMatch !== null) {
132
+ const moduleName = mvnMatch[1];
133
+ const moduleDir = relDir ? join(relDir, moduleName) : moduleName;
134
+ // Add source roots for this module
135
+ const javaRoot = join(moduleDir, 'src/main/java');
136
+ if (!roots.includes(javaRoot)) {
137
+ roots.push(javaRoot);
138
+ }
139
+ const kotlinRoot = join(moduleDir, 'src/main/kotlin');
140
+ if (!roots.includes(kotlinRoot)) {
141
+ roots.push(kotlinRoot);
142
+ }
143
+ // Recurse into the module's own pom.xml
144
+ discoverMavenModules(repoRoot, moduleDir, roots, depth + 1);
145
+ mvnMatch = moduleRegex.exec(content);
146
+ }
147
+ }
148
+ catch {
149
+ // pom.xml read failed, continue
150
+ }
151
+ }
152
+ export function resolve(_fromAbsFile, modulePath, repoRoot) {
153
+ if (STDLIB_PREFIXES.some((p) => modulePath.startsWith(p))) {
154
+ return null;
155
+ }
156
+ const sourceRoots = collectSourceRoots(repoRoot);
157
+ // --- Wildcard imports: com.example.models.* ---
158
+ if (modulePath.endsWith('.*')) {
159
+ const packagePath = modulePath.slice(0, -2).replace(/\./g, '/');
160
+ for (const srcRoot of sourceRoots) {
161
+ const dirPath = join(repoRoot, srcRoot, packagePath);
162
+ if (cachedExists(dirPath)) {
163
+ try {
164
+ const files = cachedReaddir(dirPath).filter((f) => EXTENSIONS.some((ext) => f.endsWith(ext)));
165
+ if (files.length > 0) {
166
+ return resolvePath(join(dirPath, files[0]));
167
+ }
168
+ }
169
+ catch {
170
+ // directory read failed, try next root
171
+ }
172
+ }
173
+ }
174
+ return null;
175
+ }
176
+ // --- Standard resolution: try full path with all extensions ---
177
+ const relPathNoExt = modulePath.replace(/\./g, '/');
178
+ const direct = findFile(repoRoot, relPathNoExt, sourceRoots);
179
+ if (direct) {
180
+ return direct;
181
+ }
182
+ // --- Inner class fallback: progressively shorten the path ---
183
+ // com.example.Config.DatabaseSettings → try com/example/Config
184
+ const segments = modulePath.split('.');
185
+ for (let i = segments.length - 1; i >= 2; i--) {
186
+ const shorter = segments.slice(0, i).join('/');
187
+ const found = findFile(repoRoot, shorter, sourceRoots);
188
+ if (found) {
189
+ return found;
190
+ }
191
+ }
192
+ return null;
193
+ }
@@ -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,75 @@
1
+ import { readFileSync } from 'fs';
2
+ import { dirname, join, resolve as resolvePath } from 'path';
3
+ import { cachedExists } from '../fs-cache';
4
+ const psr4Cache = new Map();
5
+ /** Clear cached composer.json PSR-4 data. Call between analysis runs or when switching repos. */
6
+ export function clearCache() {
7
+ psr4Cache.clear();
8
+ }
9
+ /** Check if the modulePath looks like a file path (require/include style) rather than a namespace. */
10
+ function looksLikeFilePath(modulePath) {
11
+ return modulePath.includes('/') || modulePath.endsWith('.php');
12
+ }
13
+ function loadPsr4(repoRoot) {
14
+ const cached = psr4Cache.get(repoRoot);
15
+ if (cached) {
16
+ return cached;
17
+ }
18
+ const map = new Map();
19
+ const composerPath = join(repoRoot, 'composer.json');
20
+ if (cachedExists(composerPath)) {
21
+ try {
22
+ const content = readFileSync(composerPath, 'utf-8');
23
+ const config = JSON.parse(content);
24
+ const psr4 = config?.autoload?.['psr-4'];
25
+ if (psr4) {
26
+ for (const [prefix, dir] of Object.entries(psr4)) {
27
+ const dirStr = Array.isArray(dir) ? dir[0] : dir;
28
+ map.set(prefix, dirStr);
29
+ }
30
+ }
31
+ }
32
+ catch {
33
+ /* ignore */
34
+ }
35
+ }
36
+ psr4Cache.set(repoRoot, map);
37
+ return map;
38
+ }
39
+ export function resolve(_fromAbsFile, modulePath, repoRoot) {
40
+ // Handle require/include style file paths before PSR-4 resolution
41
+ if (looksLikeFilePath(modulePath)) {
42
+ // Try resolving relative to the importing file's directory
43
+ if (_fromAbsFile) {
44
+ const fromDir = dirname(_fromAbsFile);
45
+ const candidate = resolvePath(join(fromDir, modulePath));
46
+ if (cachedExists(candidate)) {
47
+ return candidate;
48
+ }
49
+ }
50
+ // Try resolving relative to repo root
51
+ const rootCandidate = resolvePath(join(repoRoot, modulePath));
52
+ if (cachedExists(rootCandidate)) {
53
+ return rootCandidate;
54
+ }
55
+ }
56
+ const psr4 = loadPsr4(repoRoot);
57
+ for (const [prefix, dir] of psr4) {
58
+ if (modulePath.startsWith(prefix)) {
59
+ const rest = modulePath.slice(prefix.length);
60
+ const relPath = `${rest.replace(/\\/g, '/')}.php`;
61
+ const candidate = join(repoRoot, dir, relPath);
62
+ if (cachedExists(candidate)) {
63
+ return resolvePath(candidate);
64
+ }
65
+ }
66
+ }
67
+ const relPath = `${modulePath.replace(/\\/g, '/')}.php`;
68
+ for (const base of ['', 'src', 'lib', 'app']) {
69
+ const candidate = join(repoRoot, base, relPath);
70
+ if (cachedExists(candidate)) {
71
+ return resolvePath(candidate);
72
+ }
73
+ }
74
+ return null;
75
+ }
@@ -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,127 @@
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 { readFileSync } from 'fs';
8
+ import { dirname, join, resolve as resolvePath } from 'path';
9
+ import { cachedExists } from '../fs-cache';
10
+ /**
11
+ * Resolve a Python dotted import to a file path.
12
+ * Walks up from the importing file's directory to find the module.
13
+ */
14
+ export function resolve(fromAbsFile, modulePath, _repoRoot) {
15
+ if (!modulePath) {
16
+ return null;
17
+ }
18
+ if (modulePath.startsWith('.')) {
19
+ // Relative import: count leading dots, walk up directories
20
+ const dotMatch = modulePath.match(/^(\.+)/);
21
+ const dots = dotMatch[1].length;
22
+ const rest = modulePath.slice(dots).replace(/\./g, '/');
23
+ let base = dirname(fromAbsFile);
24
+ for (let d = 1; d < dots; d++) {
25
+ base = dirname(base);
26
+ }
27
+ const candidates = rest ? [`${rest}.py`, `${rest}/__init__.py`] : [`__init__.py`];
28
+ for (const candidate of candidates) {
29
+ const full = join(base, candidate);
30
+ if (cachedExists(full)) {
31
+ return resolvePath(full);
32
+ }
33
+ }
34
+ return null;
35
+ }
36
+ const parts = modulePath.replace(/\./g, '/');
37
+ let current = dirname(fromAbsFile);
38
+ for (let i = 0; i < 10; i++) {
39
+ for (const candidate of [`${parts}.py`, `${parts}/__init__.py`]) {
40
+ const full = join(current, candidate);
41
+ if (cachedExists(full)) {
42
+ return resolvePath(full);
43
+ }
44
+ }
45
+ const parent = dirname(current);
46
+ if (parent === current) {
47
+ break;
48
+ }
49
+ current = parent;
50
+ }
51
+ // Fallback: check setup.cfg for package_dir directive (e.g., package_dir = = src)
52
+ const setupCfgResult = resolveViaSetupCfg(parts, _repoRoot);
53
+ if (setupCfgResult) {
54
+ return setupCfgResult;
55
+ }
56
+ // Fallback: check pyproject.toml for package-dir (e.g., [tool.setuptools.package-dir] "" = "src")
57
+ const pyprojectResult = resolveViaPyprojectPackageDir(parts, _repoRoot);
58
+ if (pyprojectResult) {
59
+ return pyprojectResult;
60
+ }
61
+ return null;
62
+ }
63
+ /**
64
+ * Try resolving via setup.cfg package_dir directive.
65
+ * Looks for patterns like:
66
+ * [options]
67
+ * package_dir =
68
+ * = src
69
+ * which means the root package directory is "src/".
70
+ */
71
+ function resolveViaSetupCfg(relPath, repoRoot) {
72
+ const setupCfgPath = join(repoRoot, 'setup.cfg');
73
+ if (!cachedExists(setupCfgPath)) {
74
+ return null;
75
+ }
76
+ try {
77
+ const content = readFileSync(setupCfgPath, 'utf-8');
78
+ // Look for package_dir under [options]
79
+ // Common patterns:
80
+ // package_dir =
81
+ // = src
82
+ // package_dir = = src
83
+ const packageDirRegex = /package_dir\s*=\s*(?:\n\s+)?=\s*(\S+)/;
84
+ const match = packageDirRegex.exec(content);
85
+ if (match) {
86
+ const srcDir = match[1];
87
+ for (const candidate of [`${relPath}.py`, `${relPath}/__init__.py`]) {
88
+ const full = join(repoRoot, srcDir, candidate);
89
+ if (cachedExists(full)) {
90
+ return resolvePath(full);
91
+ }
92
+ }
93
+ }
94
+ }
95
+ catch {
96
+ // setup.cfg read failed, continue
97
+ }
98
+ return null;
99
+ }
100
+ /**
101
+ * Try resolving via pyproject.toml [tool.setuptools.package-dir] directive.
102
+ */
103
+ function resolveViaPyprojectPackageDir(relPath, repoRoot) {
104
+ const pyprojectPath = join(repoRoot, 'pyproject.toml');
105
+ if (!cachedExists(pyprojectPath)) {
106
+ return null;
107
+ }
108
+ try {
109
+ const content = readFileSync(pyprojectPath, 'utf-8');
110
+ // Look for [tool.setuptools.package-dir] section with "" = "src" or similar
111
+ const packageDirRegex = /\[tool\.setuptools\.package-dir\]\s*\n\s*""\s*=\s*"(\S+)"/;
112
+ const match = packageDirRegex.exec(content);
113
+ if (match) {
114
+ const srcDir = match[1];
115
+ for (const candidate of [`${relPath}.py`, `${relPath}/__init__.py`]) {
116
+ const full = join(repoRoot, srcDir, candidate);
117
+ if (cachedExists(full)) {
118
+ return resolvePath(full);
119
+ }
120
+ }
121
+ }
122
+ }
123
+ catch {
124
+ // pyproject.toml read failed, continue
125
+ }
126
+ return null;
127
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Ruby import resolver.
3
+ *
4
+ * Handles require_relative paths, Gemfile path: gems, and Zeitwerk autoload.
5
+ */
6
+ /**
7
+ * Resolve a Ruby require/require_relative to a file path.
8
+ */
9
+ export declare function resolve(fromAbsFile: string, modulePath: string, repoRoot: string): string | null;
10
+ /**
11
+ * Resolve a Ruby class/module constant name to a file path using Zeitwerk conventions.
12
+ *
13
+ * Zeitwerk maps constant names to file paths:
14
+ * - `User` → `user.rb`
15
+ * - `AuthService` → `auth_service.rb`
16
+ * - `Admin::UsersController` → `admin/users_controller.rb`
17
+ *
18
+ * This searches common Rails autoload paths for a matching file.
19
+ *
20
+ * @param className - The fully-qualified constant name (e.g., "Admin::UsersController")
21
+ * @param repoRoot - The root of the repository / Rails project
22
+ * @returns Absolute path to the resolved file, or null if not found
23
+ */
24
+ export declare function resolveZeitwerk(className: string, repoRoot: string): string | null;