deepdebug-local-agent 0.3.8 → 0.3.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.
@@ -0,0 +1,429 @@
1
+ import path from "path";
2
+ import { readFile } from "../fs-utils.js";
3
+
4
+ /**
5
+ * ControllerAnalyzer - ENHANCED VERSION
6
+ *
7
+ * Analyzes Java Spring Boot controllers to extract:
8
+ * - REST endpoints (@GetMapping, @PostMapping, etc.)
9
+ * - Request/Response DTOs
10
+ * - Path variables and query parameters
11
+ * - HTTP methods and paths
12
+ * - Handles constants and complex path expressions
13
+ */
14
+ export class ControllerAnalyzer {
15
+ constructor(workspaceRoot) {
16
+ this.workspaceRoot = workspaceRoot;
17
+ this.pathConstants = new Map(); // Store resolved path constants
18
+ }
19
+
20
+ /**
21
+ * Find all controller files in the workspace
22
+ */
23
+ async findControllers(files) {
24
+ const controllerFiles = files.filter(file => {
25
+ const fileName = path.basename(file.path || file);
26
+ return fileName.endsWith("Controller.java") ||
27
+ fileName.endsWith("Resource.java") ||
28
+ fileName.endsWith("Api.java");
29
+ });
30
+
31
+ return controllerFiles.map(f => f.path || f);
32
+ }
33
+
34
+ /**
35
+ * Try to resolve path constants from PathAPI or similar classes
36
+ */
37
+ async resolvePathConstants(files) {
38
+ // Look for PathAPI, ApiPaths, Routes, etc.
39
+ const pathFiles = files.filter(file => {
40
+ const filePath = file.path || file;
41
+ const fileName = path.basename(filePath);
42
+ return fileName.includes("Path") ||
43
+ fileName.includes("Route") ||
44
+ fileName.includes("Api") && fileName.endsWith(".java") &&
45
+ !fileName.includes("Controller");
46
+ });
47
+
48
+ for (const file of pathFiles) {
49
+ try {
50
+ const filePath = file.path || file;
51
+ const fullPath = path.join(this.workspaceRoot, filePath);
52
+ const content = await readFile(fullPath, "utf8");
53
+
54
+ // Extract constants like: public static final String CORE = "/v1";
55
+ const constantRegex = /(?:public\s+)?static\s+final\s+String\s+(\w+)\s*=\s*"([^"]+)"/g;
56
+ let match;
57
+ while ((match = constantRegex.exec(content)) !== null) {
58
+ this.pathConstants.set(match[1], match[2]);
59
+ }
60
+ } catch (err) {
61
+ // Ignore errors reading path files
62
+ }
63
+ }
64
+
65
+ // Common defaults
66
+ if (!this.pathConstants.has("ID")) {
67
+ this.pathConstants.set("ID", "/{id}");
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Resolve a path expression that may contain constants
73
+ */
74
+ resolvePath(pathExpr) {
75
+ if (!pathExpr) return "";
76
+
77
+ // Handle string literals
78
+ if (pathExpr.startsWith('"') && pathExpr.endsWith('"')) {
79
+ return pathExpr.slice(1, -1);
80
+ }
81
+
82
+ // Handle constant references like PathAPI.CORE + PathAPI.AGREEMENT
83
+ let resolved = pathExpr;
84
+
85
+ // Replace constant references
86
+ for (const [name, value] of this.pathConstants) {
87
+ // Match patterns like PathAPI.NAME or ClassName.NAME
88
+ const pattern = new RegExp(`\\w+\\.${name}\\b`, 'g');
89
+ resolved = resolved.replace(pattern, `"${value}"`);
90
+
91
+ // Also match just the constant name
92
+ const simplePattern = new RegExp(`\\b${name}\\b`, 'g');
93
+ resolved = resolved.replace(simplePattern, `"${value}"`);
94
+ }
95
+
96
+ // Now concatenate string parts
97
+ // "a" + "b" + "c" -> "abc"
98
+ const stringParts = resolved.match(/"([^"]*)"/g);
99
+ if (stringParts) {
100
+ return stringParts.map(s => s.slice(1, -1)).join('');
101
+ }
102
+
103
+ // If we couldn't resolve, return as-is or empty
104
+ return pathExpr.includes('.') ? "" : pathExpr;
105
+ }
106
+
107
+ /**
108
+ * Analyze a single controller file
109
+ */
110
+ async analyzeController(filePath) {
111
+ try {
112
+ const fullPath = path.join(this.workspaceRoot, filePath);
113
+ const content = await readFile(fullPath, "utf8");
114
+
115
+ const controller = {
116
+ file: filePath,
117
+ className: this.extractClassName(content),
118
+ basePath: this.extractBasePath(content),
119
+ endpoints: this.extractEndpoints(content),
120
+ imports: this.extractImports(content)
121
+ };
122
+
123
+ return controller;
124
+ } catch (err) {
125
+ console.error(`Failed to analyze controller ${filePath}:`, err.message);
126
+ return null;
127
+ }
128
+ }
129
+
130
+ /**
131
+ * Extract class name from Java file
132
+ */
133
+ extractClassName(content) {
134
+ const match = content.match(/public\s+class\s+(\w+)/);
135
+ return match ? match[1] : "Unknown";
136
+ }
137
+
138
+ /**
139
+ * Extract base path from @RequestMapping annotation on class
140
+ */
141
+ extractBasePath(content) {
142
+ // Find @RequestMapping on class level (before the class declaration)
143
+ const classMatch = content.match(/(@RequestMapping[^)]*\))\s*(?:@\w+[^)]*\)\s*)*public\s+class/s);
144
+
145
+ if (classMatch) {
146
+ const annotation = classMatch[1];
147
+
148
+ // @RequestMapping("/api/v1/users")
149
+ let match = annotation.match(/@RequestMapping\s*\(\s*"([^"]+)"\s*\)/);
150
+ if (match) return match[1];
151
+
152
+ // @RequestMapping(value = "/api/v1/users")
153
+ match = annotation.match(/@RequestMapping\s*\([^)]*value\s*=\s*"([^"]+)"/);
154
+ if (match) return match[1];
155
+
156
+ // @RequestMapping(path = "/api/v1/users")
157
+ match = annotation.match(/@RequestMapping\s*\([^)]*path\s*=\s*"([^"]+)"/);
158
+ if (match) return match[1];
159
+
160
+ // @RequestMapping(PathAPI.CORE + PathAPI.USERS)
161
+ match = annotation.match(/@RequestMapping\s*\(([^)]+)\)/);
162
+ if (match) {
163
+ const pathExpr = match[1].trim();
164
+ if (!pathExpr.includes("=") || pathExpr.includes("value")) {
165
+ // Extract the value part if present
166
+ const valueMatch = pathExpr.match(/value\s*=\s*([^,)]+)/);
167
+ const resolved = this.resolvePath(valueMatch ? valueMatch[1].trim() : pathExpr);
168
+ if (resolved) return resolved;
169
+ }
170
+ }
171
+ }
172
+
173
+ return "";
174
+ }
175
+
176
+ /**
177
+ * Extract all endpoint mappings from controller
178
+ */
179
+ extractEndpoints(content) {
180
+ const endpoints = [];
181
+
182
+ // Find all methods with mapping annotations
183
+ const methodPatterns = [
184
+ { regex: /@GetMapping(?:\s*\(\s*([^)]*)\s*\)|\s+)/g, method: "GET" },
185
+ { regex: /@PostMapping(?:\s*\(\s*([^)]*)\s*\)|\s+)/g, method: "POST" },
186
+ { regex: /@PutMapping(?:\s*\(\s*([^)]*)\s*\)|\s+)/g, method: "PUT" },
187
+ { regex: /@DeleteMapping(?:\s*\(\s*([^)]*)\s*\)|\s+)/g, method: "DELETE" },
188
+ { regex: /@PatchMapping(?:\s*\(\s*([^)]*)\s*\)|\s+)/g, method: "PATCH" }
189
+ ];
190
+
191
+ for (const { regex, method } of methodPatterns) {
192
+ let match;
193
+ const regexCopy = new RegExp(regex.source, 'g');
194
+
195
+ while ((match = regexCopy.exec(content)) !== null) {
196
+ const annotationEnd = match.index + match[0].length;
197
+
198
+ // Get the method block that follows this annotation
199
+ const methodBlock = content.substring(match.index, Math.min(annotationEnd + 500, content.length));
200
+
201
+ // Extract path from annotation
202
+ let endpointPath = "";
203
+ const annotationContent = match[1] || "";
204
+
205
+ if (annotationContent) {
206
+ // Handle: @GetMapping("/{id}")
207
+ const stringMatch = annotationContent.match(/^"([^"]*)"$/);
208
+ if (stringMatch) {
209
+ endpointPath = stringMatch[1];
210
+ } else {
211
+ // Handle: @GetMapping(value = "/{id}")
212
+ const valueMatch = annotationContent.match(/value\s*=\s*"([^"]+)"/);
213
+ if (valueMatch) {
214
+ endpointPath = valueMatch[1];
215
+ } else {
216
+ // Handle: @GetMapping(PathAPI.ID)
217
+ endpointPath = this.resolvePath(annotationContent.trim());
218
+ }
219
+ }
220
+ }
221
+
222
+ const endpoint = this.parseMethodBlock(methodBlock, method, endpointPath);
223
+ if (endpoint) {
224
+ endpoints.push(endpoint);
225
+ }
226
+ }
227
+ }
228
+
229
+ // Also check for @RequestMapping with method attribute
230
+ const requestMappingRegex = /@RequestMapping\s*\([^)]*method\s*=\s*(?:RequestMethod\.)?(\w+)[^)]*\)/g;
231
+ let match;
232
+ while ((match = requestMappingRegex.exec(content)) !== null) {
233
+ const httpMethod = match[1];
234
+ const annotationEnd = match.index + match[0].length;
235
+ const methodBlock = content.substring(match.index, Math.min(annotationEnd + 500, content.length));
236
+
237
+ // Extract path
238
+ const pathMatch = match[0].match(/value\s*=\s*"([^"]+)"|path\s*=\s*"([^"]+)"/);
239
+ const endpointPath = pathMatch ? (pathMatch[1] || pathMatch[2]) : "";
240
+
241
+ const endpoint = this.parseMethodBlock(methodBlock, httpMethod, endpointPath);
242
+ if (endpoint) {
243
+ endpoints.push(endpoint);
244
+ }
245
+ }
246
+
247
+ return endpoints;
248
+ }
249
+
250
+ /**
251
+ * Parse a method block to extract endpoint information
252
+ */
253
+ parseMethodBlock(methodBlock, httpMethod, endpointPath) {
254
+ // Find the method signature
255
+ // Pattern: returnType methodName(params)
256
+ const methodMatch = methodBlock.match(/(?:public|private|protected)\s+([\w<>,\s?]+?)\s+(\w+)\s*\(([^)]*)\)/);
257
+
258
+ if (!methodMatch) return null;
259
+
260
+ const returnType = methodMatch[1].trim();
261
+ const methodName = methodMatch[2];
262
+ const paramsString = methodMatch[3];
263
+
264
+ // Parse parameters
265
+ const requestBody = this.extractRequestBody(paramsString);
266
+ const pathVariables = this.extractPathVariables(paramsString);
267
+ const queryParams = this.extractQueryParams(paramsString);
268
+
269
+ return {
270
+ method: httpMethod,
271
+ path: endpointPath,
272
+ methodName: methodName,
273
+ returnType: this.cleanReturnType(returnType),
274
+ requestBody: requestBody,
275
+ pathVariables: pathVariables,
276
+ queryParams: queryParams
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Clean up return type (remove Mono/Flux wrappers, etc.)
282
+ */
283
+ cleanReturnType(returnType) {
284
+ let clean = returnType
285
+ .replace(/Mono<(.+)>/, "$1")
286
+ .replace(/Flux<(.+)>/, "List<$1>")
287
+ .replace(/ResponseEntity<(.+)>/, "$1")
288
+ .replace(/CompletableFuture<(.+)>/, "$1")
289
+ .trim();
290
+ return clean;
291
+ }
292
+
293
+ /**
294
+ * Extract @RequestBody parameter
295
+ */
296
+ extractRequestBody(paramsString) {
297
+ // Match @RequestBody Type name or @RequestBody @Valid Type name
298
+ const match = paramsString.match(/@RequestBody\s+(?:@\w+\s+)*(\w+)\s+(\w+)/);
299
+ if (match) {
300
+ return {
301
+ type: match[1],
302
+ name: match[2]
303
+ };
304
+ }
305
+ return null;
306
+ }
307
+
308
+ /**
309
+ * Extract @PathVariable parameters
310
+ */
311
+ extractPathVariables(paramsString) {
312
+ const pathVars = [];
313
+
314
+ // @PathVariable("id") Long id
315
+ // @PathVariable Long id
316
+ // @PathVariable(value = "id") Long id
317
+ const regex = /@PathVariable(?:\s*\(\s*(?:value\s*=\s*)?["']?(\w+)["']?\s*\))?\s+(\w+)\s+(\w+)/g;
318
+ let match;
319
+ while ((match = regex.exec(paramsString)) !== null) {
320
+ pathVars.push({
321
+ name: match[1] || match[3],
322
+ type: match[2],
323
+ paramName: match[3]
324
+ });
325
+ }
326
+ return pathVars;
327
+ }
328
+
329
+ /**
330
+ * Extract @RequestParam parameters
331
+ */
332
+ extractQueryParams(paramsString) {
333
+ const queryParams = [];
334
+
335
+ const regex = /@RequestParam(?:\s*\([^)]*\))?\s+(\w+)\s+(\w+)/g;
336
+ let match;
337
+ while ((match = regex.exec(paramsString)) !== null) {
338
+ queryParams.push({
339
+ name: match[2],
340
+ type: match[1],
341
+ paramName: match[2],
342
+ required: !paramsString.includes("required = false")
343
+ });
344
+ }
345
+ return queryParams;
346
+ }
347
+
348
+ /**
349
+ * Extract imports to understand DTOs
350
+ */
351
+ extractImports(content) {
352
+ const imports = [];
353
+ const regex = /import\s+([\w.]+);/g;
354
+ let match;
355
+ while ((match = regex.exec(content)) !== null) {
356
+ imports.push(match[1]);
357
+ }
358
+ return imports;
359
+ }
360
+
361
+ /**
362
+ * Analyze all controllers in workspace
363
+ */
364
+ async analyzeAll(files) {
365
+ // First, try to resolve path constants
366
+ await this.resolvePathConstants(files);
367
+
368
+ const controllerPaths = await this.findControllers(files);
369
+ const controllers = [];
370
+
371
+ for (const controllerPath of controllerPaths) {
372
+ const controller = await this.analyzeController(controllerPath);
373
+ if (controller) {
374
+ controllers.push(controller);
375
+ }
376
+ }
377
+
378
+ return controllers;
379
+ }
380
+
381
+ /**
382
+ * Generate full API documentation
383
+ */
384
+ async generateApiDocs(files) {
385
+ const controllers = await this.analyzeAll(files);
386
+
387
+ const endpoints = [];
388
+ for (const controller of controllers) {
389
+ const basePath = controller.basePath;
390
+
391
+ for (const endpoint of controller.endpoints) {
392
+ const fullPath = this.combinePaths(basePath, endpoint.path);
393
+ endpoints.push({
394
+ controller: controller.className,
395
+ file: controller.file,
396
+ method: endpoint.method,
397
+ path: fullPath,
398
+ methodName: endpoint.methodName,
399
+ returnType: endpoint.returnType,
400
+ requestBody: endpoint.requestBody,
401
+ pathVariables: endpoint.pathVariables,
402
+ queryParams: endpoint.queryParams
403
+ });
404
+ }
405
+ }
406
+
407
+ return {
408
+ totalControllers: controllers.length,
409
+ totalEndpoints: endpoints.length,
410
+ controllers: controllers,
411
+ endpoints: endpoints
412
+ };
413
+ }
414
+
415
+ /**
416
+ * Combine base path and endpoint path
417
+ */
418
+ combinePaths(basePath, endpointPath) {
419
+ if (!basePath && !endpointPath) return "/";
420
+ if (!basePath) return endpointPath.startsWith("/") ? endpointPath : "/" + endpointPath;
421
+ if (!endpointPath) return basePath.startsWith("/") ? basePath : "/" + basePath;
422
+
423
+ const base = basePath.endsWith("/") ? basePath.slice(0, -1) : basePath;
424
+ const endpoint = endpointPath.startsWith("/") ? endpointPath : "/" + endpointPath;
425
+ return base + endpoint;
426
+ }
427
+ }
428
+
429
+ export default ControllerAnalyzer;