tree-sitter-analyzer 0.9.6__py3-none-any.whl → 0.9.8__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.

@@ -11,7 +11,7 @@ Architecture:
11
11
  - Data Models: Generic and language-specific code element representations
12
12
  """
13
13
 
14
- __version__ = "0.9.6"
14
+ __version__ = "0.9.7"
15
15
  __author__ = "aisheng.yu"
16
16
  __email__ = "aimasteracc@gmail.com"
17
17
 
@@ -1,162 +1,162 @@
1
- #!/usr/bin/env python3
2
- """
3
- Query Service
4
-
5
- Unified query service for both CLI and MCP interfaces to avoid code duplication.
6
- Provides core tree-sitter query functionality including predefined and custom queries.
7
- """
8
-
9
- import logging
10
- from typing import Any
11
-
12
- from ..encoding_utils import read_file_safe
13
- from ..query_loader import query_loader
14
- from .parser import Parser
15
- from .query_filter import QueryFilter
16
-
17
- logger = logging.getLogger(__name__)
18
-
19
-
20
- class QueryService:
21
- """Unified query service providing tree-sitter query functionality"""
22
-
23
- def __init__(self, project_root: str | None = None) -> None:
24
- """Initialize the query service"""
25
- self.project_root = project_root
26
- self.parser = Parser()
27
- self.filter = QueryFilter()
28
-
29
- async def execute_query(
30
- self,
31
- file_path: str,
32
- language: str,
33
- query_key: str | None = None,
34
- query_string: str | None = None,
35
- filter_expression: str | None = None,
36
- ) -> list[dict[str, Any]] | None:
37
- """
38
- Execute a query
39
-
40
- Args:
41
- file_path: Path to the file to analyze
42
- language: Programming language
43
- query_key: Predefined query key (e.g., 'methods', 'class')
44
- query_string: Custom query string (e.g., '(method_declaration) @method')
45
- filter_expression: Filter expression (e.g., 'name=main', 'name=~get*,public=true')
46
-
47
- Returns:
48
- List of query results, each containing capture_name, node_type, start_line, end_line, content
49
-
50
- Raises:
51
- ValueError: If neither query_key nor query_string is provided
52
- FileNotFoundError: If file doesn't exist
53
- Exception: If query execution fails
54
- """
55
- if not query_key and not query_string:
56
- raise ValueError("Must provide either query_key or query_string")
57
-
58
- if query_key and query_string:
59
- raise ValueError("Cannot provide both query_key and query_string")
60
-
61
- try:
62
- # Read file content
63
- content, encoding = read_file_safe(file_path)
64
-
65
- # Parse file
66
- parse_result = self.parser.parse_code(content, language, file_path)
67
- if not parse_result or not parse_result.tree:
68
- raise Exception("Failed to parse file")
69
-
70
- tree = parse_result.tree
71
- language_obj = tree.language if hasattr(tree, "language") else None
72
- if not language_obj:
73
- raise Exception(f"Language object not available for {language}")
74
-
75
- # Get query string
76
- if query_key:
77
- query_string = query_loader.get_query(language, query_key)
78
- if not query_string:
79
- raise ValueError(
80
- f"Query '{query_key}' not found for language '{language}'"
81
- )
82
-
83
- # Execute tree-sitter query
84
- ts_query = language_obj.query(query_string)
85
- captures = ts_query.captures(tree.root_node)
86
-
87
- # Process capture results
88
- results = []
89
- if isinstance(captures, dict):
90
- # New tree-sitter API returns dictionary
91
- for capture_name, nodes in captures.items():
92
- for node in nodes:
93
- results.append(self._create_result_dict(node, capture_name))
94
- else:
95
- # Old tree-sitter API returns list of tuples
96
- for capture in captures:
97
- if isinstance(capture, tuple) and len(capture) == 2:
98
- node, name = capture
99
- results.append(self._create_result_dict(node, name))
100
-
101
- # Apply filters
102
- if filter_expression and results:
103
- results = self.filter.filter_results(results, filter_expression)
104
-
105
- return results
106
-
107
- except Exception as e:
108
- logger.error(f"Query execution failed: {e}")
109
- raise
110
-
111
- def _create_result_dict(self, node: Any, capture_name: str) -> dict[str, Any]:
112
- """
113
- Create result dictionary from tree-sitter node
114
-
115
- Args:
116
- node: tree-sitter node
117
- capture_name: capture name
118
-
119
- Returns:
120
- Result dictionary
121
- """
122
- return {
123
- "capture_name": capture_name,
124
- "node_type": node.type if hasattr(node, "type") else "unknown",
125
- "start_line": (
126
- node.start_point[0] + 1 if hasattr(node, "start_point") else 0
127
- ),
128
- "end_line": node.end_point[0] + 1 if hasattr(node, "end_point") else 0,
129
- "content": (
130
- node.text.decode("utf-8", errors="replace")
131
- if hasattr(node, "text") and node.text
132
- else ""
133
- ),
134
- }
135
-
136
- def get_available_queries(self, language: str) -> list[str]:
137
- """
138
- Get available query keys for specified language
139
-
140
- Args:
141
- language: Programming language
142
-
143
- Returns:
144
- List of available query keys
145
- """
146
- return query_loader.list_queries(language)
147
-
148
- def get_query_description(self, language: str, query_key: str) -> str | None:
149
- """
150
- Get description for query key
151
-
152
- Args:
153
- language: Programming language
154
- query_key: Query key
155
-
156
- Returns:
157
- Query description, or None if not found
158
- """
159
- try:
160
- return query_loader.get_query_description(language, query_key)
161
- except Exception:
162
- return None
1
+ #!/usr/bin/env python3
2
+ """
3
+ Query Service
4
+
5
+ Unified query service for both CLI and MCP interfaces to avoid code duplication.
6
+ Provides core tree-sitter query functionality including predefined and custom queries.
7
+ """
8
+
9
+ import logging
10
+ from typing import Any
11
+
12
+ from ..encoding_utils import read_file_safe
13
+ from ..query_loader import query_loader
14
+ from .parser import Parser
15
+ from .query_filter import QueryFilter
16
+
17
+ logger = logging.getLogger(__name__)
18
+
19
+
20
+ class QueryService:
21
+ """Unified query service providing tree-sitter query functionality"""
22
+
23
+ def __init__(self, project_root: str | None = None) -> None:
24
+ """Initialize the query service"""
25
+ self.project_root = project_root
26
+ self.parser = Parser()
27
+ self.filter = QueryFilter()
28
+
29
+ async def execute_query(
30
+ self,
31
+ file_path: str,
32
+ language: str,
33
+ query_key: str | None = None,
34
+ query_string: str | None = None,
35
+ filter_expression: str | None = None,
36
+ ) -> list[dict[str, Any]] | None:
37
+ """
38
+ Execute a query
39
+
40
+ Args:
41
+ file_path: Path to the file to analyze
42
+ language: Programming language
43
+ query_key: Predefined query key (e.g., 'methods', 'class')
44
+ query_string: Custom query string (e.g., '(method_declaration) @method')
45
+ filter_expression: Filter expression (e.g., 'name=main', 'name=~get*,public=true')
46
+
47
+ Returns:
48
+ List of query results, each containing capture_name, node_type, start_line, end_line, content
49
+
50
+ Raises:
51
+ ValueError: If neither query_key nor query_string is provided
52
+ FileNotFoundError: If file doesn't exist
53
+ Exception: If query execution fails
54
+ """
55
+ if not query_key and not query_string:
56
+ raise ValueError("Must provide either query_key or query_string")
57
+
58
+ if query_key and query_string:
59
+ raise ValueError("Cannot provide both query_key and query_string")
60
+
61
+ try:
62
+ # Read file content
63
+ content, encoding = read_file_safe(file_path)
64
+
65
+ # Parse file
66
+ parse_result = self.parser.parse_code(content, language, file_path)
67
+ if not parse_result or not parse_result.tree:
68
+ raise Exception("Failed to parse file")
69
+
70
+ tree = parse_result.tree
71
+ language_obj = tree.language if hasattr(tree, "language") else None
72
+ if not language_obj:
73
+ raise Exception(f"Language object not available for {language}")
74
+
75
+ # Get query string
76
+ if query_key:
77
+ query_string = query_loader.get_query(language, query_key)
78
+ if not query_string:
79
+ raise ValueError(
80
+ f"Query '{query_key}' not found for language '{language}'"
81
+ )
82
+
83
+ # Execute tree-sitter query
84
+ ts_query = language_obj.query(query_string)
85
+ captures = ts_query.captures(tree.root_node)
86
+
87
+ # Process capture results
88
+ results = []
89
+ if isinstance(captures, dict):
90
+ # New tree-sitter API returns dictionary
91
+ for capture_name, nodes in captures.items():
92
+ for node in nodes:
93
+ results.append(self._create_result_dict(node, capture_name))
94
+ else:
95
+ # Old tree-sitter API returns list of tuples
96
+ for capture in captures:
97
+ if isinstance(capture, tuple) and len(capture) == 2:
98
+ node, name = capture
99
+ results.append(self._create_result_dict(node, name))
100
+
101
+ # Apply filters
102
+ if filter_expression and results:
103
+ results = self.filter.filter_results(results, filter_expression)
104
+
105
+ return results
106
+
107
+ except Exception as e:
108
+ logger.error(f"Query execution failed: {e}")
109
+ raise
110
+
111
+ def _create_result_dict(self, node: Any, capture_name: str) -> dict[str, Any]:
112
+ """
113
+ Create result dictionary from tree-sitter node
114
+
115
+ Args:
116
+ node: tree-sitter node
117
+ capture_name: capture name
118
+
119
+ Returns:
120
+ Result dictionary
121
+ """
122
+ return {
123
+ "capture_name": capture_name,
124
+ "node_type": node.type if hasattr(node, "type") else "unknown",
125
+ "start_line": (
126
+ node.start_point[0] + 1 if hasattr(node, "start_point") else 0
127
+ ),
128
+ "end_line": node.end_point[0] + 1 if hasattr(node, "end_point") else 0,
129
+ "content": (
130
+ node.text.decode("utf-8", errors="replace")
131
+ if hasattr(node, "text") and node.text
132
+ else ""
133
+ ),
134
+ }
135
+
136
+ def get_available_queries(self, language: str) -> list[str]:
137
+ """
138
+ Get available query keys for specified language
139
+
140
+ Args:
141
+ language: Programming language
142
+
143
+ Returns:
144
+ List of available query keys
145
+ """
146
+ return query_loader.list_queries(language)
147
+
148
+ def get_query_description(self, language: str, query_key: str) -> str | None:
149
+ """
150
+ Get description for query key
151
+
152
+ Args:
153
+ language: Programming language
154
+ query_key: Query key
155
+
156
+ Returns:
157
+ Query description, or None if not found
158
+ """
159
+ try:
160
+ return query_loader.get_query_description(language, query_key)
161
+ except Exception:
162
+ return None
@@ -15,6 +15,7 @@ from ...core.analysis_engine import AnalysisRequest, get_analysis_engine
15
15
  from ...language_detector import detect_language_from_file
16
16
  from ...security import SecurityValidator
17
17
  from ...utils import setup_logger
18
+ from ..utils.path_resolver import PathResolver
18
19
 
19
20
  # Set up logging
20
21
  logger = setup_logger(__name__)
@@ -35,6 +36,7 @@ class AnalyzeScaleTool:
35
36
  self.project_root = project_root
36
37
  self.analysis_engine = get_analysis_engine(project_root)
37
38
  self.security_validator = SecurityValidator(project_root)
39
+ self.path_resolver = PathResolver(project_root)
38
40
  logger.info("AnalyzeScaleTool initialized with security validation")
39
41
 
40
42
  def _calculate_file_metrics(self, file_path: str) -> dict[str, Any]:
@@ -354,11 +356,17 @@ class AnalyzeScaleTool:
354
356
  include_details = arguments.get("include_details", False)
355
357
  include_guidance = arguments.get("include_guidance", True)
356
358
 
357
- # Security validation
358
- is_valid, error_msg = self.security_validator.validate_file_path(file_path)
359
+ # Resolve file path to absolute path
360
+ resolved_file_path = self.path_resolver.resolve(file_path)
361
+ logger.info(f"Analyzing file: {file_path} (resolved to: {resolved_file_path})")
362
+
363
+ # Security validation using resolved path
364
+ is_valid, error_msg = self.security_validator.validate_file_path(
365
+ resolved_file_path
366
+ )
359
367
  if not is_valid:
360
368
  logger.warning(
361
- f"Security validation failed for file path: {file_path} - {error_msg}"
369
+ f"Security validation failed for file path: {resolved_file_path} - {error_msg}"
362
370
  )
363
371
  raise ValueError(f"Invalid file path: {error_msg}")
364
372
 
@@ -367,16 +375,20 @@ class AnalyzeScaleTool:
367
375
  language = self.security_validator.sanitize_input(language, max_length=50)
368
376
 
369
377
  # Validate file exists
370
- if not Path(file_path).exists():
378
+ if not Path(resolved_file_path).exists():
371
379
  raise ValueError("Invalid file path: file does not exist")
372
380
 
373
381
  # Detect language if not specified
374
382
  if not language:
375
- language = detect_language_from_file(file_path)
383
+ language = detect_language_from_file(resolved_file_path)
376
384
  if language == "unknown":
377
- raise ValueError(f"Could not detect language for file: {file_path}")
385
+ raise ValueError(
386
+ f"Could not detect language for file: {resolved_file_path}"
387
+ )
378
388
 
379
- logger.info(f"Analyzing code scale for {file_path} (language: {language})")
389
+ logger.info(
390
+ f"Analyzing code scale for {resolved_file_path} (language: {language})"
391
+ )
380
392
 
381
393
  try:
382
394
  # Use performance monitoring with proper context manager
@@ -386,14 +398,14 @@ class AnalyzeScaleTool:
386
398
  "analyze_code_scale_enhanced"
387
399
  ):
388
400
  # Calculate basic file metrics
389
- file_metrics = self._calculate_file_metrics(file_path)
401
+ file_metrics = self._calculate_file_metrics(resolved_file_path)
390
402
 
391
403
  # Use appropriate analyzer based on language
392
404
  if language == "java":
393
405
  # Use AdvancedAnalyzer for comprehensive analysis
394
406
  # Use unified analysis engine instead of deprecated advanced_analyzer
395
407
  request = AnalysisRequest(
396
- file_path=file_path,
408
+ file_path=resolved_file_path,
397
409
  language=language,
398
410
  include_complexity=True,
399
411
  include_details=True,
@@ -408,7 +420,7 @@ class AnalyzeScaleTool:
408
420
  else:
409
421
  # Use universal analysis_engine for other languages
410
422
  request = AnalysisRequest(
411
- file_path=file_path,
423
+ file_path=resolved_file_path,
412
424
  language=language,
413
425
  include_details=include_details,
414
426
  )
@@ -13,6 +13,7 @@ from ...core.query_service import QueryService
13
13
  from ...language_detector import detect_language_from_file
14
14
  from ...security import SecurityValidator
15
15
  from ..utils.error_handler import handle_mcp_errors
16
+ from ..utils.path_resolver import PathResolver
16
17
 
17
18
  logger = logging.getLogger(__name__)
18
19
 
@@ -25,6 +26,7 @@ class QueryTool:
25
26
  self.project_root = project_root
26
27
  self.query_service = QueryService(project_root)
27
28
  self.security_validator = SecurityValidator(project_root)
29
+ self.path_resolver = PathResolver(project_root)
28
30
 
29
31
  def get_tool_definition(self) -> dict[str, Any]:
30
32
  """
