tree-sitter-analyzer 1.7.5__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.

Files changed (47) hide show
  1. tree_sitter_analyzer/__init__.py +1 -1
  2. tree_sitter_analyzer/api.py +26 -32
  3. tree_sitter_analyzer/cli/argument_validator.py +77 -0
  4. tree_sitter_analyzer/cli/commands/table_command.py +7 -2
  5. tree_sitter_analyzer/cli_main.py +17 -3
  6. tree_sitter_analyzer/core/cache_service.py +15 -5
  7. tree_sitter_analyzer/core/query.py +33 -22
  8. tree_sitter_analyzer/core/query_service.py +179 -154
  9. tree_sitter_analyzer/exceptions.py +334 -0
  10. tree_sitter_analyzer/file_handler.py +16 -1
  11. tree_sitter_analyzer/formatters/formatter_registry.py +355 -0
  12. tree_sitter_analyzer/formatters/html_formatter.py +462 -0
  13. tree_sitter_analyzer/formatters/language_formatter_factory.py +3 -0
  14. tree_sitter_analyzer/formatters/markdown_formatter.py +1 -1
  15. tree_sitter_analyzer/interfaces/mcp_server.py +3 -1
  16. tree_sitter_analyzer/language_detector.py +91 -7
  17. tree_sitter_analyzer/languages/css_plugin.py +390 -0
  18. tree_sitter_analyzer/languages/html_plugin.py +395 -0
  19. tree_sitter_analyzer/languages/java_plugin.py +116 -0
  20. tree_sitter_analyzer/languages/javascript_plugin.py +113 -0
  21. tree_sitter_analyzer/languages/markdown_plugin.py +266 -46
  22. tree_sitter_analyzer/languages/python_plugin.py +176 -33
  23. tree_sitter_analyzer/languages/typescript_plugin.py +130 -1
  24. tree_sitter_analyzer/mcp/tools/analyze_scale_tool.py +68 -3
  25. tree_sitter_analyzer/mcp/tools/fd_rg_utils.py +32 -7
  26. tree_sitter_analyzer/mcp/tools/find_and_grep_tool.py +10 -0
  27. tree_sitter_analyzer/mcp/tools/list_files_tool.py +9 -0
  28. tree_sitter_analyzer/mcp/tools/query_tool.py +100 -52
  29. tree_sitter_analyzer/mcp/tools/read_partial_tool.py +98 -14
  30. tree_sitter_analyzer/mcp/tools/search_content_tool.py +9 -0
  31. tree_sitter_analyzer/mcp/tools/table_format_tool.py +37 -13
  32. tree_sitter_analyzer/models.py +53 -0
  33. tree_sitter_analyzer/output_manager.py +1 -1
  34. tree_sitter_analyzer/plugins/base.py +50 -0
  35. tree_sitter_analyzer/plugins/manager.py +5 -1
  36. tree_sitter_analyzer/queries/css.py +634 -0
  37. tree_sitter_analyzer/queries/html.py +556 -0
  38. tree_sitter_analyzer/queries/markdown.py +54 -164
  39. tree_sitter_analyzer/query_loader.py +16 -3
  40. tree_sitter_analyzer/security/validator.py +343 -46
  41. tree_sitter_analyzer/utils/__init__.py +113 -0
  42. tree_sitter_analyzer/utils/tree_sitter_compat.py +282 -0
  43. tree_sitter_analyzer/utils.py +62 -24
  44. {tree_sitter_analyzer-1.7.5.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/METADATA +136 -14
  45. {tree_sitter_analyzer-1.7.5.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/RECORD +47 -38
  46. {tree_sitter_analyzer-1.7.5.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/entry_points.txt +2 -0
  47. {tree_sitter_analyzer-1.7.5.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")
@@ -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
- # Get log level from environment variable
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 == "DEBUG":
24
- level = logging.DEBUG
25
- elif env_level == "INFO":
26
- level = logging.INFO
27
- elif env_level == "WARNING":
28
- level = logging.WARNING
29
- elif env_level == "ERROR":
30
- level = logging.ERROR
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
- logger.setLevel(level)
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
- level_map = {
269
- "info": log_info,
270
- "warning": log_warning,
271
- "error": log_error,
272
- "debug": log_debug,
273
- }
274
-
275
- log_func = level_map.get(level.lower(), log_info)
276
- log_func(message)
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
- self.target_logger = (
345
- logging.getLogger()
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)