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