@@ -41,7 +43,7 @@ class QueryTool:
41
43
  "properties": {
42
44
  "file_path": {
43
45
  "type": "string",
44
- "description": "Path to the code file to query (relative to project root)",
46
+ "description": "Path to the code file to query (relative to project root or absolute path)",
45
47
  },
46
48
  "language": {
47
49
  "type": "string",
@@ -74,7 +76,7 @@ class QueryTool:
74
76
  },
75
77
  }
76
78
 
77
- @handle_mcp_errors
79
+ @handle_mcp_errors("query_code")
78
80
  async def execute(self, arguments: dict[str, Any]) -> dict[str, Any]:
79
81
  """
80
82
  Execute query tool
@@ -90,8 +92,18 @@ class QueryTool:
90
92
  if not file_path:
91
93
  raise ValueError("file_path is required")
92
94
 
93
- # Security validation
94
- validated_path = self.security_validator.validate_file_path(file_path)
95
+ # Resolve file path to absolute path
96
+ resolved_file_path = self.path_resolver.resolve(file_path)
97
+ logger.info(f"Querying file: {file_path} (resolved to: {resolved_file_path})")
98
+
99
+ # Security validation using resolved path
100
+ is_valid, error_msg = self.security_validator.validate_file_path(
101
+ resolved_file_path
102
+ )
103
+ if not is_valid:
104
+ raise ValueError(
105
+ f"Invalid or unsafe file path: {error_msg or resolved_file_path}"
106
+ )
95
107
 
96
108
  # Get query parameters
97
109
  query_key = arguments.get("query_key")
@@ -108,14 +120,14 @@ class QueryTool:
108
120
  # Detect language
109
121
  language = arguments.get("language")
110
122
  if not language:
111
- language = detect_language_from_file(validated_path)
123
+ language = detect_language_from_file(resolved_file_path)
112
124
  if not language:
113
125
  raise ValueError(f"Could not detect language for file: {file_path}")
114
126
 
115
127
  try:
116
128
  # Execute query
117
129
  results = await self.query_service.execute_query(
118
- validated_path, language, query_key, query_string, filter_expression
130
+ resolved_file_path, language, query_key, query_string, filter_expression
119
131
  )
120
132
 
121
133
  if not results:
@@ -7,13 +7,13 @@ allowing selective content extraction with line and column range support.
7
7
  """
