tree-sitter-analyzer 1.9.17.1__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.
Files changed (149) hide show
  1. tree_sitter_analyzer/__init__.py +132 -0
  2. tree_sitter_analyzer/__main__.py +11 -0
  3. tree_sitter_analyzer/api.py +853 -0
  4. tree_sitter_analyzer/cli/__init__.py +39 -0
  5. tree_sitter_analyzer/cli/__main__.py +12 -0
  6. tree_sitter_analyzer/cli/argument_validator.py +89 -0
  7. tree_sitter_analyzer/cli/commands/__init__.py +26 -0
  8. tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
  9. tree_sitter_analyzer/cli/commands/base_command.py +181 -0
  10. tree_sitter_analyzer/cli/commands/default_command.py +18 -0
  11. tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
  12. tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
  13. tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
  14. tree_sitter_analyzer/cli/commands/query_command.py +109 -0
  15. tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
  16. tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
  17. tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
  18. tree_sitter_analyzer/cli/commands/table_command.py +414 -0
  19. tree_sitter_analyzer/cli/info_commands.py +124 -0
  20. tree_sitter_analyzer/cli_main.py +472 -0
  21. tree_sitter_analyzer/constants.py +85 -0
  22. tree_sitter_analyzer/core/__init__.py +15 -0
  23. tree_sitter_analyzer/core/analysis_engine.py +580 -0
  24. tree_sitter_analyzer/core/cache_service.py +333 -0
  25. tree_sitter_analyzer/core/engine.py +585 -0
  26. tree_sitter_analyzer/core/parser.py +293 -0
  27. tree_sitter_analyzer/core/query.py +605 -0
  28. tree_sitter_analyzer/core/query_filter.py +200 -0
  29. tree_sitter_analyzer/core/query_service.py +340 -0
  30. tree_sitter_analyzer/encoding_utils.py +530 -0
  31. tree_sitter_analyzer/exceptions.py +747 -0
  32. tree_sitter_analyzer/file_handler.py +246 -0
  33. tree_sitter_analyzer/formatters/__init__.py +1 -0
  34. tree_sitter_analyzer/formatters/base_formatter.py +201 -0
  35. tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
  36. tree_sitter_analyzer/formatters/formatter_config.py +197 -0
  37. tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
  38. tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
  39. tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
  40. tree_sitter_analyzer/formatters/go_formatter.py +368 -0
  41. tree_sitter_analyzer/formatters/html_formatter.py +498 -0
  42. tree_sitter_analyzer/formatters/java_formatter.py +423 -0
  43. tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
  44. tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
  45. tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
  46. tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
  47. tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
  48. tree_sitter_analyzer/formatters/php_formatter.py +301 -0
  49. tree_sitter_analyzer/formatters/python_formatter.py +830 -0
  50. tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
  51. tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
  52. tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
  53. tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
  54. tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
  55. tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
  56. tree_sitter_analyzer/interfaces/__init__.py +9 -0
  57. tree_sitter_analyzer/interfaces/cli.py +535 -0
  58. tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
  59. tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
  60. tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
  61. tree_sitter_analyzer/language_detector.py +553 -0
  62. tree_sitter_analyzer/language_loader.py +271 -0
  63. tree_sitter_analyzer/languages/__init__.py +10 -0
  64. tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
  65. tree_sitter_analyzer/languages/css_plugin.py +449 -0
  66. tree_sitter_analyzer/languages/go_plugin.py +836 -0
  67. tree_sitter_analyzer/languages/html_plugin.py +496 -0
  68. tree_sitter_analyzer/languages/java_plugin.py +1299 -0
  69. tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
  70. tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
  71. tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
  72. tree_sitter_analyzer/languages/php_plugin.py +862 -0
  73. tree_sitter_analyzer/languages/python_plugin.py +1636 -0
  74. tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
  75. tree_sitter_analyzer/languages/rust_plugin.py +673 -0
  76. tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
  77. tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
  78. tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
  79. tree_sitter_analyzer/legacy_table_formatter.py +860 -0
  80. tree_sitter_analyzer/mcp/__init__.py +34 -0
  81. tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
  82. tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
  83. tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
  84. tree_sitter_analyzer/mcp/server.py +869 -0
  85. tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
  86. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
  87. tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
  88. tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
  89. tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
  90. tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
  91. tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
  92. tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
  93. tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
  94. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
  95. tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
  96. tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
  97. tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
  98. tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
  99. tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
  100. tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
  101. tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
  102. tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
  103. tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
  104. tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
  105. tree_sitter_analyzer/models.py +840 -0
  106. tree_sitter_analyzer/mypy_current_errors.txt +2 -0
  107. tree_sitter_analyzer/output_manager.py +255 -0
  108. tree_sitter_analyzer/platform_compat/__init__.py +3 -0
  109. tree_sitter_analyzer/platform_compat/adapter.py +324 -0
  110. tree_sitter_analyzer/platform_compat/compare.py +224 -0
  111. tree_sitter_analyzer/platform_compat/detector.py +67 -0
  112. tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
  113. tree_sitter_analyzer/platform_compat/profiles.py +217 -0
  114. tree_sitter_analyzer/platform_compat/record.py +55 -0
  115. tree_sitter_analyzer/platform_compat/recorder.py +155 -0
  116. tree_sitter_analyzer/platform_compat/report.py +92 -0
  117. tree_sitter_analyzer/plugins/__init__.py +280 -0
  118. tree_sitter_analyzer/plugins/base.py +647 -0
  119. tree_sitter_analyzer/plugins/manager.py +384 -0
  120. tree_sitter_analyzer/project_detector.py +328 -0
  121. tree_sitter_analyzer/queries/__init__.py +27 -0
  122. tree_sitter_analyzer/queries/csharp.py +216 -0
  123. tree_sitter_analyzer/queries/css.py +615 -0
  124. tree_sitter_analyzer/queries/go.py +275 -0
  125. tree_sitter_analyzer/queries/html.py +543 -0
  126. tree_sitter_analyzer/queries/java.py +402 -0
  127. tree_sitter_analyzer/queries/javascript.py +724 -0
  128. tree_sitter_analyzer/queries/kotlin.py +192 -0
  129. tree_sitter_analyzer/queries/markdown.py +258 -0
  130. tree_sitter_analyzer/queries/php.py +95 -0
  131. tree_sitter_analyzer/queries/python.py +859 -0
  132. tree_sitter_analyzer/queries/ruby.py +92 -0
  133. tree_sitter_analyzer/queries/rust.py +223 -0
  134. tree_sitter_analyzer/queries/sql.py +555 -0
  135. tree_sitter_analyzer/queries/typescript.py +871 -0
  136. tree_sitter_analyzer/queries/yaml.py +236 -0
  137. tree_sitter_analyzer/query_loader.py +272 -0
  138. tree_sitter_analyzer/security/__init__.py +22 -0
  139. tree_sitter_analyzer/security/boundary_manager.py +277 -0
  140. tree_sitter_analyzer/security/regex_checker.py +297 -0
  141. tree_sitter_analyzer/security/validator.py +599 -0
  142. tree_sitter_analyzer/table_formatter.py +782 -0
  143. tree_sitter_analyzer/utils/__init__.py +53 -0
  144. tree_sitter_analyzer/utils/logging.py +433 -0
  145. tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
  146. tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
  147. tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
  148. tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
  149. tree_sitter_analyzer-1.9.17.1.dist-info/entry_points.txt +25 -0
