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.
- tree_sitter_analyzer/__init__.py +132 -0
- tree_sitter_analyzer/__main__.py +11 -0
- tree_sitter_analyzer/api.py +853 -0
- tree_sitter_analyzer/cli/__init__.py +39 -0
- tree_sitter_analyzer/cli/__main__.py +12 -0
- tree_sitter_analyzer/cli/argument_validator.py +89 -0
- tree_sitter_analyzer/cli/commands/__init__.py +26 -0
- tree_sitter_analyzer/cli/commands/advanced_command.py +226 -0
- tree_sitter_analyzer/cli/commands/base_command.py +181 -0
- tree_sitter_analyzer/cli/commands/default_command.py +18 -0
- tree_sitter_analyzer/cli/commands/find_and_grep_cli.py +188 -0
- tree_sitter_analyzer/cli/commands/list_files_cli.py +133 -0
- tree_sitter_analyzer/cli/commands/partial_read_command.py +139 -0
- tree_sitter_analyzer/cli/commands/query_command.py +109 -0
- tree_sitter_analyzer/cli/commands/search_content_cli.py +161 -0
- tree_sitter_analyzer/cli/commands/structure_command.py +156 -0
- tree_sitter_analyzer/cli/commands/summary_command.py +116 -0
- tree_sitter_analyzer/cli/commands/table_command.py +414 -0
- tree_sitter_analyzer/cli/info_commands.py +124 -0
- tree_sitter_analyzer/cli_main.py +472 -0
- tree_sitter_analyzer/constants.py +85 -0
- tree_sitter_analyzer/core/__init__.py +15 -0
- tree_sitter_analyzer/core/analysis_engine.py +580 -0
- tree_sitter_analyzer/core/cache_service.py +333 -0
- tree_sitter_analyzer/core/engine.py +585 -0
- tree_sitter_analyzer/core/parser.py +293 -0
- tree_sitter_analyzer/core/query.py +605 -0
- tree_sitter_analyzer/core/query_filter.py +200 -0
- tree_sitter_analyzer/core/query_service.py +340 -0
- tree_sitter_analyzer/encoding_utils.py +530 -0
- tree_sitter_analyzer/exceptions.py +747 -0
- tree_sitter_analyzer/file_handler.py +246 -0
- tree_sitter_analyzer/formatters/__init__.py +1 -0
- tree_sitter_analyzer/formatters/base_formatter.py +201 -0
- tree_sitter_analyzer/formatters/csharp_formatter.py +367 -0
- tree_sitter_analyzer/formatters/formatter_config.py +197 -0
- tree_sitter_analyzer/formatters/formatter_factory.py +84 -0
- tree_sitter_analyzer/formatters/formatter_registry.py +377 -0
- tree_sitter_analyzer/formatters/formatter_selector.py +96 -0
- tree_sitter_analyzer/formatters/go_formatter.py +368 -0
- tree_sitter_analyzer/formatters/html_formatter.py +498 -0
- tree_sitter_analyzer/formatters/java_formatter.py +423 -0
- tree_sitter_analyzer/formatters/javascript_formatter.py +611 -0
- tree_sitter_analyzer/formatters/kotlin_formatter.py +268 -0
- tree_sitter_analyzer/formatters/language_formatter_factory.py +123 -0
- tree_sitter_analyzer/formatters/legacy_formatter_adapters.py +228 -0
- tree_sitter_analyzer/formatters/markdown_formatter.py +725 -0
- tree_sitter_analyzer/formatters/php_formatter.py +301 -0
- tree_sitter_analyzer/formatters/python_formatter.py +830 -0
- tree_sitter_analyzer/formatters/ruby_formatter.py +278 -0
- tree_sitter_analyzer/formatters/rust_formatter.py +233 -0
- tree_sitter_analyzer/formatters/sql_formatter_wrapper.py +689 -0
- tree_sitter_analyzer/formatters/sql_formatters.py +536 -0
- tree_sitter_analyzer/formatters/typescript_formatter.py +543 -0
- tree_sitter_analyzer/formatters/yaml_formatter.py +462 -0
- tree_sitter_analyzer/interfaces/__init__.py +9 -0
- tree_sitter_analyzer/interfaces/cli.py +535 -0
- tree_sitter_analyzer/interfaces/cli_adapter.py +359 -0
- tree_sitter_analyzer/interfaces/mcp_adapter.py +224 -0
- tree_sitter_analyzer/interfaces/mcp_server.py +428 -0
- tree_sitter_analyzer/language_detector.py +553 -0
- tree_sitter_analyzer/language_loader.py +271 -0
- tree_sitter_analyzer/languages/__init__.py +10 -0
- tree_sitter_analyzer/languages/csharp_plugin.py +1076 -0
- tree_sitter_analyzer/languages/css_plugin.py +449 -0
- tree_sitter_analyzer/languages/go_plugin.py +836 -0
- tree_sitter_analyzer/languages/html_plugin.py +496 -0
- tree_sitter_analyzer/languages/java_plugin.py +1299 -0
- tree_sitter_analyzer/languages/javascript_plugin.py +1622 -0
- tree_sitter_analyzer/languages/kotlin_plugin.py +656 -0
- tree_sitter_analyzer/languages/markdown_plugin.py +1928 -0
- tree_sitter_analyzer/languages/php_plugin.py +862 -0
- tree_sitter_analyzer/languages/python_plugin.py +1636 -0
- tree_sitter_analyzer/languages/ruby_plugin.py +757 -0
- tree_sitter_analyzer/languages/rust_plugin.py +673 -0
- tree_sitter_analyzer/languages/sql_plugin.py +2444 -0
- tree_sitter_analyzer/languages/typescript_plugin.py +1892 -0
- tree_sitter_analyzer/languages/yaml_plugin.py +695 -0
- tree_sitter_analyzer/legacy_table_formatter.py +860 -0
- tree_sitter_analyzer/mcp/__init__.py +34 -0
- tree_sitter_analyzer/mcp/resources/__init__.py +43 -0
- tree_sitter_analyzer/mcp/resources/code_file_resource.py +208 -0
- tree_sitter_analyzer/mcp/resources/project_stats_resource.py +586 -0
- tree_sitter_analyzer/mcp/server.py +869 -0
- tree_sitter_analyzer/mcp/tools/__init__.py +28 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +779 -0
- tree_sitter_analyzer/mcp/tools/analyze_scale_tool_cli_compatible.py +291 -0
- tree_sitter_analyzer/mcp/tools/base_tool.py +139 -0
- tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +816 -0
- tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +686 -0
- tree_sitter_analyzer/mcp/tools/list_files_tool.py +413 -0
- tree_sitter_analyzer/mcp/tools/output_format_validator.py +148 -0
- tree_sitter_analyzer/mcp/tools/query_tool.py +443 -0
- tree_sitter_analyzer/mcp/tools/read_partial_tool.py +464 -0
- tree_sitter_analyzer/mcp/tools/search_content_tool.py +836 -0
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +572 -0
- tree_sitter_analyzer/mcp/tools/universal_analyze_tool.py +653 -0
- tree_sitter_analyzer/mcp/utils/__init__.py +113 -0
- tree_sitter_analyzer/mcp/utils/error_handler.py +569 -0
- tree_sitter_analyzer/mcp/utils/file_output_factory.py +217 -0
- tree_sitter_analyzer/mcp/utils/file_output_manager.py +322 -0
- tree_sitter_analyzer/mcp/utils/gitignore_detector.py +358 -0
- tree_sitter_analyzer/mcp/utils/path_resolver.py +414 -0
- tree_sitter_analyzer/mcp/utils/search_cache.py +343 -0
- tree_sitter_analyzer/models.py +840 -0
- tree_sitter_analyzer/mypy_current_errors.txt +2 -0
- tree_sitter_analyzer/output_manager.py +255 -0
- tree_sitter_analyzer/platform_compat/__init__.py +3 -0
- tree_sitter_analyzer/platform_compat/adapter.py +324 -0
- tree_sitter_analyzer/platform_compat/compare.py +224 -0
- tree_sitter_analyzer/platform_compat/detector.py +67 -0
- tree_sitter_analyzer/platform_compat/fixtures.py +228 -0
- tree_sitter_analyzer/platform_compat/profiles.py +217 -0
- tree_sitter_analyzer/platform_compat/record.py +55 -0
- tree_sitter_analyzer/platform_compat/recorder.py +155 -0
- tree_sitter_analyzer/platform_compat/report.py +92 -0
- tree_sitter_analyzer/plugins/__init__.py +280 -0
- tree_sitter_analyzer/plugins/base.py +647 -0
- tree_sitter_analyzer/plugins/manager.py +384 -0
- tree_sitter_analyzer/project_detector.py +328 -0
- tree_sitter_analyzer/queries/__init__.py +27 -0
- tree_sitter_analyzer/queries/csharp.py +216 -0
- tree_sitter_analyzer/queries/css.py +615 -0
- tree_sitter_analyzer/queries/go.py +275 -0
- tree_sitter_analyzer/queries/html.py +543 -0
- tree_sitter_analyzer/queries/java.py +402 -0
- tree_sitter_analyzer/queries/javascript.py +724 -0
- tree_sitter_analyzer/queries/kotlin.py +192 -0
- tree_sitter_analyzer/queries/markdown.py +258 -0
- tree_sitter_analyzer/queries/php.py +95 -0
- tree_sitter_analyzer/queries/python.py +859 -0
- tree_sitter_analyzer/queries/ruby.py +92 -0
- tree_sitter_analyzer/queries/rust.py +223 -0
- tree_sitter_analyzer/queries/sql.py +555 -0
- tree_sitter_analyzer/queries/typescript.py +871 -0
- tree_sitter_analyzer/queries/yaml.py +236 -0
- tree_sitter_analyzer/query_loader.py +272 -0
- tree_sitter_analyzer/security/__init__.py +22 -0
- tree_sitter_analyzer/security/boundary_manager.py +277 -0
- tree_sitter_analyzer/security/regex_checker.py +297 -0
- tree_sitter_analyzer/security/validator.py +599 -0
- tree_sitter_analyzer/table_formatter.py +782 -0
- tree_sitter_analyzer/utils/__init__.py +53 -0
- tree_sitter_analyzer/utils/logging.py +433 -0
- tree_sitter_analyzer/utils/tree_sitter_compat.py +289 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/METADATA +485 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/RECORD +149 -0
- tree_sitter_analyzer-1.9.17.1.dist-info/WHEEL +4 -0
- 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)
|