8
8
 
9
9
  import json
10
- import os
11
10
  from pathlib import Path
12
11
  from typing import Any
13
12
 
14
13
  from ...file_handler import read_file_partial
15
14
  from ...security import SecurityValidator
16
15
  from ...utils import setup_logger
16
+ from ..utils.path_resolver import PathResolver
17
17
 
18
18
  # Set up logging
19
19
  logger = setup_logger(__name__)
@@ -31,6 +31,7 @@ class ReadPartialTool:
31
31
  """Initialize the read partial tool."""
32
32
  self.security_validator = SecurityValidator(project_root)
33
33
  self.project_root = project_root
34
+ self.path_resolver = PathResolver(project_root)
34
35
  logger.info("ReadPartialTool initialized with security validation")
35
36
 
36
37
  def get_tool_schema(self) -> dict[str, Any]:
@@ -106,20 +107,8 @@ class ReadPartialTool:
106
107
  end_column = arguments.get("end_column")
107
108
  # output_format = arguments.get("format", "text") # Not used currently
108
109
 
109
- # Resolve relative path against project root for consistent behavior
110
- base_root = (
111
- getattr(
112
- getattr(self.security_validator, "boundary_manager", None),
113
- "project_root",
114
- None,
115
- )
116
- or self.project_root
117
- )
118
-
119
- if not os.path.isabs(file_path) and base_root:
120
- resolved_path = os.path.realpath(os.path.join(base_root, file_path))
121
- else:
122
- resolved_path = file_path
110
+ # Resolve file path using common path resolver
111
+ resolved_path = self.path_resolver.resolve(file_path)
123
112
 
