tree-sitter-analyzer 1.7.7__py3-none-any.whl → 1.8.2__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.
- tree_sitter_analyzer/__init__.py +1 -1
- tree_sitter_analyzer/api.py +23 -30
- tree_sitter_analyzer/cli/argument_validator.py +77 -0
- tree_sitter_analyzer/cli/commands/table_command.py +7 -2
- tree_sitter_analyzer/cli_main.py +17 -3
- tree_sitter_analyzer/core/cache_service.py +15 -5
- tree_sitter_analyzer/core/query.py +33 -22
- tree_sitter_analyzer/core/query_service.py +179 -154
- tree_sitter_analyzer/formatters/formatter_registry.py +355 -0
- tree_sitter_analyzer/formatters/html_formatter.py +462 -0
- tree_sitter_analyzer/formatters/language_formatter_factory.py +3 -0
- tree_sitter_analyzer/formatters/markdown_formatter.py +1 -1
- tree_sitter_analyzer/language_detector.py +80 -7
- tree_sitter_analyzer/languages/css_plugin.py +390 -0
- tree_sitter_analyzer/languages/html_plugin.py +395 -0
- tree_sitter_analyzer/languages/java_plugin.py +116 -0
- tree_sitter_analyzer/languages/javascript_plugin.py +113 -0
- tree_sitter_analyzer/languages/markdown_plugin.py +266 -46
- tree_sitter_analyzer/languages/python_plugin.py +176 -33
- tree_sitter_analyzer/languages/typescript_plugin.py +130 -1
- tree_sitter_analyzer/mcp/tools/query_tool.py +99 -58
- tree_sitter_analyzer/mcp/tools/table_format_tool.py +24 -10
- tree_sitter_analyzer/models.py +53 -0
- tree_sitter_analyzer/output_manager.py +1 -1
- tree_sitter_analyzer/plugins/base.py +50 -0
- tree_sitter_analyzer/plugins/manager.py +5 -1
- tree_sitter_analyzer/queries/css.py +634 -0
- tree_sitter_analyzer/queries/html.py +556 -0
- tree_sitter_analyzer/queries/markdown.py +54 -164
- tree_sitter_analyzer/query_loader.py +16 -3
- tree_sitter_analyzer/security/validator.py +182 -44
- tree_sitter_analyzer/utils/__init__.py +113 -0
- tree_sitter_analyzer/utils/tree_sitter_compat.py +282 -0
- tree_sitter_analyzer/utils.py +62 -24
- {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/METADATA +120 -14
- {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/RECORD +38 -29
- {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/entry_points.txt +2 -0
- {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/WHEEL +0 -0
|
@@ -0,0 +1,282 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
Tree-sitter API Utilities
|
|
4
|
+
|
|
5
|
+
This module provides utilities for tree-sitter query execution using the modern API.
|
|
6
|
+
Supports tree-sitter 0.20+ with query.matches() method only.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from typing import Any, List, Tuple, Optional
|
|
11
|
+
|
|
12
|
+
logger = logging.getLogger(__name__)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TreeSitterQueryCompat:
|
|
16
|
+
"""
|
|
17
|
+
Tree-sitter query execution wrapper for modern API.
|
|
18
|
+
|
|
19
|
+
Uses only the modern tree-sitter API (query.matches()).
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
@staticmethod
|
|
23
|
+
def execute_query(
|
|
24
|
+
language: Any,
|
|
25
|
+
query_string: str,
|
|
26
|
+
root_node: Any
|
|
27
|
+
) -> List[Tuple[Any, str]]:
|
|
28
|
+
"""
|
|
29
|
+
Execute a tree-sitter query using the modern API.
|
|
30
|
+
|
|
31
|
+
Args:
|
|
32
|
+
language: Tree-sitter language object
|
|
33
|
+
query_string: Query string to execute
|
|
34
|
+
root_node: Root node to query against
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
List of (node, capture_name) tuples
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
Exception: If query execution fails
|
|
41
|
+
"""
|
|
42
|
+
try:
|
|
43
|
+
import tree_sitter
|
|
44
|
+
query = tree_sitter.Query(language, query_string)
|
|
45
|
+
|
|
46
|
+
# Try newest API first (tree-sitter 0.25+) with QueryCursor
|
|
47
|
+
if hasattr(tree_sitter, 'QueryCursor'):
|
|
48
|
+
logger.debug("Using newest tree-sitter API (QueryCursor)")
|
|
49
|
+
return TreeSitterQueryCompat._execute_newest_api(query, root_node)
|
|
50
|
+
# Try modern API (tree-sitter 0.20+)
|
|
51
|
+
elif hasattr(query, 'matches'):
|
|
52
|
+
logger.debug("Using modern tree-sitter API (matches)")
|
|
53
|
+
return TreeSitterQueryCompat._execute_modern_api(query, root_node)
|
|
54
|
+
# Fall back to legacy API (tree-sitter < 0.20)
|
|
55
|
+
elif hasattr(query, 'captures'):
|
|
56
|
+
logger.debug("Using legacy tree-sitter API (captures)")
|
|
57
|
+
return TreeSitterQueryCompat._execute_legacy_api(query, root_node)
|
|
58
|
+
# Try very old API with different method signature
|
|
59
|
+
else:
|
|
60
|
+
logger.debug("Using very old tree-sitter API (direct query)")
|
|
61
|
+
return TreeSitterQueryCompat._execute_old_api(query, root_node)
|
|
62
|
+
|
|
63
|
+
except Exception as e:
|
|
64
|
+
logger.error(f"Tree-sitter query execution failed: {e}")
|
|
65
|
+
# Return empty result instead of raising to prevent complete failure
|
|
66
|
+
logger.debug("Returning empty result due to query execution failure")
|
|
67
|
+
return []
|
|
68
|
+
|
|
69
|
+
@staticmethod
|
|
70
|
+
def _execute_newest_api(query: Any, root_node: Any) -> List[Tuple[Any, str]]:
|
|
71
|
+
"""Execute query using newest API (tree-sitter 0.25+) with QueryCursor"""
|
|
72
|
+
captures = []
|
|
73
|
+
try:
|
|
74
|
+
import tree_sitter
|
|
75
|
+
cursor = tree_sitter.QueryCursor(query)
|
|
76
|
+
|
|
77
|
+
# Execute query and get matches
|
|
78
|
+
matches = cursor.matches(root_node)
|
|
79
|
+
# matches is a list of tuples: (pattern_index, captures_dict)
|
|
80
|
+
for pattern_index, captures_dict in matches:
|
|
81
|
+
# captures_dict is {capture_name: [node1, node2, ...]}
|
|
82
|
+
for capture_name, nodes in captures_dict.items():
|
|
83
|
+
for node in nodes:
|
|
84
|
+
captures.append((node, capture_name))
|
|
85
|
+
|
|
86
|
+
except Exception as e:
|
|
87
|
+
logger.error(f"Newest API execution failed: {e}")
|
|
88
|
+
# Don't raise, just return empty result
|
|
89
|
+
|
|
90
|
+
return captures
|
|
91
|
+
|
|
92
|
+
@staticmethod
|
|
93
|
+
def _execute_modern_api(query: Any, root_node: Any) -> List[Tuple[Any, str]]:
|
|
94
|
+
"""Execute query using modern API (tree-sitter 0.20+)"""
|
|
95
|
+
captures = []
|
|
96
|
+
try:
|
|
97
|
+
matches = query.matches(root_node)
|
|
98
|
+
for match in matches:
|
|
99
|
+
for capture in match.captures:
|
|
100
|
+
capture_name = query.capture_names[capture.index]
|
|
101
|
+
captures.append((capture.node, capture_name))
|
|
102
|
+
except Exception as e:
|
|
103
|
+
logger.error(f"Modern API execution failed: {e}")
|
|
104
|
+
raise
|
|
105
|
+
return captures
|
|
106
|
+
|
|
107
|
+
@staticmethod
|
|
108
|
+
def _execute_legacy_api(query: Any, root_node: Any) -> List[Tuple[Any, str]]:
|
|
109
|
+
"""Execute query using legacy API (tree-sitter < 0.20)"""
|
|
110
|
+
captures = []
|
|
111
|
+
try:
|
|
112
|
+
# Use the legacy captures method
|
|
113
|
+
query_captures = query.captures(root_node)
|
|
114
|
+
for node, capture_name in query_captures:
|
|
115
|
+
captures.append((node, capture_name))
|
|
116
|
+
except Exception as e:
|
|
117
|
+
logger.error(f"Legacy API execution failed: {e}")
|
|
118
|
+
raise
|
|
119
|
+
return captures
|
|
120
|
+
|
|
121
|
+
@staticmethod
|
|
122
|
+
def _execute_old_api(query: Any, root_node: Any) -> List[Tuple[Any, str]]:
|
|
123
|
+
"""Execute query using very old API (tree-sitter < 0.19)"""
|
|
124
|
+
captures = []
|
|
125
|
+
try:
|
|
126
|
+
# Try different old API patterns
|
|
127
|
+
if hasattr(query, '__call__'):
|
|
128
|
+
# Some very old versions had callable queries
|
|
129
|
+
query_result = query(root_node)
|
|
130
|
+
if isinstance(query_result, list):
|
|
131
|
+
for item in query_result:
|
|
132
|
+
if isinstance(item, tuple) and len(item) >= 2:
|
|
133
|
+
captures.append((item[0], str(item[1])))
|
|
134
|
+
elif hasattr(item, 'node') and hasattr(item, 'name'):
|
|
135
|
+
captures.append((item.node, item.name))
|
|
136
|
+
else:
|
|
137
|
+
# If no known API is available, return empty result
|
|
138
|
+
logger.warning("No compatible tree-sitter query API found, returning empty result")
|
|
139
|
+
|
|
140
|
+
except Exception as e:
|
|
141
|
+
logger.error(f"Old API execution failed: {e}")
|
|
142
|
+
# Don't raise, just return empty result
|
|
143
|
+
|
|
144
|
+
return captures
|
|
145
|
+
|
|
146
|
+
@staticmethod
|
|
147
|
+
def safe_execute_query(
|
|
148
|
+
language: Any,
|
|
149
|
+
query_string: str,
|
|
150
|
+
root_node: Any,
|
|
151
|
+
fallback_result: Optional[List[Tuple[Any, str]]] = None
|
|
152
|
+
) -> List[Tuple[Any, str]]:
|
|
153
|
+
"""
|
|
154
|
+
Safely execute a query with fallback handling.
|
|
155
|
+
|
|
156
|
+
Args:
|
|
157
|
+
language: Tree-sitter language object
|
|
158
|
+
query_string: Query string to execute
|
|
159
|
+
root_node: Root node to query against
|
|
160
|
+
fallback_result: Result to return if query fails
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
List of (node, capture_name) tuples or fallback_result
|
|
164
|
+
"""
|
|
165
|
+
try:
|
|
166
|
+
return TreeSitterQueryCompat.execute_query(language, query_string, root_node)
|
|
167
|
+
except Exception as e:
|
|
168
|
+
logger.debug(f"Query execution failed, using fallback: {e}")
|
|
169
|
+
return fallback_result or []
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def create_query_safely(language: Any, query_string: str) -> Optional[Any]:
|
|
173
|
+
"""
|
|
174
|
+
Safely create a tree-sitter query object.
|
|
175
|
+
|
|
176
|
+
Args:
|
|
177
|
+
language: Tree-sitter language object
|
|
178
|
+
query_string: Query string
|
|
179
|
+
|
|
180
|
+
Returns:
|
|
181
|
+
Query object or None if creation fails
|
|
182
|
+
"""
|
|
183
|
+
try:
|
|
184
|
+
import tree_sitter
|
|
185
|
+
return tree_sitter.Query(language, query_string)
|
|
186
|
+
except Exception as e:
|
|
187
|
+
logger.debug(f"Query creation failed: {e}")
|
|
188
|
+
return None
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
def get_node_text_safe(node: Any, source_code: str, encoding: str = "utf-8") -> str:
|
|
192
|
+
"""
|
|
193
|
+
Safely extract text from a tree-sitter node.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
node: Tree-sitter node
|
|
197
|
+
source_code: Source code string
|
|
198
|
+
encoding: Text encoding
|
|
199
|
+
|
|
200
|
+
Returns:
|
|
201
|
+
Node text or empty string if extraction fails
|
|
202
|
+
"""
|
|
203
|
+
try:
|
|
204
|
+
# Try byte-based extraction first
|
|
205
|
+
if hasattr(node, 'start_byte') and hasattr(node, 'end_byte'):
|
|
206
|
+
start_byte = node.start_byte
|
|
207
|
+
end_byte = node.end_byte
|
|
208
|
+
source_bytes = source_code.encode(encoding)
|
|
209
|
+
if start_byte <= end_byte <= len(source_bytes):
|
|
210
|
+
return source_bytes[start_byte:end_byte].decode(encoding, errors='replace')
|
|
211
|
+
|
|
212
|
+
# Fall back to node.text if available
|
|
213
|
+
if hasattr(node, 'text') and node.text:
|
|
214
|
+
if isinstance(node.text, bytes):
|
|
215
|
+
return node.text.decode(encoding, errors='replace')
|
|
216
|
+
else:
|
|
217
|
+
return str(node.text)
|
|
218
|
+
|
|
219
|
+
# Fall back to point-based extraction
|
|
220
|
+
if hasattr(node, 'start_point') and hasattr(node, 'end_point'):
|
|
221
|
+
start_point = node.start_point
|
|
222
|
+
end_point = node.end_point
|
|
223
|
+
lines = source_code.split('\n')
|
|
224
|
+
|
|
225
|
+
if start_point[0] < len(lines) and end_point[0] < len(lines):
|
|
226
|
+
if start_point[0] == end_point[0]:
|
|
227
|
+
# Single line
|
|
228
|
+
line = lines[start_point[0]]
|
|
229
|
+
start_col = max(0, min(start_point[1], len(line)))
|
|
230
|
+
end_col = max(start_col, min(end_point[1], len(line)))
|
|
231
|
+
return line[start_col:end_col]
|
|
232
|
+
else:
|
|
233
|
+
# Multiple lines
|
|
234
|
+
result_lines = []
|
|
235
|
+
for i in range(start_point[0], min(end_point[0] + 1, len(lines))):
|
|
236
|
+
line = lines[i]
|
|
237
|
+
if i == start_point[0]:
|
|
238
|
+
start_col = max(0, min(start_point[1], len(line)))
|
|
239
|
+
result_lines.append(line[start_col:])
|
|
240
|
+
elif i == end_point[0]:
|
|
241
|
+
end_col = max(0, min(end_point[1], len(line)))
|
|
242
|
+
result_lines.append(line[:end_col])
|
|
243
|
+
else:
|
|
244
|
+
result_lines.append(line)
|
|
245
|
+
return '\n'.join(result_lines)
|
|
246
|
+
|
|
247
|
+
return ""
|
|
248
|
+
|
|
249
|
+
except Exception as e:
|
|
250
|
+
logger.debug(f"Node text extraction failed: {e}")
|
|
251
|
+
return ""
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
def log_api_info():
|
|
255
|
+
"""Log information about available tree-sitter APIs."""
|
|
256
|
+
try:
|
|
257
|
+
import tree_sitter
|
|
258
|
+
logger.debug("Tree-sitter library available")
|
|
259
|
+
|
|
260
|
+
# Check available APIs
|
|
261
|
+
try:
|
|
262
|
+
# Create a dummy query to test available methods
|
|
263
|
+
dummy_lang = None
|
|
264
|
+
dummy_query_str = "(identifier) @name"
|
|
265
|
+
|
|
266
|
+
# We can't actually test without a language, so just check the class
|
|
267
|
+
query_class = tree_sitter.Query
|
|
268
|
+
has_matches = 'matches' in dir(query_class)
|
|
269
|
+
has_captures = 'captures' in dir(query_class)
|
|
270
|
+
|
|
271
|
+
if has_matches:
|
|
272
|
+
logger.debug("Tree-sitter modern API (matches) available")
|
|
273
|
+
elif has_captures:
|
|
274
|
+
logger.debug("Tree-sitter legacy API (captures) available")
|
|
275
|
+
else:
|
|
276
|
+
logger.warning("No compatible tree-sitter API found")
|
|
277
|
+
|
|
278
|
+
except Exception as e:
|
|
279
|
+
logger.debug(f"API detection failed: {e}")
|
|
280
|
+
|
|
281
|
+
except ImportError:
|
|
282
|
+
logger.debug("Tree-sitter library not available")
|
tree_sitter_analyzer/utils.py
CHANGED
|
@@ -15,21 +15,41 @@ from typing import Any
|
|
|
15
15
|
|
|
16
16
|
# Configure global logger
|
|
17
17
|
def setup_logger(
|
|
18
|
-
name: str = "tree_sitter_analyzer", level: int = logging.WARNING
|
|
18
|
+
name: str = "tree_sitter_analyzer", level: int | str = logging.WARNING
|
|
19
19
|
) -> logging.Logger:
|
|
20
20
|
"""Setup unified logger for the project"""
|
|
21
|
-
#
|
|
21
|
+
# Handle string level parameter
|
|
22
|
+
if isinstance(level, str):
|
|
23
|
+
level_upper = level.upper()
|
|
24
|
+
if level_upper == "DEBUG":
|
|
25
|
+
level = logging.DEBUG
|
|
26
|
+
elif level_upper == "INFO":
|
|
27
|
+
level = logging.INFO
|
|
28
|
+
elif level_upper == "WARNING":
|
|
29
|
+
level = logging.WARNING
|
|
30
|
+
elif level_upper == "ERROR":
|
|
31
|
+
level = logging.ERROR
|
|
32
|
+
else:
|
|
33
|
+
level = logging.WARNING # Default fallback
|
|
34
|
+
|
|
35
|
+
# Get log level from environment variable (only if set and not empty)
|
|
22
36
|
env_level = os.environ.get("LOG_LEVEL", "").upper()
|
|
23
|
-
if env_level
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
37
|
+
if env_level and env_level in ["DEBUG", "INFO", "WARNING", "ERROR"]:
|
|
38
|
+
if env_level == "DEBUG":
|
|
39
|
+
level = logging.DEBUG
|
|
40
|
+
elif env_level == "INFO":
|
|
41
|
+
level = logging.INFO
|
|
42
|
+
elif env_level == "WARNING":
|
|
43
|
+
level = logging.WARNING
|
|
44
|
+
elif env_level == "ERROR":
|
|
45
|
+
level = logging.ERROR
|
|
46
|
+
# If env_level is empty or not recognized, use the passed level parameter
|
|
31
47
|
|
|
32
48
|
logger = logging.getLogger(name)
|
|
49
|
+
|
|
50
|
+
# Clear existing handlers if this is a test logger to ensure clean state
|
|
51
|
+
if name.startswith("test_"):
|
|
52
|
+
logger.handlers.clear()
|
|
33
53
|
|
|
34
54
|
if not logger.handlers: # Avoid duplicate handlers
|
|
35
55
|
# Create a safe handler that writes to stderr to avoid breaking MCP stdio
|
|
@@ -58,7 +78,15 @@ def setup_logger(
|
|
|
58
78
|
except Exception:
|
|
59
79
|
...
|
|
60
80
|
|
|
61
|
-
|
|
81
|
+
# Always set the level, even if handlers already exist
|
|
82
|
+
# Ensure the level is properly set, not inherited
|
|
83
|
+
logger.setLevel(level)
|
|
84
|
+
|
|
85
|
+
# For test loggers, ensure they don't inherit from parent and force level
|
|
86
|
+
if logger.name.startswith("test_"):
|
|
87
|
+
logger.propagate = False
|
|
88
|
+
# Force the level setting for test loggers
|
|
89
|
+
logger.level = level
|
|
62
90
|
|
|
63
91
|
return logger
|
|
64
92
|
|
|
@@ -260,20 +288,26 @@ class QuietMode:
|
|
|
260
288
|
logger.setLevel(self.old_level)
|
|
261
289
|
|
|
262
290
|
|
|
263
|
-
def safe_print(message: str, level: str = "info", quiet: bool = False) -> None:
|
|
291
|
+
def safe_print(message: str | None, level: str = "info", quiet: bool = False) -> None:
|
|
264
292
|
"""Safe print function that can be controlled"""
|
|
265
293
|
if quiet:
|
|
266
294
|
return
|
|
267
295
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
296
|
+
# Handle None message by converting to string - always call log function even for None
|
|
297
|
+
msg = str(message) if message is not None else "None"
|
|
298
|
+
|
|
299
|
+
# Use dynamic lookup to support mocking
|
|
300
|
+
level_lower = level.lower()
|
|
301
|
+
if level_lower == "info":
|
|
302
|
+
log_info(msg)
|
|
303
|
+
elif level_lower == "warning":
|
|
304
|
+
log_warning(msg)
|
|
305
|
+
elif level_lower == "error":
|
|
306
|
+
log_error(msg)
|
|
307
|
+
elif level_lower == "debug":
|
|
308
|
+
log_debug(msg)
|
|
309
|
+
else:
|
|
310
|
+
log_info(msg) # Default to info
|
|
277
311
|
|
|
278
312
|
|
|
279
313
|
def create_performance_logger(name: str) -> logging.Logger:
|
|
@@ -341,16 +375,20 @@ class LoggingContext:
|
|
|
341
375
|
self.enabled = enabled
|
|
342
376
|
self.level = level
|
|
343
377
|
self.old_level: int | None = None
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
) # Use root logger for compatibility with tests
|
|
378
|
+
# Use a specific logger name for testing to avoid interference
|
|
379
|
+
self.target_logger = logging.getLogger("tree_sitter_analyzer")
|
|
347
380
|
|
|
348
381
|
def __enter__(self) -> "LoggingContext":
|
|
349
382
|
if self.enabled and self.level is not None:
|
|
383
|
+
# Always save the current level before changing
|
|
350
384
|
self.old_level = self.target_logger.level
|
|
385
|
+
# Ensure we have a valid level to restore to (not NOTSET)
|
|
386
|
+
if self.old_level == logging.NOTSET:
|
|
387
|
+
self.old_level = logging.INFO # Default fallback
|
|
351
388
|
self.target_logger.setLevel(self.level)
|
|
352
389
|
return self
|
|
353
390
|
|
|
354
391
|
def __exit__(self, exc_type: Any, exc_val: Any, exc_tb: Any) -> None:
|
|
355
392
|
if self.enabled and self.old_level is not None:
|
|
393
|
+
# Always restore the saved level
|
|
356
394
|
self.target_logger.setLevel(self.old_level)
|