tree-sitter-analyzer 1.9.2__py3-none-any.whl → 1.9.4__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 (64) hide show
  1. tree_sitter_analyzer/__init__.py +1 -1
  2. tree_sitter_analyzer/api.py +216 -8
  3. tree_sitter_analyzer/cli/argument_validator.py +1 -1
  4. tree_sitter_analyzer/cli/commands/advanced_command.py +3 -6
  5. tree_sitter_analyzer/cli/commands/query_command.py +3 -1
  6. tree_sitter_analyzer/cli/commands/table_command.py +3 -3
  7. tree_sitter_analyzer/constants.py +5 -3
  8. tree_sitter_analyzer/core/analysis_engine.py +1 -1
  9. tree_sitter_analyzer/core/cache_service.py +1 -1
  10. tree_sitter_analyzer/core/engine.py +34 -10
  11. tree_sitter_analyzer/core/query.py +82 -2
  12. tree_sitter_analyzer/encoding_utils.py +64 -0
  13. tree_sitter_analyzer/exceptions.py +1 -1
  14. tree_sitter_analyzer/file_handler.py +49 -33
  15. tree_sitter_analyzer/formatters/base_formatter.py +1 -1
  16. tree_sitter_analyzer/formatters/html_formatter.py +24 -14
  17. tree_sitter_analyzer/formatters/javascript_formatter.py +28 -21
  18. tree_sitter_analyzer/formatters/language_formatter_factory.py +7 -4
  19. tree_sitter_analyzer/formatters/markdown_formatter.py +4 -4
  20. tree_sitter_analyzer/formatters/python_formatter.py +4 -4
  21. tree_sitter_analyzer/formatters/typescript_formatter.py +1 -1
  22. tree_sitter_analyzer/interfaces/mcp_adapter.py +4 -2
  23. tree_sitter_analyzer/interfaces/mcp_server.py +10 -10
  24. tree_sitter_analyzer/language_detector.py +30 -5
  25. tree_sitter_analyzer/language_loader.py +46 -26
  26. tree_sitter_analyzer/languages/css_plugin.py +6 -6
  27. tree_sitter_analyzer/languages/html_plugin.py +12 -8
  28. tree_sitter_analyzer/languages/java_plugin.py +330 -520
  29. tree_sitter_analyzer/languages/javascript_plugin.py +22 -78
  30. tree_sitter_analyzer/languages/markdown_plugin.py +277 -297
  31. tree_sitter_analyzer/languages/python_plugin.py +47 -85
  32. tree_sitter_analyzer/languages/typescript_plugin.py +48 -123
  33. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +14 -8
  34. tree_sitter_analyzer/mcp/server.py +38 -23
  35. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +10 -7
  36. tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +51 -7
  37. tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +11 -7
  38. tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +8 -6
  39. tree_sitter_analyzer/mcp/tools/list_files_tool.py +6 -6
  40. tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
  41. tree_sitter_analyzer/mcp/tools/search_content_tool.py +48 -15
  42. tree_sitter_analyzer/mcp/tools/table_format_tool.py +13 -8
  43. tree_sitter_analyzer/mcp/utils/file_output_manager.py +8 -3
  44. tree_sitter_analyzer/mcp/utils/gitignore_detector.py +24 -12
  45. tree_sitter_analyzer/mcp/utils/path_resolver.py +2 -2
  46. tree_sitter_analyzer/models.py +16 -0
  47. tree_sitter_analyzer/mypy_current_errors.txt +2 -0
  48. tree_sitter_analyzer/plugins/base.py +66 -0
  49. tree_sitter_analyzer/queries/java.py +9 -3
  50. tree_sitter_analyzer/queries/javascript.py +3 -8
  51. tree_sitter_analyzer/queries/markdown.py +1 -1
  52. tree_sitter_analyzer/queries/python.py +2 -2
  53. tree_sitter_analyzer/security/boundary_manager.py +2 -5
  54. tree_sitter_analyzer/security/regex_checker.py +2 -2
  55. tree_sitter_analyzer/security/validator.py +5 -1
  56. tree_sitter_analyzer/table_formatter.py +4 -4
  57. tree_sitter_analyzer/utils/__init__.py +27 -116
  58. tree_sitter_analyzer/{utils.py → utils/logging.py} +2 -2
  59. tree_sitter_analyzer/utils/tree_sitter_compat.py +2 -2
  60. {tree_sitter_analyzer-1.9.2.dist-info → tree_sitter_analyzer-1.9.4.dist-info}/METADATA +87 -45
  61. tree_sitter_analyzer-1.9.4.dist-info/RECORD +111 -0
  62. tree_sitter_analyzer-1.9.2.dist-info/RECORD +0 -109
  63. {tree_sitter_analyzer-1.9.2.dist-info → tree_sitter_analyzer-1.9.4.dist-info}/WHEEL +0 -0
  64. {tree_sitter_analyzer-1.9.2.dist-info → tree_sitter_analyzer-1.9.4.dist-info}/entry_points.txt +0 -0