124
113
  # Security validation (validate resolved absolute path when possible)
125
114
  is_valid, error_msg = self.security_validator.validate_file_path(resolved_path)
@@ -1,12 +1,11 @@
1
1
  #!/usr/bin/env python3
2
2
  """
3
- Table Format MCP Tool
3
+ Table Format Tool for MCP
4
4
 
5
- This tool provides table-formatted output for code analysis results through the MCP protocol,
6
- equivalent to the CLI --table=full option functionality.
5
+ This tool provides code structure analysis and table formatting through the MCP protocol,
6
+ converting analysis results into structured table formats for better readability.
7
7
  """
8
8
 
9
- import os
10
9
  from pathlib import Path
11
10
  from typing import Any
12
11
 
@@ -16,6 +15,7 @@ from ...security import SecurityValidator
16
15
  from ...table_formatter import TableFormatter
17
16
  from ...utils import setup_logger
18
17
  from ..utils import get_performance_monitor
18
+ from ..utils.path_resolver import PathResolver
19
19
 
20
20
  # Set up logging
21
21
  logger = setup_logger(__name__)
@@ -23,20 +23,19 @@ logger = setup_logger(__name__)
23
23
 
24
24
  class TableFormatTool:
25
25
  """
26
- MCP Tool for formatting code analysis results as tables.
26
+ MCP Tool for code structure analysis and table formatting.
27
27
 
28
- This tool integrates with existing table_formatter and analyzer components
29
- to provide table-formatted output through the MCP protocol, equivalent to
30
- the CLI --table=full option.
28
+ This tool integrates with existing analyzer components to provide
29
+ structured table output through the MCP protocol.
31
30
  """
