pkgviz 0.7.2 → 0.7.4
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/.next/BUILD_ID +1 -1
- package/.next/app-path-routes-manifest.json +1 -1
- package/.next/build-manifest.json +2 -2
- package/.next/cache/webpack/client-production/10.pack +0 -0
- package/.next/cache/webpack/client-production/7.pack +0 -0
- package/.next/cache/webpack/client-production/8.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack +0 -0
- package/.next/cache/webpack/client-production/index.pack.old +0 -0
- package/.next/cache/webpack/server-production/11.pack +0 -0
- package/.next/cache/webpack/server-production/12.pack +0 -0
- package/.next/cache/webpack/server-production/13.pack +0 -0
- package/.next/cache/webpack/server-production/14.pack +0 -0
- package/.next/cache/webpack/server-production/15.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack +0 -0
- package/.next/cache/webpack/server-production/index.pack.old +0 -0
- package/.next/server/app/_not-found.html +1 -1
- package/.next/server/app/_not-found.rsc +1 -1
- package/.next/server/app/favicon.ico/route.js +1 -1
- package/.next/server/app/index.html +1 -1
- package/.next/server/app/index.rsc +1 -1
- package/.next/server/app-paths-manifest.json +1 -1
- package/.next/server/chunks/610.js +1 -1
- package/.next/server/pages/404.html +1 -1
- package/.next/server/pages/500.html +1 -1
- package/.next/trace +2 -2
- package/package.json +3 -3
- package/src/app/actions/graph.actions.ts +25 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +77 -0
- package/src/app/layout.tsx +30 -0
- package/src/app/page.tsx +5 -0
- package/src/app/utils/buildGraph.ts +119 -0
- package/src/app/utils/getParsedFileStructure.ts +225 -0
- package/src/app/utils/markCyclicPackages.ts +275 -0
- package/src/app/utils/parser/cpp/extractCppPackageFromImport.ts +18 -0
- package/src/app/utils/parser/cpp/parseCppFile.ts +150 -0
- package/src/app/utils/parser/delphi/extractPackageFromImport.ts +21 -0
- package/src/app/utils/parser/delphi/parseFile.ts +179 -0
- package/src/app/utils/parser/java/extractJavaPackageFromImport.ts +39 -0
- package/src/app/utils/parser/java/findEntryPoint.ts +24 -0
- package/src/app/utils/parser/java/getIntrinsicPackagesRecursive.ts +33 -0
- package/src/app/utils/parser/java/parseJavaFile.ts +114 -0
- package/src/app/utils/parser/kotlin/extractPackageFromImport.ts +19 -0
- package/src/app/utils/parser/kotlin/parseFile.ts +147 -0
- package/src/app/utils/parser/python/extractPythonPackageFromImport.ts +18 -0
- package/src/app/utils/parser/python/parseFile.ts +171 -0
- package/src/app/utils/parser/typescript/extractTypeScriptPackageFromImport.ts +18 -0
- package/src/app/utils/parser/typescript/parseFile.ts +130 -0
- package/src/components/Breadcrumb.tsx +34 -0
- package/src/components/Cytoscape.tsx +23 -0
- package/src/components/Header.tsx +28 -0
- package/src/components/Loader.tsx +10 -0
- package/src/components/Setting.tsx +17 -0
- package/src/components/Settings.tsx +189 -0
- package/src/components/Switch.tsx +31 -0
- package/src/components/ThemeToggle.tsx +25 -0
- package/src/components/ZoomInput.tsx +94 -0
- package/src/components/useCytoscape.ts +343 -0
- package/src/contexts/SettingsContext.tsx +88 -0
- package/src/i18n/en.ts +27 -0
- package/src/i18n/i18n.ts +12 -0
- package/src/layouts/breadthfirst/layout.ts +30 -0
- package/src/layouts/breadthfirst/style.ts +8 -0
- package/src/layouts/circle/layout.ts +11 -0
- package/src/layouts/circle/style.ts +18 -0
- package/src/layouts/concentric/layout.ts +10 -0
- package/src/layouts/concentric/style.ts +16 -0
- package/src/layouts/constants.ts +17 -0
- package/src/layouts/elk/layout.ts +55 -0
- package/src/layouts/elk/style.ts +14 -0
- package/src/layouts/getLayoutStyle.ts +19 -0
- package/src/layouts/getWeightBuckets.ts +58 -0
- package/src/layouts/grid/layout.ts +11 -0
- package/src/layouts/grid/style.ts +20 -0
- package/src/layouts/index.ts +14 -0
- package/src/layouts/style.ts +191 -0
- package/src/screens/home/Home.tsx +48 -0
- package/src/shared/constants/index.ts +7 -0
- package/src/shared/types/index.ts +68 -0
- package/src/shared/utils/detectLanguage.ts +255 -0
- package/src/shared/utils/getJsonAsync.ts +13 -0
- package/src/shared/utils/getProjectName.ts +3 -0
- package/src/shared/utils/parseEnv.ts +91 -0
- package/src/shared/utils/parseProjectPath.ts +8 -0
- package/src/store/useLocalStorage.ts +29 -0
- package/src/utils/filter/filterByPackagePrefix.ts +23 -0
- package/src/utils/filter/filterEmptyPackages.ts +36 -0
- package/src/utils/filter/filterSubPackagesFromDepth.ts +170 -0
- package/src/utils/filter/filterVendorPackages.ts +17 -0
- package/src/utils/filter/toggleCompoundNodes.ts +40 -0
- package/src/utils/hasChildren.ts +7 -0
- package/tsconfig.json +29 -0
- /package/.next/static/{VVBfFRai9p1x3oVXujjYO → F9v-zmBCEef1qcQb8JFxR}/_buildManifest.js +0 -0
- /package/.next/static/{VVBfFRai9p1x3oVXujjYO → F9v-zmBCEef1qcQb8JFxR}/_ssgManifest.js +0 -0
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
import { Language, type ParsedDirectory } from '@/shared/types';
|
|
3
|
+
|
|
4
|
+
import { existsSync, readdirSync } from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { toPosix } from '@/shared/utils/toPosix';
|
|
7
|
+
import { detectLanguage } from '@/shared/utils/detectLanguage';
|
|
8
|
+
import { parseJavaFile } from '@/app/utils/parser/java/parseJavaFile';
|
|
9
|
+
import { parseFile as parseTypeScriptFile } from '@/app/utils/parser/typescript/parseFile';
|
|
10
|
+
import { parseCppFile } from '@/app/utils/parser/cpp/parseCppFile';
|
|
11
|
+
import { parsePythonFile } from '@/app/utils/parser/python/parseFile';
|
|
12
|
+
import { parseDelphiFile } from '@/app/utils/parser/delphi/parseFile';
|
|
13
|
+
import { parseKotlinFile } from '@/app/utils/parser/kotlin/parseFile';
|
|
14
|
+
import { parseProjectPath } from '@/shared/utils/parseProjectPath';
|
|
15
|
+
import { JAVA_ROOT } from '@/shared/constants';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Returns resolved root
|
|
19
|
+
*/
|
|
20
|
+
export async function resolveRoot(dir: string, detectedLanguage: Language) {
|
|
21
|
+
switch (detectedLanguage) {
|
|
22
|
+
case Language.Java: {
|
|
23
|
+
const javaRoot = toPosix(path.resolve(dir, JAVA_ROOT));
|
|
24
|
+
if (!existsSync(javaRoot)) {
|
|
25
|
+
console.error('Failed to find:', JAVA_ROOT);
|
|
26
|
+
throw new Error(`Invalid Java project structure. Missing ${JAVA_ROOT}`);
|
|
27
|
+
}
|
|
28
|
+
return javaRoot;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
case Language.TypeScript:
|
|
32
|
+
// Normalize to an absolute project root
|
|
33
|
+
return toPosix(path.resolve(dir));
|
|
34
|
+
|
|
35
|
+
case Language.Cpp:
|
|
36
|
+
// For C++, look for src directory or use project root
|
|
37
|
+
const cppSrcRoot = toPosix(path.resolve(dir, 'src'));
|
|
38
|
+
if (existsSync(cppSrcRoot)) {
|
|
39
|
+
return cppSrcRoot;
|
|
40
|
+
}
|
|
41
|
+
return toPosix(path.resolve(dir));
|
|
42
|
+
|
|
43
|
+
case Language.Python:
|
|
44
|
+
// For Python, look for src directory or use project root
|
|
45
|
+
const pythonSrcRoot = toPosix(path.resolve(dir, 'src'));
|
|
46
|
+
if (existsSync(pythonSrcRoot)) {
|
|
47
|
+
return pythonSrcRoot;
|
|
48
|
+
}
|
|
49
|
+
// Also check for common Python app structure
|
|
50
|
+
const appRoot = toPosix(path.resolve(dir, 'app'));
|
|
51
|
+
if (existsSync(appRoot)) {
|
|
52
|
+
return appRoot;
|
|
53
|
+
}
|
|
54
|
+
return toPosix(path.resolve(dir));
|
|
55
|
+
|
|
56
|
+
case Language.Delphi:
|
|
57
|
+
// For Delphi, look for common source directories
|
|
58
|
+
const delphiSrcRoot = toPosix(path.resolve(dir, 'src'));
|
|
59
|
+
if (existsSync(delphiSrcRoot)) {
|
|
60
|
+
return delphiSrcRoot;
|
|
61
|
+
}
|
|
62
|
+
// Also check for Source directory (common in Delphi projects)
|
|
63
|
+
const sourceRoot = toPosix(path.resolve(dir, 'Source'));
|
|
64
|
+
if (existsSync(sourceRoot)) {
|
|
65
|
+
return sourceRoot;
|
|
66
|
+
}
|
|
67
|
+
return toPosix(path.resolve(dir));
|
|
68
|
+
|
|
69
|
+
case Language.Kotlin:
|
|
70
|
+
// For Kotlin, look for src/main/kotlin directory (Gradle/Maven structure)
|
|
71
|
+
const kotlinSrcRoot = toPosix(path.resolve(dir, 'src/main/kotlin'));
|
|
72
|
+
if (existsSync(kotlinSrcRoot)) {
|
|
73
|
+
return kotlinSrcRoot;
|
|
74
|
+
}
|
|
75
|
+
// Fallback to src directory
|
|
76
|
+
const kotlinAltSrcRoot = toPosix(path.resolve(dir, 'src'));
|
|
77
|
+
if (existsSync(kotlinAltSrcRoot)) {
|
|
78
|
+
return kotlinAltSrcRoot;
|
|
79
|
+
}
|
|
80
|
+
return toPosix(path.resolve(dir));
|
|
81
|
+
|
|
82
|
+
default:
|
|
83
|
+
throw new Error(`Invalid file structure for ${detectedLanguage}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Read directory recursively
|
|
89
|
+
*/
|
|
90
|
+
export async function readDirRecursively(
|
|
91
|
+
dir: string,
|
|
92
|
+
result: ParsedDirectory = {},
|
|
93
|
+
projectRoot: string,
|
|
94
|
+
language: Language
|
|
95
|
+
): Promise<ParsedDirectory> {
|
|
96
|
+
const resolvedRoot = path.resolve(projectRoot);
|
|
97
|
+
const resolvedDir = path.resolve(dir);
|
|
98
|
+
|
|
99
|
+
// Validate dir is inside projectRoot (avoid path traversal / accidental escapes)
|
|
100
|
+
const relative = path.relative(resolvedRoot, resolvedDir);
|
|
101
|
+
if (relative.startsWith('..') || path.isAbsolute(relative)) {
|
|
102
|
+
throw new Error(`Path traversal detected: ${dir} is outside of project root ${projectRoot}`);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// 1. Read current directory
|
|
106
|
+
const entries = readdirSync(resolvedDir, { withFileTypes: true });
|
|
107
|
+
|
|
108
|
+
const ignores = [
|
|
109
|
+
'@types',
|
|
110
|
+
'.cache',
|
|
111
|
+
'.git',
|
|
112
|
+
'.github',
|
|
113
|
+
'.meta',
|
|
114
|
+
'.next',
|
|
115
|
+
'.vscode',
|
|
116
|
+
'coverage',
|
|
117
|
+
'examples',
|
|
118
|
+
'node_modules',
|
|
119
|
+
'packages',
|
|
120
|
+
'test',
|
|
121
|
+
];
|
|
122
|
+
|
|
123
|
+
for (const entry of entries) {
|
|
124
|
+
if (ignores.includes(entry.name)) continue;
|
|
125
|
+
|
|
126
|
+
const fullPath = path.resolve(resolvedDir, entry.name);
|
|
127
|
+
|
|
128
|
+
// Directory: Recursively continue to read
|
|
129
|
+
if (entry.isDirectory()) {
|
|
130
|
+
result[entry.name] = await readDirRecursively(fullPath, {}, projectRoot, language);
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// File: Parse file according to detected project language
|
|
135
|
+
switch (language) {
|
|
136
|
+
// Java
|
|
137
|
+
case Language.Java:
|
|
138
|
+
if (entry.name.endsWith('.java')) {
|
|
139
|
+
result[entry.name] = await parseJavaFile(fullPath, projectRoot);
|
|
140
|
+
}
|
|
141
|
+
break;
|
|
142
|
+
|
|
143
|
+
// TypeScript
|
|
144
|
+
case Language.TypeScript:
|
|
145
|
+
if (entry.name.endsWith('.ts') || entry.name.endsWith('.tsx')) {
|
|
146
|
+
result[entry.name] = await parseTypeScriptFile(fullPath, projectRoot);
|
|
147
|
+
}
|
|
148
|
+
break;
|
|
149
|
+
|
|
150
|
+
// C++
|
|
151
|
+
case Language.Cpp:
|
|
152
|
+
if (
|
|
153
|
+
entry.name.endsWith('.cpp') ||
|
|
154
|
+
entry.name.endsWith('.cc') ||
|
|
155
|
+
entry.name.endsWith('.cxx') ||
|
|
156
|
+
entry.name.endsWith('.h') ||
|
|
157
|
+
entry.name.endsWith('.hpp') ||
|
|
158
|
+
entry.name.endsWith('.hxx')
|
|
159
|
+
) {
|
|
160
|
+
result[entry.name] = await parseCppFile(fullPath, projectRoot);
|
|
161
|
+
}
|
|
162
|
+
break;
|
|
163
|
+
|
|
164
|
+
// Python
|
|
165
|
+
case Language.Python:
|
|
166
|
+
if (entry.name.endsWith('.py')) {
|
|
167
|
+
result[entry.name] = await parsePythonFile(fullPath, projectRoot);
|
|
168
|
+
}
|
|
169
|
+
break;
|
|
170
|
+
|
|
171
|
+
// Delphi
|
|
172
|
+
case Language.Delphi:
|
|
173
|
+
if (
|
|
174
|
+
entry.name.endsWith('.pas') ||
|
|
175
|
+
entry.name.endsWith('.pp') ||
|
|
176
|
+
entry.name.endsWith('.dpr')
|
|
177
|
+
) {
|
|
178
|
+
result[entry.name] = await parseDelphiFile(fullPath, projectRoot);
|
|
179
|
+
}
|
|
180
|
+
break;
|
|
181
|
+
|
|
182
|
+
// Kotlin
|
|
183
|
+
case Language.Kotlin:
|
|
184
|
+
if (entry.name.endsWith('.kt') || entry.name.endsWith('.kts')) {
|
|
185
|
+
result[entry.name] = await parseKotlinFile(fullPath, projectRoot);
|
|
186
|
+
}
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return result;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Entrypoint
|
|
196
|
+
*/
|
|
197
|
+
export async function getParsedFileStructure() {
|
|
198
|
+
const projectPath = parseProjectPath();
|
|
199
|
+
|
|
200
|
+
// 1. Detect language & filter non-supported
|
|
201
|
+
const detectedLanguage = (await detectLanguage(projectPath)).language;
|
|
202
|
+
console.log('1. Detected language:', detectedLanguage);
|
|
203
|
+
|
|
204
|
+
if (
|
|
205
|
+
![
|
|
206
|
+
Language.Java,
|
|
207
|
+
Language.TypeScript,
|
|
208
|
+
Language.Cpp,
|
|
209
|
+
Language.Python,
|
|
210
|
+
Language.Delphi,
|
|
211
|
+
Language.Kotlin,
|
|
212
|
+
].includes(detectedLanguage)
|
|
213
|
+
) {
|
|
214
|
+
throw new Error(
|
|
215
|
+
"Supported language is 'Java', 'TypeScript', 'C++', 'Python', 'Delphi' & 'Kotlin'. More to follow."
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// 2. Get validated root directory by detectedLanguage
|
|
220
|
+
const rootDir = await resolveRoot(projectPath, detectedLanguage);
|
|
221
|
+
console.log('2. rootDir:', rootDir);
|
|
222
|
+
|
|
223
|
+
// 3. Read directory recursively (pass resolved root as both dir and projectRoot)
|
|
224
|
+
return await readDirRecursively(rootDir, {}, rootDir, detectedLanguage);
|
|
225
|
+
}
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
import type { ElementsDefinition } from 'cytoscape';
|
|
2
|
+
import type { ParsedDirectory, ParsedFile } from '@/shared/types';
|
|
3
|
+
|
|
4
|
+
/** Collect all files from your ParsedDirectory tree. */
|
|
5
|
+
function collectFiles(root: ParsedDirectory): ParsedFile[] {
|
|
6
|
+
const files: ParsedFile[] = [];
|
|
7
|
+
const walk = (dir: ParsedDirectory) => {
|
|
8
|
+
for (const key in dir) {
|
|
9
|
+
const entry = (dir as Record<string, unknown>)[key];
|
|
10
|
+
if (entry && typeof entry === 'object' && 'path' in entry && 'package' in entry) {
|
|
11
|
+
files.push(entry as ParsedFile);
|
|
12
|
+
} else if (entry && typeof entry === 'object') {
|
|
13
|
+
walk(entry as ParsedDirectory);
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
walk(root);
|
|
18
|
+
return files;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Build per-edge evidence: key "from->to" → list of ImportEvidence. */
|
|
22
|
+
function buildEdgeEvidence(dir: ParsedDirectory): Map<string, ImportEvidence[]> {
|
|
23
|
+
const files = collectFiles(dir);
|
|
24
|
+
const map = new Map<string, ImportEvidence[]>();
|
|
25
|
+
|
|
26
|
+
for (const f of files) {
|
|
27
|
+
const from = f.package as TUniquePackageName;
|
|
28
|
+
for (const imp of f.imports ?? []) {
|
|
29
|
+
const to = imp.pkg as TUniquePackageName;
|
|
30
|
+
if (!to || to === from) continue;
|
|
31
|
+
const key = `${from}->${to}`;
|
|
32
|
+
if (!map.has(key)) map.set(key, []);
|
|
33
|
+
map.get(key)!.push({
|
|
34
|
+
filePath: f.path,
|
|
35
|
+
fileClass: f.className,
|
|
36
|
+
importName: imp.name,
|
|
37
|
+
isIntrinsic: imp.isIntrinsic,
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return map;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Convert ElementsDefinition (from buildGraph) to adjacency map. */
|
|
45
|
+
function elementsToAdj(elements: ElementsDefinition) {
|
|
46
|
+
const adj = new Map<TUniquePackageName, Set<TUniquePackageName>>();
|
|
47
|
+
for (const n of elements.nodes) {
|
|
48
|
+
const id = String(n.data.id) as TUniquePackageName;
|
|
49
|
+
if (!adj.has(id)) adj.set(id, new Set());
|
|
50
|
+
}
|
|
51
|
+
for (const e of elements.edges) {
|
|
52
|
+
const s = String(e.data.source) as TUniquePackageName;
|
|
53
|
+
const t = String(e.data.target) as TUniquePackageName;
|
|
54
|
+
if (!adj.has(s)) adj.set(s, new Set());
|
|
55
|
+
if (!adj.has(t)) adj.set(t, new Set());
|
|
56
|
+
adj.get(s)!.add(t);
|
|
57
|
+
}
|
|
58
|
+
return adj;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Tarjan SCC on adjacency. */
|
|
62
|
+
function tarjanSCC(
|
|
63
|
+
graph: Map<TUniquePackageName, Set<TUniquePackageName>>
|
|
64
|
+
): TUniquePackageName[][] {
|
|
65
|
+
let index = 0;
|
|
66
|
+
const idx = new Map<TUniquePackageName, number>();
|
|
67
|
+
const low = new Map<TUniquePackageName, number>();
|
|
68
|
+
const stack: TUniquePackageName[] = [];
|
|
69
|
+
const onStack = new Set<TUniquePackageName>();
|
|
70
|
+
const out: TUniquePackageName[][] = [];
|
|
71
|
+
|
|
72
|
+
function strong(v: TUniquePackageName) {
|
|
73
|
+
idx.set(v, index);
|
|
74
|
+
low.set(v, index);
|
|
75
|
+
index++;
|
|
76
|
+
stack.push(v);
|
|
77
|
+
onStack.add(v);
|
|
78
|
+
|
|
79
|
+
for (const w of graph.get(v) ?? []) {
|
|
80
|
+
if (!idx.has(w)) {
|
|
81
|
+
strong(w);
|
|
82
|
+
low.set(v, Math.min(low.get(v)!, low.get(w)!));
|
|
83
|
+
} else if (onStack.has(w)) {
|
|
84
|
+
low.set(v, Math.min(low.get(v)!, idx.get(w)!));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (low.get(v) === idx.get(v)) {
|
|
89
|
+
const comp: TUniquePackageName[] = [];
|
|
90
|
+
let w: TUniquePackageName;
|
|
91
|
+
do {
|
|
92
|
+
w = stack.pop()!;
|
|
93
|
+
onStack.delete(w);
|
|
94
|
+
comp.push(w);
|
|
95
|
+
} while (w !== v);
|
|
96
|
+
out.push(comp);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const v of graph.keys()) if (!idx.has(v)) strong(v);
|
|
101
|
+
return out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Find one simple cycle ordering inside a given SCC. */
|
|
105
|
+
function findOneCycleInScc(
|
|
106
|
+
graph: Map<TUniquePackageName, Set<TUniquePackageName>>,
|
|
107
|
+
sccSet: Set<TUniquePackageName>
|
|
108
|
+
): TUniquePackageName[] | null {
|
|
109
|
+
const nodes = Array.from(sccSet);
|
|
110
|
+
for (const start of nodes) {
|
|
111
|
+
const path: TUniquePackageName[] = [];
|
|
112
|
+
const seen = new Set<TUniquePackageName>();
|
|
113
|
+
function dfs(v: TUniquePackageName): TUniquePackageName[] | null {
|
|
114
|
+
path.push(v);
|
|
115
|
+
seen.add(v);
|
|
116
|
+
for (const w of graph.get(v) ?? []) {
|
|
117
|
+
if (!sccSet.has(w)) continue;
|
|
118
|
+
if (w === start && path.length > 1) return [...path, start];
|
|
119
|
+
if (!seen.has(w)) {
|
|
120
|
+
const r = dfs(w);
|
|
121
|
+
if (r) return r;
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
path.pop();
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
const cyc = dfs(start);
|
|
128
|
+
if (cyc) return cyc;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* High-level API: build graph via `buildGraph`, detect package cycles,
|
|
135
|
+
* and attach **member evidence** (files/imports) per cycle edge.
|
|
136
|
+
*/
|
|
137
|
+
export function getPackageCyclesWithMembers(
|
|
138
|
+
dir: ParsedDirectory,
|
|
139
|
+
graph: ElementsDefinition
|
|
140
|
+
): {
|
|
141
|
+
cycles: PackageCycleDetail[];
|
|
142
|
+
packageSet: Set<TUniquePackageName>;
|
|
143
|
+
graph: ElementsDefinition; // for convenience (already built)
|
|
144
|
+
} {
|
|
145
|
+
const adj = elementsToAdj(graph);
|
|
146
|
+
const sccs = tarjanSCC(adj);
|
|
147
|
+
const evidence = buildEdgeEvidence(dir);
|
|
148
|
+
|
|
149
|
+
const cycles: PackageCycleDetail[] = [];
|
|
150
|
+
const packageSet = new Set<TUniquePackageName>();
|
|
151
|
+
|
|
152
|
+
for (const scc of sccs) {
|
|
153
|
+
const selfLoop = scc.length === 1 && (adj.get(scc[0])?.has(scc[0]) ?? false);
|
|
154
|
+
if (scc.length > 1 || selfLoop) {
|
|
155
|
+
scc.forEach(p => packageSet.add(p));
|
|
156
|
+
const sccSet = new Set(scc);
|
|
157
|
+
const cycle = findOneCycleInScc(adj, sccSet) ?? [...scc, scc[0]];
|
|
158
|
+
|
|
159
|
+
const edges: CycleEdgeEvidence[] = [];
|
|
160
|
+
for (let i = 0; i < cycle.length - 1; i++) {
|
|
161
|
+
const from = cycle[i];
|
|
162
|
+
const to = cycle[i + 1];
|
|
163
|
+
const key = `${from}->${to}`;
|
|
164
|
+
edges.push({
|
|
165
|
+
from,
|
|
166
|
+
to,
|
|
167
|
+
via: evidence.get(key) ?? [],
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
cycles.push({ packages: cycle, edges });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return { cycles, packageSet, graph };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Annotate Cytoscape nodes:
|
|
180
|
+
* - adds class "packageCycle" for cyclic packages
|
|
181
|
+
* - sets data.packageCycle (boolean)
|
|
182
|
+
* - sets data.cycleEvidence (edges where this pkg is the "from" side)
|
|
183
|
+
*/
|
|
184
|
+
export function markCyclicPackagesWithEvidence(
|
|
185
|
+
elements: ElementsDefinition,
|
|
186
|
+
dir: ParsedDirectory
|
|
187
|
+
): ElementsDefinition {
|
|
188
|
+
const { cycles, packageSet } = getPackageCyclesWithMembers(dir, elements);
|
|
189
|
+
|
|
190
|
+
// pkg → list of outgoing cycle edges (evidence)
|
|
191
|
+
const pkgToEdges = new Map<TUniquePackageName, CycleEdgeEvidence[]>();
|
|
192
|
+
|
|
193
|
+
// Track cycle edges by (from → to) for quick lookup when mapping Cytoscape edges
|
|
194
|
+
const cycleEdgeKeys = new Set<string>();
|
|
195
|
+
|
|
196
|
+
for (const c of cycles) {
|
|
197
|
+
for (const e of c.edges) {
|
|
198
|
+
if (!pkgToEdges.has(e.from)) pkgToEdges.set(e.from, []);
|
|
199
|
+
pkgToEdges.get(e.from)!.push(e);
|
|
200
|
+
cycleEdgeKeys.add(`${e.from}→${e.to}`);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
const nodes = (elements.nodes ?? []).map(n => {
|
|
205
|
+
const id = String(n.data.id) as TUniquePackageName;
|
|
206
|
+
const isCyclic = packageSet.has(id);
|
|
207
|
+
const existing = n.classes ? String(n.classes) : '';
|
|
208
|
+
const classes = isCyclic ? (existing ? `${existing} packageCycle` : 'packageCycle') : existing;
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
...n,
|
|
212
|
+
classes,
|
|
213
|
+
data: {
|
|
214
|
+
...n.data,
|
|
215
|
+
packageCycle: isCyclic,
|
|
216
|
+
cycleEvidence: isCyclic ? (pkgToEdges.get(id) ?? []) : undefined,
|
|
217
|
+
},
|
|
218
|
+
};
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
const edges = (elements.edges ?? []).map(e => {
|
|
222
|
+
const source = String(e.data.source) as TUniquePackageName;
|
|
223
|
+
const target = String(e.data.target) as TUniquePackageName;
|
|
224
|
+
const isCycleEdge = cycleEdgeKeys.has(`${source}→${target}`);
|
|
225
|
+
|
|
226
|
+
const existing = e.classes ? String(e.classes) : '';
|
|
227
|
+
const classes = isCycleEdge
|
|
228
|
+
? existing
|
|
229
|
+
? `${existing} packageCycle`
|
|
230
|
+
: 'packageCycle'
|
|
231
|
+
: existing;
|
|
232
|
+
|
|
233
|
+
return {
|
|
234
|
+
...e,
|
|
235
|
+
classes,
|
|
236
|
+
data: {
|
|
237
|
+
...e.data,
|
|
238
|
+
packageCycle: isCycleEdge,
|
|
239
|
+
},
|
|
240
|
+
};
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
return { ...elements, nodes, edges };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Convenience that only returns the set of cyclic packages (no evidence)
|
|
248
|
+
*/
|
|
249
|
+
export function getCyclicPackageSet(
|
|
250
|
+
dir: ParsedDirectory,
|
|
251
|
+
graph: ElementsDefinition
|
|
252
|
+
): Set<TUniquePackageName> {
|
|
253
|
+
const { packageSet } = getPackageCyclesWithMembers(dir, graph);
|
|
254
|
+
return packageSet;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
type TUniquePackageName = string;
|
|
258
|
+
|
|
259
|
+
interface ImportEvidence {
|
|
260
|
+
readonly filePath: string; // ParsedFile.path (relative to project)
|
|
261
|
+
readonly fileClass: string; // ParsedFile.className
|
|
262
|
+
readonly importName: string; // IJavaImport.name
|
|
263
|
+
readonly isIntrinsic?: boolean; // IJavaImport.isIntrinsic
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
interface CycleEdgeEvidence {
|
|
267
|
+
readonly from: TUniquePackageName;
|
|
268
|
+
readonly to: TUniquePackageName;
|
|
269
|
+
readonly via: ImportEvidence[]; // files in `from` that import `to`
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export interface PackageCycleDetail {
|
|
273
|
+
readonly packages: TUniquePackageName[]; // ordered cycle incl. closing node, e.g. ["A","B","A"]
|
|
274
|
+
readonly edges: CycleEdgeEvidence[]; // evidence aligned with packages[i] -> packages[i+1]
|
|
275
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts the package/namespace from a C++ include statement
|
|
3
|
+
* For example: "myapp/utils/Helper.h" -> "myapp.utils"
|
|
4
|
+
*/
|
|
5
|
+
export function extractCppPackageFromImport(importPath: string): string {
|
|
6
|
+
// Remove file extension
|
|
7
|
+
const withoutExt = importPath.replace(/\.(h|hpp|hxx)$/, '');
|
|
8
|
+
|
|
9
|
+
// Split by path separator and take all but the last segment
|
|
10
|
+
const segments = withoutExt.split('/');
|
|
11
|
+
|
|
12
|
+
if (segments.length <= 1) {
|
|
13
|
+
return '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Join with dots to create package-like structure
|
|
17
|
+
return segments.slice(0, -1).join('.');
|
|
18
|
+
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
'use server';
|
|
2
|
+
import type { ParsedFile, MethodCall, MethodDefinition, ImportDefinition } from '@/shared/types';
|
|
3
|
+
|
|
4
|
+
import fs from 'node:fs';
|
|
5
|
+
import path from 'node:path';
|
|
6
|
+
import { extractCppPackageFromImport } from '@/app/utils/parser/cpp/extractCppPackageFromImport';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Extracts namespace from C++ code.
|
|
10
|
+
*/
|
|
11
|
+
function extractNamespace(content: string): string {
|
|
12
|
+
const match = content.match(/namespace\s+([a-zA-Z0-9_:]+)\s*\{/);
|
|
13
|
+
return match?.[1]?.replace(/::/g, '.') || '';
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Extracts include statements from C++ code.
|
|
18
|
+
*/
|
|
19
|
+
function extractIncludes(content: string, projectRoot: string): ImportDefinition[] {
|
|
20
|
+
console.log('[CPP] projectRoot:', projectRoot);
|
|
21
|
+
const includeRegex = /#include\s+["<]([^">]+)[">]/g;
|
|
22
|
+
const includes: ImportDefinition[] = [];
|
|
23
|
+
|
|
24
|
+
let match;
|
|
25
|
+
while ((match = includeRegex.exec(content)) !== null) {
|
|
26
|
+
const includePath = match[1];
|
|
27
|
+
|
|
28
|
+
// System includes use angle brackets, local includes use quotes
|
|
29
|
+
const isSystemInclude = content.includes(`<${includePath}>`);
|
|
30
|
+
const pkg = extractCppPackageFromImport(includePath);
|
|
31
|
+
|
|
32
|
+
includes.push({
|
|
33
|
+
name: includePath,
|
|
34
|
+
pkg,
|
|
35
|
+
isIntrinsic: !isSystemInclude, // Local includes are intrinsic
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return includes;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Extracts the class name from the content and filename fallback.
|
|
44
|
+
*/
|
|
45
|
+
function extractClassName(content: string, fileName: string): string {
|
|
46
|
+
// Try to find class declaration
|
|
47
|
+
const classMatch = content.match(/class\s+([A-Za-z0-9_]+)/);
|
|
48
|
+
if (classMatch) {
|
|
49
|
+
return classMatch[1];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Try to find struct declaration
|
|
53
|
+
const structMatch = content.match(/struct\s+([A-Za-z0-9_]+)/);
|
|
54
|
+
if (structMatch) {
|
|
55
|
+
return structMatch[1];
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Fallback to filename without extension
|
|
59
|
+
return path.basename(fileName, path.extname(fileName));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Extracts method definitions from C++ content.
|
|
64
|
+
*/
|
|
65
|
+
function extractMethodDefinitions(content: string): MethodDefinition[] {
|
|
66
|
+
const methods: MethodDefinition[] = [];
|
|
67
|
+
|
|
68
|
+
// Match method definitions (simplified regex, may need refinement)
|
|
69
|
+
const methodRegex = /(?:(public|protected|private):\s*)?([\w<>:&*\s]+)\s+(\w+)\s*$$([^)]*)$$/g;
|
|
70
|
+
|
|
71
|
+
let match;
|
|
72
|
+
let currentVisibility: 'public' | 'protected' | 'private' | 'default' = 'default';
|
|
73
|
+
|
|
74
|
+
// Also track visibility changes
|
|
75
|
+
const visibilityRegex = /(public|protected|private):/g;
|
|
76
|
+
const lines = content.split('\n');
|
|
77
|
+
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
const visMatch = visibilityRegex.exec(line);
|
|
80
|
+
if (visMatch) {
|
|
81
|
+
currentVisibility = visMatch[1] as 'public' | 'protected' | 'private';
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
methodRegex.lastIndex = 0;
|
|
86
|
+
while ((match = methodRegex.exec(content)) !== null) {
|
|
87
|
+
const visibility = (match[1] as 'public' | 'protected' | 'private') || currentVisibility;
|
|
88
|
+
const returnType = match[2]?.trim() || 'void';
|
|
89
|
+
const name = match[3];
|
|
90
|
+
const params = match[4]
|
|
91
|
+
.split(',')
|
|
92
|
+
.map(p => p.trim())
|
|
93
|
+
.filter(Boolean);
|
|
94
|
+
|
|
95
|
+
// Skip obvious non-methods (keywords, control structures)
|
|
96
|
+
if (['if', 'while', 'for', 'switch', 'catch'].includes(name)) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
methods.push({
|
|
101
|
+
name,
|
|
102
|
+
returnType,
|
|
103
|
+
parameters: params,
|
|
104
|
+
visibility,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return methods;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extract method calls from C++ content.
|
|
113
|
+
*/
|
|
114
|
+
function extractMethodCalls(content: string): MethodCall[] {
|
|
115
|
+
const callRegex = /(\b\w+)(?:\.|->)(\w+)\s*\(/g;
|
|
116
|
+
const calls: MethodCall[] = [];
|
|
117
|
+
|
|
118
|
+
let match;
|
|
119
|
+
while ((match = callRegex.exec(content)) !== null) {
|
|
120
|
+
const callee = match[1];
|
|
121
|
+
const method = match[2];
|
|
122
|
+
calls.push({ callee, method });
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return calls;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parses a C++ file and returns metadata useful for diagram generation.
|
|
130
|
+
*/
|
|
131
|
+
export async function parseCppFile(fullPath: string, projectRoot: string): Promise<ParsedFile> {
|
|
132
|
+
const content = fs.readFileSync(fullPath, 'utf-8');
|
|
133
|
+
const fileName = path.basename(fullPath);
|
|
134
|
+
|
|
135
|
+
const className = extractClassName(content, fileName);
|
|
136
|
+
const namespace = extractNamespace(content);
|
|
137
|
+
const includes = extractIncludes(content, projectRoot);
|
|
138
|
+
const methods = extractMethodDefinitions(content);
|
|
139
|
+
const calls = extractMethodCalls(content);
|
|
140
|
+
const relativePath = path.relative(projectRoot, fullPath);
|
|
141
|
+
|
|
142
|
+
return {
|
|
143
|
+
className,
|
|
144
|
+
package: namespace,
|
|
145
|
+
imports: includes,
|
|
146
|
+
methods,
|
|
147
|
+
calls,
|
|
148
|
+
path: relativePath,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extracts the package/unit name from a Delphi uses clause import.
|
|
3
|
+
*
|
|
4
|
+
* Delphi imports are in the format:
|
|
5
|
+
* - uses UnitName;
|
|
6
|
+
* - uses UnitName in 'path/to/UnitName.pas';
|
|
7
|
+
* - uses Vcl.Forms, System.SysUtils;
|
|
8
|
+
*
|
|
9
|
+
* @param importStatement - The full import statement (e.g., "System.SysUtils")
|
|
10
|
+
* @returns The package/namespace name
|
|
11
|
+
*/
|
|
12
|
+
export function extractPackageFromImport(importStatement: string): string {
|
|
13
|
+
const trimmed = importStatement.trim();
|
|
14
|
+
|
|
15
|
+
// Handle dotted notation (e.g., "System.SysUtils" -> "System")
|
|
16
|
+
const parts = trimmed.split('.');
|
|
17
|
+
if (parts.length > 1) return parts[0];
|
|
18
|
+
|
|
19
|
+
// For single unit names without namespace, return as-is
|
|
20
|
+
return trimmed;
|
|
21
|
+
}
|