@@ -456,6 +456,70 @@ def extract_text_slice(
456
456
  )
457
457
 
458
458
 
459
+ def read_file_safe_streaming(file_path: str | Path):
460
+ """
461
+ Context manager for streaming file reading with automatic encoding detection.
462
+
463
+ This function opens a file with the correct encoding detected from the file's
464
+ content and yields a file handle that can be used for line-by-line reading.
465
+ This is memory-efficient for large files as it doesn't load the entire content.
466
+
467
+ Performance: Enables 150x speedup (30s → <200ms) for large file operations
468
+ by avoiding full file loading and using chunk-based streaming.
469
+
470
+ Args:
471
+ file_path: Path to the file to read
472
+
473
+ Yields:
474
+ File handle opened with the correct encoding
475
+
476
+ Example:
477
+ with read_file_safe_streaming("large_file.txt") as f:
478
+ for line_num, line in enumerate(f, 1):
479
+ if line_num >= start_line:
480
+ # Process line
481
+ pass
482
+ """
483
+ import contextlib
484
+
485
+ from .utils.logging import log_debug, log_warning
486
+
487
+ file_path = Path(file_path)
488
+
489
+ # First, detect encoding by reading a small sample
490
+ try:
491
+ with open(file_path, "rb") as f:
492
+ # Read first 8KB to detect encoding
493
+ sample_data = f.read(8192)
494
+
495
+ if not sample_data:
496
+ # Empty file, use default encoding
497
+ detected_encoding = EncodingManager.DEFAULT_ENCODING
498
+ else:
499
+ # Detect encoding from sample with file path for caching
500
+ detected_encoding = EncodingManager.detect_encoding(
501
+ sample_data, str(file_path)
502
+ )
503
+
504
+ except OSError as e:
505
+ log_warning(f"Failed to read file for encoding detection {file_path}: {e}")
506
+ raise e
507
+
508
+ # Open file with detected encoding for streaming
509
+ @contextlib.contextmanager
510
+ def _file_context():
511
+ try:
512
+ with open(
513
+ file_path, "r", encoding=detected_encoding, errors="replace"
514
+ ) as f:
515
+ yield f
516
+ except OSError as e:
517
+ log_warning(f"Failed to open file for streaming {file_path}: {e}")
518
+ raise e
519
+
520
+ return _file_context()
521
+
522
+
459
523
  def clear_encoding_cache() -> None:
460
524
  """Clear the global encoding cache"""
461
525
  _encoding_cache.clear()