32
31
 
33
32
  def __init__(self, project_root: str = None) -> None:
34
33
  """Initialize the table format tool."""
35
- self.logger = logger
36
34
  self.project_root = project_root
37
35
  self.analysis_engine = get_analysis_engine(project_root)
38
36
  self.security_validator = SecurityValidator(project_root)
39
- logger.info("TableFormatTool initialized with security validation")
37
+ self.path_resolver = PathResolver(project_root)
38
+ self.logger = logger
40
39
 
41
40
  def get_tool_schema(self) -> dict[str, Any]:
42
41
  """
@@ -272,20 +271,8 @@ class TableFormatTool:
272
271
  format_type = args.get("format_type", "full")
273
272
  language = args.get("language")
274
273
 
275
- # Resolve relative path against project root for consistent behavior
276
- base_root = (
277
- getattr(
278
- getattr(self.security_validator, "boundary_manager", None),
279
- "project_root",
280
- None,
281
- )
282
- or self.project_root
283
- )
284
-
285
- if not os.path.isabs(file_path) and base_root:
286
- resolved_path = os.path.realpath(os.path.join(base_root, file_path))
287
- else:
288
- resolved_path = file_path
274
+ # Resolve file path using common path resolver
275
+ resolved_path = self.path_resolver.resolve(file_path)
289
276
 
290
277
  # Security validation
291
278
  is_valid, error_msg = self.security_validator.validate_file_path(