tree-sitter-analyzer 1.0.0__py3-none-any.whl → 1.1.0__py3-none-any.whl

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.

Potentially problematic release.


This version of tree-sitter-analyzer might be problematic. Click here for more details.

Files changed (29) hide show
  1. tree_sitter_analyzer/__init__.py +132 -132
  2. tree_sitter_analyzer/api.py +542 -542
  3. tree_sitter_analyzer/cli/commands/base_command.py +181 -181
  4. tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -139
  5. tree_sitter_analyzer/cli/info_commands.py +124 -124
  6. tree_sitter_analyzer/cli_main.py +327 -327
  7. tree_sitter_analyzer/core/analysis_engine.py +584 -584
  8. tree_sitter_analyzer/core/query_service.py +162 -162
  9. tree_sitter_analyzer/file_handler.py +212 -212
  10. tree_sitter_analyzer/formatters/base_formatter.py +169 -169
  11. tree_sitter_analyzer/interfaces/cli.py +535 -535
  12. tree_sitter_analyzer/mcp/__init__.py +1 -1
  13. tree_sitter_analyzer/mcp/resources/__init__.py +1 -1
  14. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +16 -5
  15. tree_sitter_analyzer/mcp/server.py +655 -655
  16. tree_sitter_analyzer/mcp/tools/__init__.py +30 -30
  17. tree_sitter_analyzer/mcp/utils/__init__.py +2 -2
  18. tree_sitter_analyzer/mcp/utils/error_handler.py +569 -569
  19. tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -414
  20. tree_sitter_analyzer/output_manager.py +257 -257
  21. tree_sitter_analyzer/project_detector.py +330 -330
  22. tree_sitter_analyzer/security/boundary_manager.py +260 -260
  23. tree_sitter_analyzer/security/validator.py +257 -257
  24. tree_sitter_analyzer/table_formatter.py +710 -710
  25. tree_sitter_analyzer/utils.py +335 -335
  26. {tree_sitter_analyzer-1.0.0.dist-info → tree_sitter_analyzer-1.1.0.dist-info}/METADATA +11 -11
  27. {tree_sitter_analyzer-1.0.0.dist-info → tree_sitter_analyzer-1.1.0.dist-info}/RECORD +29 -29
  28. {tree_sitter_analyzer-1.0.0.dist-info → tree_sitter_analyzer-1.1.0.dist-info}/WHEEL +0 -0
  29. {tree_sitter_analyzer-1.0.0.dist-info → tree_sitter_analyzer-1.1.0.dist-info}/entry_points.txt +0 -0