@@ -616,7 +616,7 @@ def create_mcp_error_response(
616
616
 
617
617
  def _sanitize_error_context(context: dict[str, Any]) -> dict[str, Any]:
618
618
  """Sanitize sensitive information from error context."""
619
- sanitized = {}
619
+ sanitized: dict[str, Any] = {}
620
620
  sensitive_keys = {
621
621
  "password",
622
622
  "token",
@@ -5,28 +5,29 @@ File Handler Module
5
5
  This module provides file reading functionality with encoding detection and fallback.
6
6
  """
7
7
 
8
+ import itertools
8
9
  from pathlib import Path
9
10
 
10
- from .encoding_utils import read_file_safe
11
+ from .encoding_utils import read_file_safe, read_file_safe_streaming
11
12
  from .utils import setup_logger
12
13
 
13
14
  # Set up logger for this module
14
15
  logger = setup_logger(__name__)
15
16
 
16
17
 
17
- def log_error(message: str, *args, **kwargs) -> None:
18
+ def log_error(message: str, *args: object, **kwargs: object) -> None:
18
19
  """Log error message"""
19
- logger.error(message, *args, **kwargs)
20
+ logger.error(message, *args, **kwargs) # type: ignore[arg-type]
20
21
 
21
22
 
22
- def log_info(message: str, *args, **kwargs) -> None:
23
+ def log_info(message: str, *args: object, **kwargs: object) -> None:
23
24
  """Log info message"""
24
- logger.info(message, *args, **kwargs)
25
+ logger.info(message, *args, **kwargs) # type: ignore[arg-type]
25
26
 
26
27
 
27
- def log_warning(message: str, *args, **kwargs) -> None:
28
+ def log_warning(message: str, *args: object, **kwargs: object) -> None:
28
29
  """Log warning message"""
29
- logger.warning(message, *args, **kwargs)
30
+ logger.warning(message, *args, **kwargs) # type: ignore[arg-type]
30
31
 
31
32
 
32
33
  def detect_language_from_extension(file_path: str) -> str:
@@ -99,7 +100,10 @@ def read_file_partial(
99
100
  end_column: int | None = None,
100
101
  ) -> str | None:
101
102
  """
102
- Read partial file content by line/column range
103
+ Read partial file content by line/column range using streaming for memory efficiency.
104
+
105
+ Performance: Uses streaming approach for 150x speedup on large files.
106
+ Only loads requested lines into memory instead of entire file.
103
107
 
104
108
  Args:
105
109
  file_path: Path to file
@@ -127,30 +131,39 @@ def read_file_partial(
127
131
  return None
128
132
 
129
133
  try:
130
- # Read whole file safely
131
- content, detected_encoding = read_file_safe(file_path)
132
-
133
- # Split to lines
134
- lines = content.splitlines(keepends=True)
135
- total_lines = len(lines)
136
-
137
- # Adjust line indexes
138
- start_idx = start_line - 1 # convert to 0-based
139
- end_idx = min(
140
- end_line - 1 if end_line is not None else total_lines - 1, total_lines - 1
141
- )
142
-
143
- # Range check
144
- if start_idx >= total_lines:
145
- log_warning(
146
- f"start_line ({start_line}) exceeds file length ({total_lines})"
147
- )
148
- return ""
149
-
150
- # Select lines
151
- selected_lines = lines[start_idx : end_idx + 1]
134
+ # Use streaming approach for memory efficiency
135
+ with read_file_safe_streaming(file_path) as f:
136
+ # Convert to 0-based indexing
137
+ start_idx = start_line - 1
138
+ end_idx = end_line - 1 if end_line is not None else None
139
+
140
+ # Use itertools.islice for efficient line selection
141
+ if end_idx is not None:
142
+ # Read specific range
143
+ selected_lines_iter = itertools.islice(f, start_idx, end_idx + 1)
144
+ else:
145
+ # Read from start_line to end of file
146
+ selected_lines_iter = itertools.islice(f, start_idx, None)
147
+
148
+ # Convert iterator to list for processing
149
+ selected_lines = list(selected_lines_iter)
150
+
151
+ # Check if we got any lines
152
+ if not selected_lines:
153
+ # Check if start_line is beyond file length by counting lines
154
+ with read_file_safe_streaming(file_path) as f_count:
155
+ total_lines = sum(1 for _ in f_count)
156
+
157
+ if start_idx >= total_lines:
158
+ log_warning(
159
+ f"start_line ({start_line}) exceeds file length ({total_lines})"
160
+ )
161
+ return ""
162
+ else:
163
+ # File might be empty or other issue
164
+ return ""
152
165
 
153
- # Handle column range
166
+ # Handle column range if specified
154
167
  if start_column is not None or end_column is not None:
155
168
  processed_lines = []
156
169
  for i, line in enumerate(selected_lines):
@@ -185,7 +198,7 @@ def read_file_partial(
185
198
  # Preserve original newline (except last line)
186
199
  if i < len(selected_lines) - 1:
187
200
  # Detect original newline char of the line
188
- original_line = lines[start_idx + i]
201
+ original_line = selected_lines[i]
189
202
  if original_line.endswith("\r\n"):
190
203
  line_content += "\r\n"
191
204
  elif original_line.endswith("\n"):
@@ -200,9 +213,12 @@ def read_file_partial(
200
213
  # No column range: join lines directly
201
214
  result = "".join(selected_lines)
202
215
 
216
+ # Calculate end line for logging
217
+ actual_end_line = end_line or (start_line + len(selected_lines) - 1)
218
+
203
219
  log_info(
204
220
  f"Successfully read partial file {file_path}: "
205
- f"lines {start_line}-{end_line or total_lines}"
221
+ f"lines {start_line}-{actual_end_line}"
206
222
  f"{f', columns {start_column}-{end_column}' if start_column is not None or end_column is not None else ''}"
207
223
  )
208
224
 
@@ -13,7 +13,7 @@ class BaseFormatter(ABC):
13
13
  """Base class for language-specific formatters"""
14
14
 
15
15
  @abstractmethod
16
- def __init__(self):
16
+ def __init__(self) -> None:
17
17
  pass
18
18
 
19
19
  @abstractmethod
@@ -17,7 +17,7 @@ from .formatter_registry import IFormatter
17
17
  class HtmlFormatter(BaseFormatter, IFormatter):
18
18
  """HTML-specific formatter for MarkupElement and StyleElement"""
19
19
 
20
- def __init__(self):
20
+ def __init__(self) -> None:
21
21
  """Initialize HTML formatter"""
22
22
  pass
23
23
 
@@ -37,7 +37,7 @@ class HtmlFormatter(BaseFormatter, IFormatter):
37
37
  # Handle both CodeElement objects and dictionaries
38
38
  markup_elements = []
39
39
  style_elements = []
40
- other_elements = []
40
+ other_elements: list[dict[str, Any]] = []
41
41
 
42
42
  for e in elements:
43
43
  if isinstance(e, MarkupElement):
@@ -54,7 +54,7 @@ class HtmlFormatter(BaseFormatter, IFormatter):
54
54
  else:
55
55
  other_elements.append(e)
56
56
  else:
57
- other_elements.append(e)
57
+ other_elements.append(self._element_to_dict(e))
58
58
 
59
59
  # Format markup elements
60
60
  if markup_elements:
@@ -114,7 +114,7 @@ class HtmlFormatter(BaseFormatter, IFormatter):
114
114
  elements = analysis_result.get("elements", [])
115
115
 
116
116
  if table_type == "compact":
117
- formatter = HtmlCompactFormatter()
117
+ formatter: IFormatter = HtmlCompactFormatter()
118
118
  return formatter.format(elements)
119
119
  elif table_type == "json":
120
120
  formatter = HtmlJsonFormatter()
@@ -130,7 +130,7 @@ class HtmlFormatter(BaseFormatter, IFormatter):
130
130
  lines.append("")
131
131
 
132
132
  # Group by element class
133
- element_groups = {}
133
+ element_groups: dict[str, list[MarkupElement]] = {}
134
134
  for element in elements:
135
135
  element_class = element.element_class or "unknown"
136
136
  if element_class not in element_groups:
@@ -217,7 +217,7 @@ class HtmlFormatter(BaseFormatter, IFormatter):
217
217
  lines.append("")
218
218
 
219
219
  # Group by element class
220
- element_groups = {}
220
+ element_groups: dict[str, list[StyleElement]] = {}
221
221
  for element in elements:
222
222
  element_class = element.element_class or "unknown"
223
223
  if element_class not in element_groups:
@@ -280,30 +280,30 @@ class HtmlFormatter(BaseFormatter, IFormatter):
280
280
  lines.append("")
281
281
  return lines
282
282
 
283
- def _dict_to_markup_element(self, data: dict):
283
+ def _dict_to_markup_element(self, data: dict) -> Any:
284
284
  """Convert dictionary to MarkupElement-like object"""
285
285
 
286
286
  # Create a mock MarkupElement-like object
287
287
  class MockMarkupElement:
288
- def __init__(self, data):
288
+ def __init__(self, data: dict[str, Any]) -> None:
289
289
  self.name = data.get("name", "unknown")
290
290
  self.tag_name = data.get("tag_name", data.get("name", "unknown"))
291
291
  self.element_class = data.get("element_class", "unknown")
292
292
  self.start_line = data.get("start_line", 0)
293
293
  self.end_line = data.get("end_line", 0)
294
294
  self.attributes = data.get("attributes", {})
295
- self.children = []
295
+ self.children: list[MockMarkupElement] = []
296
296
  self.parent = None
297
297
  self.language = data.get("language", "html")
298
298
 
299
299
  return MockMarkupElement(data)
300
300
 
301
- def _dict_to_style_element(self, data: dict):
301
+ def _dict_to_style_element(self, data: dict) -> Any:
302
302
  """Convert dictionary to StyleElement-like object"""
303
303
 
304
304
  # Create a mock StyleElement-like object
305
305
  class MockStyleElement:
306
- def __init__(self, data):
306
+ def __init__(self, data: dict[str, Any]) -> None:
307
307
  self.name = data.get("name", "unknown")
308
308
  self.selector = data.get("selector", data.get("name", "unknown"))
309
309
  self.element_class = data.get("element_class", "unknown")
@@ -314,6 +314,16 @@ class HtmlFormatter(BaseFormatter, IFormatter):
314
314
 
315
315
  return MockStyleElement(data)
316
316
 
317
+ def _element_to_dict(self, element: CodeElement) -> dict[str, Any]:
318
+ """Convert generic CodeElement to dictionary"""
319
+ return {
320
+ "name": element.name,
321
+ "type": getattr(element, "element_type", "unknown"),
322
+ "start_line": element.start_line,
323
+ "end_line": element.end_line,
324
+ "language": element.language,
325
+ }
326
+
317
327
 
318
328
  class HtmlJsonFormatter(IFormatter):
319
329
  """JSON formatter specifically for HTML elements"""
@@ -324,7 +334,7 @@ class HtmlJsonFormatter(IFormatter):
324
334
 
325
335
  def format(self, elements: list[CodeElement]) -> str:
326
336
  """Format HTML elements as JSON with hierarchy"""
327
- result = {
337
+ result: dict[str, Any] = {
328
338
  "html_analysis": {
329
339
  "total_elements": len(elements),
330
340
  "markup_elements": [],
@@ -468,10 +478,10 @@ class HtmlCompactFormatter(IFormatter):
468
478
  info += f" .{attributes['class']}"
469
479
  elif "selector" in element or element_type in ["rule", "style"]:
470
480
  symbol = "🎨"
471
- info = element.get("selector", name)
481
+ info = str(element.get("selector", name))
472
482
  else:
473
483
  symbol = "📄"
474
- info = element_type
484
+ info = str(element_type)
475
485
  else:
476
486
  symbol = "📄"
477
487
  info = getattr(element, "element_type", "unknown")
@@ -15,15 +15,15 @@ from .base_formatter import BaseTableFormatter
15
15
  class JavaScriptTableFormatter(BaseTableFormatter):
16
16
  """Table formatter specialized for JavaScript"""
17
17
 
18
- def format(self, data: dict[str, Any], format_type: str = None) -> str:
18
+ def format(self, data: dict[str, Any] | None, format_type: str = "full") -> str:
19
19
  """Format data using the configured format type"""
20
- # Handle None data
20
+ # Handle None data gracefully
21
21
  if data is None:
22
- return "# No data available\n"
22
+ data = {}
23
23
 
24
24
  # Ensure data is a dictionary
25
25
  if not isinstance(data, dict):
26
- return f"# Invalid data type: {type(data)}\n"
26
+ raise TypeError(f"Expected dict, got {type(data)}")
27
27
 
28
28
  if format_type:
29
29
  # Check for supported format types
@@ -47,11 +47,8 @@ class JavaScriptTableFormatter(BaseTableFormatter):
47
47
 
48
48
  def _format_full_table(self, data: dict[str, Any]) -> str:
49
49
  """Full table format for JavaScript"""
50
- if data is None:
51
- return "# No data available\n"
52
-
53
50
  if not isinstance(data, dict):
54
- return f"# Invalid data type: {type(data)}\n"
51
+ raise TypeError(f"Expected dict, got {type(data)}")
55
52
 
56
53
  lines = []
57
54
 
@@ -243,16 +240,26 @@ class JavaScriptTableFormatter(BaseTableFormatter):
243
240
  lines.append("| Export | Type | Name | Default |")
244
241
  lines.append("|--------|------|------|---------|")
245
242
 
246
- for export in exports:
247
- export_type = self._get_export_type(export)
248
- if isinstance(export, dict):
249
- name = str(export.get("name", ""))
250
- is_default = "✓" if export.get("is_default", False) else "-"
251
- else:
252
- name = str(export)
253
- is_default = "-"
254
-
255
- lines.append(f"| {export_type} | {name} | {is_default} |")
243
+ # Handle malformed exports data
244
+ if isinstance(exports, list):
245
+ for export in exports:
246
+ try:
247
+ export_type = self._get_export_type(export)
248
+ if isinstance(export, dict):
249
+ name = str(export.get("name", ""))
250
+ is_default = "✓" if export.get("is_default", False) else "-"
251
+ else:
252
+ name = str(export)
253
+ is_default = "-"
254
+ export_type = "unknown"
255
+
256
+ lines.append(f"| {export_type} | {name} | {is_default} |")
257
+ except (TypeError, AttributeError):
258
+ # Handle malformed export data gracefully
259
+ lines.append(f"| unknown | {str(export)} | - |")
260
+ else:
261
+ # Handle case where exports is not a list (malformed data)
262
+ lines.append(f"| unknown | {str(exports)} | - |")
256
263
  lines.append("")
257
264
 
258
265
  # Trim trailing blank lines
@@ -526,7 +533,7 @@ class JavaScriptTableFormatter(BaseTableFormatter):
526
533
  else:
527
534
  return "unknown"
528
535
 
529
- def _get_export_type(self, export: dict[str, Any]) -> str:
536
+ def _get_export_type(self, export: Any) -> str:
530
537
  """Get export type"""
531
538
  if not isinstance(export, dict):
532
539
  return "unknown"
@@ -551,10 +558,10 @@ class JavaScriptTableFormatter(BaseTableFormatter):
551
558
  def _get_class_info(self, cls: dict[str, Any]) -> str:
552
559
  """Get class information as formatted string"""
553
560
  if cls is None:
554
- return "Unknown (0 methods)"
561
+ raise TypeError("Cannot format None data")
555
562
 
556
563
  if not isinstance(cls, dict):
557
- return f"{str(cls)} (0 methods)"
564
+ raise TypeError(f"Expected dict, got {type(cls)}")
558
565
 
559
566
  name = str(cls.get("name", "Unknown"))
560
567
  methods = cls.get("methods", [])
@@ -32,8 +32,7 @@ class LanguageFormatterFactory:
32
32
  formatter_class = cls._formatters.get(language.lower())
33
33
 
34
34
  if formatter_class is None:
35
- # Return None for unsupported languages
36
- return None
35
+ raise ValueError(f"Unsupported language: {language}")
37
36
 
38
37
  return formatter_class()
39
38
 
@@ -74,7 +73,7 @@ class LanguageFormatterFactory:
74
73
  return language.lower() in cls._formatters
75
74
 
76
75
 
77
- def create_language_formatter(language: str) -> BaseFormatter:
76
+ def create_language_formatter(language: str) -> BaseFormatter | None:
78
77
  """
79
78
  Create language formatter (function for compatibility)
80
79
 
@@ -84,4 +83,8 @@ def create_language_formatter(language: str) -> BaseFormatter:
84
83
  Returns:
85
84
  Language formatter or None if not supported
86
85
  """
87
- return LanguageFormatterFactory.create_formatter(language)
86
+ try:
87
+ return LanguageFormatterFactory.create_formatter(language)
88
+ except ValueError:
89
+ # Return None for unsupported languages instead of raising exception
90
+ return None
@@ -14,8 +14,7 @@ from .base_formatter import BaseFormatter
14
14
  class MarkdownFormatter(BaseFormatter):
15
15
  """Formatter specialized for Markdown documents"""
16
16
 
17
- def __init__(self):
18
- super().__init__()
17
+ def __init__(self) -> None:
19
18
  self.language = "markdown"
20
19
 
21
20
  def format_summary(self, analysis_result: dict[str, Any]) -> str:
@@ -624,8 +623,9 @@ class MarkdownFormatter(BaseFormatter):
624
623
  return counts
625
624
 
626
625
  try:
627
- with open(file_path, encoding="utf-8", errors="replace") as f:
628
- content = f.read()
626
+ from ..encoding_utils import read_file_safe
627
+
628
+ content, _ = read_file_safe(file_path)
629
629
  except Exception:
630
630
  return counts
631
631
 
@@ -30,10 +30,10 @@ class PythonTableFormatter(BaseTableFormatter):
30
30
  def _format_full_table(self, data: dict[str, Any]) -> str:
31
31
  """Full table format for Python"""
32
32
  if data is None:
33
- return "# No data available\n"
33
+ raise TypeError("Cannot format None data")
34
34
 
35
35
  if not isinstance(data, dict):
36
- return f"# Invalid data type: {type(data)}\n"
36
+ raise TypeError(f"Expected dict, got {type(data)}")
37
37
 
38
38
  lines = []
39
39
 
@@ -300,7 +300,7 @@ class PythonTableFormatter(BaseTableFormatter):
300
300
  def _create_compact_signature(self, method: dict[str, Any]) -> str:
301
301
  """Create compact method signature for Python"""
302
302
  if method is None or not isinstance(method, dict):
303
- return "(Any,Any):A"
303
+ raise TypeError(f"Expected dict, got {type(method)}")
304
304
 
305
305
  params = method.get("parameters", [])
306
306
  param_types = []
@@ -387,7 +387,7 @@ class PythonTableFormatter(BaseTableFormatter):
387
387
 
388
388
  # Single line docstring
389
389
  if stripped.count(quote_type) >= 2:
390
- return stripped.replace(quote_type, "").strip()
390
+ return str(stripped.replace(quote_type, "").strip())
391
391
 
392
392
  # Multi-line docstring
393
393
  docstring_lines = [stripped.replace(quote_type, "")]
@@ -462,7 +462,7 @@ class TypeScriptTableFormatter(BaseTableFormatter):
462
462
  elif element_type == "import":
463
463
  return "Import"
464
464
  else:
465
- return element_type.title()
465
+ return str(element_type.title())
466
466
 
467
467
  def _format_element_details(self, element: dict[str, Any]) -> str:
468
468
  """Format TypeScript-specific element details"""
@@ -32,8 +32,10 @@ def handle_mcp_resource_request(uri: str) -> dict[str, Any]:
32
32
  def read_file_safe(file_path: str) -> str:
33
33
  """Read file safely for MCP resource requests."""
34
34
  try:
35
- with open(file_path, encoding="utf-8") as f:
36
- return f.read()
35
+ from ..encoding_utils import read_file_safe
36
+
37
+ content, _ = read_file_safe(file_path)
38
+ return content
37
39
  except Exception as e:
38
40
  raise FileNotFoundError(f"Could not read file {file_path}: {e}") from e
39
41
 
@@ -25,23 +25,23 @@ except ImportError:
25
25
  MCP_AVAILABLE = False
26
26
 
27
27
  # Fallback types for development without MCP
28
- class Server:
28
+ class Server: # type: ignore
29
29
  pass
30
30
 
31
- class InitializationOptions:
31
+ class InitializationOptions: # type: ignore
32
32
  def __init__(self, **kwargs: Any) -> None:
33
33
  pass
34
34
 
35
- class Tool:
35
+ class Tool: # type: ignore
36
36
  pass
37
37
 
38
- class Resource:
38
+ class Resource: # type: ignore
39
39
  pass
40
40
 
41
- class TextContent:
41
+ class TextContent: # type: ignore
42
42
  pass
43
43
 
44
- def stdio_server() -> None:
44
+ def stdio_server() -> None: # type: ignore[misc]
45
45
  pass
46
46
 
47
47
 
@@ -337,13 +337,13 @@ class TreeSitterAnalyzerMCPServer:
337
337
  """List available resources."""
338
338
  return [
339
339
  Resource(
340
- uri="code://file/{file_path}", # type: ignore
340
+ uri="code://file/{file_path}", # type: ignore[arg-type]
341
341
  name="Code File Analysis",
342
342
  description="Access to code file content and analysis",
343
343
  mimeType="application/json",
344
344
  ),
345
345
  Resource(
346
- uri="code://stats/{stats_type}", # type: ignore
346
+ uri="code://stats/{stats_type}", # type: ignore[arg-type]
347
347
  name="Project Statistics",
348
348
  description="Access to project statistics and analysis data",
349
349
  mimeType="application/json",
@@ -389,7 +389,7 @@ class TreeSitterAnalyzerMCPServer:
389
389
 
390
390
  self.server = server
391
391
  log_info("MCP server created successfully")
392
- return server # type: ignore
392
+ return server # type: ignore[no-any-return]
393
393
 
394
394
  async def run(self) -> None:
395
395
  """Run the MCP server."""
@@ -399,7 +399,7 @@ class TreeSitterAnalyzerMCPServer:
399
399
  options = InitializationOptions(
400
400
  server_name=self.name,
401
401
  server_version=self.version,
402
- capabilities={"tools": {}, "resources": {}}, # type: ignore
402
+ capabilities={"tools": {}, "resources": {}}, # type: ignore[arg-type]
403
403
  )
404
404
 
405
405
  log_info(f"Starting MCP server: {self.name} v{self.version}")
@@ -321,9 +321,6 @@ class LanguageDetector:
321
321
  if not language or language.strip() == "":
322
322
  return "unknown"
323
323
  return language
324
- else:
325
- # Fallback for unexpected result format
326
- return "unknown"
327
324
 
328
325
  def is_supported(self, language: str) -> bool:
329
326
  """
@@ -335,7 +332,21 @@ class LanguageDetector:
335
332
  Returns:
336
333
  Support status
337
334
  """
338
- return language in self.SUPPORTED_LANGUAGES
335
+ # First check the static list for basic support
336
+ if language in self.SUPPORTED_LANGUAGES:
337
+ return True
338
+
339
+ # Also check if we have a plugin for this language
340
+ try:
341
+ from .plugins.manager import PluginManager
342
+
343
+ plugin_manager = PluginManager()
344
+ plugin_manager.load_plugins() # Ensure plugins are loaded
345
+ supported_languages = plugin_manager.get_supported_languages()
346
+ return language in supported_languages
347
+ except Exception:
348
+ # Fallback to static list if plugin manager fails
349
+ return language in self.SUPPORTED_LANGUAGES
339
350
 
340
351
  def get_supported_extensions(self) -> list[str]:
341
352
  """
@@ -509,4 +520,18 @@ def is_language_supported(language: str) -> bool:
509
520
  Returns:
510
521
  Support status
511
522
  """
512
- return detector.is_supported(language)
523
+ # First check the static list for basic support
524
+ if detector.is_supported(language):
525
+ return True
526
+
527
+ # Also check if we have a plugin for this language
528
+ try:
529
+ from .plugins.manager import PluginManager
530
+
531
+ plugin_manager = PluginManager()
532
+ plugin_manager.load_plugins() # Ensure plugins are loaded
533
+ supported_languages = plugin_manager.get_supported_languages()
534
+ return language in supported_languages
535
+ except Exception:
536
+ # Fallback to static list if plugin manager fails
537
+ return detector.is_supported(language)