@kodus/kodus-graph 0.2.7 → 0.2.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +252 -0
- package/dist/analysis/blast-radius.d.ts +2 -0
- package/dist/analysis/blast-radius.js +57 -0
- package/dist/analysis/communities.d.ts +28 -0
- package/dist/analysis/communities.js +100 -0
- package/dist/analysis/context-builder.d.ts +34 -0
- package/dist/analysis/context-builder.js +83 -0
- package/dist/analysis/diff.d.ts +35 -0
- package/dist/analysis/diff.js +140 -0
- package/dist/analysis/enrich.d.ts +5 -0
- package/dist/analysis/enrich.js +98 -0
- package/dist/analysis/flows.d.ts +27 -0
- package/dist/analysis/flows.js +86 -0
- package/dist/analysis/inheritance.d.ts +3 -0
- package/dist/analysis/inheritance.js +31 -0
- package/dist/analysis/prompt-formatter.d.ts +2 -0
- package/dist/analysis/prompt-formatter.js +166 -0
- package/dist/analysis/risk-score.d.ts +4 -0
- package/dist/analysis/risk-score.js +51 -0
- package/dist/analysis/search.d.ts +11 -0
- package/dist/analysis/search.js +64 -0
- package/dist/analysis/test-gaps.d.ts +2 -0
- package/dist/analysis/test-gaps.js +14 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +208 -0
- package/dist/commands/analyze.d.ts +9 -0
- package/dist/commands/analyze.js +114 -0
- package/dist/commands/communities.d.ts +8 -0
- package/dist/commands/communities.js +9 -0
- package/dist/commands/context.d.ts +12 -0
- package/dist/commands/context.js +130 -0
- package/dist/commands/diff.d.ts +9 -0
- package/dist/commands/diff.js +89 -0
- package/dist/commands/flows.d.ts +8 -0
- package/dist/commands/flows.js +9 -0
- package/dist/commands/parse.d.ts +10 -0
- package/dist/commands/parse.js +101 -0
- package/dist/commands/search.d.ts +12 -0
- package/dist/commands/search.js +27 -0
- package/dist/commands/update.d.ts +7 -0
- package/dist/commands/update.js +154 -0
- package/dist/graph/builder.d.ts +2 -0
- package/dist/graph/builder.js +216 -0
- package/dist/graph/edges.d.ts +19 -0
- package/dist/graph/edges.js +105 -0
- package/dist/graph/json-writer.d.ts +9 -0
- package/dist/graph/json-writer.js +38 -0
- package/dist/graph/loader.d.ts +13 -0
- package/dist/graph/loader.js +101 -0
- package/dist/graph/merger.d.ts +7 -0
- package/dist/graph/merger.js +18 -0
- package/dist/graph/types.d.ts +249 -0
- package/dist/graph/types.js +1 -0
- package/dist/parser/batch.d.ts +4 -0
- package/dist/parser/batch.js +78 -0
- package/dist/parser/discovery.d.ts +7 -0
- package/dist/parser/discovery.js +61 -0
- package/dist/parser/extractor.d.ts +4 -0
- package/dist/parser/extractor.js +33 -0
- package/dist/parser/extractors/generic.d.ts +8 -0
- package/dist/parser/extractors/generic.js +471 -0
- package/dist/parser/extractors/python.d.ts +8 -0
- package/dist/parser/extractors/python.js +133 -0
- package/dist/parser/extractors/ruby.d.ts +8 -0
- package/dist/parser/extractors/ruby.js +153 -0
- package/dist/parser/extractors/typescript.d.ts +10 -0
- package/dist/parser/extractors/typescript.js +365 -0
- package/dist/parser/languages.d.ts +32 -0
- package/dist/parser/languages.js +303 -0
- package/dist/resolver/call-resolver.d.ts +36 -0
- package/dist/resolver/call-resolver.js +178 -0
- package/dist/resolver/import-map.d.ts +12 -0
- package/dist/resolver/import-map.js +21 -0
- package/dist/resolver/import-resolver.d.ts +19 -0
- package/dist/resolver/import-resolver.js +212 -0
- package/dist/resolver/languages/csharp.d.ts +1 -0
- package/dist/resolver/languages/csharp.js +31 -0
- package/dist/resolver/languages/go.d.ts +3 -0
- package/dist/resolver/languages/go.js +196 -0
- package/dist/resolver/languages/java.d.ts +1 -0
- package/dist/resolver/languages/java.js +108 -0
- package/dist/resolver/languages/php.d.ts +3 -0
- package/dist/resolver/languages/php.js +54 -0
- package/dist/resolver/languages/python.d.ts +11 -0
- package/dist/resolver/languages/python.js +51 -0
- package/dist/resolver/languages/ruby.d.ts +9 -0
- package/dist/resolver/languages/ruby.js +59 -0
- package/dist/resolver/languages/rust.d.ts +1 -0
- package/dist/resolver/languages/rust.js +196 -0
- package/dist/resolver/languages/typescript.d.ts +27 -0
- package/dist/resolver/languages/typescript.js +240 -0
- package/dist/resolver/re-export-resolver.d.ts +24 -0
- package/dist/resolver/re-export-resolver.js +57 -0
- package/dist/resolver/symbol-table.d.ts +17 -0
- package/dist/resolver/symbol-table.js +60 -0
- package/dist/shared/extract-calls.d.ts +26 -0
- package/dist/shared/extract-calls.js +57 -0
- package/dist/shared/file-hash.d.ts +3 -0
- package/dist/shared/file-hash.js +10 -0
- package/dist/shared/filters.d.ts +3 -0
- package/dist/shared/filters.js +240 -0
- package/dist/shared/logger.d.ts +6 -0
- package/dist/shared/logger.js +17 -0
- package/dist/shared/qualified-name.d.ts +1 -0
- package/dist/shared/qualified-name.js +9 -0
- package/dist/shared/safe-path.d.ts +6 -0
- package/dist/shared/safe-path.js +29 -0
- package/dist/shared/schemas.d.ts +43 -0
- package/dist/shared/schemas.js +30 -0
- package/dist/shared/temp.d.ts +11 -0
- package/{src/shared/temp.ts → dist/shared/temp.js} +4 -5
- package/package.json +20 -6
- package/src/analysis/blast-radius.ts +0 -54
- package/src/analysis/communities.ts +0 -135
- package/src/analysis/context-builder.ts +0 -130
- package/src/analysis/diff.ts +0 -131
- package/src/analysis/enrich.ts +0 -110
- package/src/analysis/flows.ts +0 -112
- package/src/analysis/inheritance.ts +0 -34
- package/src/analysis/prompt-formatter.ts +0 -175
- package/src/analysis/risk-score.ts +0 -62
- package/src/analysis/search.ts +0 -76
- package/src/analysis/test-gaps.ts +0 -21
- package/src/cli.ts +0 -207
- package/src/commands/analyze.ts +0 -128
- package/src/commands/communities.ts +0 -19
- package/src/commands/context.ts +0 -139
- package/src/commands/diff.ts +0 -96
- package/src/commands/flows.ts +0 -19
- package/src/commands/parse.ts +0 -124
- package/src/commands/search.ts +0 -41
- package/src/commands/update.ts +0 -166
- package/src/graph/builder.ts +0 -209
- package/src/graph/edges.ts +0 -101
- package/src/graph/json-writer.ts +0 -43
- package/src/graph/loader.ts +0 -113
- package/src/graph/merger.ts +0 -25
- package/src/graph/types.ts +0 -283
- package/src/parser/batch.ts +0 -82
- package/src/parser/discovery.ts +0 -75
- package/src/parser/extractor.ts +0 -37
- package/src/parser/extractors/generic.ts +0 -132
- package/src/parser/extractors/python.ts +0 -133
- package/src/parser/extractors/ruby.ts +0 -147
- package/src/parser/extractors/typescript.ts +0 -350
- package/src/parser/languages.ts +0 -122
- package/src/resolver/call-resolver.ts +0 -244
- package/src/resolver/import-map.ts +0 -27
- package/src/resolver/import-resolver.ts +0 -72
- package/src/resolver/languages/csharp.ts +0 -7
- package/src/resolver/languages/go.ts +0 -7
- package/src/resolver/languages/java.ts +0 -7
- package/src/resolver/languages/php.ts +0 -7
- package/src/resolver/languages/python.ts +0 -35
- package/src/resolver/languages/ruby.ts +0 -21
- package/src/resolver/languages/rust.ts +0 -7
- package/src/resolver/languages/typescript.ts +0 -168
- package/src/resolver/re-export-resolver.ts +0 -66
- package/src/resolver/symbol-table.ts +0 -67
- package/src/shared/extract-calls.ts +0 -75
- package/src/shared/file-hash.ts +0 -12
- package/src/shared/filters.ts +0 -243
- package/src/shared/logger.ts +0 -14
- package/src/shared/qualified-name.ts +0 -5
- package/src/shared/safe-path.ts +0 -31
- 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,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,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
|
+
}
|