@@ -1,710 +1,710 @@
1
- #!/usr/bin/env python3
2
- """
3
- Table Formatter for Tree-sitter Analyzer
4
-
5
- Provides table-formatted output for Java code analysis results.
6
- """
7
-
8
- import csv
9
- import io
10
- from typing import Any
11
-
12
-
13
- class TableFormatter:
14
- """Table formatter for code analysis results"""
15
-
16
- def __init__(
17
- self,
18
- format_type: str = "full",
19
- language: str = "java",
20
- include_javadoc: bool = False,
21
- ):
22
- self.format_type = format_type
23
- self.language = language
24
- self.include_javadoc = include_javadoc
25
-
26
- def _get_platform_newline(self) -> str:
27
- """Get platform-specific newline character"""
28
- import os
29
-
30
- return "\r\n" if os.name == "nt" else "\n" # Windows uses \r\n, others use \n
31
-
32
- def _convert_to_platform_newlines(self, text: str) -> str:
33
- """Convert standard \\n to platform-specific newline characters"""
34
- platform_newline = self._get_platform_newline()
35
- if platform_newline != "\n":
36
- return text.replace("\n", platform_newline)
37
- return text
38
-
39
- def format_structure(self, structure_data: dict[str, Any]) -> str:
40
- """Format structure data as table"""
41
- if self.format_type == "full":
42
- result = self._format_full_table(structure_data)
43
- elif self.format_type == "compact":
44
- result = self._format_compact_table(structure_data)
45
- elif self.format_type == "csv":
46
- result = self._format_csv(structure_data)
47
- else:
48
- raise ValueError(f"Unsupported format type: {self.format_type}")
49
-
50
- # Finally convert to platform-specific newline characters
51
- # Skip newline conversion for CSV format (newline control is handled within _format_csv)
52
- if self.format_type == "csv":
53
- return result
54
-
55
- return self._convert_to_platform_newlines(result)
56
-
57
- def _format_full_table(self, data: dict[str, Any]) -> str:
58
- """Full table format - organized by class"""
59
- lines = []
60
-
61
- # Header - use package.class format for single class, filename for multi-class files
62
- classes = data.get("classes", [])
63
- if classes is None:
64
- classes = []
65
-
66
- # Determine header format
67
- package_name = (data.get("package") or {}).get("name", "")
68
- if len(classes) == 1:
69
- # Single class: use package.ClassName format
70
- class_name = classes[0].get("name", "Unknown")
71
- if package_name:
72
- header = f"{package_name}.{class_name}"
73
- else:
74
- header = class_name
75
- else:
76
- # Multiple classes or no classes: use filename or default
77
- file_path = data.get("file_path", "")
78
- if file_path and file_path != "Unknown":
79
- file_name = file_path.split("/")[-1].split("\\")[-1]
80
- if file_name.endswith(".java"):
81
- file_name = file_name[:-5] # Remove .java extension
82
- elif file_name.endswith(".py"):
83
- file_name = file_name[:-3] # Remove .py extension
84
- elif file_name.endswith(".js"):
85
- file_name = file_name[:-3] # Remove .js extension
86
-
87
- if package_name and len(classes) == 0:
88
- # No classes but has package: use package.filename
89
- header = f"{package_name}.{file_name}"
90
- else:
91
- header = file_name
92
- else:
93
- # No file path: use default format
94
- if package_name:
95
- header = f"{package_name}.Unknown"
96
- else:
97
- header = "unknown.Unknown"
98
-
99
- lines.append(f"# {header}")
100
- lines.append("")
101
-
102
- # Package info
103
- package_name = (data.get("package") or {}).get("name", "")
104
- if package_name:
105
- lines.append("## Package")
106
- lines.append(f"`{package_name}`")
107
- lines.append("")
108
-
109
- # Imports
110
- imports = data.get("imports", [])
111
- if imports:
112
- lines.append("## Imports")
113
- lines.append(f"```{self.language}")
114
- for imp in imports:
115
- lines.append(str(imp.get("statement", "")))
116
- lines.append("```")
117
- lines.append("")
118
-
119
- # Class Info section (for single class files or empty data)
120
- if len(classes) == 1 or len(classes) == 0:
121
- lines.append("## Class Info")
122
- lines.append("| Property | Value |")
123
- lines.append("|----------|-------|")
124
-
125
- package_name = (data.get("package") or {}).get("name", "unknown")
126
-
127
- if len(classes) == 1:
128
- class_info = classes[0]
129
- lines.append(f"| Package | {package_name} |")
130
- lines.append(f"| Type | {str(class_info.get('type', 'class'))} |")
131
- lines.append(
132
- f"| Visibility | {str(class_info.get('visibility', 'public'))} |"
133
- )
134
-
135
- # Lines
136
- line_range = class_info.get("line_range", {})
137
- lines_str = f"{line_range.get('start', 1)}-{line_range.get('end', 50)}"
138
- lines.append(f"| Lines | {lines_str} |")
139
- else:
140
- # Empty data case
141
- lines.append(f"| Package | {package_name} |")
142
- lines.append("| Type | class |")
143
- lines.append("| Visibility | public |")
144
- lines.append("| Lines | 0-0 |")
145
-
146
- # Count methods and fields
147
- all_methods = data.get("methods", []) or []
148
- all_fields = data.get("fields", []) or []
149
- lines.append(f"| Total Methods | {len(all_methods)} |")
150
- lines.append(f"| Total Fields | {len(all_fields)} |")
151
- lines.append("")
152
-
153
- # Classes Overview
154
- if len(classes) > 1:
155
- lines.append("## Classes Overview")
156
- lines.append("| Class | Type | Visibility | Lines | Methods | Fields |")
157
- lines.append("|-------|------|------------|-------|---------|--------|")
158
-
159
- for class_info in classes:
160
- name = str(class_info.get("name", "Unknown"))
161
- class_type = str(class_info.get("type", "class"))
162
- visibility = str(class_info.get("visibility", "public"))
163
- line_range = class_info.get("line_range", {})
164
- lines_str = f"{line_range.get('start', 0)}-{line_range.get('end', 0)}"
165
-
166
- # Calculate method and field counts for this class
167
- class_methods = self._get_class_methods(data, line_range)
168
- class_fields = self._get_class_fields(data, line_range)
169
-
170
- lines.append(
171
- f"| {name} | {class_type} | {visibility} | {lines_str} | {len(class_methods)} | {len(class_fields)} |"
172
- )
173
- lines.append("")
174
-
175
- # Detailed class information - organized by class
176
- for class_info in classes:
177
- lines.extend(self._format_class_details(class_info, data))
178
-
179
- # Remove trailing empty lines
180
- while lines and lines[-1] == "":
181
- lines.pop()
182
-
183
- return "\n".join(lines)
184
-
185
- def _get_class_methods(
186
- self, data: dict[str, Any], class_line_range: dict[str, int]
187
- ) -> list[dict[str, Any]]:
188
- """Get methods that belong to a specific class based on line range, excluding nested classes."""
189
- methods = data.get("methods", [])
190
- classes = data.get("classes", [])
191
- class_methods = []
192
-
193
- # Get nested class ranges to exclude their methods
194
- nested_class_ranges = []
195
- for cls in classes:
196
- cls_range = cls.get("line_range", {})
197
- cls_start = cls_range.get("start", 0)
198
- cls_end = cls_range.get("end", 0)
199
-
200
- # If this class is nested within the current class range
201
- if class_line_range.get(
202
- "start", 0
203
- ) < cls_start and cls_end < class_line_range.get("end", 0):
204
- nested_class_ranges.append((cls_start, cls_end))
205
-
206
- for method in methods:
207
- method_line = method.get("line_range", {}).get("start", 0)
208
-
209
- # Check if method is within the class range
210
- if (
211
- class_line_range.get("start", 0)
212
- <= method_line
213
- <= class_line_range.get("end", 0)
214
- ):
215
- # Check if method is NOT within any nested class
216
- in_nested_class = False
217
- for nested_start, nested_end in nested_class_ranges:
218
- if nested_start <= method_line <= nested_end:
219
- in_nested_class = True
220
- break
221
-
222
- if not in_nested_class:
223
- class_methods.append(method)
224
-
225
- return class_methods
226
-
227
- def _get_class_fields(
228
- self, data: dict[str, Any], class_line_range: dict[str, int]
229
- ) -> list[dict[str, Any]]:
230
- """Get fields that belong to a specific class based on line range, excluding nested classes."""
231
- fields = data.get("fields", [])
232
- classes = data.get("classes", [])
233
- class_fields = []
234
-
235
- # Get nested class ranges to exclude their fields
236
- nested_class_ranges = []
237
- for cls in classes:
238
- cls_range = cls.get("line_range", {})
239
- cls_start = cls_range.get("start", 0)
240
- cls_end = cls_range.get("end", 0)
241
-
242
- # If this class is nested within the current class range
243
- if class_line_range.get(
244
- "start", 0
245
- ) < cls_start and cls_end < class_line_range.get("end", 0):
246
- nested_class_ranges.append((cls_start, cls_end))
247
-
248
- for field in fields:
249
- field_line = field.get("line_range", {}).get("start", 0)
250
-
251
- # Check if field is within the class range
252
- if (
253
- class_line_range.get("start", 0)
254
- <= field_line
255
- <= class_line_range.get("end", 0)
256
- ):
257
- # Check if field is NOT within any nested class
258
- in_nested_class = False
259
- for nested_start, nested_end in nested_class_ranges:
260
- if nested_start <= field_line <= nested_end:
261
- in_nested_class = True
262
- break
263
-
264
- if not in_nested_class:
265
- class_fields.append(field)
266
-
267
- return class_fields
268
-
269
- def _format_class_details(
270
- self, class_info: dict[str, Any], data: dict[str, Any]
271
- ) -> list[str]:
272
- """Format detailed information for a single class."""
273
- lines = []
274
-
275
- name = str(class_info.get("name", "Unknown"))
276
- line_range = class_info.get("line_range", {})
277
- lines_str = f"{line_range.get('start', 0)}-{line_range.get('end', 0)}"
278
-
279
- # Class header
280
- lines.append(f"## {name} ({lines_str})")
281
-
282
- # Get class-specific methods and fields
283
- class_methods = self._get_class_methods(data, line_range)
284
- class_fields = self._get_class_fields(data, line_range)
285
-
286
- # Fields section
287
- if class_fields:
288
- lines.append("### Fields")
289
- lines.append("| Name | Type | Vis | Modifiers | Line | Doc |")
290
- lines.append("|------|------|-----|-----------|------|-----|")
291
-
292
- for field in class_fields:
293
- name_field = str(field.get("name", ""))
294
- type_field = str(field.get("type", ""))
295
- visibility = self._convert_visibility(str(field.get("visibility", "")))
296
- modifiers = ",".join(field.get("modifiers", []))
297
- line_num = field.get("line_range", {}).get("start", 0)
298
- doc = (
299
- self._extract_doc_summary(str(field.get("javadoc", "")))
300
- if self.include_javadoc
301
- else "-"
302
- )
303
-
304
- lines.append(
305
- f"| {name_field} | {type_field} | {visibility} | {modifiers} | {line_num} | {doc} |"
306
- )
307
- lines.append("")
308
-
309
- # Methods section - separate by type
310
- constructors = [m for m in class_methods if m.get("is_constructor", False)]
311
- regular_methods = [
312
- m for m in class_methods if not m.get("is_constructor", False)
313
- ]
314
-
315
- # Constructors
316
- if constructors:
317
- lines.append("### Constructors")
318
- lines.append("| Constructor | Signature | Vis | Lines | Cx | Doc |")
319
- lines.append("|-------------|-----------|-----|-------|----|----|")
320
-
321
- for method in constructors:
322
- lines.append(self._format_method_row_detailed(method))
323
- lines.append("")
324
-
325
- # Methods grouped by visibility
326
- public_methods = [
327
- m for m in regular_methods if m.get("visibility", "") == "public"
328
- ]
329
- protected_methods = [
330
- m for m in regular_methods if m.get("visibility", "") == "protected"
331
- ]
332
- package_methods = [
333
- m for m in regular_methods if m.get("visibility", "") == "package"
334
- ]
335
- private_methods = [
336
- m for m in regular_methods if m.get("visibility", "") == "private"
337
- ]
338
-
339
- for method_group, title in [
340
- (public_methods, "Public Methods"),
341
- (protected_methods, "Protected Methods"),
342
- (package_methods, "Package Methods"),
343
- (private_methods, "Private Methods"),
344
- ]:
345
- if method_group:
346
- lines.append(f"### {title}")
347
- lines.append("| Method | Signature | Vis | Lines | Cx | Doc |")
348
- lines.append("|--------|-----------|-----|-------|----|----|")
349
-
350
- for method in method_group:
351
- lines.append(self._format_method_row_detailed(method))
352
- lines.append("")
353
-
354
- return lines
355
-
356
- def _format_method_row_detailed(self, method: dict[str, Any]) -> str:
357
- """Format method row for detailed class view."""
358
- name = str(method.get("name", ""))
359
- signature = self._create_full_signature(method)
360
- visibility = self._convert_visibility(str(method.get("visibility", "")))
361
- line_range = method.get("line_range", {})
362
- lines_str = f"{line_range.get('start', 0)}-{line_range.get('end', 0)}"
363
- complexity = method.get("complexity_score", 0)
364
- doc = (
365
- self._extract_doc_summary(str(method.get("javadoc", "")))
366
- if self.include_javadoc
367
- else "-"
368
- )
369
-
370
- return f"| {name} | {signature} | {visibility} | {lines_str} | {complexity} | {doc} |"
371
-
372
- def _format_traditional_sections(self, data: dict[str, Any]) -> list[str]:
373
- """Format traditional sections when no classes are found."""
374
- lines = []
375
-
376
- # Traditional class info
377
- lines.append("## Class Info")
378
- lines.append("| Property | Value |")
379
- lines.append("|----------|-------|")
380
-
381
- package_name = (data.get("package") or {}).get("name", "unknown")
382
- class_info = data.get("classes", [{}])[0] if data.get("classes") else {}
383
- stats = data.get("statistics") or {}
384
-
385
- lines.append(f"| Package | {package_name} |")
386
- lines.append(f"| Type | {str(class_info.get('type', 'class'))} |")
387
- lines.append(f"| Visibility | {str(class_info.get('visibility', 'public'))} |")
388
- lines.append(
389
- f"| Lines | {class_info.get('line_range', {}).get('start', 0)}-{class_info.get('line_range', {}).get('end', 0)} |"
390
- )
391
- lines.append(f"| Total Methods | {stats.get('method_count', 0)} |")
392
- lines.append(f"| Total Fields | {stats.get('field_count', 0)} |")
393
- lines.append("")
394
-
395
- # Fields
396
- fields = data.get("fields", [])
397
- if fields:
398
- lines.append("## Fields")
399
- lines.append("| Name | Type | Vis | Modifiers | Line | Doc |")
400
- lines.append("|------|------|-----|-----------|------|-----|")
401
-
402
- for field in fields:
403
- name = str(field.get("name", ""))
404
- field_type = str(field.get("type", ""))
405
- visibility = self._convert_visibility(str(field.get("visibility", "")))
406
- modifiers = ",".join([str(m) for m in field.get("modifiers", [])])
407
- line = field.get("line_range", {}).get("start", 0)
408
- doc = (
409
- self._extract_doc_summary(str(field.get("javadoc", "")))
410
- if self.include_javadoc
411
- else "-"
412
- )
413
-
414
- lines.append(
415
- f"| {name} | {field_type} | {visibility} | {modifiers} | {line} | {doc} |"
416
- )
417
- lines.append("")
418
-
419
- # Methods by type
420
- methods = data.get("methods", [])
421
- constructors = [m for m in methods if m.get("is_constructor", False)]
422
- regular_methods = [m for m in methods if not m.get("is_constructor", False)]
423
-
424
- # Constructors
425
- if constructors:
426
- lines.append("## Constructor")
427
- lines.append("| Method | Signature | Vis | Lines | Cols | Cx | Doc |")
428
- lines.append("|--------|-----------|-----|-------|------|----|----|")
429
- for method in constructors:
430
- lines.append(self._format_method_row(method))
431
- lines.append("")
432
-
433
- # Methods by visibility
434
- for visibility, title in [
435
- ("public", "Public Methods"),
436
- ("private", "Private Methods"),
437
- ]:
438
- visibility_methods = [
439
- m for m in regular_methods if str(m.get("visibility")) == visibility
440
- ]
441
- if visibility_methods:
442
- lines.append(f"## {title}")
443
- lines.append("| Method | Signature | Vis | Lines | Cols | Cx | Doc |")
444
- lines.append("|--------|-----------|-----|-------|------|----|----|")
445
- for method in visibility_methods:
446
- lines.append(self._format_method_row(method))
447
- lines.append("")
448
-
449
- return lines
450
-
451
- def _format_compact_table(self, data: dict[str, Any]) -> str:
452
- """Compact table format"""
453
- lines = []
454
-
455
- # Header
456
- package_name = (data.get("package") or {}).get("name", "unknown")
457
- classes = data.get("classes", [])
458
- if classes is None:
459
- classes = []
460
- class_name = classes[0].get("name", "Unknown") if classes else "Unknown"
461
- lines.append(f"# {package_name}.{class_name}")
462
- lines.append("")
463
-
464
- # Basic information
465
- stats = data.get("statistics") or {}
466
- lines.append("## Info")
467
- lines.append("| Property | Value |")
468
- lines.append("|----------|-------|")
469
- lines.append(f"| Package | {package_name} |")
470
- lines.append(f"| Methods | {stats.get('method_count', 0)} |")
471
- lines.append(f"| Fields | {stats.get('field_count', 0)} |")
472
- lines.append("")
473
-
474
- # Methods (simplified version)
475
- methods = data.get("methods", [])
476
- if methods is None:
477
- methods = []
478
- if methods:
479
- lines.append("## Methods")
480
- lines.append("| Method | Sig | V | L | Cx | Doc |")
481
- lines.append("|--------|-----|---|---|----|----|")
482
-
483
- for method in methods:
484
- name = str(method.get("name", ""))
485
- signature = self._create_compact_signature(method)
486
- visibility = self._convert_visibility(str(method.get("visibility", "")))
487
- line_range = method.get("line_range", {})
488
- lines_str = f"{line_range.get('start', 0)}-{line_range.get('end', 0)}"
489
- complexity = method.get("complexity_score", 0)
490
- doc = self._clean_csv_text(
491
- self._extract_doc_summary(str(method.get("javadoc", "")))
492
- )
493
-
494
- lines.append(
495
- f"| {name} | {signature} | {visibility} | {lines_str} | {complexity} | {doc} |"
496
- )
497
- lines.append("")
498
-
499
- # Remove trailing empty lines
500
- while lines and lines[-1] == "":
501
- lines.pop()
502
-
503
- return "\n".join(lines)
504
-
505
- def _format_csv(self, data: dict[str, Any]) -> str:
506
- """CSV format"""
507
- output = io.StringIO()
508
- writer = csv.writer(
509
- output, lineterminator="\n"
510
- ) # Explicitly specify newline character
511
-
512
- # Header
513
- writer.writerow(
514
- ["Type", "Name", "Signature", "Visibility", "Lines", "Complexity", "Doc"]
515
- )
516
-
517
- # Fields
518
- for field in data.get("fields", []):
519
- writer.writerow(
520
- [
521
- "Field",
522
- str(field.get("name", "")),
523
- f"{str(field.get('name', ''))}:{str(field.get('type', ''))}",
524
- str(field.get("visibility", "")),
525
- f"{field.get('line_range', {}).get('start', 0)}-{field.get('line_range', {}).get('end', 0)}",
526
- "",
527
- self._clean_csv_text(
528
- self._extract_doc_summary(str(field.get("javadoc", "")))
529
- ),
530
- ]
531
- )
532
-
533
- # Methods
534
- for method in data.get("methods", []):
535
- writer.writerow(
536
- [
537
- "Constructor" if method.get("is_constructor", False) else "Method",
538
- str(method.get("name", "")),
539
- self._clean_csv_text(self._create_full_signature(method)),
540
- str(method.get("visibility", "")),
541
- f"{method.get('line_range', {}).get('start', 0)}-{method.get('line_range', {}).get('end', 0)}",
542
- method.get("complexity_score", 0),
543
- self._clean_csv_text(
544
- self._extract_doc_summary(str(method.get("javadoc", "")))
545
- ),
546
- ]
547
- )
548
-
549
- # Completely control CSV output newlines
550
- csv_content = output.getvalue()
551
- # Unify all newline patterns and remove trailing newlines
552
- csv_content = csv_content.replace("\r\n", "\n").replace("\r", "\n")
553
- csv_content = csv_content.rstrip("\n")
554
- output.close()
555
-
556
- return csv_content
557
-
558
- def _format_method_row(self, method: dict[str, Any]) -> str:
559
- """Format method row"""
560
- name = str(method.get("name", ""))
561
- signature = self._create_full_signature(method)
562
- visibility = self._convert_visibility(str(method.get("visibility", "")))
563
- line_range = method.get("line_range", {})
564
- lines_str = f"{line_range.get('start', 0)}-{line_range.get('end', 0)}"
565
- cols_str = (
566
- "5-6" # Default value (actual implementation should get accurate values)
567
- )
568
- complexity = method.get("complexity_score", 0)
569
- if self.include_javadoc:
570
- doc = self._clean_csv_text(
571
- self._extract_doc_summary(str(method.get("javadoc", "")))
572
- )
573
- else:
574
- doc = "-"
575
-
576
- return f"| {name} | {signature} | {visibility} | {lines_str} | {cols_str} | {complexity} | {doc} |"
577
-
578
- def _create_full_signature(self, method: dict[str, Any]) -> str:
579
- """Create complete method signature"""
580
- params = method.get("parameters", [])
581
- param_strs = []
582
- for param in params:
583
- param_type = str(param.get("type", "Object"))
584
- param_name = str(param.get("name", "param"))
585
- param_strs.append(f"{param_name}:{param_type}")
586
-
587
- params_str = ", ".join(param_strs)
588
- return_type = str(method.get("return_type", "void"))
589
-
590
- modifiers = []
591
- if method.get("is_static", False):
592
- modifiers.append("[static]")
593
-
594
- modifier_str = " ".join(modifiers)
595
- signature = f"({params_str}):{return_type}"
596
-
597
- if modifier_str:
598
- signature += f" {modifier_str}"
599
-
600
- return signature
601
-
602
- def _create_compact_signature(self, method: dict[str, Any]) -> str:
603
- """Create compact method signature"""
604
- params = method.get("parameters", [])
605
- param_types = [self._shorten_type(p.get("type", "O")) for p in params]
606
- params_str = ",".join(param_types)
607
- return_type = self._shorten_type(method.get("return_type", "void"))
608
-
609
- return f"({params_str}):{return_type}"
610
-
611
- def _shorten_type(self, type_name: Any) -> str:
612
- """Shorten type name"""
613
- if type_name is None:
614
- return "O"
615
-
616
- # Convert non-string types to string
617
- if not isinstance(type_name, str):
618
- type_name = str(type_name)
619
-
620
- # At this point, type_name is guaranteed to be a string
621
- # Defensive check (avoid using assert for runtime safety and security checks)
622
- if not isinstance(type_name, str):
623
- try:
624
- type_name = str(type_name)
625
- except Exception:
626
- type_name = "O"
627
-
628
- type_mapping = {
629
- "String": "S",
630
- "int": "i",
631
- "long": "l",
632
- "double": "d",
633
- "boolean": "b",
634
- "void": "void",
635
- "Object": "O",
636
- "Exception": "E",
637
- "SQLException": "SE",
638
- "IllegalArgumentException": "IAE",
639
- "RuntimeException": "RE",
640
- }
641
-
642
- # Map<String,Object> -> M<S,O>
643
- if "Map<" in type_name:
644
- return (
645
- type_name.replace("Map<", "M<")
646
- .replace("String", "S")
647
- .replace("Object", "O")
648
- )
649
-
650
- # List<String> -> L<S>
651
- if "List<" in type_name:
652
- return type_name.replace("List<", "L<").replace("String", "S")
653
-
654
- # String[] -> S[]
655
- if "[]" in type_name:
656
- base_type = type_name.replace("[]", "")
657
- if base_type:
658
- return type_mapping.get(base_type, base_type[0].upper()) + "[]"
659
- else:
660
- return "O[]"
661
-
662
- return type_mapping.get(type_name, type_name)
663
-
664
- def _convert_visibility(self, visibility: str) -> str:
665
- """Convert visibility to symbol"""
666
- mapping = {"public": "+", "private": "-", "protected": "#", "package": "~"}
667
- return mapping.get(visibility, visibility)
668
-
669
- def _extract_doc_summary(self, javadoc: str) -> str:
670
- """Extract summary from JavaDoc"""
671
- if not javadoc:
672
- return "-"
673
-
674
- # Remove comment symbols
675
- clean_doc = (
676
- javadoc.replace("/**", "").replace("*/", "").replace("*", "").strip()
677
- )
678
-
679
- # Get first line (use standard \\n only)
680
- lines = clean_doc.split("\n")
681
- first_line = lines[0].strip()
682
-
683
- # Truncate if too long
684
- if len(first_line) > 50:
685
- first_line = first_line[:47] + "..."
686
-
687
- # Escape characters that cause problems in Markdown tables (use standard \\n only)
688
- return first_line.replace("|", "\\|").replace("\n", " ")
689
-
690
- def _clean_csv_text(self, text: str) -> str:
691
- """Text cleaning for CSV format"""
692
- if not text:
693
- return ""
694
-
695
- # Replace all newline characters with spaces
696
- cleaned = text.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")
697
- # Convert consecutive spaces to single space
698
- cleaned = " ".join(cleaned.split())
699
- # Escape characters that cause problems in CSV
700
- cleaned = cleaned.replace('"', '""') # Escape double quotes
701
-
702
- return cleaned
703
-
704
-
705
- def create_table_formatter(
706
- format_type: str, language: str = "java", include_javadoc: bool = False
707
- ) -> "TableFormatter":
708
- """Create table formatter (using new factory)"""
709
- # Create TableFormatter directly (for JavaDoc support)
710
- return TableFormatter(format_type, language, include_javadoc)
1
+ #!/usr/bin/env python3
2
+ """
3
+ Table Formatter for Tree-sitter Analyzer
4
+
5
+ Provides table-formatted output for Java code analysis results.
6
+ """
7
+
8
+ import csv
9
+ import io
10
+ from typing import Any
11
+
12
+
13
+ class TableFormatter:
14
+ """Table formatter for code analysis results"""
15
+
16
+ def __init__(
17
+ self,
18
+ format_type: str = "full",
19
+ language: str = "java",
20
+ include_javadoc: bool = False,
21
+ ):
22
+ self.format_type = format_type
23
+ self.language = language
24
+ self.include_javadoc = include_javadoc
25
+
26
+ def _get_platform_newline(self) -> str:
27
+ """Get platform-specific newline character"""
28
+ import os
29
+
30
+ return "\r\n" if os.name == "nt" else "\n" # Windows uses \r\n, others use \n
31
+
32
+ def _convert_to_platform_newlines(self, text: str) -> str:
33
+ """Convert standard \\n to platform-specific newline characters"""
34
+ platform_newline = self._get_platform_newline()
35
+ if platform_newline != "\n":
36
+ return text.replace("\n", platform_newline)
37
+ return text
38
+
39
+ def format_structure(self, structure_data: dict[str, Any]) -> str:
40
+ """Format structure data as table"""
41
+ if self.format_type == "full":
42
+ result = self._format_full_table(structure_data)
43
+ elif self.format_type == "compact":
44
+ result = self._format_compact_table(structure_data)
45
+ elif self.format_type == "csv":
46
+ result = self._format_csv(structure_data)
47
+ else:
48
+ raise ValueError(f"Unsupported format type: {self.format_type}")
49
+
50
+ # Finally convert to platform-specific newline characters
51
+ # Skip newline conversion for CSV format (newline control is handled within _format_csv)
52
+ if self.format_type == "csv":
53
+ return result
54
+
55
+ return self._convert_to_platform_newlines(result)
56
+
57
+ def _format_full_table(self, data: dict[str, Any]) -> str:
58
+ """Full table format - organized by class"""
59
+ lines = []
60
+
61
+ # Header - use package.class format for single class, filename for multi-class files
62
+ classes = data.get("classes", [])
63
+ if classes is None:
64
+ classes = []
65
+
66
+ # Determine header format
67
+ package_name = (data.get("package") or {}).get("name", "")
68
+ if len(classes) == 1:
69
+ # Single class: use package.ClassName format
70
+ class_name = classes[0].get("name", "Unknown")
71
+ if package_name:
72
+ header = f"{package_name}.{class_name}"
73
+ else:
74
+ header = class_name
75
+ else:
76
+ # Multiple classes or no classes: use filename or default
77
+ file_path = data.get("file_path", "")
78
+ if file_path and file_path != "Unknown":
79
+ file_name = file_path.split("/")[-1].split("\\")[-1]
80
+ if file_name.endswith(".java"):
81
+ file_name = file_name[:-5] # Remove .java extension
82
+ elif file_name.endswith(".py"):
83
+ file_name = file_name[:-3] # Remove .py extension
84
+ elif file_name.endswith(".js"):
85
+ file_name = file_name[:-3] # Remove .js extension
86
+
87
+ if package_name and len(classes) == 0:
88
+ # No classes but has package: use package.filename
89
+ header = f"{package_name}.{file_name}"
90
+ else:
91
+ header = file_name
92
+ else:
93
+ # No file path: use default format
94
+ if package_name:
95
+ header = f"{package_name}.Unknown"
96
+ else:
97
+ header = "unknown.Unknown"
98
+
99
+ lines.append(f"# {header}")
100
+ lines.append("")
101
+
102
+ # Package info
103
+ package_name = (data.get("package") or {}).get("name", "")
104
+ if package_name:
105
+ lines.append("## Package")
106
+ lines.append(f"`{package_name}`")
107
+ lines.append("")
108
+
109
+ # Imports
110
+ imports = data.get("imports", [])
111
+ if imports:
112
+ lines.append("## Imports")
113
+ lines.append(f"```{self.language}")
114
+ for imp in imports:
115
+ lines.append(str(imp.get("statement", "")))
116
+ lines.append("```")
117
+ lines.append("")
118
+
119
+ # Class Info section (for single class files or empty data)
120
+ if len(classes) == 1 or len(classes) == 0:
121
+ lines.append("## Class Info")
122
+ lines.append("| Property | Value |")
123
+ lines.append("|----------|-------|")
124
+
125
+ package_name = (data.get("package") or {}).get("name", "unknown")
126
+
127
+ if len(classes) == 1:
128
+ class_info = classes[0]
129
+ lines.append(f"| Package | {package_name} |")
130
+ lines.append(f"| Type | {str(class_info.get('type', 'class'))} |")
131
+ lines.append(
132
+ f"| Visibility | {str(class_info.get('visibility', 'public'))} |"
133
+ )
134
+
135
+ # Lines
136
+ line_range = class_info.get("line_range", {})
137
+ lines_str = f"{line_range.get('start', 1)}-{line_range.get('end', 50)}"
138
+ lines.append(f"| Lines | {lines_str} |")
139
+ else:
140
+ # Empty data case
141
+ lines.append(f"| Package | {package_name} |")
142
+ lines.append("| Type | class |")
143
+ lines.append("| Visibility | public |")
144
+ lines.append("| Lines | 0-0 |")
145
+
146
+ # Count methods and fields
147
+ all_methods = data.get("methods", []) or []
148
+ all_fields = data.get("fields", []) or []
149
+ lines.append(f"| Total Methods | {len(all_methods)} |")
150
+ lines.append(f"| Total Fields | {len(all_fields)} |")
151
+ lines.append("")
152
+
153
+ # Classes Overview
154
+ if len(classes) > 1:
155
+ lines.append("## Classes Overview")
156
+ lines.append("| Class | Type | Visibility | Lines | Methods | Fields |")
157
+ lines.append("|-------|------|------------|-------|---------|--------|")
158
+
159
+ for class_info in classes:
160
+ name = str(class_info.get("name", "Unknown"))
161
+ class_type = str(class_info.get("type", "class"))
162
+ visibility = str(class_info.get("visibility", "public"))
163
+ line_range = class_info.get("line_range", {})
164
+ lines_str = f"{line_range.get('start', 0)}-{line_range.get('end', 0)}"
165
+
166
+ # Calculate method and field counts for this class
167
+ class_methods = self._get_class_methods(data, line_range)
168
+ class_fields = self._get_class_fields(data, line_range)
169
+
170
+ lines.append(
171
+ f"| {name} | {class_type} | {visibility} | {lines_str} | {len(class_methods)} | {len(class_fields)} |"
172
+ )
173
+ lines.append("")
174
+
175
+ # Detailed class information - organized by class
176
+ for class_info in classes:
177
+ lines.extend(self._format_class_details(class_info, data))
178
+
179
+ # Remove trailing empty lines
180
+ while lines and lines[-1] == "":
181
+ lines.pop()
182
+
183
+ return "\n".join(lines)
184
+
185
+ def _get_class_methods(
186
+ self, data: dict[str, Any], class_line_range: dict[str, int]
187
+ ) -> list[dict[str, Any]]:
188
+ """Get methods that belong to a specific class based on line range, excluding nested classes."""
189
+ methods = data.get("methods", [])
190
+ classes = data.get("classes", [])
191
+ class_methods = []
192
+
193
+ # Get nested class ranges to exclude their methods
194
+ nested_class_ranges = []
195
+ for cls in classes:
196
+ cls_range = cls.get("line_range", {})
197
+ cls_start = cls_range.get("start", 0)
198
+ cls_end = cls_range.get("end", 0)
199
+
200
+ # If this class is nested within the current class range
201
+ if class_line_range.get(
202
+ "start", 0
203
+ ) < cls_start and cls_end < class_line_range.get("end", 0):
204
+ nested_class_ranges.append((cls_start, cls_end))
205
+
206
+ for method in methods:
207
+ method_line = method.get("line_range", {}).get("start", 0)
208
+
209
+ # Check if method is within the class range
210
+ if (
211
+ class_line_range.get("start", 0)
212
+ <= method_line
213
+ <= class_line_range.get("end", 0)
214
+ ):
215
+ # Check if method is NOT within any nested class
216
+ in_nested_class = False
217
+ for nested_start, nested_end in nested_class_ranges:
218
+ if nested_start <= method_line <= nested_end:
219
+ in_nested_class = True
220
+ break
221
+
222
+ if not in_nested_class:
223
+ class_methods.append(method)
224
+
225
+ return class_methods
226
+
227
+ def _get_class_fields(
228
+ self, data: dict[str, Any], class_line_range: dict[str, int]
229
+ ) -> list[dict[str, Any]]:
230
+ """Get fields that belong to a specific class based on line range, excluding nested classes."""
231
+ fields = data.get("fields", [])
232
+ classes = data.get("classes", [])
233
+ class_fields = []
234
+
235
+ # Get nested class ranges to exclude their fields
236
+ nested_class_ranges = []
237
+ for cls in classes:
238
+ cls_range = cls.get("line_range", {})
239
+ cls_start = cls_range.get("start", 0)
240
+ cls_end = cls_range.get("end", 0)
241
+
242
+ # If this class is nested within the current class range
243
+ if class_line_range.get(
244
+ "start", 0
245
+ ) < cls_start and cls_end < class_line_range.get("end", 0):
246
+ nested_class_ranges.append((cls_start, cls_end))
247
+
248
+ for field in fields:
249
+ field_line = field.get("line_range", {}).get("start", 0)
250
+
251
+ # Check if field is within the class range
252
+ if (
253
+ class_line_range.get("start", 0)
254
+ <= field_line
255
+ <= class_line_range.get("end", 0)
256
+ ):
257
+ # Check if field is NOT within any nested class
258
+ in_nested_class = False
259
+ for nested_start, nested_end in nested_class_ranges:
260
+ if nested_start <= field_line <= nested_end:
261
+ in_nested_class = True
262
+ break
263
+
264
+ if not in_nested_class:
265
+ class_fields.append(field)
266
+
267
+ return class_fields
268
+
269
+ def _format_class_details(
270
+ self, class_info: dict[str, Any], data: dict[str, Any]
271
+ ) -> list[str]:
272
+ """Format detailed information for a single class."""
273
+ lines = []
274
+
275
+ name = str(class_info.get("name", "Unknown"))
276
+ line_range = class_info.get("line_range", {})
277
+ lines_str = f"{line_range.get('start', 0)}-{line_range.get('end', 0)}"
278
+
279
+ # Class header
280
+ lines.append(f"## {name} ({lines_str})")
281
+
282
+ # Get class-specific methods and fields
283
+ class_methods = self._get_class_methods(data, line_range)
284
+ class_fields = self._get_class_fields(data, line_range)
285
+
286
+ # Fields section
287
+ if class_fields:
288
+ lines.append("### Fields")
289
+ lines.append("| Name | Type | Vis | Modifiers | Line | Doc |")
290
+ lines.append("|------|------|-----|-----------|------|-----|")
291
+
292
+ for field in class_fields:
293
+ name_field = str(field.get("name", ""))
294
+ type_field = str(field.get("type", ""))
295
+ visibility = self._convert_visibility(str(field.get("visibility", "")))
296
+ modifiers = ",".join(field.get("modifiers", []))
297
+ line_num = field.get("line_range", {}).get("start", 0)
298
+ doc = (
299
+ self._extract_doc_summary(str(field.get("javadoc", "")))
300
+ if self.include_javadoc
301
+ else "-"
302
+ )
303
+
304
+ lines.append(
305
+ f"| {name_field} | {type_field} | {visibility} | {modifiers} | {line_num} | {doc} |"
306
+ )
307
+ lines.append("")
308
+
309
+ # Methods section - separate by type
310
+ constructors = [m for m in class_methods if m.get("is_constructor", False)]
311
+ regular_methods = [
312
+ m for m in class_methods if not m.get("is_constructor", False)
313
+ ]
314
+
315
+ # Constructors
316
+ if constructors:
317
+ lines.append("### Constructors")
318
+ lines.append("| Constructor | Signature | Vis | Lines | Cx | Doc |")
319
+ lines.append("|-------------|-----------|-----|-------|----|----|")
320
+
321
+ for method in constructors:
322
+ lines.append(self._format_method_row_detailed(method))
323
+ lines.append("")
324
+
325
+ # Methods grouped by visibility
326
+ public_methods = [
327
+ m for m in regular_methods if m.get("visibility", "") == "public"
328
+ ]
329
+ protected_methods = [
330
+ m for m in regular_methods if m.get("visibility", "") == "protected"
331
+ ]
332
+ package_methods = [
333
+ m for m in regular_methods if m.get("visibility", "") == "package"
334
+ ]
335
+ private_methods = [
336
+ m for m in regular_methods if m.get("visibility", "") == "private"
337
+ ]
338
+
339
+ for method_group, title in [
340
+ (public_methods, "Public Methods"),
341
+ (protected_methods, "Protected Methods"),
342
+ (package_methods, "Package Methods"),
343
+ (private_methods, "Private Methods"),
344
+ ]:
345
+ if method_group:
346
+ lines.append(f"### {title}")
347
+ lines.append("| Method | Signature | Vis | Lines | Cx | Doc |")
348
+ lines.append("|--------|-----------|-----|-------|----|----|")
349
+
350
+ for method in method_group:
351
+ lines.append(self._format_method_row_detailed(method))
352
+ lines.append("")
353
+
354
+ return lines
355
+
356
+ def _format_method_row_detailed(self, method: dict[str, Any]) -> str:
357
+ """Format method row for detailed class view."""
358
+ name = str(method.get("name", ""))
359
+ signature = self._create_full_signature(method)
360
+ visibility = self._convert_visibility(str(method.get("visibility", "")))
361
+ line_range = method.get("line_range", {})
362
+ lines_str = f"{line_range.get('start', 0)}-{line_range.get('end', 0)}"
363
+ complexity = method.get("complexity_score", 0)
364
+ doc = (
365
+ self._extract_doc_summary(str(method.get("javadoc", "")))
366
+ if self.include_javadoc
367
+ else "-"
368
+ )
369
+
370
+ return f"| {name} | {signature} | {visibility} | {lines_str} | {complexity} | {doc} |"
371
+
372
+ def _format_traditional_sections(self, data: dict[str, Any]) -> list[str]:
373
+ """Format traditional sections when no classes are found."""
374
+ lines = []
375
+
376
+ # Traditional class info
377
+ lines.append("## Class Info")
378
+ lines.append("| Property | Value |")
379
+ lines.append("|----------|-------|")
380
+
381
+ package_name = (data.get("package") or {}).get("name", "unknown")
382
+ class_info = data.get("classes", [{}])[0] if data.get("classes") else {}
383
+ stats = data.get("statistics") or {}
384
+
385
+ lines.append(f"| Package | {package_name} |")
386
+ lines.append(f"| Type | {str(class_info.get('type', 'class'))} |")
387
+ lines.append(f"| Visibility | {str(class_info.get('visibility', 'public'))} |")
388
+ lines.append(
389
+ f"| Lines | {class_info.get('line_range', {}).get('start', 0)}-{class_info.get('line_range', {}).get('end', 0)} |"
390
+ )
391
+ lines.append(f"| Total Methods | {stats.get('method_count', 0)} |")
392
+ lines.append(f"| Total Fields | {stats.get('field_count', 0)} |")
393
+ lines.append("")
394
+
395
+ # Fields
396
+ fields = data.get("fields", [])
397
+ if fields:
398
+ lines.append("## Fields")
399
+ lines.append("| Name | Type | Vis | Modifiers | Line | Doc |")
400
+ lines.append("|------|------|-----|-----------|------|-----|")
401
+
402
+ for field in fields:
403
+ name = str(field.get("name", ""))
404
+ field_type = str(field.get("type", ""))
405
+ visibility = self._convert_visibility(str(field.get("visibility", "")))
406
+ modifiers = ",".join([str(m) for m in field.get("modifiers", [])])
407
+ line = field.get("line_range", {}).get("start", 0)
408
+ doc = (
409
+ self._extract_doc_summary(str(field.get("javadoc", "")))
410
+ if self.include_javadoc
411
+ else "-"
412
+ )
413
+
414
+ lines.append(
415
+ f"| {name} | {field_type} | {visibility} | {modifiers} | {line} | {doc} |"
416
+ )
417
+ lines.append("")
418
+
419
+ # Methods by type
420
+ methods = data.get("methods", [])
421
+ constructors = [m for m in methods if m.get("is_constructor", False)]
422
+ regular_methods = [m for m in methods if not m.get("is_constructor", False)]
423
+
424
+ # Constructors
425
+ if constructors:
426
+ lines.append("## Constructor")
427
+ lines.append("| Method | Signature | Vis | Lines | Cols | Cx | Doc |")
428
+ lines.append("|--------|-----------|-----|-------|------|----|----|")
429
+ for method in constructors:
430
+ lines.append(self._format_method_row(method))
431
+ lines.append("")
432
+
433
+ # Methods by visibility
434
+ for visibility, title in [
435
+ ("public", "Public Methods"),
436
+ ("private", "Private Methods"),
437
+ ]:
438
+ visibility_methods = [
439
+ m for m in regular_methods if str(m.get("visibility")) == visibility
440
+ ]
441
+ if visibility_methods:
442
+ lines.append(f"## {title}")
443
+ lines.append("| Method | Signature | Vis | Lines | Cols | Cx | Doc |")
444
+ lines.append("|--------|-----------|-----|-------|------|----|----|")
445
+ for method in visibility_methods:
446
+ lines.append(self._format_method_row(method))
447
+ lines.append("")
448
+
449
+ return lines
450
+
451
+ def _format_compact_table(self, data: dict[str, Any]) -> str:
452
+ """Compact table format"""
453
+ lines = []
454
+
455
+ # Header
456
+ package_name = (data.get("package") or {}).get("name", "unknown")
457
+ classes = data.get("classes", [])
458
+ if classes is None:
459
+ classes = []
460
+ class_name = classes[0].get("name", "Unknown") if classes else "Unknown"
461
+ lines.append(f"# {package_name}.{class_name}")
462
+ lines.append("")
463
+
464
+ # Basic information
465
+ stats = data.get("statistics") or {}
466
+ lines.append("## Info")
467
+ lines.append("| Property | Value |")
468
+ lines.append("|----------|-------|")
469
+ lines.append(f"| Package | {package_name} |")
470
+ lines.append(f"| Methods | {stats.get('method_count', 0)} |")
471
+ lines.append(f"| Fields | {stats.get('field_count', 0)} |")
472
+ lines.append("")
473
+
474
+ # Methods (simplified version)
475
+ methods = data.get("methods", [])
476
+ if methods is None:
477
+ methods = []
478
+ if methods:
479
+ lines.append("## Methods")
480
+ lines.append("| Method | Sig | V | L | Cx | Doc |")
481
+ lines.append("|--------|-----|---|---|----|----|")
482
+
483
+ for method in methods:
484
+ name = str(method.get("name", ""))
485
+ signature = self._create_compact_signature(method)
486
+ visibility = self._convert_visibility(str(method.get("visibility", "")))
487
+ line_range = method.get("line_range", {})
488
+ lines_str = f"{line_range.get('start', 0)}-{line_range.get('end', 0)}"
489
+ complexity = method.get("complexity_score", 0)
490
+ doc = self._clean_csv_text(
491
+ self._extract_doc_summary(str(method.get("javadoc", "")))
492
+ )
493
+
494
+ lines.append(
495
+ f"| {name} | {signature} | {visibility} | {lines_str} | {complexity} | {doc} |"
496
+ )
497
+ lines.append("")
498
+
499
+ # Remove trailing empty lines
500
+ while lines and lines[-1] == "":
501
+ lines.pop()
502
+
503
+ return "\n".join(lines)
504
+
505
+ def _format_csv(self, data: dict[str, Any]) -> str:
506
+ """CSV format"""
507
+ output = io.StringIO()
508
+ writer = csv.writer(
509
+ output, lineterminator="\n"
510
+ ) # Explicitly specify newline character
511
+
512
+ # Header
513
+ writer.writerow(
514
+ ["Type", "Name", "Signature", "Visibility", "Lines", "Complexity", "Doc"]
515
+ )
516
+
517
+ # Fields
518
+ for field in data.get("fields", []):
519
+ writer.writerow(
520
+ [
521
+ "Field",
522
+ str(field.get("name", "")),
523
+ f"{str(field.get('name', ''))}:{str(field.get('type', ''))}",
524
+ str(field.get("visibility", "")),
525
+ f"{field.get('line_range', {}).get('start', 0)}-{field.get('line_range', {}).get('end', 0)}",
526
+ "",
527
+ self._clean_csv_text(
528
+ self._extract_doc_summary(str(field.get("javadoc", "")))
529
+ ),
530
+ ]
531
+ )
532
+
533
+ # Methods
534
+ for method in data.get("methods", []):
535
+ writer.writerow(
536
+ [
537
+ "Constructor" if method.get("is_constructor", False) else "Method",
538
+ str(method.get("name", "")),
539
+ self._clean_csv_text(self._create_full_signature(method)),
540
+ str(method.get("visibility", "")),
541
+ f"{method.get('line_range', {}).get('start', 0)}-{method.get('line_range', {}).get('end', 0)}",
542
+ method.get("complexity_score", 0),
543
+ self._clean_csv_text(
544
+ self._extract_doc_summary(str(method.get("javadoc", "")))
545
+ ),
546
+ ]
547
+ )
548
+
549
+ # Completely control CSV output newlines
550
+ csv_content = output.getvalue()
551
+ # Unify all newline patterns and remove trailing newlines
552
+ csv_content = csv_content.replace("\r\n", "\n").replace("\r", "\n")
553
+ csv_content = csv_content.rstrip("\n")
554
+ output.close()
555
+
556
+ return csv_content
557
+
558
+ def _format_method_row(self, method: dict[str, Any]) -> str:
559
+ """Format method row"""
560
+ name = str(method.get("name", ""))
561
+ signature = self._create_full_signature(method)
562
+ visibility = self._convert_visibility(str(method.get("visibility", "")))
563
+ line_range = method.get("line_range", {})
564
+ lines_str = f"{line_range.get('start', 0)}-{line_range.get('end', 0)}"
565
+ cols_str = (
566
+ "5-6" # Default value (actual implementation should get accurate values)
567
+ )
568
+ complexity = method.get("complexity_score", 0)
569
+ if self.include_javadoc:
570
+ doc = self._clean_csv_text(
571
+ self._extract_doc_summary(str(method.get("javadoc", "")))
572
+ )
573
+ else:
574
+ doc = "-"
575
+
576
+ return f"| {name} | {signature} | {visibility} | {lines_str} | {cols_str} | {complexity} | {doc} |"
577
+
578
+ def _create_full_signature(self, method: dict[str, Any]) -> str:
579
+ """Create complete method signature"""
580
+ params = method.get("parameters", [])
581
+ param_strs = []
582
+ for param in params:
583
+ param_type = str(param.get("type", "Object"))
584
+ param_name = str(param.get("name", "param"))
585
+ param_strs.append(f"{param_name}:{param_type}")
586
+
587
+ params_str = ", ".join(param_strs)
588
+ return_type = str(method.get("return_type", "void"))
589
+
590
+ modifiers = []
591
+ if method.get("is_static", False):
592
+ modifiers.append("[static]")
593
+
594
+ modifier_str = " ".join(modifiers)
595
+ signature = f"({params_str}):{return_type}"
596
+
597
+ if modifier_str:
598
+ signature += f" {modifier_str}"
599
+
600
+ return signature
601
+
602
+ def _create_compact_signature(self, method: dict[str, Any]) -> str:
603
+ """Create compact method signature"""
604
+ params = method.get("parameters", [])
605
+ param_types = [self._shorten_type(p.get("type", "O")) for p in params]
606
+ params_str = ",".join(param_types)
607
+ return_type = self._shorten_type(method.get("return_type", "void"))
608
+
609
+ return f"({params_str}):{return_type}"
610
+
611
+ def _shorten_type(self, type_name: Any) -> str:
612
+ """Shorten type name"""
613
+ if type_name is None:
614
+ return "O"
615
+
616
+ # Convert non-string types to string
617
+ if not isinstance(type_name, str):
618
+ type_name = str(type_name)
619
+
620
+ # At this point, type_name is guaranteed to be a string
621
+ # Defensive check (avoid using assert for runtime safety and security checks)
622
+ if not isinstance(type_name, str):
623
+ try:
624
+ type_name = str(type_name)
625
+ except Exception:
626
+ type_name = "O"
627
+
628
+ type_mapping = {
629
+ "String": "S",
630
+ "int": "i",
631
+ "long": "l",
632
+ "double": "d",
633
+ "boolean": "b",
634
+ "void": "void",
635
+ "Object": "O",
636
+ "Exception": "E",
637
+ "SQLException": "SE",
638
+ "IllegalArgumentException": "IAE",
639
+ "RuntimeException": "RE",
640
+ }
641
+
642
+ # Map<String,Object> -> M<S,O>
643
+ if "Map<" in type_name:
644
+ return (
645
+ type_name.replace("Map<", "M<")
646
+ .replace("String", "S")
647
+ .replace("Object", "O")
648
+ )
649
+
650
+ # List<String> -> L<S>
651
+ if "List<" in type_name:
652
+ return type_name.replace("List<", "L<").replace("String", "S")
653
+
654
+ # String[] -> S[]
655
+ if "[]" in type_name:
656
+ base_type = type_name.replace("[]", "")
657
+ if base_type:
658
+ return type_mapping.get(base_type, base_type[0].upper()) + "[]"
659
+ else:
660
+ return "O[]"
661
+
662
+ return type_mapping.get(type_name, type_name)
663
+
664
+ def _convert_visibility(self, visibility: str) -> str:
665
+ """Convert visibility to symbol"""
666
+ mapping = {"public": "+", "private": "-", "protected": "#", "package": "~"}
667
+ return mapping.get(visibility, visibility)
668
+
669
+ def _extract_doc_summary(self, javadoc: str) -> str:
670
+ """Extract summary from JavaDoc"""
671
+ if not javadoc:
672
+ return "-"
673
+
674
+ # Remove comment symbols
675
+ clean_doc = (
676
+ javadoc.replace("/**", "").replace("*/", "").replace("*", "").strip()
677
+ )
678
+
679
+ # Get first line (use standard \\n only)
680
+ lines = clean_doc.split("\n")
681
+ first_line = lines[0].strip()
682
+
683
+ # Truncate if too long
684
+ if len(first_line) > 50:
685
+ first_line = first_line[:47] + "..."
686
+
687
+ # Escape characters that cause problems in Markdown tables (use standard \\n only)
688
+ return first_line.replace("|", "\\|").replace("\n", " ")
689
+
690
+ def _clean_csv_text(self, text: str) -> str:
691
+ """Text cleaning for CSV format"""
692
+ if not text:
693
+ return ""
694
+
695
+ # Replace all newline characters with spaces
696
+ cleaned = text.replace("\r\n", " ").replace("\r", " ").replace("\n", " ")
697
+ # Convert consecutive spaces to single space
698
+ cleaned = " ".join(cleaned.split())
699
+ # Escape characters that cause problems in CSV
700
+ cleaned = cleaned.replace('"', '""') # Escape double quotes
701
+
702
+ return cleaned
703
+
704
+
705
+ def create_table_formatter(
706
+ format_type: str, language: str = "java", include_javadoc: bool = False
707
+ ) -> "TableFormatter":
708
+ """Create table formatter (using new factory)"""
709
+ # Create TableFormatter directly (for JavaDoc support)
710
+ return TableFormatter(format_type, language, include_javadoc)