@@ -0,0 +1,53 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Utilities package for tree_sitter_analyzer.
4
+
5
+ This package contains utility modules for various functionality
6
+ including tree-sitter API compatibility and logging.
7
+ """
8
+
9
+ # Import from tree-sitter compatibility module
10
+ # Import logging functions directly from logging module
11
+ from .logging import (
12
+ LoggingContext,
13
+ QuietMode,
14
+ SafeStreamHandler,
15
+ create_performance_logger,
16
+ log_debug,
17
+ log_error,
18
+ log_info,
19
+ log_performance,
20
+ log_warning,
21
+ logger,
22
+ perf_logger,
23
+ safe_print,
24
+ setup_logger,
25
+ setup_performance_logger,
26
+ setup_safe_logging_shutdown,
27
+ suppress_output,
28
+ )
29
+ from .tree_sitter_compat import TreeSitterQueryCompat, get_node_text_safe, log_api_info
30
+
31
+ __all__ = [
32
+ # Tree-sitter compatibility
33
+ "TreeSitterQueryCompat",
34
+ "get_node_text_safe",
35
+ "log_api_info",
36
+ # Logging functionality
37
+ "setup_logger",
38
+ "log_debug",
39
+ "log_error",
40
+ "log_warning",
41
+ "log_info",
42
+ "log_performance",
43
+ "QuietMode",
44
+ "safe_print",
45
+ "LoggingContext",
46
+ "setup_performance_logger",
47
+ "create_performance_logger",
48
+ "SafeStreamHandler",
49
+ "setup_safe_logging_shutdown",
50
+ "suppress_output",
51
+ "logger",
52
+ "perf_logger",
53
+ ]
@@ -0,0 +1,433 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ Utilities for Tree-sitter Analyzer
4
+
5
+ Provides logging, debugging, and common utility functions.
6
+ """
7
+
8
+ import atexit
9
+ import contextlib
10
+ import logging
11
+ import os
12
+ import sys
13
+ import tempfile
14
+ from functools import wraps
15
+ from pathlib import Path
16
+ from typing import Any
17
+
18
+
19
+ # Configure global logger
20
+ def setup_logger(
21
+ name: str = "tree_sitter_analyzer", level: int | str = logging.WARNING
22
+ ) -> logging.Logger:
23
+ """Setup unified logger for the project"""
24
+ # Handle string level parameter
25
+ if isinstance(level, str):
26
+ level_upper = level.upper()
27
+ if level_upper == "DEBUG":
28
+ level = logging.DEBUG
29
+ elif level_upper == "INFO":
30
+ level = logging.INFO
31
+ elif level_upper == "WARNING":
32
+ level = logging.WARNING
33
+ elif level_upper == "ERROR":
34
+ level = logging.ERROR
35
+ else:
36
+ level = logging.WARNING # Default fallback
37
+
38
+ # Get log level from environment variable (only if set and not empty)
39
+ env_level = os.environ.get("LOG_LEVEL", "").upper()
40
+ if env_level and env_level in ["DEBUG", "INFO", "WARNING", "ERROR"]:
41
+ if env_level == "DEBUG":
42
+ level = logging.DEBUG
43
+ elif env_level == "INFO":
44
+ level = logging.INFO
45
+ elif env_level == "WARNING":
46
+ level = logging.WARNING
47
+ elif env_level == "ERROR":
48
+ level = logging.ERROR
49
+ # If env_level is empty or not recognized, use the passed level parameter
50
+
51
+ logger = logging.getLogger(name)
52
+
53
+ # Clear existing handlers if this is a test logger to ensure clean state
54
+ if name.startswith("test_"):
55
+ logger.handlers.clear()
56
+
57
+ # Initialize file logging variables at function scope
58
+ enable_file_log = (
59
+ os.environ.get("TREE_SITTER_ANALYZER_ENABLE_FILE_LOG", "").lower() == "true"
60
+ )
61
+ file_log_level = level # Default to main logger level
62
+
63
+ if not logger.handlers: # Avoid duplicate handlers
64
+ # Create a safe handler that writes to stderr to avoid breaking MCP stdio
65
+ handler = SafeStreamHandler()
66
+ formatter = logging.Formatter(
67
+ "%(asctime)s - %(name)s - %(levelname)s - %(message)s"
68
+ )
69
+ handler.setFormatter(formatter)
70
+ logger.addHandler(handler)
71
+
72
+ # Optional file logging for debugging when launched by clients (e.g., Cursor)
73
+ # This helps diagnose cases where stdio is captured by the client and logs are hidden.
74
+ # Only enabled when TREE_SITTER_ANALYZER_ENABLE_FILE_LOG is set to 'true'
75
+ if enable_file_log:
76
+ try:
77
+ # Determine log directory
78
+ log_dir = os.environ.get("TREE_SITTER_ANALYZER_LOG_DIR")
79
+ if log_dir:
80
+ # Use specified directory
81
+ log_path = Path(log_dir) / "tree_sitter_analyzer.log"
82
+ # Ensure directory exists
83
+ Path(log_dir).mkdir(parents=True, exist_ok=True)
84
+ else:
85
+ # Use system temporary directory
86
+ temp_dir = tempfile.gettempdir()
87
+ log_path = Path(temp_dir) / "tree_sitter_analyzer.log"
88
+
89
+ # Determine file log level
90
+ file_log_level_str = os.environ.get(
91
+ "TREE_SITTER_ANALYZER_FILE_LOG_LEVEL", ""
92
+ ).upper()
93
+ if file_log_level_str and file_log_level_str in [
94
+ "DEBUG",
95
+ "INFO",
96
+ "WARNING",
97
+ "ERROR",
98
+ ]:
99
+ if file_log_level_str == "DEBUG":
100
+ file_log_level = logging.DEBUG
101
+ elif file_log_level_str == "INFO":
102
+ file_log_level = logging.INFO
103
+ elif file_log_level_str == "WARNING":
104
+ file_log_level = logging.WARNING
105
+ elif file_log_level_str == "ERROR":
106
+ file_log_level = logging.ERROR
107
+ else:
108
+ # Use same level as main logger
109
+ file_log_level = level
110
+
111
+ file_handler = logging.FileHandler(str(log_path), encoding="utf-8")
112
+ file_handler.setFormatter(formatter)
113
+ file_handler.setLevel(file_log_level)
114
+ logger.addHandler(file_handler)
115
+
116
+ # Log the file location for debugging purposes
117
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
118
+ with contextlib.suppress(Exception):
119
+ sys.stderr.write(
120
+ f"[logging_setup] File logging enabled: {log_path}\n"
121
+ )
122
+
123
+ except Exception as e:
124
+ # Never let logging configuration break runtime behavior; log to stderr if possible
125
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
126
+ with contextlib.suppress(Exception):
127
+ sys.stderr.write(
128
+ f"[logging_setup] file handler init skipped: {e}\n"
129
+ )
130
+
131
+ # Set the logger level to the minimum of main level and file log level
132
+ # This ensures that all messages that should go to any handler are processed
133
+ final_level = level
134
+ if enable_file_log:
135
+ # Use the minimum level to ensure all messages reach their intended handlers
136
+ final_level = min(level, file_log_level)
137
+
138
+ logger.setLevel(final_level)
139
+
140
+ # For test loggers, ensure they don't inherit from parent and force level
141
+ if logger.name.startswith("test_"):
142
+ logger.propagate = False
143
+ # Force the level setting for test loggers
144
+ logger.level = level
145
+
146
+ return logger
147
+
148
+
149
+ class SafeStreamHandler(logging.StreamHandler):
150
+ """
151
+ A StreamHandler that safely handles closed streams
152
+ """
153
+
154
+ def __init__(self, stream: Any = None) -> None:
155
+ # Default to sys.stderr to keep stdout clean for MCP stdio
156
+ super().__init__(stream if stream is not None else sys.stderr)
157
+
158
+ def emit(self, record: Any) -> None:
159
+ """
160
+ Emit a record, safely handling closed streams and pytest capture
161
+ """
162
+ try:
163
+ # Check if stream is closed before writing
164
+ if hasattr(self.stream, "closed") and self.stream.closed:
165
+ return
166
+
167
+ # Check if we can write to the stream
168
+ if not hasattr(self.stream, "write"):
169
+ return
170
+
171
+ # Special handling for pytest capture scenarios
172
+ # Check if this is a pytest capture stream that might be problematic
173
+ stream_name = getattr(self.stream, "name", "")
174
+ if stream_name is None or "pytest" in str(type(self.stream)).lower():
175
+ # For pytest streams, be extra cautious
176
+ try:
177
+ # Just try to emit without any pre-checks
178
+ super().emit(record)
179
+ return
180
+ except (ValueError, OSError, AttributeError, UnicodeError):
181
+ return
182
+
183
+ # Additional safety checks for stream validity for non-pytest streams
184
+ try:
185
+ # Test if we can actually write to the stream without flushing
186
+ # Avoid flush() as it can cause "I/O operation on closed file" in pytest
187
+ if hasattr(self.stream, "writable") and not self.stream.writable():
188
+ return
189
+ except (ValueError, OSError, AttributeError, UnicodeError):
190
+ return
191
+
192
+ super().emit(record)
193
+ except (ValueError, OSError, AttributeError, UnicodeError):
194
+ # Silently ignore I/O errors during shutdown or pytest capture
195
+ pass # nosec
196
+ except Exception:
197
+ # For any other unexpected errors, silently ignore to prevent test failures
198
+ pass # nosec
199
+
200
+
201
+ def setup_safe_logging_shutdown() -> None:
202
+ """
203
+ Setup safe logging shutdown to prevent I/O errors
204
+ """
205
+
206
+ def cleanup_logging() -> None:
207
+ """Clean up logging handlers safely"""
208
+ try:
209
+ # Get all loggers
210
+ loggers = [logging.getLogger()] + [
211
+ logging.getLogger(name) for name in logging.Logger.manager.loggerDict
212
+ ]
213
+
214
+ for logger in loggers:
215
+ for handler in logger.handlers[:]:
216
+ try:
217
+ handler.close()
218
+ logger.removeHandler(handler)
219
+ except Exception as e:
220
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
221
+ with contextlib.suppress(Exception):
222
+ sys.stderr.write(
223
+ f"[logging_cleanup] handler close/remove skipped: {e}\n"
224
+ )
225
+ except Exception as e:
226
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
227
+ with contextlib.suppress(Exception):
228
+ sys.stderr.write(f"[logging_cleanup] cleanup skipped: {e}\n")
229
+
230
+ # Register cleanup function
231
+ atexit.register(cleanup_logging)
232
+
233
+
234
+ # Setup safe shutdown on import
235
+ setup_safe_logging_shutdown()
236
+
237
+
238
+ # Global logger instance
239
+ logger = setup_logger()
240
+
241
+
242
+ def log_info(message: str, *args: Any, **kwargs: Any) -> None:
243
+ """Log info message"""
244
+ try:
245
+ logger.info(message, *args, **kwargs)
246
+ except (ValueError, OSError) as e:
247
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
248
+ with contextlib.suppress(Exception):
249
+ sys.stderr.write(f"[log_info] suppressed: {e}\n")
250
+
251
+
252
+ def log_warning(message: str, *args: Any, **kwargs: Any) -> None:
253
+ """Log warning message"""
254
+ try:
255
+ logger.warning(message, *args, **kwargs)
256
+ except (ValueError, OSError) as e:
257
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
258
+ with contextlib.suppress(Exception):
259
+ sys.stderr.write(f"[log_warning] suppressed: {e}\n")
260
+
261
+
262
+ def log_error(message: str, *args: Any, **kwargs: Any) -> None:
263
+ """Log error message"""
264
+ try:
265
+ logger.error(message, *args, **kwargs)
266
+ except (ValueError, OSError) as e:
267
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
268
+ with contextlib.suppress(Exception):
269
+ sys.stderr.write(f"[log_error] suppressed: {e}\n")
270
+
271
+
272
+ def log_debug(message: str, *args: Any, **kwargs: Any) -> None:
273
+ """Log debug message"""
274
+ try:
275
+ logger.debug(message, *args, **kwargs)
276
+ except (ValueError, OSError) as e:
277
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
278
+ with contextlib.suppress(Exception):
279
+ sys.stderr.write(f"[log_debug] suppressed: {e}\n")
280
+
281
+
282
+ def suppress_output(func: Any) -> Any:
283
+ """Decorator to suppress print statements in production"""
284
+
285
+ @wraps(func)
286
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
287
+ # Check if we're in test/debug mode
288
+ if getattr(sys, "_testing", False):
289
+ return func(*args, **kwargs)
290
+
291
+ # Redirect stdout to suppress prints
292
+ old_stdout = sys.stdout
293
+ try:
294
+ sys.stdout = (
295
+ open("/dev/null", "w") if sys.platform != "win32" else open("nul", "w")
296
+ )
297
+ result = func(*args, **kwargs)
298
+ finally:
299
+ try:
300
+ sys.stdout.close()
301
+ except Exception as e:
302
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
303
+ with contextlib.suppress(Exception):
304
+ sys.stderr.write(
305
+ f"[suppress_output] stdout close suppressed: {e}\n"
306
+ )
307
+ sys.stdout = old_stdout
308
+
309
+ return result
310
+
311
+ return wrapper
312
+
313
+
314
+ class QuietMode:
315
+ """Context manager for quiet execution"""
316
+
317
+ def __init__(self, enabled: bool = True):
318
+ self.enabled = enabled
319
+ self.old_level: int | None = None
320
+
321
+ def __enter__(self) -> "QuietMode":
322
+ if self.enabled:
323
+ self.old_level = logger.level
324
+ logger.setLevel(logging.ERROR)
325
+ return self
326
+
327
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
328
+ if self.enabled and self.old_level is not None:
329
+ logger.setLevel(self.old_level)
330
+
331
+
332
+ def safe_print(message: str | None, level: str = "info", quiet: bool = False) -> None:
333
+ """Safe print function that can be controlled"""
334
+ if quiet:
335
+ return
336
+
337
+ # Handle None message by converting to string - always call log function even for None
338
+ msg = str(message) if message is not None else "None"
339
+
340
+ # Use dynamic lookup to support mocking
341
+ level_lower = level.lower()
342
+ if level_lower == "info":
343
+ log_info(msg)
344
+ elif level_lower == "warning":
345
+ log_warning(msg)
346
+ elif level_lower == "error":
347
+ log_error(msg)
348
+ elif level_lower == "debug":
349
+ log_debug(msg)
350
+ else:
351
+ log_info(msg) # Default to info
352
+
353
+
354
+ def create_performance_logger(name: str) -> logging.Logger:
355
+ """Create performance-focused logger"""
356
+ perf_logger = logging.getLogger(f"{name}.performance")
357
+
358
+ if not perf_logger.handlers:
359
+ handler = SafeStreamHandler()
360
+ formatter = logging.Formatter("%(asctime)s - PERF - %(message)s")
361
+ handler.setFormatter(formatter)
362
+ perf_logger.addHandler(handler)
363
+ perf_logger.setLevel(logging.DEBUG) # Change to DEBUG level
364
+
365
+ return perf_logger
366
+
367
+
368
+ # Performance logger instance
369
+ perf_logger = create_performance_logger("tree_sitter_analyzer")
370
+
371
+
372
+ def log_performance(
373
+ operation: str,
374
+ execution_time: float | None = None,
375
+ details: dict[Any, Any] | str | None = None,
376
+ ) -> None:
377
+ """Log performance metrics"""
378
+ try:
379
+ message = f"{operation}"
380
+ if execution_time is not None:
381
+ message += f": {execution_time:.4f}s"
382
+ if details:
383
+ if isinstance(details, dict):
384
+ detail_str = ", ".join([f"{k}: {v}" for k, v in details.items()])
385
+ else:
386
+ detail_str = str(details)
387
+ message += f" - {detail_str}"
388
+ perf_logger.debug(message) # Change to DEBUG level
389
+ except (ValueError, OSError) as e:
390
+ if hasattr(sys, "stderr") and hasattr(sys.stderr, "write"):
391
+ with contextlib.suppress(Exception):
392
+ sys.stderr.write(f"[log_performance] suppressed: {e}\n")
393
+
394
+
395
+ def setup_performance_logger() -> logging.Logger:
396
+ """Set up performance logging"""
397
+ perf_logger = logging.getLogger("performance")
398
+
399
+ # Add handler if not already configured
400
+ if not perf_logger.handlers:
401
+ handler = SafeStreamHandler()
402
+ formatter = logging.Formatter("%(asctime)s - Performance - %(message)s")
403
+ handler.setFormatter(formatter)
404
+ perf_logger.addHandler(handler)
405
+ perf_logger.setLevel(logging.INFO)
406
+
407
+ return perf_logger
408
+
409
+
410
+ class LoggingContext:
411
+ """Context manager for controlling logging behavior"""
412
+
413
+ def __init__(self, enabled: bool = True, level: int | None = None):
414
+ self.enabled = enabled
415
+ self.level = level
416
+ self.old_level: int | None = None
417
+ # Use a specific logger name for testing to avoid interference
418
+ self.target_logger = logging.getLogger("tree_sitter_analyzer")
419
+
420
+ def __enter__(self) -> "LoggingContext":
421
+ if self.enabled and self.level is not None:
422
+ # Always save the current level before changing
423
+ self.old_level = self.target_logger.level
424
+ # Ensure we have a valid level to restore to (not NOTSET)
425
+ if self.old_level == logging.NOTSET:
426
+ self.old_level = logging.INFO # Default fallback
427
+ self.target_logger.setLevel(self.level)
428
+ return self
429
+
430
+ def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
431
+ if self.enabled and self.old_level is not None:
432
+ # Always restore the saved level
433
+ self.target_logger.setLevel(self.old_level)