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.

Files changed (38) hide show
  1. tree_sitter_analyzer/__init__.py +1 -1
  2. tree_sitter_analyzer/api.py +23 -30
  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/formatters/formatter_registry.py +355 -0
  10. tree_sitter_analyzer/formatters/html_formatter.py +462 -0
  11. tree_sitter_analyzer/formatters/language_formatter_factory.py +3 -0
  12. tree_sitter_analyzer/formatters/markdown_formatter.py +1 -1
  13. tree_sitter_analyzer/language_detector.py +80 -7
  14. tree_sitter_analyzer/languages/css_plugin.py +390 -0
  15. tree_sitter_analyzer/languages/html_plugin.py +395 -0
  16. tree_sitter_analyzer/languages/java_plugin.py +116 -0
  17. tree_sitter_analyzer/languages/javascript_plugin.py +113 -0
  18. tree_sitter_analyzer/languages/markdown_plugin.py +266 -46
  19. tree_sitter_analyzer/languages/python_plugin.py +176 -33
  20. tree_sitter_analyzer/languages/typescript_plugin.py +130 -1
  21. tree_sitter_analyzer/mcp/tools/query_tool.py +99 -58
  22. tree_sitter_analyzer/mcp/tools/table_format_tool.py +24 -10
  23. tree_sitter_analyzer/models.py +53 -0
  24. tree_sitter_analyzer/output_manager.py +1 -1
  25. tree_sitter_analyzer/plugins/base.py +50 -0
  26. tree_sitter_analyzer/plugins/manager.py +5 -1
  27. tree_sitter_analyzer/queries/css.py +634 -0
  28. tree_sitter_analyzer/queries/html.py +556 -0
  29. tree_sitter_analyzer/queries/markdown.py +54 -164
  30. tree_sitter_analyzer/query_loader.py +16 -3
  31. tree_sitter_analyzer/security/validator.py +182 -44
  32. tree_sitter_analyzer/utils/__init__.py +113 -0
  33. tree_sitter_analyzer/utils/tree_sitter_compat.py +282 -0
  34. tree_sitter_analyzer/utils.py +62 -24
  35. {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/METADATA +120 -14
  36. {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/RECORD +38 -29
  37. {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/entry_points.txt +2 -0
  38. {tree_sitter_analyzer-1.7.7.dist-info → tree_sitter_analyzer-1.8.2.dist-info}/WHEEL +0 -0
@@ -11,7 +11,7 @@ Architecture:
11
11
  - Data Models: Generic and language-specific code element representations
12
12
  """
13
13
 
14
- __version__ = "1.7.7"
14
+ __version__ = "1.8.2"
15
15
  __author__ = "aisheng.yu"
16
16
  __email__ = "aimasteracc@gmail.com"
17
17
 
@@ -58,30 +58,20 @@ def analyze_file(
58
58
  include_complexity: Whether to include complexity metrics (backward compatibility)
59
59
 
60
60
  Returns:
61
- Analysis results dictionary containing:
62
- - success: Whether the analysis was successful
63
- - file_info: Basic file information
64
- - language_info: Detected/specified language information
65
- - ast_info: Abstract syntax tree information
66
- - query_results: Results from executed queries (if include_queries=True)
67
- - elements: Extracted code elements (if include_elements=True)
68
- - error: Error message (if success=False)
61
+ Analysis results dictionary
69
62
  """
70
63
  try:
71
64
  engine = get_engine()
72
65
 
73
66
  # Perform the analysis
74
67
  analysis_result = engine.analyze_file(file_path, language)
75
-
76
- # Convert AnalysisResult to expected API format
68
+
69
+ # Convert AnalysisResult to expected API format (same as analyze_code)
77
70
  result = {
78
71
  "success": analysis_result.success,
79
72
  "file_info": {
80
73
  "path": str(file_path),
81
- "exists": Path(file_path).exists(),
82
- "size": (
83
- Path(file_path).stat().st_size if Path(file_path).exists() else 0
84
- ),
74
+ "exists": True,
85
75
  },
86
76
  "language_info": {
87
77
  "language": analysis_result.language,
@@ -177,21 +167,14 @@ def analyze_file(
177
167
  except FileNotFoundError as e:
178
168
  # Re-raise FileNotFoundError for tests that expect it
179
169
  raise e
180
- except (ValueError, TypeError, OSError) as e:
181
- # Handle specific expected errors
182
- log_error(f"API analyze_file failed with {type(e).__name__}: {e}")
183
- return {
184
- "success": False,
185
- "error": f"{type(e).__name__}: {str(e)}",
186
- "file_info": {"path": str(file_path), "exists": Path(file_path).exists()},
187
- }
188
170
  except Exception as e:
189
- # Handle unexpected errors
190
- log_error(f"API analyze_file failed with unexpected error: {e}")
171
+ log_error(f"API analyze_file failed: {e}")
191
172
  return {
192
173
  "success": False,
193
- "error": f"Unexpected error: {str(e)}",
194
- "file_info": {"path": str(file_path), "exists": Path(file_path).exists()},
174
+ "error": str(e),
175
+ "file_info": {"path": str(file_path), "exists": False},
176
+ "language_info": {"language": language or "unknown", "detected": False},
177
+ "ast_info": {"node_count": 0, "line_count": 0},
195
178
  }
196
179
 
197
180
 
@@ -378,7 +361,7 @@ def is_language_supported(language: str) -> bool:
378
361
  return False
379
362
 
380
363
 
381
- def detect_language(file_path: str | Path) -> str | None:
364
+ def detect_language(file_path: str | Path) -> str:
382
365
  """
383
366
  Detect programming language from file path.
384
367
 
@@ -386,15 +369,25 @@ def detect_language(file_path: str | Path) -> str | None:
386
369
  file_path: Path to the file
387
370
 
388
371
  Returns:
389
- Detected language name or None
372
+ Detected language name - 常に有効な文字列を返す
390
373
  """
391
374
  try:
375
+ # Handle invalid input
376
+ if not file_path:
377
+ return "unknown"
378
+
392
379
  engine = get_engine()
393
380
  # Use language_detector instead of language_registry
394
- return engine.language_detector.detect_from_extension(str(file_path))
381
+ result = engine.language_detector.detect_from_extension(str(file_path))
382
+
383
+ # Ensure result is valid
384
+ if not result or result.strip() == "":
385
+ return "unknown"
386
+
387
+ return result
395
388
  except Exception as e:
396
389
  log_error(f"Failed to detect language for {file_path}: {e}")
397
- return None
390
+ return "unknown"
398
391
 
399
392
 
400
393
  def get_file_extensions(language: str) -> list[str]:
@@ -0,0 +1,77 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ CLI Argument Validator
4
+
5
+ Validates CLI argument combinations and provides clear error messages.
6
+ """
7
+
8
+ from typing import Any, Optional
9
+
10
+
11
+ class CLIArgumentValidator:
12
+ """Validator for CLI argument combinations."""
13
+
14
+ def __init__(self):
15
+ """Initialize the validator."""
16
+ pass
17
+
18
+ def validate_arguments(self, args: Any) -> Optional[str]:
19
+ """
20
+ Validate CLI argument combinations.
21
+
22
+ Args:
23
+ args: Parsed command line arguments
24
+
25
+ Returns:
26
+ None if valid, error message string if invalid
27
+ """
28
+ # Check for --table and --query-key combination
29
+ table_specified = hasattr(args, 'table') and args.table is not None and args.table != ""
30
+ query_key_specified = hasattr(args, 'query_key') and args.query_key is not None and args.query_key != ""
31
+
32
+ if table_specified and query_key_specified:
33
+ return "--table and --query-key cannot be used together. Use --query-key with --filter instead."
34
+
35
+ # All validations passed
36
+ return None
37
+
38
+ def validate_table_query_exclusivity(self, args: Any) -> Optional[str]:
39
+ """
40
+ Validate that --table and --query-key are mutually exclusive.
41
+
42
+ Args:
43
+ args: Parsed command line arguments
44
+
45
+ Returns:
46
+ None if valid, error message string if invalid
47
+ """
48
+ table_specified = hasattr(args, 'table') and args.table is not None and args.table != ""
49
+ query_key_specified = hasattr(args, 'query_key') and args.query_key is not None and args.query_key != ""
50
+
51
+ if table_specified and query_key_specified:
52
+ return "--table and --query-key cannot be used together. Use --query-key with --filter instead."
53
+
54
+ return None
55
+
56
+ def get_usage_examples(self) -> str:
57
+ """
58
+ Get usage examples for correct argument combinations.
59
+
60
+ Returns:
61
+ String containing usage examples
62
+ """
63
+ return """
64
+ Correct usage examples:
65
+
66
+ # Use table format only:
67
+ uv run python -m tree_sitter_analyzer examples/BigService.java --table full
68
+
69
+ # Use query-key only:
70
+ uv run python -m tree_sitter_analyzer examples/BigService.java --query-key methods
71
+
72
+ # Use query-key with filter:
73
+ uv run python -m tree_sitter_analyzer examples/BigService.java --query-key methods --filter "name=main"
74
+
75
+ # Invalid combination (will cause error):
76
+ uv run python -m tree_sitter_analyzer examples/BigService.java --table full --query-key methods
77
+ """
@@ -16,7 +16,7 @@ from ...constants import (
16
16
  ELEMENT_TYPE_VARIABLE,
17
17
  get_element_type,
18
18
  )
19
- from ...output_manager import output_error
19
+ from ...output_manager import output_error, output_info
20
20
  from ...table_formatter import create_table_formatter
21
21
  from ...formatters.language_formatter_factory import create_language_formatter
22
22
  from .base_command import BaseCommand
@@ -25,10 +25,14 @@ from .base_command import BaseCommand
25
25
  class TableCommand(BaseCommand):
26
26
  """Command for generating table format output."""
27
27
 
28
+ def __init__(self, args):
29
+ """Initialize the table command."""
30
+ super().__init__(args)
31
+
28
32
  async def execute_async(self, language: str) -> int:
29
33
  """Execute table format generation."""
30
34
  try:
31
- # Perform analysis
35
+ # Perform standard analysis
32
36
  analysis_result = await self.analyze_file(language)
33
37
  if not analysis_result:
34
38
  return 1
@@ -64,6 +68,7 @@ class TableCommand(BaseCommand):
64
68
  output_error(f"An error occurred during table format analysis: {e}")
65
69
  return 1
66
70
 
71
+
67
72
  def _convert_to_formatter_format(self, analysis_result: Any) -> dict[str, Any]:
68
73
  """Convert AnalysisResult to format expected by formatters."""
69
74
  return {
@@ -25,6 +25,7 @@ from .cli.info_commands import (
25
25
  )
26
26
  from .output_manager import output_error, output_info, output_list
27
27
  from .query_loader import query_loader
28
+ from .cli.argument_validator import CLIArgumentValidator
28
29
 
29
30
 
30
31
  class CLICommandFactory:
@@ -33,6 +34,14 @@ class CLICommandFactory:
33
34
  @staticmethod
34
35
  def create_command(args: argparse.Namespace) -> Any:
35
36
  """Create appropriate command based on arguments."""
37
+
38
+ # Validate argument combinations first
39
+ validator = CLIArgumentValidator()
40
+ validation_error = validator.validate_arguments(args)
41
+ if validation_error:
42
+ output_error(validation_error)
43
+ output_info(validator.get_usage_examples())
44
+ return None
36
45
 
37
46
  # Information commands (no file analysis required)
38
47
  if args.list_queries:
@@ -62,6 +71,7 @@ class CLICommandFactory:
62
71
  if hasattr(args, "partial_read") and args.partial_read:
63
72
  return PartialReadCommand(args)
64
73
 
74
+ # Handle table command with or without query-key
65
75
  if hasattr(args, "table") and args.table:
66
76
  return TableCommand(args)
67
77
 
@@ -274,14 +284,18 @@ def main() -> None:
274
284
  if "--quiet" in sys.argv:
275
285
  os.environ["LOG_LEVEL"] = "ERROR"
276
286
  else:
277
- # Set default log level to WARNING to reduce output
278
- os.environ.setdefault("LOG_LEVEL", "WARNING")
287
+ # Set default log level to ERROR to prevent log output in CLI
288
+ os.environ["LOG_LEVEL"] = "ERROR"
279
289
 
280
290
  parser = create_argument_parser()
281
291
  args = parser.parse_args()
282
292
 
283
- # Configure default logging levels to reduce output
293
+ # Configure all logging to ERROR level to prevent output contamination
294
+ logging.getLogger().setLevel(logging.ERROR)
295
+ logging.getLogger("tree_sitter_analyzer").setLevel(logging.ERROR)
284
296
  logging.getLogger("tree_sitter_analyzer.performance").setLevel(logging.ERROR)
297
+ logging.getLogger("tree_sitter_analyzer.plugins").setLevel(logging.ERROR)
298
+ logging.getLogger("tree_sitter_analyzer.plugins.manager").setLevel(logging.ERROR)
285
299
 
286
300
  # Configure logging for table output
287
301
  if hasattr(args, "table") and args.table:
@@ -225,7 +225,10 @@ class CacheService:
225
225
  for key in self._stats:
226
226
  self._stats[key] = 0
227
227
 
228
- log_info("All caches cleared")
228
+ # Only log if not in quiet mode (check log level)
229
+ import logging
230
+ if logging.getLogger("tree_sitter_analyzer").level <= logging.INFO:
231
+ log_info("All caches cleared")
229
232
 
230
233
  def size(self) -> int:
231
234
  """
@@ -315,7 +318,14 @@ class CacheService:
315
318
  def __del__(self) -> None:
316
319
  """デストラクタ - リソースクリーンアップ"""
317
320
  try:
318
- self.clear()
319
- log_debug("CacheService destroyed and cleaned up")
320
- except Exception as e:
321
- log_error(f"Error during CacheService cleanup: {e}")
321
+ # Only clear if not in shutdown mode
322
+ import sys
323
+ if sys.meta_path is not None: # Check if Python is not shutting down
324
+ # Clear caches without logging to avoid shutdown issues
325
+ with self._lock:
326
+ self._l1_cache.clear()
327
+ self._l2_cache.clear()
328
+ self._l3_cache.clear()
329
+ except Exception:
330
+ # Silently ignore all errors during shutdown to prevent ImportError
331
+ pass
@@ -13,6 +13,7 @@ from typing import Any
13
13
  from tree_sitter import Language, Node, Tree
14
14
 
15
15
  from ..query_loader import get_query_loader
16
+ from ..utils.tree_sitter_compat import TreeSitterQueryCompat, get_node_text_safe
16
17
 
17
18
  # Configure logging
18
19
  logger = logging.getLogger(__name__)
@@ -69,18 +70,33 @@ class QueryExecutor:
69
70
  "Language is None", query_name=query_name
70
71
  )
71
72
 
72
- # Get the query string
73
- language_name = language.name if hasattr(language, "name") else "unknown"
73
+ # Get the query string with robust language name handling
74
+ language_name = None
75
+ if language:
76
+ # Try multiple ways to get language name
77
+ language_name = getattr(language, "name", None)
78
+ if not language_name:
79
+ language_name = getattr(language, "_name", None)
80
+ if not language_name:
81
+ language_name = str(language).split('.')[-1] if hasattr(language, '__class__') else None
82
+
83
+ # Ensure we have a valid language name
84
+ if not language_name or language_name.strip() == "" or language_name == "None":
85
+ language_name = "unknown"
86
+ else:
87
+ language_name = language_name.strip().lower()
88
+
74
89
  query_string = self._query_loader.get_query(language_name, query_name)
75
90
  if query_string is None:
76
91
  return self._create_error_result(
77
92
  f"Query '{query_name}' not found", query_name=query_name
78
93
  )
79
94
 
80
- # Create and execute the query
95
+ # Create and execute the query using modern API
81
96
  try:
82
- query = language.query(query_string)
83
- captures = query.captures(tree.root_node)
97
+ captures = TreeSitterQueryCompat.safe_execute_query(
98
+ language, query_string, tree.root_node, fallback_result=[]
99
+ )
84
100
 
85
101
  # Process captures
86
102
  try:
@@ -146,10 +162,11 @@ class QueryExecutor:
146
162
  if language is None:
147
163
  return self._create_error_result("Language is None") # type: ignore[unreachable]
148
164
 
149
- # Create and execute the query
165
+ # Create and execute the query using modern API
150
166
  try:
151
- query = language.query(query_string)
152
- captures = query.captures(tree.root_node)
167
+ captures = TreeSitterQueryCompat.safe_execute_query(
168
+ language, query_string, tree.root_node, fallback_result=[]
169
+ )
153
170
 
154
171
  # Process captures
155
172
  try:
@@ -223,14 +240,13 @@ class QueryExecutor:
223
240
  try:
224
241
  for capture in captures:
225
242
  try:
226
- # Handle both dictionary and tuple formats
227
- if isinstance(capture, dict):
228
- # New Tree-sitter API format
229
- node = capture.get("node")
230
- name = capture.get("name", "unknown")
231
- elif isinstance(capture, tuple) and len(capture) == 2:
232
- # Old Tree-sitter API format
243
+ # Handle tuple format from modern API
244
+ if isinstance(capture, tuple) and len(capture) == 2:
233
245
  node, name = capture
246
+ # Handle dictionary format (legacy API compatibility)
247
+ elif isinstance(capture, dict) and "node" in capture and "name" in capture:
248
+ node = capture["node"]
249
+ name = capture["name"]
234
250
  else:
235
251
  logger.warning(f"Unexpected capture format: {type(capture)}")
236
252
  continue
@@ -265,13 +281,8 @@ class QueryExecutor:
265
281
  Dictionary containing node information
266
282
  """
267
283
  try:
268
- # Extract node text
269
- node_text = ""
270
- if hasattr(node, "text") and node.text:
271
- try:
272
- node_text = node.text.decode("utf-8", errors="replace")
273
- except Exception:
274
- node_text = str(node.text)
284
+ # Extract node text using safe utility
285
+ node_text = get_node_text_safe(node, source_code)
275
286
 
276
287
  return {
277
288
  "capture_name": capture_name,